From 2e3005c471ba6520b17438c93f4a36369cbc3acd Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Wed, 20 Jul 2022 13:44:00 +0200 Subject: Implement RequireCapabilitiesFilter in jrt + misc Add peerSpec to Target/Connection. Always provide ConnectionAuthContext. Add helper for creating default, all-granting ConnectionAuthContext. --- .../rpc/security/MultiTenantRpcAuthorizerTest.java | 4 +- jrt/src/com/yahoo/jrt/Connection.java | 15 ++++-- jrt/src/com/yahoo/jrt/CryptoSocket.java | 10 +--- jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java | 5 +- .../com/yahoo/jrt/RequireCapabilitiesFilter.java | 54 ++++++++++++++++++++++ jrt/src/com/yahoo/jrt/Target.java | 10 ++-- jrt/src/com/yahoo/jrt/TlsCryptoSocket.java | 8 ++-- jrt/tests/com/yahoo/jrt/EchoTest.java | 5 +- .../yahoo/security/tls/ConnectionAuthContext.java | 10 ++-- .../com/yahoo/security/tls/PeerAuthorizer.java | 4 +- .../security/tls/PeerAuthorizerTrustManager.java | 3 +- 11 files changed, 90 insertions(+), 38 deletions(-) create mode 100644 jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java index efac33a740b..bffed6eb0b1 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java @@ -250,9 +250,9 @@ public class MultiTenantRpcAuthorizerTest { private static Request mockJrtRpcRequest(String payload) { ConnectionAuthContext authContext = - new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.none(), Set.of()); + new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.all(), Set.of()); Target target = mock(Target.class); - when(target.getConnectionAuthContext()).thenReturn(Optional.of(authContext)); + when(target.connectionAuthContext()).thenReturn(authContext); Request request = mock(Request.class); when(request.target()).thenReturn(target); Values values = new Values(); diff --git a/jrt/src/com/yahoo/jrt/Connection.java b/jrt/src/com/yahoo/jrt/Connection.java index 5f40acad192..1e4092efb75 100644 --- a/jrt/src/com/yahoo/jrt/Connection.java +++ b/jrt/src/com/yahoo/jrt/Connection.java @@ -4,6 +4,7 @@ package com.yahoo.jrt; import com.yahoo.security.tls.ConnectionAuthContext; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; @@ -11,7 +12,6 @@ import java.nio.channels.SocketChannel; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -438,9 +438,16 @@ class Connection extends Target { } @Override - public Optional getConnectionAuthContext() { - return Optional.ofNullable(socket) - .flatMap(CryptoSocket::getConnectionAuthContext); + public ConnectionAuthContext connectionAuthContext() { + if (socket == null) throw new IllegalStateException("Not connected"); + return socket.connectionAuthContext(); + } + + @Override + public Spec peerSpec() { + if (socket == null) throw new IllegalStateException("Not connected"); + InetSocketAddress addr = (InetSocketAddress) socket.channel().socket().getRemoteSocketAddress(); + return new Spec(addr.getHostString(), addr.getPort()); } public boolean isClient() { diff --git a/jrt/src/com/yahoo/jrt/CryptoSocket.java b/jrt/src/com/yahoo/jrt/CryptoSocket.java index d7dac8d1023..e30579d2bdc 100644 --- a/jrt/src/com/yahoo/jrt/CryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/CryptoSocket.java @@ -7,7 +7,6 @@ import com.yahoo.security.tls.ConnectionAuthContext; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -import java.util.Optional; /** @@ -104,11 +103,6 @@ public interface CryptoSocket { **/ public void dropEmptyBuffers(); - /** - * Returns the auth context for the current connection (given handshake completed), - * or empty if the current connection is not secure. - */ - default public Optional getConnectionAuthContext() { - return Optional.empty(); - } + /** Returns the auth context for the current connection (given handshake completed) */ + default ConnectionAuthContext connectionAuthContext() { return ConnectionAuthContext.defaultAllCapabilities(); } } diff --git a/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java index 2b5d456da11..ab9d78d2676 100644 --- a/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/MaybeTlsCryptoSocket.java @@ -6,7 +6,6 @@ import com.yahoo.security.tls.ConnectionAuthContext; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -import java.util.Optional; /** * A crypto socket for the server side of a connection that @@ -132,7 +131,5 @@ public class MaybeTlsCryptoSocket implements CryptoSocket { @Override public int write(ByteBuffer src) throws IOException { return socket.write(src); } @Override public FlushResult flush() throws IOException { return socket.flush(); } @Override public void dropEmptyBuffers() { socket.dropEmptyBuffers(); } - @Override public Optional getConnectionAuthContext() { - return Optional.ofNullable(socket).flatMap(CryptoSocket::getConnectionAuthContext); - } + @Override public ConnectionAuthContext connectionAuthContext() { return socket.connectionAuthContext(); } } diff --git a/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java new file mode 100644 index 00000000000..bb2eafcf711 --- /dev/null +++ b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java @@ -0,0 +1,54 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jrt; + +import com.yahoo.security.tls.Capability; +import com.yahoo.security.tls.CapabilityMode; +import com.yahoo.security.tls.CapabilitySet; +import com.yahoo.security.tls.ConnectionAuthContext; +import com.yahoo.security.tls.TransportSecurityUtils; + +import java.util.logging.Logger; + +import static com.yahoo.security.tls.CapabilityMode.DISABLE; +import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY; + +/** + * @author bjorncs + */ +public class RequireCapabilitiesFilter implements RequestAccessFilter { + + private static final Logger log = Logger.getLogger(RequireCapabilitiesFilter.class.getName()); + private static final CapabilityMode MODE = TransportSecurityUtils.getCapabilityMode(); + + private final CapabilitySet requiredCapabilities; + + public RequireCapabilitiesFilter(CapabilitySet requiredCapabilities) { + this.requiredCapabilities = requiredCapabilities; + } + + public RequireCapabilitiesFilter(Capability... requiredCapabilities) { + this(CapabilitySet.from(requiredCapabilities)); + } + + @Override + public boolean allow(Request r) { + if (MODE == DISABLE) return true; + ConnectionAuthContext ctx = r.target().connectionAuthContext(); + CapabilitySet peerCapabilities = ctx.capabilities(); + boolean authorized = peerCapabilities.has(requiredCapabilities); + if (!authorized) { + String msg = "%sPermission denied for RPC method '%s'. Peer at %s with %s. Call requires %s, but peer has %s" + .formatted(MODE == LOG_ONLY ? "Dry-run: " : "", r.methodName(), r.target().peerSpec(), ctx.peerCertificateString().orElseThrow(), + requiredCapabilities.toNames(), peerCapabilities.toNames()); + if (MODE == LOG_ONLY) { + log.info(msg); + return true; + } else { + log.warning(msg); + return false; + } + } + return true; + } + +} diff --git a/jrt/src/com/yahoo/jrt/Target.java b/jrt/src/com/yahoo/jrt/Target.java index 8bca6069721..0e8c27deac5 100644 --- a/jrt/src/com/yahoo/jrt/Target.java +++ b/jrt/src/com/yahoo/jrt/Target.java @@ -3,8 +3,6 @@ package com.yahoo.jrt; import com.yahoo.security.tls.ConnectionAuthContext; -import java.util.Optional; - /** * A Target represents a connection endpoint with RPC * capabilities. Each such connection has a client and a server @@ -71,9 +69,13 @@ public abstract class Target { public Exception getConnectionLostReason() { return null; } /** - * Returns the connection auth context associated with this target, or empty if no connection or is insecure. + * Returns the connection auth context associated with this target. */ - public abstract Optional getConnectionAuthContext(); + public abstract ConnectionAuthContext connectionAuthContext(); + + + /** @return address spec of socket peer */ + public abstract Spec peerSpec(); /** * Check if this target represents the client side of a diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java index 5ef4d149c6c..13274dc3ba5 100644 --- a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java @@ -14,7 +14,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; -import java.util.Optional; +import java.util.Objects; import java.util.logging.Logger; import static javax.net.ssl.SSLEngineResult.Status; @@ -219,9 +219,9 @@ public class TlsCryptoSocket implements CryptoSocket { } @Override - public Optional getConnectionAuthContext() { - if (handshakeState != HandshakeState.COMPLETED) return Optional.empty(); - return Optional.ofNullable(authContext); + public ConnectionAuthContext connectionAuthContext() { + if (handshakeState != HandshakeState.COMPLETED) throw new IllegalStateException("Handshake not complete"); + return Objects.requireNonNull(authContext); } private boolean handshakeWrap() throws IOException { diff --git a/jrt/tests/com/yahoo/jrt/EchoTest.java b/jrt/tests/com/yahoo/jrt/EchoTest.java index e65422bc66e..47c6e806635 100644 --- a/jrt/tests/com/yahoo/jrt/EchoTest.java +++ b/jrt/tests/com/yahoo/jrt/EchoTest.java @@ -16,7 +16,6 @@ import java.util.List; import static com.yahoo.jrt.CryptoUtils.createTestTlsContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) @@ -147,7 +146,7 @@ public class EchoTest { for (int i = 0; i < p.size(); i++) { r.add(p.get(i)); } - connAuthCtx = req.target().getConnectionAuthContext().orElse(null); + connAuthCtx = req.target().connectionAuthContext(); } @org.junit.Test @@ -168,8 +167,6 @@ public class EchoTest { if (connAuthCtxAssertion != null) { assertNotNull(connAuthCtx); connAuthCtxAssertion.assertConnectionAuthContext(connAuthCtx); - } else { - assertNull(connAuthCtx); } } } 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 e244d5ad23f..3ee6ed1dcaa 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 @@ -18,8 +18,10 @@ public record ConnectionAuthContext(List peerCertificateChain, CapabilitySet capabilities, Set matchedPolicies) { + private static final ConnectionAuthContext DEFAULT_ALL_CAPABILITIES = + new ConnectionAuthContext(List.of(), CapabilitySet.all(), Set.of()); + public ConnectionAuthContext { - if (peerCertificateChain.isEmpty()) throw new IllegalArgumentException("Peer certificate chain is empty"); peerCertificateChain = List.copyOf(peerCertificateChain); matchedPolicies = Set.copyOf(matchedPolicies); } @@ -33,7 +35,7 @@ public record ConnectionAuthContext(List peerCertificateChain, public Optional peerCertificateString() { X509Certificate cert = peerCertificate().orElse(null); if (cert == null) return Optional.empty(); - StringBuilder b = new StringBuilder("X.509Cert{"); + StringBuilder b = new StringBuilder("["); String cn = X509CertificateUtils.getSubjectCommonName(cert).orElse(null); if (cn != null) { b.append("CN='").append(cn).append("'"); @@ -55,7 +57,9 @@ public record ConnectionAuthContext(List peerCertificateChain, if (cn != null || !dnsNames.isEmpty()) b.append(", "); b.append("SAN_URI=").append(uris); } - return Optional.of(b.append("}").toString()); + return Optional.of(b.append("]").toString()); } + public static ConnectionAuthContext defaultAllCapabilities() { return DEFAULT_ALL_CAPABILITIES; } + } 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 608a8c9c933..99787725063 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 @@ -35,9 +35,7 @@ public class PeerAuthorizer { public ConnectionAuthContext authorizePeer(X509Certificate cert) { return authorizePeer(List.of(cert)); } public ConnectionAuthContext authorizePeer(List certChain) { - if (authorizedPeers.isEmpty()) { - return new ConnectionAuthContext(certChain, CapabilitySet.all(), Set.of()); - } + if (authorizedPeers.isEmpty()) return ConnectionAuthContext.defaultAllCapabilities(); X509Certificate cert = certChain.get(0); Set matchedPolicies = new HashSet<>(); Set grantedCapabilities = new HashSet<>(); 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 089023e55f1..e6239e3f694 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 @@ -14,7 +14,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.logging.Logger; /** @@ -106,7 +105,7 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { log.fine(() -> "Verifying certificate: " + createInfoString(certChain[0], authType, isVerifyingClient)); ConnectionAuthContext result = mode != AuthorizationMode.DISABLE ? authorizer.authorizePeer(List.of(certChain)) - : new ConnectionAuthContext(List.of(certChain), CapabilitySet.all(), Set.of()); + : ConnectionAuthContext.defaultAllCapabilities(); if (sslEngine != null) { // getHandshakeSession() will never return null in this context sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); } -- cgit v1.2.3