/*- * -\-\- * docker-client * -- * Copyright (C) 2016 Spotify AB * -- * Licensed 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 com.spotify.docker.client; import com.google.common.base.Optional; import com.spotify.docker.client.exceptions.DockerCertificateException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyFactory; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.List; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.ssl.SSLContexts; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * DockerCertificates holds certificates for connecting to an HTTPS-secured Docker instance with * client/server authentication. */ public class DockerCertificates implements DockerCertificatesStore { public static final String DEFAULT_CA_CERT_NAME = "ca.pem"; public static final String DEFAULT_CLIENT_CERT_NAME = "cert.pem"; public static final String DEFAULT_CLIENT_KEY_NAME = "key.pem"; private static final char[] KEY_STORE_PASSWORD = "docker!!11!!one!".toCharArray(); private static final Logger log = LoggerFactory.getLogger(DockerCertificates.class); private final SSLContext sslContext; public DockerCertificates(final Path dockerCertPath) throws DockerCertificateException { this(new Builder().dockerCertPath(dockerCertPath)); } private DockerCertificates(final Builder builder) throws DockerCertificateException { if ((builder.caCertPath == null) || (builder.clientCertPath == null) || (builder.clientKeyPath == null)) { throw new DockerCertificateException( "caCertPath, clientCertPath, and clientKeyPath must all be specified"); } try { final PrivateKey clientKey = readPrivateKey(builder.clientKeyPath); final List<Certificate> clientCerts = readCertificates(builder.clientCertPath); final KeyStore keyStore = newKeyStore(); keyStore.setKeyEntry("key", clientKey, KEY_STORE_PASSWORD, clientCerts.toArray(new Certificate[clientCerts.size()])); final List<Certificate> caCerts = readCertificates(builder.caCertPath); final KeyStore trustStore = newKeyStore(); for (Certificate caCert : caCerts) { X509Certificate crt = (X509Certificate) caCert; String alias = crt.getSubjectX500Principal() .getName(); trustStore.setCertificateEntry(alias, caCert); } this.sslContext = builder.sslContextFactory .newSslContext(keyStore, KEY_STORE_PASSWORD, trustStore); } catch (DockerCertificateException e) { throw e; } catch (CertificateException | IOException | NoSuchAlgorithmException | InvalidKeySpecException | KeyStoreException | UnrecoverableKeyException | KeyManagementException e) { throw new DockerCertificateException(e); } } private KeyStore newKeyStore() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException { final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); return keyStore; } private PrivateKey readPrivateKey(Path file) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException, DockerCertificateException { try (BufferedReader reader = Files.newBufferedReader(file, Charset.defaultCharset()); PEMParser pemParser = new PEMParser(reader)) { final Object readObject = pemParser.readObject(); if (readObject instanceof PEMKeyPair) { PEMKeyPair clientKeyPair = (PEMKeyPair) readObject; return generatePrivateKey(clientKeyPair.getPrivateKeyInfo()); } else if (readObject instanceof PrivateKeyInfo) { return generatePrivateKey((PrivateKeyInfo) readObject); } throw new DockerCertificateException("Can not generate private key from file: " + file.toString()); } } private static PrivateKey generatePrivateKey(PrivateKeyInfo privateKeyInfo) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded()); final KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(spec); } private List<Certificate> readCertificates(Path file) throws CertificateException, IOException { try (InputStream inputStream = Files.newInputStream(file)) { final CertificateFactory cf = CertificateFactory.getInstance("X.509"); return new ArrayList<>(cf.generateCertificates(inputStream)); } } public SSLContext sslContext() { return this.sslContext; } public HostnameVerifier hostnameVerifier() { return NoopHostnameVerifier.INSTANCE; } public interface SslContextFactory { SSLContext newSslContext(KeyStore keyStore, char[] keyPassword, KeyStore trustStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException; } private static class DefaultSslContextFactory implements SslContextFactory { @Override public SSLContext newSslContext(KeyStore keyStore, char[] keyPassword, KeyStore trustStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { return SSLContexts.custom() .loadTrustMaterial(trustStore, null) .loadKeyMaterial(keyStore, keyPassword) .build(); } } public static Builder builder() { return new Builder(); } public static class Builder { private SslContextFactory sslContextFactory = new DefaultSslContextFactory(); private Path caCertPath; private Path clientKeyPath; private Path clientCertPath; public Builder dockerCertPath(final Path dockerCertPath) { this.caCertPath = dockerCertPath.resolve(DEFAULT_CA_CERT_NAME); this.clientKeyPath = dockerCertPath.resolve(DEFAULT_CLIENT_KEY_NAME); this.clientCertPath = dockerCertPath.resolve(DEFAULT_CLIENT_CERT_NAME); return this; } public Builder caCertPath(final Path caCertPath) { this.caCertPath = caCertPath; return this; } public Builder clientKeyPath(final Path clientKeyPath) { this.clientKeyPath = clientKeyPath; return this; } public Builder clientCertPath(final Path clientCertPath) { this.clientCertPath = clientCertPath; return this; } public Builder sslFactory(final SslContextFactory sslContextFactory) { this.sslContextFactory = sslContextFactory; return this; } public Optional<DockerCertificatesStore> build() throws DockerCertificateException { if (this.caCertPath == null || this.clientKeyPath == null || this.clientCertPath == null) { log.debug("caCertPath, clientKeyPath or clientCertPath not specified, not using SSL"); return Optional.absent(); } else if (Files.exists(this.caCertPath) && Files.exists(this.clientKeyPath) && Files.exists(this.clientCertPath)) { return Optional.of((DockerCertificatesStore) new DockerCertificates(this)); } else { log.debug("{}, {} or {} does not exist, not using SSL", this.caCertPath, this.clientKeyPath, this.clientCertPath); return Optional.absent(); } } } }