summaryrefslogtreecommitdiffstats
path: root/security-utils/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'security-utils/src/main')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java8
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java60
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java13
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java23
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java13
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java51
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java21
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());