diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2018-11-28 13:40:34 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-28 13:40:34 +0100 |
commit | f78715cad1b7a8124ad95720b1a74403f68443ab (patch) | |
tree | b7b0bba7f75a55e71b67914fd8a3c4691c2227cb | |
parent | 125dc30240e2ac6a00d2a4d3f503d2511ea592e2 (diff) | |
parent | 20317de277a40a6c7711ada685f7564707dc0573 (diff) |
Merge pull request #7783 from vespa-engine/bjorncs/tls-certificate-validation
Bjorncs/tls certificate validation
7 files changed, 216 insertions, 4 deletions
diff --git a/jrt/pom.xml b/jrt/pom.xml index 6852c6deee6..641ed7e69ac 100644 --- a/jrt/pom.xml +++ b/jrt/pom.xml @@ -35,6 +35,11 @@ <version>${project.version}</version> <scope>compile</scope> </dependency> + <dependency> <!-- required due to bug in maven dependency resolving - bouncycastle is compile scope in security-utils, yet it is not part of test scope here --> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> <sourceDirectory>src</sourceDirectory> diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java b/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java index 4c483072f5f..db18ddf8c9d 100644 --- a/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java +++ b/jrt/src/com/yahoo/jrt/TlsCryptoEngine.java @@ -3,6 +3,8 @@ package com.yahoo.jrt; import com.yahoo.security.SslContextBuilder; import com.yahoo.security.tls.TransportSecurityOptions; +import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager.Mode; +import com.yahoo.security.tls.authz.PeerAuthorizerTrustManagersFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -33,11 +35,14 @@ public class TlsCryptoEngine implements CryptoEngine { return new TlsCryptoSocket(channel, sslEngine); } + // TODO Move to dedicated factory type controlling certificate hot-reloading in security-utils private static SSLContext createSslContext(TransportSecurityOptions options) { SslContextBuilder builder = new SslContextBuilder(); options.getCertificatesFile() .ifPresent(certificates -> builder.withKeyStore(options.getPrivateKeyFile().get(), certificates)); options.getCaCertificatesFile().ifPresent(builder::withTrustStore); + options.getAuthorizedPeers().ifPresent( + authorizedPeers -> builder.withTrustManagerFactory(new PeerAuthorizerTrustManagersFactory(authorizedPeers, Mode.DRY_RUN))); return builder.build(); } } diff --git a/jrt/tests/com/yahoo/jrt/CryptoUtils.java b/jrt/tests/com/yahoo/jrt/CryptoUtils.java index c3128e09bd3..6c843000779 100644 --- a/jrt/tests/com/yahoo/jrt/CryptoUtils.java +++ b/jrt/tests/com/yahoo/jrt/CryptoUtils.java @@ -5,6 +5,14 @@ import com.yahoo.security.KeyStoreBuilder; import com.yahoo.security.KeyUtils; import com.yahoo.security.SslContextBuilder; import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager.Mode; +import com.yahoo.security.tls.authz.PeerAuthorizerTrustManagersFactory; +import com.yahoo.security.tls.policy.AuthorizedPeers; +import com.yahoo.security.tls.policy.HostGlobPattern; +import com.yahoo.security.tls.policy.PeerPolicy; +import com.yahoo.security.tls.policy.RequiredPeerCredential; +import com.yahoo.security.tls.policy.RequiredPeerCredential.Field; +import com.yahoo.security.tls.policy.Role; import javax.net.ssl.SSLContext; import javax.security.auth.x500.X500Principal; @@ -19,6 +27,8 @@ import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_RSA; import static com.yahoo.security.X509CertificateBuilder.generateRandomSerialNumber; import static java.time.Instant.EPOCH; import static java.time.temporal.ChronoUnit.DAYS; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; /** * @author bjorncs @@ -35,9 +45,23 @@ class CryptoUtils { .withCertificateEntry("self-signed", certificate) .build(); + return new SslContextBuilder() .withTrustStore(trustStore) .withKeyStore(keyPair.getPrivate(), certificate) + .withTrustManagerFactory(new PeerAuthorizerTrustManagersFactory(createAuthorizedPeers(), Mode.ENFORCE)) .build(); } + + private static AuthorizedPeers createAuthorizedPeers() { + return new AuthorizedPeers( + singleton( + new PeerPolicy( + "dummy-policy", + singleton( + new Role("dummy-role")), + singletonList( + new RequiredPeerCredential( + Field.CN, new HostGlobPattern("dummy")))))); + } } diff --git a/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java index 75ab2417edf..17d425578ee 100644 --- a/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java +++ b/security-utils/src/main/java/com/yahoo/security/SslContextBuilder.java @@ -14,7 +14,6 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; -import java.util.Collections; import java.util.List; import static java.util.Collections.singletonList; @@ -27,6 +26,7 @@ public class SslContextBuilder { private KeyStoreSupplier trustStoreSupplier; private KeyStoreSupplier keyStoreSupplier; private char[] keyStorePassword; + private TrustManagersFactory trustManagersFactory = SslContextBuilder::createDefaultTrustManagers; public SslContextBuilder() {} @@ -90,11 +90,16 @@ public class SslContextBuilder { return this; } + public SslContextBuilder withTrustManagerFactory(TrustManagersFactory trustManagersFactory) { + this.trustManagersFactory = trustManagersFactory; + return this; + } + public SSLContext build() { try { SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); TrustManager[] trustManagers = - trustStoreSupplier != null ? createTrustManagers(trustStoreSupplier) : null; + trustStoreSupplier != null ? createTrustManagers(trustManagersFactory, trustStoreSupplier) : null; KeyManager[] keyManagers = keyStoreSupplier != null ? createKeyManagers(keyStoreSupplier, keyStorePassword) : null; sslContext.init(keyManagers, trustManagers, null); @@ -106,11 +111,16 @@ public class SslContextBuilder { } } - private static TrustManager[] createTrustManagers(KeyStoreSupplier trustStoreSupplier) + private static TrustManager[] createTrustManagers(TrustManagersFactory trustManagersFactory, KeyStoreSupplier trustStoreSupplier) throws GeneralSecurityException, IOException { + KeyStore truststore = trustStoreSupplier.get(); + return trustManagersFactory.createTrustManagers(truststore); + } + + private static TrustManager[] createDefaultTrustManagers(KeyStore truststore) throws GeneralSecurityException { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStoreSupplier.get()); + trustManagerFactory.init(truststore); return trustManagerFactory.getTrustManagers(); } @@ -134,4 +144,12 @@ public class SslContextBuilder { KeyStore get() throws IOException, GeneralSecurityException; } + /** + * A factory interface that is similar to {@link TrustManagerFactory}, but is an interface instead of a class. + */ + @FunctionalInterface + public interface TrustManagersFactory { + TrustManager[] createTrustManagers(KeyStore truststore) throws GeneralSecurityException; + } + } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java index bead32fe309..a40813be96f 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.logging.Logger; import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; import static com.yahoo.security.SubjectAlternativeName.Type.IP_ADDRESS; @@ -24,6 +25,9 @@ import static java.util.stream.Collectors.toList; * @author bjorncs */ public class PeerAuthorizer { + + private static final Logger log = Logger.getLogger(PeerAuthorizer.class.getName()); + private final AuthorizedPeers authorizedPeers; public PeerAuthorizer(AuthorizedPeers authorizedPeers) { @@ -35,6 +39,7 @@ public class PeerAuthorizer { Set<String> matchedPolicies = new HashSet<>(); String cn = getCommonName(peerCertificate).orElse(null); List<String> sans = getSubjectAlternativeNames(peerCertificate); + log.fine(() -> String.format("Subject info from x509 certificate: CN=[%s], 'SAN=%s", cn, sans)); for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) { if (matchesPolicy(peerPolicy, cn, sans)) { assumedRoles.addAll(peerPolicy.assumedRoles()); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java new file mode 100644 index 00000000000..aca4f86b639 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java @@ -0,0 +1,128 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls.authz; + +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.tls.policy.AuthorizedPeers; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.logging.Logger; + +/** + * A {@link X509ExtendedTrustManager} that performs additional certificate verification through {@link PeerAuthorizer}. + * + * @author bjorncs + */ +// Note: Implementation assumes that provided X509ExtendedTrustManager will throw IllegalArgumentException when chain is empty or null +public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { + + public static final String HANDSHAKE_SESSION_AUTHZ_RESULT_PROPERTY = "vespa.tls.authorization.result"; + + private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName()); + + public enum Mode { DRY_RUN, ENFORCE } + + private final PeerAuthorizer authorizer; + private final X509ExtendedTrustManager defaultTrustManager; + private final Mode mode; + + public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, Mode mode, X509ExtendedTrustManager defaultTrustManager) { + this.authorizer = new PeerAuthorizer(authorizedPeers); + this.mode = mode; + this.defaultTrustManager = defaultTrustManager; + } + + public static TrustManager[] wrapTrustManagersFromKeystore(AuthorizedPeers authorizedPeers, Mode mode, KeyStore keystore) throws GeneralSecurityException { + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(keystore); + return wrapTrustManagers(authorizedPeers, mode, factory.getTrustManagers()); + } + + public static TrustManager[] wrapTrustManagers(AuthorizedPeers authorizedPeers, Mode mode, TrustManager[] managers) { + TrustManager[] wrappedManagers = new TrustManager[managers.length]; + for (int i = 0; i < managers.length; i++) { + if (managers[i] instanceof X509ExtendedTrustManager) { + wrappedManagers[i] = new PeerAuthorizerTrustManager(authorizedPeers, mode, (X509ExtendedTrustManager) managers[i]); + } else { + wrappedManagers[i] = managers[i]; + } + } + return wrappedManagers; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType); + authorizePeer(chain[0], authType, true, null); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkServerTrusted(chain, authType); + authorizePeer(chain[0], authType, false, null); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType, socket); + authorizePeer(chain[0], authType, true, null); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + defaultTrustManager.checkServerTrusted(chain, authType, socket); + authorizePeer(chain[0], authType, false, null); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType, sslEngine); + authorizePeer(chain[0], authType, true, sslEngine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + defaultTrustManager.checkServerTrusted(chain, authType, sslEngine); + authorizePeer(chain[0], authType, false, sslEngine); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return defaultTrustManager.getAcceptedIssuers(); + } + + private void authorizePeer(X509Certificate certificate, String authType, boolean isVerifyingClient, SSLEngine sslEngine) throws CertificateException { + log.fine(() -> "Verifying certificate: " + createInfoString(certificate, authType, isVerifyingClient)); + AuthorizationResult result = authorizer.authorizePeer(certificate); + if (sslEngine != null) { // getHandshakeSession() will never return null in this context + sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTHZ_RESULT_PROPERTY, result); + } + if (result.succeeded()) { + log.fine(() -> String.format("Verification result: %s", result)); + } else { + String errorMessage = "Authorization failed: " + createInfoString(certificate, authType, isVerifyingClient); + log.warning(errorMessage); + switch (mode) { + case ENFORCE: + throw new CertificateException(errorMessage); + case DRY_RUN: + break; + default: + throw new UnsupportedOperationException(); + } + } + } + + private static String createInfoString(X509Certificate certificate, String authType, boolean isVerifyingClient) { + return String.format("DN='%s', SANs=%s, authType='%s', isVerifyingClient='%b'", + certificate.getSubjectX500Principal(), X509CertificateUtils.getSubjectAlternativeNames(certificate), authType, isVerifyingClient); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java new file mode 100644 index 00000000000..0bb99aea886 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManagersFactory.java @@ -0,0 +1,27 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls.authz; + +import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.tls.policy.AuthorizedPeers; + +import javax.net.ssl.TrustManager; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +/** + * @author bjorncs + */ +public class PeerAuthorizerTrustManagersFactory implements SslContextBuilder.TrustManagersFactory { + private final AuthorizedPeers authorizedPeers; + private PeerAuthorizerTrustManager.Mode mode; + + public PeerAuthorizerTrustManagersFactory(AuthorizedPeers authorizedPeers, PeerAuthorizerTrustManager.Mode mode) { + this.authorizedPeers = authorizedPeers; + this.mode = mode; + } + + @Override + public TrustManager[] createTrustManagers(KeyStore truststore) throws GeneralSecurityException { + return PeerAuthorizerTrustManager.wrapTrustManagersFromKeystore(authorizedPeers, mode, truststore); + } +} |