diff options
Diffstat (limited to 'security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java')
-rw-r--r-- | security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java new file mode 100644 index 00000000000..c3dcdf1dc9e --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java @@ -0,0 +1,164 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import com.yahoo.security.TrustManagerUtils; +import com.yahoo.security.X509CertificateUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link X509ExtendedTrustManager} that performs additional certificate verification through {@link PeerAuthorizer}. + * + * Implementation assumes that provided {@link X509ExtendedTrustManager} will throw {@link IllegalArgumentException} + * when chain is empty or null. + * + * @author bjorncs + */ +class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { + + static final String AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; + + private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName()); + + private final PeerAuthorizer authorizer; + private final X509ExtendedTrustManager defaultTrustManager; + private final AuthorizationMode mode; + private final HostnameVerification hostnameVerification; + + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, X509ExtendedTrustManager defaultTrustManager) { + this.authorizer = new PeerAuthorizer(authorizedPeers); + this.mode = mode; + this.hostnameVerification = hostnameVerification; + this.defaultTrustManager = defaultTrustManager; + } + + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, KeyStore truststore) { + this(authorizedPeers, mode, hostnameVerification, TrustManagerUtils.createDefaultX509TrustManager(truststore)); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType); + authorizePeer(chain, authType, true, null); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkServerTrusted(chain, authType); + authorizePeer(chain, authType, false, null); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType, socket); + authorizePeer(chain, authType, true, ((SSLSocket)socket).getHandshakeSession()); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + overrideHostnameVerificationForClient(socket); + defaultTrustManager.checkServerTrusted(chain, authType, socket); + authorizePeer(chain, authType, false, ((SSLSocket)socket).getHandshakeSession()); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType, sslEngine); + authorizePeer(chain, authType, true, sslEngine.getHandshakeSession()); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + overrideHostnameVerificationForClient(sslEngine); + defaultTrustManager.checkServerTrusted(chain, authType, sslEngine); + authorizePeer(chain, authType, false, sslEngine.getHandshakeSession()); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return defaultTrustManager.getAcceptedIssuers(); + } + + private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient, + SSLSession handshakeSessionOrNull) throws PeerAuthorizationFailedException { + log.fine(() -> "Verifying certificate: " + createInfoString(certChain[0], authType, isVerifyingClient)); + ConnectionAuthContext result = mode != AuthorizationMode.DISABLE + ? authorizer.authorizePeer(List.of(certChain)) + : ConnectionAuthContext.defaultAllCapabilities(List.of(certChain)); + if (handshakeSessionOrNull != null) { + handshakeSessionOrNull.putValue(AUTH_CONTEXT_PROPERTY, result); + } else { + log.log(Level.FINE, + () -> "Warning: unable to provide ConnectionAuthContext as no SSLSession is available"); + } + if (result.authorized()) { + log.fine(() -> String.format("Verification result: %s", result)); + } else { + String errorMessage = "Authorization failed: " + createInfoString(certChain[0], authType, isVerifyingClient); + log.warning(errorMessage); + if (mode == AuthorizationMode.ENFORCE) { + throw new PeerAuthorizationFailedException(errorMessage, List.of(certChain)); + } + } + } + + private String createInfoString(X509Certificate certificate, String authType, boolean isVerifyingClient) { + return String.format("DN='%s', SANs=%s, authType='%s', isVerifyingClient='%b', mode=%s", + certificate.getSubjectX500Principal(), X509CertificateUtils.getSubjectAlternativeNames(certificate), + authType, isVerifyingClient, mode); + } + + private void overrideHostnameVerificationForClient(SSLEngine engine) { + SSLParameters params = engine.getSSLParameters(); + if (overrideHostnameVerificationForClient(params)) { + engine.setSSLParameters(params); + } + } + + private void overrideHostnameVerificationForClient(Socket socket) { + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + SSLParameters params = sslSocket.getSSLParameters(); + if (overrideHostnameVerificationForClient(params)) { + sslSocket.setSSLParameters(params); + } + } + } + + // Overrides the endpoint identification algorithm specified in the ssl parameters of the ssl engine/socket. + // The underlying trust manager will perform hostname verification if endpoint identification algorithm is set to 'HTTPS'. + // Returns true if the parameter instance was modified + private boolean overrideHostnameVerificationForClient(SSLParameters params) { + String configuredAlgorithm = params.getEndpointIdentificationAlgorithm(); + switch (hostnameVerification) { + case ENABLED: + if (!"HTTPS".equals(configuredAlgorithm)) { + params.setEndpointIdentificationAlgorithm("HTTPS"); + return true; + } + return false; + case DISABLED: + if (configuredAlgorithm != null && !configuredAlgorithm.isEmpty()) { + params.setEndpointIdentificationAlgorithm(""); // disable any configured endpoint identification algorithm + return true; + } + return false; + default: + throw new IllegalStateException("Unknown host verification type: " + hostnameVerification); + } + } + +} |