From 8a9b9e4107e69d818dd3544879db404245cc0867 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Sun, 19 Oct 2014 08:19:25 -0400 Subject: [PATCH 1/3] Added SSL support --- .../dockerjava/core/DockerClientConfig.java | 48 ++++++++++++++++-- .../jaxrs/DockerCmdExecFactoryImpl.java | 49 ++++++++++++++----- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java index b2adea7fc..9c2b58673 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java @@ -10,7 +10,7 @@ public class DockerClientConfig { private final URI uri; - private final String version, username, password, email; + private final String version, username, password, email, keystore, keystorePassword, truststore, truststorePassword; private final Integer readTimeout; private final boolean loggingFilterEnabled; @@ -22,6 +22,10 @@ private DockerClientConfig(DockerClientConfigBuilder builder) { this.email = builder.email; this.readTimeout = builder.readTimeout; this.loggingFilterEnabled = builder.loggingFilterEnabled; + this.keystore = builder.keystore; + this.keystorePassword = builder.keystorePassword; + this.truststore = builder.truststore; + this.truststorePassword = builder.truststorePassword; } public URI getUri() { @@ -51,6 +55,22 @@ public Integer getReadTimeout() { public boolean isLoggingFilterEnabled() { return loggingFilterEnabled; } + + public String getKeystore() { + return keystore; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public String getTruststore() { + return truststore; + } + + public String getTruststorePassword() { + return truststorePassword; + } public static Properties loadIncludedDockerProperties() { try { @@ -97,7 +117,7 @@ public static Properties overrideDockerPropertiesWithSystemProperties(Properties overriddenProperties.putAll(p); // TODO Add all values from system properties that begin with docker.io.* - for (String s : new String[]{ "url", "version", "username", "password", "email", "readTimeout", "enableLoggingFilter"}) { + for (String s : new String[]{ "url", "version", "username", "password", "email", "readTimeout", "enableLoggingFilter", "keystore", "keystorePassword", "truststore", "truststorePassword"}) { final String key = "docker.io." + s; if (System.getProperties().containsKey(key)) { overriddenProperties.setProperty(key, System.getProperty(key)); @@ -115,7 +135,7 @@ public static DockerClientConfigBuilder createDefaultConfigBuilder() { public static class DockerClientConfigBuilder { private URI uri; - private String version, username, password, email; + private String version, username, password, email, keystore, keystorePassword, truststore, truststorePassword; private Integer readTimeout; private boolean loggingFilterEnabled; @@ -138,7 +158,11 @@ public DockerClientConfigBuilder withProperties(Properties p) { .withPassword(p.getProperty("docker.io.password")) .withEmail(p.getProperty("docker.io.email")) .withReadTimeout(Integer.valueOf(p.getProperty("docker.io.readTimeout", "0"))) - .withLoggingFilter(Boolean.valueOf(p.getProperty("docker.io.enableLoggingFilter", "true"))); + .withLoggingFilter(Boolean.valueOf(p.getProperty("docker.io.enableLoggingFilter", "true"))) + .withKeystore(p.getProperty("docker.io.keystore")) + .withKeystorePassword(p.getProperty("docker.io.keystorePassword")) + .withTruststore(p.getProperty("docker.io.truststore")) + .withTruststorePassword(p.getProperty("docker.io.truststorePassword")); } public final DockerClientConfigBuilder withUri(String uri) { @@ -170,6 +194,22 @@ public final DockerClientConfigBuilder withLoggingFilter(boolean loggingFilterEn this.loggingFilterEnabled = loggingFilterEnabled; return this; } + public final DockerClientConfigBuilder withKeystore(String keystore) { + this.keystore = keystore; + return this; + } + public final DockerClientConfigBuilder withKeystorePassword(String keystorePassword) { + this.keystorePassword = keystorePassword; + return this; + } + public final DockerClientConfigBuilder withTruststore(String truststore) { + this.truststore = truststore; + return this; + } + public final DockerClientConfigBuilder withTruststorePassword(String truststorePassword) { + this.truststorePassword = truststorePassword; + return this; + } public DockerClientConfig build() { return new DockerClientConfig(this); } diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java index 74d3073f0..1a70ccbe4 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java +++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java @@ -1,17 +1,5 @@ package com.github.dockerjava.jaxrs; -import java.io.IOException; -import java.util.logging.Logger; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.WebTarget; - -import com.github.dockerjava.api.command.EventsCmd; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.CommonProperties; - import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import com.github.dockerjava.api.command.AttachContainerCmd; import com.github.dockerjava.api.command.AuthCmd; @@ -22,6 +10,7 @@ import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateImageCmd; import com.github.dockerjava.api.command.DockerCmdExecFactory; +import com.github.dockerjava.api.command.EventsCmd; import com.github.dockerjava.api.command.InfoCmd; import com.github.dockerjava.api.command.InspectContainerCmd; import com.github.dockerjava.api.command.InspectImageCmd; @@ -50,6 +39,19 @@ import com.github.dockerjava.jaxrs.util.SelectiveLoggingFilter; import com.google.common.base.Preconditions; +import java.io.IOException; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; + public class DockerCmdExecFactoryImpl implements DockerCmdExecFactory { private Client client; @@ -78,7 +80,28 @@ public void init(DockerClientConfig dockerClientConfig) { int readTimeout = dockerClientConfig.getReadTimeout(); clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout); } - client = ClientBuilder.newClient(clientConfig); + + ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig); + + + if((dockerClientConfig.getKeystore() != null && dockerClientConfig.getKeystorePassword() != null) || (dockerClientConfig.getTruststore() != null && dockerClientConfig.getTruststorePassword() != null)) { + SslConfigurator sslConfig = SslConfigurator.newInstance(); + + if(dockerClientConfig.getKeystore() != null && dockerClientConfig.getKeystorePassword() != null) { + sslConfig.keyStoreFile(dockerClientConfig.getKeystore()); + sslConfig.keyStorePassword(dockerClientConfig.getKeystorePassword()); + } + + if(dockerClientConfig.getTruststore() != null && dockerClientConfig.getTruststorePassword() != null) { + sslConfig.trustStoreFile(dockerClientConfig.getTruststore()); + sslConfig.trustStorePassword(dockerClientConfig.getTruststorePassword()); + } + + SSLContext sslContext = sslConfig.createSSLContext(); + clientBuilder.sslContext(sslContext); + } + + client = clientBuilder.build(); WebTarget webResource = client.target(dockerClientConfig.getUri()); From 2ce62f4ab93de79e74b5db8dbba70552bc46f005 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Tue, 21 Oct 2014 21:04:23 -0500 Subject: [PATCH 2/3] Removed options to specify keystore and truststore files. Added ability to leverage environment variable or explicitly specify location of default certificates --- pom.xml | 7 + .../dockerjava/core/CertificateUtils.java | 142 ++++++++++++++++++ .../dockerjava/core/DockerClientConfig.java | 48 ++---- .../jaxrs/DockerCmdExecFactoryImpl.java | 59 ++++++-- 4 files changed, 204 insertions(+), 52 deletions(-) create mode 100644 src/main/java/com/github/dockerjava/core/CertificateUtils.java diff --git a/pom.xml b/pom.xml index 13c71b63f..4b3d7cc0d 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ 1.3.9 0.3 18.0 + 1.51 1.0.1 @@ -142,6 +143,12 @@ guava ${guava.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + diff --git a/src/main/java/com/github/dockerjava/core/CertificateUtils.java b/src/main/java/com/github/dockerjava/core/CertificateUtils.java new file mode 100644 index 000000000..89722a94d --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/CertificateUtils.java @@ -0,0 +1,142 @@ +package com.github.dockerjava.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.apache.commons.io.IOUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; + +public class CertificateUtils { + + public static boolean verifyCertificatesExist(String dockerCertPath) { + String[] files = {"ca.pem", "cert.pem", "key.pem"}; + for (String file : files) { + Path path = Paths.get(dockerCertPath, file); + boolean exists = Files.exists(path); + if(!exists) { + return false; + } + } + + return true; + } + + public static KeyStore createKeyStore(final String dockerCertPath) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, CertificateException, KeyStoreException { + KeyPair keyPair = loadPrivateKey(dockerCertPath); + Certificate privateCertificate = loadCertificate(dockerCertPath); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + + keyStore.setKeyEntry("docker", keyPair.getPrivate(), "docker".toCharArray(), new Certificate[]{privateCertificate}); + return keyStore; + } + + public static KeyStore createTrustStore(final String dockerCertPath) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + Path caPath = Paths.get(dockerCertPath, "ca.pem"); + BufferedReader reader = Files.newBufferedReader(caPath, Charset.defaultCharset()); + PEMParser pemParser = null; + + try { + pemParser = new PEMParser(reader); + X509CertificateHolder certificateHolder = (X509CertificateHolder) pemParser.readObject(); + Certificate caCertificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder); + + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null); + trustStore.setCertificateEntry("ca", caCertificate); + return trustStore; + + } + finally { + if(pemParser != null) { + IOUtils.closeQuietly(pemParser); + } + + if(reader != null) { + IOUtils.closeQuietly(reader); + } + } + + } + + private static Certificate loadCertificate(final String dockerCertPath) throws IOException, CertificateException { + Path certificate = Paths.get(dockerCertPath, "cert.pem"); + BufferedReader reader = Files.newBufferedReader(certificate, Charset.defaultCharset()); + PEMParser pemParser = null; + + try { + pemParser = new PEMParser(reader); + X509CertificateHolder certificateHolder = (X509CertificateHolder) pemParser.readObject(); + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder); + } + finally { + if(pemParser != null) { + IOUtils.closeQuietly(pemParser); + } + + if(reader != null) { + IOUtils.closeQuietly(reader); + } + } + + } + + private static KeyPair loadPrivateKey(final String dockerCertPath) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + Path certificate = Paths.get(dockerCertPath, "key.pem"); + BufferedReader reader = Files.newBufferedReader(certificate, Charset.defaultCharset()); + + PEMParser pemParser = null; + + try { + pemParser = new PEMParser(reader); + + PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject(); + + byte[] pemPrivateKeyEncoded = pemKeyPair.getPrivateKeyInfo().getEncoded(); + byte[] pemPublicKeyEncoded = pemKeyPair.getPublicKeyInfo().getEncoded(); + + KeyFactory factory = KeyFactory.getInstance("RSA"); + + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pemPublicKeyEncoded); + PublicKey publicKey = factory.generatePublic(publicKeySpec); + + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pemPrivateKeyEncoded); + PrivateKey privateKey = factory.generatePrivate(privateKeySpec); + + return new KeyPair(publicKey, privateKey); + + } + finally { + if(pemParser != null) { + IOUtils.closeQuietly(pemParser); + } + + if(reader != null) { + IOUtils.closeQuietly(reader); + } + } + + + } + +} diff --git a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java index 9c2b58673..4308f4be5 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java @@ -10,7 +10,7 @@ public class DockerClientConfig { private final URI uri; - private final String version, username, password, email, keystore, keystorePassword, truststore, truststorePassword; + private final String version, username, password, email, dockerCertPath; private final Integer readTimeout; private final boolean loggingFilterEnabled; @@ -22,10 +22,7 @@ private DockerClientConfig(DockerClientConfigBuilder builder) { this.email = builder.email; this.readTimeout = builder.readTimeout; this.loggingFilterEnabled = builder.loggingFilterEnabled; - this.keystore = builder.keystore; - this.keystorePassword = builder.keystorePassword; - this.truststore = builder.truststore; - this.truststorePassword = builder.truststorePassword; + this.dockerCertPath = builder.dockerCertPath; } public URI getUri() { @@ -56,20 +53,8 @@ public boolean isLoggingFilterEnabled() { return loggingFilterEnabled; } - public String getKeystore() { - return keystore; - } - - public String getKeystorePassword() { - return keystorePassword; - } - - public String getTruststore() { - return truststore; - } - - public String getTruststorePassword() { - return truststorePassword; + public String getDockerCertPath() { + return dockerCertPath; } public static Properties loadIncludedDockerProperties() { @@ -117,7 +102,7 @@ public static Properties overrideDockerPropertiesWithSystemProperties(Properties overriddenProperties.putAll(p); // TODO Add all values from system properties that begin with docker.io.* - for (String s : new String[]{ "url", "version", "username", "password", "email", "readTimeout", "enableLoggingFilter", "keystore", "keystorePassword", "truststore", "truststorePassword"}) { + for (String s : new String[]{ "url", "version", "username", "password", "email", "readTimeout", "enableLoggingFilter", "dockerCertPath"}) { final String key = "docker.io." + s; if (System.getProperties().containsKey(key)) { overriddenProperties.setProperty(key, System.getProperty(key)); @@ -135,7 +120,7 @@ public static DockerClientConfigBuilder createDefaultConfigBuilder() { public static class DockerClientConfigBuilder { private URI uri; - private String version, username, password, email, keystore, keystorePassword, truststore, truststorePassword; + private String version, username, password, email, dockerCertPath; private Integer readTimeout; private boolean loggingFilterEnabled; @@ -159,10 +144,7 @@ public DockerClientConfigBuilder withProperties(Properties p) { .withEmail(p.getProperty("docker.io.email")) .withReadTimeout(Integer.valueOf(p.getProperty("docker.io.readTimeout", "0"))) .withLoggingFilter(Boolean.valueOf(p.getProperty("docker.io.enableLoggingFilter", "true"))) - .withKeystore(p.getProperty("docker.io.keystore")) - .withKeystorePassword(p.getProperty("docker.io.keystorePassword")) - .withTruststore(p.getProperty("docker.io.truststore")) - .withTruststorePassword(p.getProperty("docker.io.truststorePassword")); + .withDockerCertPath(p.getProperty("docker.io.dockerCertPath")); } public final DockerClientConfigBuilder withUri(String uri) { @@ -194,20 +176,8 @@ public final DockerClientConfigBuilder withLoggingFilter(boolean loggingFilterEn this.loggingFilterEnabled = loggingFilterEnabled; return this; } - public final DockerClientConfigBuilder withKeystore(String keystore) { - this.keystore = keystore; - return this; - } - public final DockerClientConfigBuilder withKeystorePassword(String keystorePassword) { - this.keystorePassword = keystorePassword; - return this; - } - public final DockerClientConfigBuilder withTruststore(String truststore) { - this.truststore = truststore; - return this; - } - public final DockerClientConfigBuilder withTruststorePassword(String truststorePassword) { - this.truststorePassword = truststorePassword; + public final DockerClientConfigBuilder withDockerCertPath(String dockerCertPath) { + this.dockerCertPath = dockerCertPath; return this; } public DockerClientConfig build() { diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java index 1a70ccbe4..58450c8a6 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java +++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java @@ -1,6 +1,7 @@ package com.github.dockerjava.jaxrs; import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; +import com.github.dockerjava.api.DockerClientException; import com.github.dockerjava.api.command.AttachContainerCmd; import com.github.dockerjava.api.command.AuthCmd; import com.github.dockerjava.api.command.BuildImageCmd; @@ -33,13 +34,17 @@ import com.github.dockerjava.api.command.UnpauseContainerCmd; import com.github.dockerjava.api.command.VersionCmd; import com.github.dockerjava.api.command.WaitContainerCmd; +import com.github.dockerjava.core.CertificateUtils; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.jaxrs.util.JsonClientFilter; import com.github.dockerjava.jaxrs.util.ResponseStatusExceptionFilter; import com.github.dockerjava.jaxrs.util.SelectiveLoggingFilter; import com.google.common.base.Preconditions; +import java.io.File; import java.io.IOException; +import java.security.KeyStore; +import java.security.Security; import java.util.logging.Logger; import javax.net.ssl.SSLContext; @@ -47,6 +52,7 @@ import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.glassfish.jersey.CommonProperties; import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.ClientConfig; @@ -83,22 +89,49 @@ public void init(DockerClientConfig dockerClientConfig) { ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig); - - if((dockerClientConfig.getKeystore() != null && dockerClientConfig.getKeystorePassword() != null) || (dockerClientConfig.getTruststore() != null && dockerClientConfig.getTruststorePassword() != null)) { - SslConfigurator sslConfig = SslConfigurator.newInstance(); + // Attempt to load Docker SSL certificates from location in following order: + // 1. User Defined + // 2. Environment Variable + // 3. User Home Directory + String dockerCertPath = dockerClientConfig.getDockerCertPath(); - if(dockerClientConfig.getKeystore() != null && dockerClientConfig.getKeystorePassword() != null) { - sslConfig.keyStoreFile(dockerClientConfig.getKeystore()); - sslConfig.keyStorePassword(dockerClientConfig.getKeystorePassword()); - } + if(dockerCertPath == null) { + dockerCertPath = System.getenv("DOCKER_CERT_PATH"); + } - if(dockerClientConfig.getTruststore() != null && dockerClientConfig.getTruststorePassword() != null) { - sslConfig.trustStoreFile(dockerClientConfig.getTruststore()); - sslConfig.trustStorePassword(dockerClientConfig.getTruststorePassword()); - } + if(dockerCertPath == null) { + dockerCertPath = System.getProperty("USER_HOME") + File.separator + ".docker"; + } + + if(dockerCertPath != null) { + boolean certificatesExist = CertificateUtils.verifyCertificatesExist(dockerCertPath); - SSLContext sslContext = sslConfig.createSSLContext(); - clientBuilder.sslContext(sslContext); + if(certificatesExist) { + + try { + + Security.addProvider(new BouncyCastleProvider()); + + SslConfigurator sslConfig = SslConfigurator.newInstance(); + + KeyStore keyStore = CertificateUtils.createKeyStore(dockerCertPath); + KeyStore trustStore = CertificateUtils.createTrustStore(dockerCertPath); + + sslConfig.keyStore(keyStore); + sslConfig.keyStorePassword("docker"); + + sslConfig.trustStore(trustStore); + + SSLContext sslContext = sslConfig.createSSLContext(); + clientBuilder.sslContext(sslContext); + + } + catch(Exception e) { + throw new DockerClientException(e.getMessage(), e); + } + + } + } client = clientBuilder.build(); From 077df9fad765e8b14c0decca302a1952e79a3e40 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Tue, 21 Oct 2014 21:38:28 -0500 Subject: [PATCH 3/3] Minor javadoc addition --- .../java/com/github/dockerjava/core/DockerClientConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java index 4308f4be5..6e3795e97 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java @@ -129,7 +129,7 @@ public DockerClientConfigBuilder() { /** * This will set all fields in the builder to those contained in the Properties object. The Properties object - * should contain the following docker.io.* keys: url, version, username, password, and email. If + * should contain the following docker.io.* keys: url, version, username, password, email, and dockerCertPath. If * docker.io.readTimeout or docker.io.enableLoggingFilter are not contained, they will be set to 1000 and true, * respectively. *