diff options
| author | Robert Godfrey <rgodfrey@apache.org> | 2014-07-30 13:07:51 +0000 |
|---|---|---|
| committer | Robert Godfrey <rgodfrey@apache.org> | 2014-07-30 13:07:51 +0000 |
| commit | 6fae60887199cdcd6b2db87996eb838b519cffcf (patch) | |
| tree | cccd9c1efcec647b81d1c650c670eb18e70ecb56 /qpid/java/broker-core | |
| parent | 96e8753e5647100138b87ae27036e407a0cef818 (diff) | |
| download | qpid-python-6fae60887199cdcd6b2db87996eb838b519cffcf.tar.gz | |
QPID-5946 : [Java Broker] Add alternative KeyStore implementation that can use standard crt/pem rather than jks files
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk@1614652 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'qpid/java/broker-core')
3 files changed, 601 insertions, 0 deletions
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStore.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStore.java new file mode 100644 index 0000000000..9563f98579 --- /dev/null +++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStore.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.server.security; + +import org.apache.qpid.server.model.DerivedAttribute; +import org.apache.qpid.server.model.KeyStore; +import org.apache.qpid.server.model.ManagedAttribute; +import org.apache.qpid.server.model.ManagedObject; + +@ManagedObject( category = false, type = "NonJavaKeyStore" ) +public interface NonJavaKeyStore<X extends NonJavaKeyStore<X>> extends KeyStore<X> +{ + + @ManagedAttribute( mandatory = true, secure = true ) + String getPrivateKeyUrl(); + + @ManagedAttribute( mandatory = true ) + String getCertificateUrl(); + + @ManagedAttribute + String getIntermediateCertificateUrl(); + + @DerivedAttribute + String getSubjectName(); + + @DerivedAttribute + public long getCertificateValidEnd(); + + @DerivedAttribute + public long getCertificateValidStart(); +} diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStoreImpl.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStoreImpl.java new file mode 100644 index 0000000000..efcd40c638 --- /dev/null +++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/NonJavaKeyStoreImpl.java @@ -0,0 +1,404 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.server.security; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.AccessControlException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.xml.bind.DatatypeConverter; + +import org.apache.log4j.Logger; + +import org.apache.qpid.server.configuration.IllegalConfigurationException; +import org.apache.qpid.server.model.AbstractConfiguredObject; +import org.apache.qpid.server.model.Broker; +import org.apache.qpid.server.model.ConfiguredObject; +import org.apache.qpid.server.model.IntegrityViolationException; +import org.apache.qpid.server.model.KeyStore; +import org.apache.qpid.server.model.ManagedAttributeField; +import org.apache.qpid.server.model.ManagedObject; +import org.apache.qpid.server.model.ManagedObjectFactoryConstructor; +import org.apache.qpid.server.model.Port; +import org.apache.qpid.server.model.State; +import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.util.urlstreamhandler.data.Handler; + +@ManagedObject( category = false ) +public class NonJavaKeyStoreImpl extends AbstractConfiguredObject<NonJavaKeyStoreImpl> implements NonJavaKeyStore<NonJavaKeyStoreImpl> +{ + private static final Logger LOGGER = Logger.getLogger(NonJavaKeyStoreImpl.class); + + private final Broker<?> _broker; + + @ManagedAttributeField( afterSet = "updateKeyManagers" ) + private String _privateKeyUrl; + @ManagedAttributeField( afterSet = "updateKeyManagers" ) + private String _certificateUrl; + @ManagedAttributeField( afterSet = "updateKeyManagers" ) + private String _intermediateCertificateUrl; + + private volatile KeyManager[] _keyManagers = new KeyManager[0]; + + private static final SecureRandom RANDOM = new SecureRandom(); + + static + { + Handler.register(); + } + + private X509Certificate _certificate; + + @ManagedObjectFactoryConstructor + public NonJavaKeyStoreImpl(final Map<String, Object> attributes, Broker<?> broker) + { + super(parentsMap(broker), attributes); + _broker = broker; + } + + @Override + public String getPrivateKeyUrl() + { + return _privateKeyUrl; + } + + @Override + public String getCertificateUrl() + { + return _certificateUrl; + } + + @Override + public String getIntermediateCertificateUrl() + { + return _intermediateCertificateUrl; + } + + @Override + public String getSubjectName() + { + if(_certificate != null) + { + try + { + String dn = _certificate.getSubjectX500Principal().getName(); + LdapName ldapDN = new LdapName(dn); + String name = dn; + for (Rdn rdn : ldapDN.getRdns()) + { + if (rdn.getType().equalsIgnoreCase("CN")) + { + name = String.valueOf(rdn.getValue()); + break; + } + } + return name; + } + catch (InvalidNameException e) + { + LOGGER.error("Error getting subject name from certificate"); + return null; + } + } + else + { + return null; + } + } + + @Override + public long getCertificateValidEnd() + { + return _certificate == null ? 0 : _certificate.getNotAfter().getTime(); + } + + @Override + public long getCertificateValidStart() + { + return _certificate == null ? 0 : _certificate.getNotBefore().getTime(); + } + + + @Override + public KeyManager[] getKeyManagers() throws GeneralSecurityException + { + + return _keyManagers; + } + + @Override + public void onValidate() + { + super.onValidate(); + validateKeyStoreAttributes(this); + } + + @Override + public State getState() + { + return State.ACTIVE; + } + + @Override + public Object getAttribute(String name) + { + if (KeyStore.STATE.equals(name)) + { + return getState(); + } + + return super.getAttribute(name); + } + + @Override + protected boolean setState(State desiredState) + { + if (desiredState == State.DELETED) + { + // verify that it is not in use + String storeName = getName(); + + Collection<Port> ports = new ArrayList<Port>(_broker.getPorts()); + for (Port port : ports) + { + if (port.getKeyStore() == this) + { + throw new IntegrityViolationException("Key store '" + + storeName + + "' can't be deleted as it is in use by a port:" + + port.getName()); + } + } + deleted(); + return true; + } + + return false; + } + + @Override + protected void authoriseSetDesiredState(State desiredState) throws AccessControlException + { + if (desiredState == State.DELETED) + { + if (!_broker.getSecurityManager().authoriseConfiguringBroker(getName(), KeyStore.class, Operation.DELETE)) + { + throw new AccessControlException("Deletion of key store is denied"); + } + } + } + + @Override + protected void authoriseSetAttributes(ConfiguredObject<?> modified, Set<String> attributes) + throws AccessControlException + { + if (!_broker.getSecurityManager().authoriseConfiguringBroker(getName(), KeyStore.class, Operation.UPDATE)) + { + throw new AccessControlException("Setting key store attributes is denied"); + } + } + + @Override + protected void validateChange(final ConfiguredObject<?> proxyForValidation, final Set<String> changedAttributes) + { + super.validateChange(proxyForValidation, changedAttributes); + NonJavaKeyStore changedStore = (NonJavaKeyStore) proxyForValidation; + if (changedAttributes.contains(NAME) && !getName().equals(changedStore.getName())) + { + throw new IllegalConfigurationException("Changing the key store name is not allowed"); + } + validateKeyStoreAttributes(changedStore); + } + + private void validateKeyStoreAttributes(NonJavaKeyStore<?> keyStore) + { + try + { + getUrlFromString(keyStore.getPrivateKeyUrl()).openStream(); + getUrlFromString(keyStore.getCertificateUrl()).openStream(); + if(keyStore.getIntermediateCertificateUrl() != null) + { + getUrlFromString(keyStore.getIntermediateCertificateUrl()).openStream(); + } + } + catch (IOException e) + { + throw new IllegalArgumentException(e); + } + } + + @SuppressWarnings("unused") + private void updateKeyManagers() + { + try + { + if (_privateKeyUrl != null && _certificateUrl != null) + { + PrivateKey privateKey = readPrivateKey(getUrlFromString(_privateKeyUrl)); + X509Certificate[] certs = readCertificates(getUrlFromString(_certificateUrl)); + if(_intermediateCertificateUrl != null) + { + List<X509Certificate> allCerts = new ArrayList<>(Arrays.asList(certs)); + allCerts.addAll(Arrays.asList(readCertificates(getUrlFromString(_intermediateCertificateUrl)))); + certs = allCerts.toArray(new X509Certificate[allCerts.size()]); + } + + java.security.KeyStore inMemoryKeyStore = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); + + byte[] bytes = new byte[64]; + char[] chars = new char[64]; + RANDOM.nextBytes(bytes); + StandardCharsets.US_ASCII.decode(ByteBuffer.wrap(bytes)).get(chars); + inMemoryKeyStore.load(null, chars); + inMemoryKeyStore.setKeyEntry("1", privateKey, chars, certs); + + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(inMemoryKeyStore, chars); + _keyManagers = kmf.getKeyManagers(); + _certificate = certs[0]; + } + + } + catch (IOException | GeneralSecurityException e) + { + LOGGER.error("Error attempting to create KeyStore from private key and certificates", e); + _keyManagers = new KeyManager[0]; + } + } + + private URL getUrlFromString(String urlString) throws MalformedURLException + { + URL url; + + try + { + url = new URL(urlString); + } + catch (MalformedURLException e) + { + File file = new File(urlString); + url = file.toURI().toURL(); + + } + return url; + } + + public static X509Certificate[] readCertificates(URL certFile) + throws IOException, GeneralSecurityException + { + List<X509Certificate> crt = new ArrayList<>(); + try (InputStream is = certFile.openStream()) + { + do + { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + crt.add( (X509Certificate) cf.generateCertificate(is)); + } while(is.available() != 0); + } + catch(CertificateException e) + { + if(crt.isEmpty()) + { + throw e; + } + } + return crt.toArray(new X509Certificate[crt.size()]); + } + + private static PrivateKey readPrivateKey(final URL url) + throws IOException, GeneralSecurityException + { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (InputStream urlStream = url.openStream()) + { + byte[] tmp = new byte[1024]; + int read; + while((read = urlStream.read(tmp)) != -1) + { + buffer.write(tmp,0,read); + } + } + + byte[] content = buffer.toByteArray(); + String contentAsString = new String(content, StandardCharsets.US_ASCII); + if(contentAsString.contains("-----BEGIN ") && contentAsString.contains(" PRIVATE KEY-----")) + { + BufferedReader lineReader = new BufferedReader(new StringReader(contentAsString)); + + String line; + do + { + line = lineReader.readLine(); + } while(line != null && !(line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----"))); + + if(line != null) + { + StringBuilder keyBuilder = new StringBuilder(); + + while((line = lineReader.readLine()) != null) + { + if(line.startsWith("-----END ") && line.endsWith(" PRIVATE KEY-----")) + { + break; + } + keyBuilder.append(line); + } + + content = DatatypeConverter.parseBase64Binary(keyBuilder.toString()); + } + } + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(content); + KeyFactory kf = KeyFactory.getInstance("RSA"); + PrivateKey key = kf.generatePrivate(keySpec); + return key; + } + + + +} diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/util/urlstreamhandler/data/Handler.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/util/urlstreamhandler/data/Handler.java new file mode 100644 index 0000000000..fb0ab4f696 --- /dev/null +++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/util/urlstreamhandler/data/Handler.java @@ -0,0 +1,148 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.server.util.urlstreamhandler.data; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.net.URLStreamHandler; +import java.nio.charset.StandardCharsets; + +import javax.xml.bind.DatatypeConverter; + +public class Handler extends URLStreamHandler +{ + public static final String PROTOCOL_HANDLER_PROPERTY = "java.protocol.handler.pkgs"; + private static boolean _registered; + + @Override + protected URLConnection openConnection(final URL u) throws IOException + { + return new DataUrlConnection(u); + } + + public synchronized static void register() + { + if(!_registered) + { + String registeredPackages = System.getProperty(PROTOCOL_HANDLER_PROPERTY); + String thisPackage = Handler.class.getPackage().getName(); + String packageToRegister = thisPackage.substring(0, thisPackage.lastIndexOf('.') ); + System.setProperty(PROTOCOL_HANDLER_PROPERTY, + registeredPackages == null + ? packageToRegister + : packageToRegister + "|" + registeredPackages); + + _registered = true; + } + + + + } + + private static class DataUrlConnection extends URLConnection + { + private final byte[] _content; + private final String _contentType; + private final boolean _base64; + + public DataUrlConnection(final URL u) throws IOException + { + super(u); + String externalForm = u.toExternalForm(); + if(externalForm.startsWith("data:")) + { + String[] parts = externalForm.substring(5).split(",",2); + _base64 = parts[0].endsWith(";base64"); + if(_base64) + { + _content = DatatypeConverter.parseBase64Binary(parts[1]); + } + else + { + try + { + _content = URLDecoder.decode(parts[1], StandardCharsets.US_ASCII.name()).getBytes(StandardCharsets.US_ASCII); + } + catch (UnsupportedEncodingException e) + { + throw new IOException(e); + } + } + String mediaType = (_base64 + ? parts[0].substring(0,parts[0].length()-";base64".length()) + : parts[0]).split(";")[0]; + + _contentType = "".equals(mediaType) ? "text/plain" : mediaType; + } + else + { + throw new MalformedURLException("'"+externalForm+"' does not start with 'data:'"); + } + } + + + + @Override + public void connect() throws IOException + { + + } + + @Override + public int getContentLength() + { + return _content.length; + } + + @Override + public String getContentType() + { + return _contentType; + } + + @Override + public String getContentEncoding() + { + return _base64 ? "base64" : null; + } + + @Override + public InputStream getInputStream() throws IOException + { + return new ByteArrayInputStream(_content); + } + } + + public static void main(String[] args) throws IOException + { + register(); + URL url = new URL("data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7"); + InputStream is = url.openStream(); + url = new URL("data:,A%20brief%20note"); + is = url.openStream(); + } +} |
