diff options
Diffstat (limited to 'security-utils/src/main')
7 files changed, 145 insertions, 44 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java index ec402719efa..7e6c7f394cd 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java @@ -30,15 +30,17 @@ public class CapabilitySet { ; private final String name; - private final EnumSet<Capability> caps; + private final CapabilitySet set; Predefined(String name, Capability... caps) { this.name = name; - this.caps = caps.length == 0 ? EnumSet.noneOf(Capability.class) : EnumSet.copyOf(List.of(caps)); } + this.set = caps.length == 0 ? CapabilitySet.none() : CapabilitySet.from(caps); } public static Optional<Predefined> fromName(String name) { return Arrays.stream(values()).filter(p -> p.name.equals(name)).findAny(); } + + public CapabilitySet capabilities() { return set; } } private static final CapabilitySet ALL_CAPABILITIES = new CapabilitySet(EnumSet.allOf(Capability.class)); @@ -52,7 +54,7 @@ public class CapabilitySet { EnumSet<Capability> caps = EnumSet.noneOf(Capability.class); for (String name : names) { Predefined predefined = Predefined.fromName(name).orElse(null); - if (predefined != null) caps.addAll(predefined.caps); + if (predefined != null) caps.addAll(predefined.set.caps); else caps.add(Capability.fromName(name)); } return new CapabilitySet(caps); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java index b4e8878fb01..f231e8429ce 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java @@ -7,28 +7,78 @@ import java.security.cert.X509Certificate; 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; import static com.yahoo.security.SubjectAlternativeName.Type.URI; +import static com.yahoo.security.tls.CapabilityMode.DISABLE; +import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY; /** * @author bjorncs */ public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain, CapabilitySet capabilities, - Set<String> matchedPolicies) { + Set<String> matchedPolicies, + CapabilityMode capabilityMode) { - private static final ConnectionAuthContext DEFAULT_ALL_CAPABILITIES = new ConnectionAuthContext(List.of()); + private static final Logger log = Logger.getLogger(ConnectionAuthContext.class.getName()); public ConnectionAuthContext { peerCertificateChain = List.copyOf(peerCertificateChain); matchedPolicies = Set.copyOf(matchedPolicies); } - private ConnectionAuthContext(List<X509Certificate> certs) { this(certs, CapabilitySet.all(), Set.of()); } + private ConnectionAuthContext(List<X509Certificate> certs, CapabilityMode capabilityMode) { + this(certs, CapabilitySet.all(), Set.of(), capabilityMode); + } public boolean authorized() { return !capabilities.hasNone(); } + /** Throws checked exception to force caller to handle verification failed. */ + public void verifyCapabilities(CapabilitySet requiredCapabilities) throws MissingCapabilitiesException { + verifyCapabilities(requiredCapabilities, null, null, null); + } + + /** + * Throws checked exception to force caller to handle verification failed. + * Provided strings are used for improved logging only + * */ + public void verifyCapabilities(CapabilitySet requiredCapabilities, String action, String resource, String peer) + throws MissingCapabilitiesException { + if (capabilityMode == DISABLE) return; + boolean hasCapabilities = capabilities.has(requiredCapabilities); + if (!hasCapabilities) { + String msg = createPermissionDeniedErrorMessage(requiredCapabilities, action, resource, peer); + if (capabilityMode == LOG_ONLY) { + log.info(msg); + } else { + // Ideally log as warning, but we have no mechanism for de-duplicating repeated log spamming. + log.fine(msg); + throw new MissingCapabilitiesException(msg); + } + } + } + + String createPermissionDeniedErrorMessage( + CapabilitySet required, String action, String resource, String peer) { + StringBuilder b = new StringBuilder(); + if (capabilityMode == LOG_ONLY) b.append("Dry-run: "); + b.append("Permission denied"); + if (resource != null) { + b.append(" for '"); + if (action != null) { + b.append(action).append("' on '"); + } + b.append(resource).append("'"); + } + b.append(". Peer "); + if (peer != null) b.append("'").append(peer).append("' "); + return b.append("with ").append(peerCertificateString().orElse("<missing-certificate>")).append(". Requires capabilities ") + .append(required.toNames()).append(" but peer has ").append(capabilities.toNames()) + .append(".").toString(); + } + public Optional<X509Certificate> peerCertificate() { return peerCertificateChain.isEmpty() ? Optional.empty() : Optional.of(peerCertificateChain.get(0)); } @@ -62,11 +112,11 @@ public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain, } /** Construct instance with all capabilities */ - public static ConnectionAuthContext defaultAllCapabilities() { return DEFAULT_ALL_CAPABILITIES; } + public static ConnectionAuthContext defaultAllCapabilities() { return new ConnectionAuthContext(List.of(), DISABLE); } /** Construct instance with all capabilities */ public static ConnectionAuthContext defaultAllCapabilities(List<X509Certificate> certs) { - return new ConnectionAuthContext(certs); + return new ConnectionAuthContext(certs, DISABLE); } } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java new file mode 100644 index 00000000000..1c3ad9444e4 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java @@ -0,0 +1,13 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +/** + * Intentionally checked to force caller to handle missing permissions at call site. + * + * @author bjorncs + */ +public class MissingCapabilitiesException extends Exception { + + public MissingCapabilitiesException(String message) { super(message); } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java new file mode 100644 index 00000000000..02dbf3bb8e7 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author bjorncs + */ +public class PeerAuthorizationFailedException extends CertificateException { + private final List<X509Certificate> certChain; + + public PeerAuthorizationFailedException(String msg, List<X509Certificate> certChain) { + super(msg); + this.certChain = certChain; + } + + public PeerAuthorizationFailedException(String msg) { this(msg, List.of()); } + + public List<X509Certificate> peerCertificateChain() { return certChain; } +} + diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java index 5db86fd93bc..951b5c57c9e 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java @@ -7,7 +7,6 @@ import com.yahoo.security.X509CertificateUtils; import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.logging.Logger; @@ -39,7 +38,7 @@ public class PeerAuthorizer { X509Certificate cert = certChain.get(0); Set<String> matchedPolicies = new HashSet<>(); Set<CapabilitySet> grantedCapabilities = new HashSet<>(); - String cn = getCommonName(cert).orElse(null); + String cn = X509CertificateUtils.getSubjectCommonName(cert).orElse(null); List<String> sans = getSubjectAlternativeNames(cert); log.fine(() -> String.format("Subject info from x509 certificate: CN=[%s], 'SAN=%s", cn, sans)); for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) { @@ -48,7 +47,10 @@ public class PeerAuthorizer { grantedCapabilities.add(peerPolicy.capabilities()); } } - return new ConnectionAuthContext(certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies); + // TODO Pass this through constructor + CapabilityMode capabilityMode = TransportSecurityUtils.getCapabilityMode(); + return new ConnectionAuthContext( + certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies, capabilityMode); } private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List<String> sans) { @@ -69,11 +71,6 @@ public class PeerAuthorizer { } } - private static Optional<String> getCommonName(X509Certificate peerCertificate) { - return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream() - .findFirst(); - } - private static List<String> getSubjectAlternativeNames(X509Certificate peerCertificate) { return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream() .filter(san -> san.getType() == DNS || san.getType() == IP || san.getType() == URI) 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 index b92cd6c9538..c3dcdf1dc9e 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java @@ -6,6 +6,7 @@ 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; @@ -13,18 +14,20 @@ import java.security.KeyStore; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; -import java.util.Optional; +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 */ -// Note: Implementation assumes that provided X509ExtendedTrustManager will throw IllegalArgumentException when chain is empty or null -public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { +class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { - public static final String HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; + static final String AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName()); @@ -33,20 +36,16 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { private final AuthorizationMode mode; private final HostnameVerification hostnameVerification; - public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, - AuthorizationMode mode, - HostnameVerification hostnameVerification, - X509ExtendedTrustManager defaultTrustManager) { + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, X509ExtendedTrustManager defaultTrustManager) { this.authorizer = new PeerAuthorizer(authorizedPeers); this.mode = mode; this.hostnameVerification = hostnameVerification; this.defaultTrustManager = defaultTrustManager; } - public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, - AuthorizationMode mode, - HostnameVerification hostnameVerification, - KeyStore truststore) { + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, KeyStore truststore) { this(authorizedPeers, mode, hostnameVerification, TrustManagerUtils.createDefaultX509TrustManager(truststore)); } @@ -65,27 +64,27 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { defaultTrustManager.checkClientTrusted(chain, authType, socket); - authorizePeer(chain, authType, true, null); + 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, null); + 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); + 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); + authorizePeer(chain, authType, false, sslEngine.getHandshakeSession()); } @Override @@ -93,21 +92,17 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { return defaultTrustManager.getAcceptedIssuers(); } - /** - * Note: The authorization result is only available during handshake. The underlying handshake session is removed once handshake is complete. - */ - public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine sslEngine) { - return Optional.ofNullable(sslEngine.getHandshakeSession()) - .flatMap(session -> Optional.ofNullable((ConnectionAuthContext) session.getValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY))); - } - - private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient, SSLEngine sslEngine) throws CertificateException { + 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 (sslEngine != null) { // getHandshakeSession() will never return null in this context - sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); + 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)); @@ -115,7 +110,7 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { String errorMessage = "Authorization failed: " + createInfoString(certChain[0], authType, isVerifyingClient); log.warning(errorMessage); if (mode == AuthorizationMode.ENFORCE) { - throw new CertificateException(errorMessage); + throw new PeerAuthorizationFailedException(errorMessage, List.of(certChain)); } } } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java index 21d97613f95..ae6cef65156 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java @@ -1,6 +1,9 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security.tls; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -87,6 +90,24 @@ public class TransportSecurityUtils { } } + /** + * @return {@link ConnectionAuthContext} instance if {@link SSLEngine} was constructed by a {@link TlsContext}. + * Only available after TLS handshake is completed. + */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSession s) { + return Optional.ofNullable((ConnectionAuthContext) s.getValue(PeerAuthorizerTrustManager.AUTH_CONTEXT_PROPERTY)); + } + + /** @see #getConnectionAuthContext(SSLSession) */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine e) { + return getConnectionAuthContext(e.getSession()); + } + + /** @see #getConnectionAuthContext(SSLSession) */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSocket s) { + return getConnectionAuthContext(s.getSession()); + } + private static Optional<String> getEnvironmentVariable(Map<String, String> environmentVariables, String variableName) { return Optional.ofNullable(environmentVariables.get(variableName)) .filter(var -> !var.isEmpty()); |