From 91b46555d137dcdf73a534ba5fa10e07510eb0f9 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Tue, 19 Jul 2022 14:45:41 +0200 Subject: Merge Java package 'c.y.s.tls.{auth,json,policy}' into 'c.y.s.tls' Facilitate improved encapsulation of Vespa mTLS related classes --- .../com/yahoo/security/tls/AuthorizedPeers.java | 32 ++++ .../java/com/yahoo/security/tls/Capability.java | 32 ++++ .../java/com/yahoo/security/tls/CapabilitySet.java | 104 +++++++++++ .../security/tls/ConfigFileBasedTlsContext.java | 1 - .../yahoo/security/tls/ConnectionAuthContext.java | 24 +++ .../com/yahoo/security/tls/DefaultTlsContext.java | 2 - .../java/com/yahoo/security/tls/GlobPattern.java | 82 +++++++++ .../com/yahoo/security/tls/HostGlobPattern.java | 46 +++++ .../com/yahoo/security/tls/PeerAuthorizer.java | 85 +++++++++ .../security/tls/PeerAuthorizerTrustManager.java | 169 ++++++++++++++++++ .../java/com/yahoo/security/tls/PeerPolicy.java | 24 +++ .../yahoo/security/tls/RequiredPeerCredential.java | 71 ++++++++ .../security/tls/TransportSecurityOptions.java | 3 - .../tls/TransportSecurityOptionsEntity.java | 49 ++++++ .../TransportSecurityOptionsJsonSerializer.java | 187 ++++++++++++++++++++ .../com/yahoo/security/tls/UriGlobPattern.java | 42 +++++ .../security/tls/authz/ConnectionAuthContext.java | 26 --- .../yahoo/security/tls/authz/PeerAuthorizer.java | 89 ---------- .../tls/authz/PeerAuthorizerTrustManager.java | 174 ------------------- .../com/yahoo/security/tls/authz/package-info.java | 8 - .../tls/json/TransportSecurityOptionsEntity.java | 49 ------ .../TransportSecurityOptionsJsonSerializer.java | 192 --------------------- .../com/yahoo/security/tls/json/package-info.java | 8 - .../yahoo/security/tls/policy/AuthorizedPeers.java | 32 ---- .../com/yahoo/security/tls/policy/Capability.java | 32 ---- .../yahoo/security/tls/policy/CapabilitySet.java | 104 ----------- .../com/yahoo/security/tls/policy/GlobPattern.java | 82 --------- .../yahoo/security/tls/policy/HostGlobPattern.java | 46 ----- .../com/yahoo/security/tls/policy/PeerPolicy.java | 24 --- .../tls/policy/RequiredPeerCredential.java | 71 -------- .../yahoo/security/tls/policy/UriGlobPattern.java | 42 ----- .../yahoo/security/tls/policy/package-info.java | 8 - .../yahoo/security/tls/AuthorizedPeersTest.java | 26 +++ .../com/yahoo/security/tls/CapabilitySetTest.java | 27 +++ .../yahoo/security/tls/DefaultTlsContextTest.java | 3 - .../com/yahoo/security/tls/GlobPatternTest.java | 106 ++++++++++++ .../yahoo/security/tls/HostGlobPatternTest.java | 69 ++++++++ .../com/yahoo/security/tls/PeerAuthorizerTest.java | 167 ++++++++++++++++++ ...TransportSecurityOptionsJsonSerializerTest.java | 99 +++++++++++ .../com/yahoo/security/tls/UriGlobPatternTest.java | 37 ++++ .../security/tls/authz/PeerAuthorizerTest.java | 172 ------------------ ...TransportSecurityOptionsJsonSerializerTest.java | 105 ----------- .../security/tls/policy/AuthorizedPeersTest.java | 24 --- .../security/tls/policy/CapabilitySetTest.java | 27 --- .../yahoo/security/tls/policy/GlobPatternTest.java | 106 ------------ .../security/tls/policy/HostGlobPatternTest.java | 69 -------- .../security/tls/policy/UriGlobPatternTest.java | 37 ---- 47 files changed, 1478 insertions(+), 1536 deletions(-) create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/AuthorizedPeers.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/Capability.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/GlobPattern.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/HostGlobPattern.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/PeerPolicy.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/RequiredPeerCredential.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsEntity.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java create mode 100644 security-utils/src/main/java/com/yahoo/security/tls/UriGlobPattern.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/json/package-info.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/AuthorizedPeers.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/Capability.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/GlobPattern.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/HostGlobPattern.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/PeerPolicy.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/RequiredPeerCredential.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/UriGlobPattern.java delete mode 100644 security-utils/src/main/java/com/yahoo/security/tls/policy/package-info.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/AuthorizedPeersTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/CapabilitySetTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/GlobPatternTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/HostGlobPatternTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/PeerAuthorizerTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializerTest.java create mode 100644 security-utils/src/test/java/com/yahoo/security/tls/UriGlobPatternTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/policy/AuthorizedPeersTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/policy/CapabilitySetTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/policy/GlobPatternTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/policy/HostGlobPatternTest.java delete mode 100644 security-utils/src/test/java/com/yahoo/security/tls/policy/UriGlobPatternTest.java (limited to 'security-utils') diff --git a/security-utils/src/main/java/com/yahoo/security/tls/AuthorizedPeers.java b/security-utils/src/main/java/com/yahoo/security/tls/AuthorizedPeers.java new file mode 100644 index 00000000000..9631ab32334 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/AuthorizedPeers.java @@ -0,0 +1,32 @@ +// 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.util.Set; + +/** + * @author bjorncs + */ +public record AuthorizedPeers(Set peerPolicies) { + + private static final AuthorizedPeers EMPTY = new AuthorizedPeers(Set.of()); + + public AuthorizedPeers { + peerPolicies = verifyPeerPolicies(peerPolicies); + } + + public static AuthorizedPeers empty() { return EMPTY; } + + private static Set verifyPeerPolicies(Set peerPolicies) { + long distinctNames = peerPolicies.stream() + .map(PeerPolicy::policyName) + .distinct() + .count(); + if (distinctNames != peerPolicies.size()) { + throw new IllegalArgumentException("'authorized-peers' contains entries with duplicate names"); + } + return Set.copyOf(peerPolicies); + } + + public boolean isEmpty() { return peerPolicies.isEmpty(); } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/Capability.java b/security-utils/src/main/java/com/yahoo/security/tls/Capability.java new file mode 100644 index 00000000000..0ae253985a6 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/Capability.java @@ -0,0 +1,32 @@ +// 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.util.Arrays; + +/** + * @author bjorncs + */ +public enum Capability { + CONTENT__CLUSTER_CONTROLLER__INTERNAL_STATE_API("vespa.content.cluster_controller.internal_state_api"), + CONTENT__DOCUMENT_API("vespa.content.document_api"), + CONTENT__METRICS_API("vespa.content.metrics_api"), + CONTENT__SEARCH_API("vespa.content.search_api"), + CONTENT__STATUS_PAGES("vespa.content.status_pages"), + CONTENT__STORAGE_API("vespa.content.storage_api"), + SLOBROK__API("vespa.slobrok.api"), + ; + + private final String name; + + Capability(String name) { this.name = name; } + + public String asString() { return name; } + + public static Capability fromName(String name) { + return Arrays.stream(values()) + .filter(c -> c.name.equals(name)) + .findAny().orElseThrow(() -> + new IllegalArgumentException("Cannot find predefined capability set with name '" + name + "'")); + } + +} 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 new file mode 100644 index 00000000000..ec402719efa --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java @@ -0,0 +1,104 @@ +// 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.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * @author bjorncs + */ +public class CapabilitySet { + public enum Predefined { + CONTENT_NODE("vespa.content_node", + Capability.CONTENT__STORAGE_API, Capability.CONTENT__DOCUMENT_API, Capability.SLOBROK__API), + CONTAINER_NODE("vespa.container_node", + Capability.CONTENT__DOCUMENT_API, Capability.CONTENT__SEARCH_API, Capability.SLOBROK__API), + TELEMETRY("vespa.telemetry", + Capability.CONTENT__STATUS_PAGES, Capability.CONTENT__METRICS_API), + CLUSTER_CONTROLLER_NODE("vespa.cluster_controller_node", + Capability.CONTENT__CLUSTER_CONTROLLER__INTERNAL_STATE_API, Capability.SLOBROK__API), + CONFIG_SERVER("vespa.config_server"), + ; + + private final String name; + private final EnumSet caps; + + Predefined(String name, Capability... caps) { + this.name = name; + this.caps = caps.length == 0 ? EnumSet.noneOf(Capability.class) : EnumSet.copyOf(List.of(caps)); } + + public static Optional fromName(String name) { + return Arrays.stream(values()).filter(p -> p.name.equals(name)).findAny(); + } + } + + private static final CapabilitySet ALL_CAPABILITIES = new CapabilitySet(EnumSet.allOf(Capability.class)); + private static final CapabilitySet NO_CAPABILITIES = new CapabilitySet(EnumSet.noneOf(Capability.class)); + + private final EnumSet caps; + + private CapabilitySet(EnumSet caps) { this.caps = caps; } + + public static CapabilitySet fromNames(Collection names) { + EnumSet caps = EnumSet.noneOf(Capability.class); + for (String name : names) { + Predefined predefined = Predefined.fromName(name).orElse(null); + if (predefined != null) caps.addAll(predefined.caps); + else caps.add(Capability.fromName(name)); + } + return new CapabilitySet(caps); + } + + public static CapabilitySet unionOf(Collection capSets) { + EnumSet union = EnumSet.noneOf(Capability.class); + capSets.forEach(cs -> union.addAll(cs.caps)); + return new CapabilitySet(union); + } + + public static CapabilitySet from(EnumSet caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } + public static CapabilitySet from(Collection caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } + public static CapabilitySet from(Capability... caps) { return new CapabilitySet(EnumSet.copyOf(List.of(caps))); } + public static CapabilitySet all() { return ALL_CAPABILITIES; } + public static CapabilitySet none() { return NO_CAPABILITIES; } + + public boolean hasAll() { return this.caps.equals(ALL_CAPABILITIES.caps); } + public boolean hasNone() { return this.caps.equals(NO_CAPABILITIES.caps); } + public boolean has(CapabilitySet caps) { return this.caps.containsAll(caps.caps); } + public boolean has(Collection caps) { return this.caps.containsAll(caps); } + public boolean has(Capability... caps) { return this.caps.containsAll(List.of(caps)); } + + public SortedSet toNames() { + return caps.stream().map(Capability::asString).collect(Collectors.toCollection(TreeSet::new)); + } + + public Set asSet() { return Collections.unmodifiableSet(caps); } + + @Override + public String toString() { + return "CapabilitySet{" + + "caps=" + caps + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CapabilitySet that = (CapabilitySet) o; + return Objects.equals(caps, that.caps); + } + + @Override + public int hashCode() { + return Objects.hash(caps); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/ConfigFileBasedTlsContext.java b/security-utils/src/main/java/com/yahoo/security/tls/ConfigFileBasedTlsContext.java index cc664786734..d89e4631103 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/ConfigFileBasedTlsContext.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/ConfigFileBasedTlsContext.java @@ -6,7 +6,6 @@ import com.yahoo.security.KeyStoreType; import com.yahoo.security.KeyUtils; import com.yahoo.security.SslContextBuilder; import com.yahoo.security.X509CertificateUtils; -import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; 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 new file mode 100644 index 00000000000..821d41cfabe --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java @@ -0,0 +1,24 @@ +package com.yahoo.security.tls; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +/** + * @author bjorncs + */ +public record ConnectionAuthContext(List peerCertificateChain, + CapabilitySet capabilities, + Set matchedPolicies) { + + public ConnectionAuthContext { + if (peerCertificateChain.isEmpty()) throw new IllegalArgumentException("Peer certificate chain is empty"); + peerCertificateChain = List.copyOf(peerCertificateChain); + matchedPolicies = Set.copyOf(matchedPolicies); + } + + public boolean authorized() { return !capabilities.hasNone(); } + + public X509Certificate peerCertificate() { return peerCertificateChain.get(0); } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java b/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java index c2ee573dfc6..88e4f409260 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/DefaultTlsContext.java @@ -2,8 +2,6 @@ package com.yahoo.security.tls; import com.yahoo.security.SslContextBuilder; -import com.yahoo.security.tls.authz.PeerAuthorizerTrustManager; -import com.yahoo.security.tls.policy.AuthorizedPeers; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; diff --git a/security-utils/src/main/java/com/yahoo/security/tls/GlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/GlobPattern.java new file mode 100644 index 00000000000..c945e48a361 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/GlobPattern.java @@ -0,0 +1,82 @@ +// 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.util.Arrays; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Matching engine for glob patterns having where one ore more alternative characters acts a boundary for wildcard matching. + * + * @author bjorncs + */ +class GlobPattern { + private final String pattern; + private final char[] boundaries; + private final Pattern regexPattern; + + GlobPattern(String pattern, char[] boundaries, boolean enableSingleCharWildcard) { + this.pattern = pattern; + this.boundaries = boundaries; + this.regexPattern = toRegexPattern(pattern, boundaries, enableSingleCharWildcard); + } + + boolean matches(String value) { return regexPattern.matcher(value).matches(); } + + String asString() { return pattern; } + Pattern regexPattern() { return regexPattern; } + char[] boundaries() { return boundaries; } + + private static Pattern toRegexPattern(String pattern, char[] boundaries, boolean enableSingleCharWildcard) { + StringBuilder builder = new StringBuilder("^"); + StringBuilder precedingCharactersToQuote = new StringBuilder(); + char[] chars = pattern.toCharArray(); + for (char c : chars) { + if ((enableSingleCharWildcard && c == '?') || c == '*') { + builder.append(quotePrecedingLiteralsAndReset(precedingCharactersToQuote)); + // Note: we explicitly stop matching at a separator boundary. + // This is to make matching less vulnerable to dirty tricks (e.g dot as boundary for hostnames). + // Same applies for single chars; they should only match _within_ a boundary. + builder.append("[^").append(Pattern.quote(new String(boundaries))).append("]"); + if (c == '*') builder.append('*'); + } else { + precedingCharactersToQuote.append(c); + } + } + return Pattern.compile(builder.append(quotePrecedingLiteralsAndReset(precedingCharactersToQuote)).append('$').toString()); + } + + // Combines multiple subsequent literals inside a single quote to simplify produced regex patterns + private static String quotePrecedingLiteralsAndReset(StringBuilder literals) { + if (literals.length() > 0) { + String quoted = literals.toString(); + literals.setLength(0); + return Pattern.quote(quoted); + } + return ""; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GlobPattern that = (GlobPattern) o; + return Objects.equals(pattern, that.pattern) && Arrays.equals(boundaries, that.boundaries); + } + + @Override + public int hashCode() { + int result = Objects.hash(pattern); + result = 31 * result + Arrays.hashCode(boundaries); + return result; + } + + @Override + public String toString() { + return "GlobPattern{" + + "pattern='" + pattern + '\'' + + ", boundaries=" + Arrays.toString(boundaries) + + ", regexPattern=" + regexPattern + + '}'; + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/HostGlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/HostGlobPattern.java new file mode 100644 index 00000000000..7e2c40182f0 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/HostGlobPattern.java @@ -0,0 +1,46 @@ +// 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.util.Objects; + +/** + * @author bjorncs + */ +class HostGlobPattern implements RequiredPeerCredential.Pattern { + + private final GlobPattern globPattern; + + HostGlobPattern(String pattern) { + this.globPattern = new GlobPattern(pattern, new char[] {'.'}, true); + } + + @Override + public String asString() { + return globPattern.asString(); + } + + @Override + public boolean matches(String hostString) { + return globPattern.matches(hostString); + } + + @Override + public String toString() { + return "HostGlobPattern{" + + "pattern='" + globPattern + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HostGlobPattern that = (HostGlobPattern) o; + return Objects.equals(globPattern, that.globPattern); + } + + @Override + public int hashCode() { + return Objects.hash(globPattern); + } +} 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 new file mode 100644 index 00000000000..e026c611d0c --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java @@ -0,0 +1,85 @@ +// 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.SubjectAlternativeName; +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; + +import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; +import static com.yahoo.security.SubjectAlternativeName.Type.IP_ADDRESS; +import static com.yahoo.security.SubjectAlternativeName.Type.UNIFORM_RESOURCE_IDENTIFIER; +import static java.util.stream.Collectors.toList; + +/** + * Uses rules from {@link AuthorizedPeers} to evaluate X509 certificates + * + * @author bjorncs + */ +public class PeerAuthorizer { + + private static final Logger log = Logger.getLogger(PeerAuthorizer.class.getName()); + + private final AuthorizedPeers authorizedPeers; + + public PeerAuthorizer(AuthorizedPeers authorizedPeers) { + this.authorizedPeers = authorizedPeers; + } + + + 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()); + } + X509Certificate cert = certChain.get(0); + Set matchedPolicies = new HashSet<>(); + Set grantedCapabilities = new HashSet<>(); + String cn = getCommonName(cert).orElse(null); + List sans = getSubjectAlternativeNames(cert); + 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)) { + matchedPolicies.add(peerPolicy.policyName()); + grantedCapabilities.add(peerPolicy.capabilities()); + } + } + return new ConnectionAuthContext(certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies); + } + + private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List sans) { + return peerPolicy.requiredCredentials().stream() + .allMatch(requiredCredential -> matchesRequiredCredentials(requiredCredential, cn, sans)); + } + + private static boolean matchesRequiredCredentials(RequiredPeerCredential requiredCredential, String cn, List sans) { + switch (requiredCredential.field()) { + case CN: + return cn != null && requiredCredential.pattern().matches(cn); + case SAN_DNS: + case SAN_URI: + return sans.stream() + .anyMatch(san -> requiredCredential.pattern().matches(san)); + default: + throw new RuntimeException("Unknown field: " + requiredCredential.field()); + } + } + + private static Optional getCommonName(X509Certificate peerCertificate) { + return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream() + .findFirst(); + } + + private static List getSubjectAlternativeNames(X509Certificate peerCertificate) { + return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream() + .filter(san -> san.getType() == DNS_NAME || san.getType() == IP_ADDRESS || san.getType() == UNIFORM_RESOURCE_IDENTIFIER) + .map(SubjectAlternativeName::getValue) + .collect(toList()); + } +} 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..8e7dd039081 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java @@ -0,0 +1,169 @@ +// 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.X509CertificateUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +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.Optional; +import java.util.Set; +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_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; + + public 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) { + 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, null); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + overrideHostnameVerificationForClient(socket); + defaultTrustManager.checkServerTrusted(chain, authType, socket); + authorizePeer(chain, authType, false, null); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType, sslEngine); + authorizePeer(chain, authType, true, sslEngine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + overrideHostnameVerificationForClient(sslEngine); + defaultTrustManager.checkServerTrusted(chain, authType, sslEngine); + authorizePeer(chain, authType, false, sslEngine); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + 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 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 { + 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()); + if (sslEngine != null) { // getHandshakeSession() will never return null in this context + sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); + } + 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 CertificateException(errorMessage); + } + } + } + + 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); + } + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerPolicy.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerPolicy.java new file mode 100644 index 00000000000..ea3d4cfe002 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerPolicy.java @@ -0,0 +1,24 @@ +// 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.util.List; +import java.util.Optional; + +/** + * @author bjorncs + */ +public record PeerPolicy(String policyName, Optional description, CapabilitySet capabilities, + List requiredCredentials) { + + public PeerPolicy { + requiredCredentials = List.copyOf(requiredCredentials); + } + + public PeerPolicy(String policyName, List requiredCredentials) { + this(policyName, Optional.empty(), CapabilitySet.all(), requiredCredentials); + } + + public PeerPolicy(String policyName, String description, List requiredCredentials) { + this(policyName, Optional.ofNullable(description), CapabilitySet.all(), requiredCredentials); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/RequiredPeerCredential.java b/security-utils/src/main/java/com/yahoo/security/tls/RequiredPeerCredential.java new file mode 100644 index 00000000000..9a18da9dffd --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/RequiredPeerCredential.java @@ -0,0 +1,71 @@ +// 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.util.Objects; + +/** + * @author bjorncs + */ +public class RequiredPeerCredential { + + public enum Field { CN, SAN_DNS, SAN_URI } + + private final Field field; + private final Pattern pattern; + + private RequiredPeerCredential(Field field, Pattern pattern) { + this.field = field; + this.pattern = pattern; + } + + public static RequiredPeerCredential of(Field field, String pattern) { + return new RequiredPeerCredential(field, createPattern(field, pattern)); + } + + private static Pattern createPattern(Field field, String pattern) { + switch (field) { + case CN: + case SAN_DNS: + return new HostGlobPattern(pattern); + case SAN_URI: + return new UriGlobPattern(pattern); + default: + throw new IllegalArgumentException("Unknown field: " + field); + } + } + + public Field field() { + return field; + } + + public Pattern pattern() { + return pattern; + } + + @Override + public String toString() { + return "RequiredPeerCredential{" + + "field=" + field + + ", pattern=" + pattern + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequiredPeerCredential that = (RequiredPeerCredential) o; + return field == that.field && + Objects.equals(pattern, that.pattern); + } + + @Override + public int hashCode() { + return Objects.hash(field, pattern); + } + + public interface Pattern { + String asString(); + boolean matches(String fieldValue); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java index a8802b7f0d3..4397f27ebb7 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptions.java @@ -1,9 +1,6 @@ // 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.tls.json.TransportSecurityOptionsJsonSerializer; -import com.yahoo.security.tls.policy.AuthorizedPeers; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsEntity.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsEntity.java new file mode 100644 index 00000000000..f1799a64a57 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsEntity.java @@ -0,0 +1,49 @@ +// 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.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +/** + * Jackson bindings for transport security options + * + * @author bjorncs + */ +@JsonIgnoreProperties(ignoreUnknown = true) +class TransportSecurityOptionsEntity { + + @JsonProperty("files") Files files; + @JsonProperty("authorized-peers") @JsonInclude(NON_EMPTY) List authorizedPeers; + @JsonProperty("accepted-ciphers") @JsonInclude(NON_EMPTY) List acceptedCiphers; + @JsonProperty("accepted-protocols") @JsonInclude(NON_EMPTY) List acceptedProtocols; + @JsonProperty("disable-hostname-validation") @JsonInclude(NON_NULL) Boolean isHostnameValidationDisabled; + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Files { + @JsonProperty("private-key") String privateKeyFile; + @JsonProperty("certificates") String certificatesFile; + @JsonProperty("ca-certificates") String caCertificatesFile; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class AuthorizedPeer { + @JsonProperty("required-credentials") List requiredCredentials; + @JsonProperty("name") String name; + @JsonProperty("description") @JsonInclude(NON_NULL) String description; + @JsonProperty("capabilities") @JsonInclude(NON_EMPTY) List capabilities; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class RequiredCredential { + @JsonProperty("field") CredentialField field; + @JsonProperty("must-match") String matchExpression; + } + + enum CredentialField { CN, SAN_DNS, SAN_URI } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java new file mode 100644 index 00000000000..0349d4085db --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializer.java @@ -0,0 +1,187 @@ +// 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.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.security.tls.TransportSecurityOptionsEntity.AuthorizedPeer; +import com.yahoo.security.tls.TransportSecurityOptionsEntity.CredentialField; +import com.yahoo.security.tls.TransportSecurityOptionsEntity.Files; +import com.yahoo.security.tls.TransportSecurityOptionsEntity.RequiredCredential; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +/** + * @author bjorncs + */ +class TransportSecurityOptionsJsonSerializer { + + private static final ObjectMapper mapper = new ObjectMapper(); + + TransportSecurityOptions deserialize(InputStream in) { + try { + TransportSecurityOptionsEntity entity = mapper.readValue(in, TransportSecurityOptionsEntity.class); + return toTransportSecurityOptions(entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + void serialize(OutputStream out, TransportSecurityOptions options) { + try { + mapper.writerWithDefaultPrettyPrinter().writeValue(out, toTransportSecurityOptionsEntity(options)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static TransportSecurityOptions toTransportSecurityOptions(TransportSecurityOptionsEntity entity) { + TransportSecurityOptions.Builder builder = new TransportSecurityOptions.Builder(); + Files files = entity.files; + if (files != null) { + if (files.certificatesFile != null && files.privateKeyFile != null) { + builder.withCertificates(Paths.get(files.certificatesFile), Paths.get(files.privateKeyFile)); + } else if (files.certificatesFile != null || files.privateKeyFile != null) { + throw new IllegalArgumentException("Both 'private-key' and 'certificates' must be configured together"); + } + if (files.caCertificatesFile != null) { + builder.withCaCertificates(Paths.get(files.caCertificatesFile)); + } + } + List authorizedPeersEntity = entity.authorizedPeers; + if (authorizedPeersEntity != null) { + if (authorizedPeersEntity.size() == 0) { + throw new IllegalArgumentException("'authorized-peers' cannot be empty"); + } + builder.withAuthorizedPeers(new AuthorizedPeers(toPeerPolicies(authorizedPeersEntity))); + } + if (entity.acceptedCiphers != null) { + if (entity.acceptedCiphers.isEmpty()) { + throw new IllegalArgumentException("'accepted-ciphers' cannot be empty"); + } + builder.withAcceptedCiphers(entity.acceptedCiphers); + } + if (entity.acceptedProtocols != null) { + if (entity.acceptedProtocols.isEmpty()) { + throw new IllegalArgumentException("'accepted-protocols' cannot be empty"); + } + builder.withAcceptedProtocols(entity.acceptedProtocols); + } + if (entity.isHostnameValidationDisabled != null) { + builder.withHostnameValidationDisabled(entity.isHostnameValidationDisabled); + } + return builder.build(); + } + + private static Set toPeerPolicies(List authorizedPeersEntity) { + return authorizedPeersEntity.stream() + .map(TransportSecurityOptionsJsonSerializer::toPeerPolicy) + .collect(toSet()); + } + + private static PeerPolicy toPeerPolicy(AuthorizedPeer authorizedPeer) { + if (authorizedPeer.name == null) { + throw missingFieldException("name"); + } + if (authorizedPeer.requiredCredentials == null) { + throw missingFieldException("required-credentials"); + } + return new PeerPolicy(authorizedPeer.name, Optional.ofNullable(authorizedPeer.description), + toCapabilities(authorizedPeer.capabilities), toRequestPeerCredentials(authorizedPeer.requiredCredentials)); + } + + private static CapabilitySet toCapabilities(List capabilities) { + if (capabilities == null) return CapabilitySet.all(); + if (capabilities.isEmpty()) + throw new IllegalArgumentException("\"capabilities\" array must either be not present " + + "(implies all capabilities) or contain at least one capability name"); + return CapabilitySet.fromNames(capabilities); + } + + private static List toRequestPeerCredentials(List requiredCredentials) { + return requiredCredentials.stream() + .map(TransportSecurityOptionsJsonSerializer::toRequiredPeerCredential) + .collect(toList()); + } + + private static RequiredPeerCredential toRequiredPeerCredential(RequiredCredential requiredCredential) { + if (requiredCredential.field == null) { + throw missingFieldException("field"); + } + if (requiredCredential.matchExpression == null) { + throw missingFieldException("must-match"); + } + return RequiredPeerCredential.of(toField(requiredCredential.field), requiredCredential.matchExpression); + } + + private static RequiredPeerCredential.Field toField(CredentialField field) { + switch (field) { + case CN: return RequiredPeerCredential.Field.CN; + case SAN_DNS: return RequiredPeerCredential.Field.SAN_DNS; + case SAN_URI: return RequiredPeerCredential.Field.SAN_URI; + default: throw new IllegalArgumentException("Invalid field type: " + field); + } + } + + private static TransportSecurityOptionsEntity toTransportSecurityOptionsEntity(TransportSecurityOptions options) { + TransportSecurityOptionsEntity entity = new TransportSecurityOptionsEntity(); + entity.files = new Files(); + options.getCaCertificatesFile().ifPresent(value -> entity.files.caCertificatesFile = value.toString()); + options.getCertificatesFile().ifPresent(value -> entity.files.certificatesFile = value.toString()); + options.getPrivateKeyFile().ifPresent(value -> entity.files.privateKeyFile = value.toString()); + entity.authorizedPeers = options.getAuthorizedPeers().peerPolicies().stream() + // Makes tests stable + .sorted(Comparator.comparing(PeerPolicy::policyName)) + .map(peerPolicy -> { + AuthorizedPeer authorizedPeer = new AuthorizedPeer(); + authorizedPeer.name = peerPolicy.policyName(); + authorizedPeer.requiredCredentials = new ArrayList<>(); + authorizedPeer.description = peerPolicy.description().orElse(null); + CapabilitySet caps = peerPolicy.capabilities(); + if (!caps.hasAll()) { + authorizedPeer.capabilities = List.copyOf(caps.toNames()); + } + for (RequiredPeerCredential requiredPeerCredential : peerPolicy.requiredCredentials()) { + RequiredCredential requiredCredential = new RequiredCredential(); + requiredCredential.field = toField(requiredPeerCredential.field()); + requiredCredential.matchExpression = requiredPeerCredential.pattern().asString(); + authorizedPeer.requiredCredentials.add(requiredCredential); + } + return authorizedPeer; + }) + .toList(); + if (!options.getAcceptedCiphers().isEmpty()) { + entity.acceptedCiphers = options.getAcceptedCiphers(); + } + if (!options.getAcceptedProtocols().isEmpty()) { + entity.acceptedProtocols = options.getAcceptedProtocols(); + } + if (options.isHostnameValidationDisabled()) { + entity.isHostnameValidationDisabled = true; + } + return entity; + } + + private static CredentialField toField(RequiredPeerCredential.Field field) { + switch (field) { + case CN: return CredentialField.CN; + case SAN_DNS: return CredentialField.SAN_DNS; + case SAN_URI: return CredentialField.SAN_URI; + default: throw new IllegalArgumentException("Invalid field type: " + field); + } + } + + private static IllegalArgumentException missingFieldException(String fieldName) { + return new IllegalArgumentException(String.format("'%s' missing", fieldName)); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/UriGlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/UriGlobPattern.java new file mode 100644 index 00000000000..18d18a5ab3c --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/UriGlobPattern.java @@ -0,0 +1,42 @@ +// 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.util.Objects; + +/** + * Pattern used for matching URIs in X.509 certificate subject alternative names. + * + * @author bjorncs + */ +class UriGlobPattern implements RequiredPeerCredential.Pattern { + + private final GlobPattern globPattern; + + UriGlobPattern(String globPattern) { + this.globPattern = new GlobPattern(globPattern, new char[] {'/'}, false); + } + + @Override public String asString() { return globPattern.asString(); } + + @Override public boolean matches(String fieldValue) { return globPattern.matches(fieldValue); } + + @Override + public String toString() { + return "UriPattern{" + + "pattern='" + globPattern + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UriGlobPattern that = (UriGlobPattern) o; + return Objects.equals(globPattern, that.globPattern); + } + + @Override + public int hashCode() { + return Objects.hash(globPattern); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java deleted file mode 100644 index 877ba4e74bd..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/ConnectionAuthContext.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.yahoo.security.tls.authz; - -import com.yahoo.security.tls.policy.CapabilitySet; - -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -/** - * @author bjorncs - */ -public record ConnectionAuthContext(List peerCertificateChain, - CapabilitySet capabilities, - Set matchedPolicies) { - - public ConnectionAuthContext { - if (peerCertificateChain.isEmpty()) throw new IllegalArgumentException("Peer certificate chain is empty"); - peerCertificateChain = List.copyOf(peerCertificateChain); - matchedPolicies = Set.copyOf(matchedPolicies); - } - - public boolean authorized() { return !capabilities.hasNone(); } - - public X509Certificate peerCertificate() { return peerCertificateChain.get(0); } - -} 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 deleted file mode 100644 index a87c578f8c6..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright Yahoo. 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.SubjectAlternativeName; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.CapabilitySet; -import com.yahoo.security.tls.policy.PeerPolicy; -import com.yahoo.security.tls.policy.RequiredPeerCredential; - -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; - -import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; -import static com.yahoo.security.SubjectAlternativeName.Type.IP_ADDRESS; -import static com.yahoo.security.SubjectAlternativeName.Type.UNIFORM_RESOURCE_IDENTIFIER; -import static java.util.stream.Collectors.toList; - -/** - * Uses rules from {@link AuthorizedPeers} to evaluate X509 certificates - * - * @author bjorncs - */ -public class PeerAuthorizer { - - private static final Logger log = Logger.getLogger(PeerAuthorizer.class.getName()); - - private final AuthorizedPeers authorizedPeers; - - public PeerAuthorizer(AuthorizedPeers authorizedPeers) { - this.authorizedPeers = authorizedPeers; - } - - - 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()); - } - X509Certificate cert = certChain.get(0); - Set matchedPolicies = new HashSet<>(); - Set grantedCapabilities = new HashSet<>(); - String cn = getCommonName(cert).orElse(null); - List sans = getSubjectAlternativeNames(cert); - 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)) { - matchedPolicies.add(peerPolicy.policyName()); - grantedCapabilities.add(peerPolicy.capabilities()); - } - } - return new ConnectionAuthContext(certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies); - } - - private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List sans) { - return peerPolicy.requiredCredentials().stream() - .allMatch(requiredCredential -> matchesRequiredCredentials(requiredCredential, cn, sans)); - } - - private static boolean matchesRequiredCredentials(RequiredPeerCredential requiredCredential, String cn, List sans) { - switch (requiredCredential.field()) { - case CN: - return cn != null && requiredCredential.pattern().matches(cn); - case SAN_DNS: - case SAN_URI: - return sans.stream() - .anyMatch(san -> requiredCredential.pattern().matches(san)); - default: - throw new RuntimeException("Unknown field: " + requiredCredential.field()); - } - } - - private static Optional getCommonName(X509Certificate peerCertificate) { - return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream() - .findFirst(); - } - - private static List getSubjectAlternativeNames(X509Certificate peerCertificate) { - return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream() - .filter(san -> san.getType() == DNS_NAME || san.getType() == IP_ADDRESS || san.getType() == UNIFORM_RESOURCE_IDENTIFIER) - .map(SubjectAlternativeName::getValue) - .collect(toList()); - } -} 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 deleted file mode 100644 index 334216a2c19..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizerTrustManager.java +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright Yahoo. 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.AuthorizationMode; -import com.yahoo.security.tls.HostnameVerification; -import com.yahoo.security.tls.TrustManagerUtils; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.CapabilitySet; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; -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.Optional; -import java.util.Set; -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_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; - - public 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) { - 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, null); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - overrideHostnameVerificationForClient(socket); - defaultTrustManager.checkServerTrusted(chain, authType, socket); - authorizePeer(chain, authType, false, null); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { - defaultTrustManager.checkClientTrusted(chain, authType, sslEngine); - authorizePeer(chain, authType, true, sslEngine); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { - overrideHostnameVerificationForClient(sslEngine); - defaultTrustManager.checkServerTrusted(chain, authType, sslEngine); - authorizePeer(chain, authType, false, sslEngine); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - 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 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 { - 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()); - if (sslEngine != null) { // getHandshakeSession() will never return null in this context - sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); - } - 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 CertificateException(errorMessage); - } - } - } - - 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); - } - } - -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java deleted file mode 100644 index 5066026757d..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@ExportPackage -package com.yahoo.security.tls.authz; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java deleted file mode 100644 index b80a7e4f2fb..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.json; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; - -/** - * Jackson bindings for transport security options - * - * @author bjorncs - */ -@JsonIgnoreProperties(ignoreUnknown = true) -class TransportSecurityOptionsEntity { - - @JsonProperty("files") Files files; - @JsonProperty("authorized-peers") @JsonInclude(NON_EMPTY) List authorizedPeers; - @JsonProperty("accepted-ciphers") @JsonInclude(NON_EMPTY) List acceptedCiphers; - @JsonProperty("accepted-protocols") @JsonInclude(NON_EMPTY) List acceptedProtocols; - @JsonProperty("disable-hostname-validation") @JsonInclude(NON_NULL) Boolean isHostnameValidationDisabled; - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Files { - @JsonProperty("private-key") String privateKeyFile; - @JsonProperty("certificates") String certificatesFile; - @JsonProperty("ca-certificates") String caCertificatesFile; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class AuthorizedPeer { - @JsonProperty("required-credentials") List requiredCredentials; - @JsonProperty("name") String name; - @JsonProperty("description") @JsonInclude(NON_NULL) String description; - @JsonProperty("capabilities") @JsonInclude(NON_EMPTY) List capabilities; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class RequiredCredential { - @JsonProperty("field") CredentialField field; - @JsonProperty("must-match") String matchExpression; - } - - enum CredentialField { CN, SAN_DNS, SAN_URI } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java deleted file mode 100644 index fcd84056212..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.json; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.security.tls.TransportSecurityOptions; -import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.AuthorizedPeer; -import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.CredentialField; -import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.Files; -import com.yahoo.security.tls.json.TransportSecurityOptionsEntity.RequiredCredential; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.CapabilitySet; -import com.yahoo.security.tls.policy.PeerPolicy; -import com.yahoo.security.tls.policy.RequiredPeerCredential; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; - -/** - * @author bjorncs - */ -public class TransportSecurityOptionsJsonSerializer { - - private static final ObjectMapper mapper = new ObjectMapper(); - - public TransportSecurityOptions deserialize(InputStream in) { - try { - TransportSecurityOptionsEntity entity = mapper.readValue(in, TransportSecurityOptionsEntity.class); - return toTransportSecurityOptions(entity); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public void serialize(OutputStream out, TransportSecurityOptions options) { - try { - mapper.writerWithDefaultPrettyPrinter().writeValue(out, toTransportSecurityOptionsEntity(options)); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private static TransportSecurityOptions toTransportSecurityOptions(TransportSecurityOptionsEntity entity) { - TransportSecurityOptions.Builder builder = new TransportSecurityOptions.Builder(); - Files files = entity.files; - if (files != null) { - if (files.certificatesFile != null && files.privateKeyFile != null) { - builder.withCertificates(Paths.get(files.certificatesFile), Paths.get(files.privateKeyFile)); - } else if (files.certificatesFile != null || files.privateKeyFile != null) { - throw new IllegalArgumentException("Both 'private-key' and 'certificates' must be configured together"); - } - if (files.caCertificatesFile != null) { - builder.withCaCertificates(Paths.get(files.caCertificatesFile)); - } - } - List authorizedPeersEntity = entity.authorizedPeers; - if (authorizedPeersEntity != null) { - if (authorizedPeersEntity.size() == 0) { - throw new IllegalArgumentException("'authorized-peers' cannot be empty"); - } - builder.withAuthorizedPeers(new AuthorizedPeers(toPeerPolicies(authorizedPeersEntity))); - } - if (entity.acceptedCiphers != null) { - if (entity.acceptedCiphers.isEmpty()) { - throw new IllegalArgumentException("'accepted-ciphers' cannot be empty"); - } - builder.withAcceptedCiphers(entity.acceptedCiphers); - } - if (entity.acceptedProtocols != null) { - if (entity.acceptedProtocols.isEmpty()) { - throw new IllegalArgumentException("'accepted-protocols' cannot be empty"); - } - builder.withAcceptedProtocols(entity.acceptedProtocols); - } - if (entity.isHostnameValidationDisabled != null) { - builder.withHostnameValidationDisabled(entity.isHostnameValidationDisabled); - } - return builder.build(); - } - - private static Set toPeerPolicies(List authorizedPeersEntity) { - return authorizedPeersEntity.stream() - .map(TransportSecurityOptionsJsonSerializer::toPeerPolicy) - .collect(toSet()); - } - - private static PeerPolicy toPeerPolicy(AuthorizedPeer authorizedPeer) { - if (authorizedPeer.name == null) { - throw missingFieldException("name"); - } - if (authorizedPeer.requiredCredentials == null) { - throw missingFieldException("required-credentials"); - } - return new PeerPolicy(authorizedPeer.name, Optional.ofNullable(authorizedPeer.description), - toCapabilities(authorizedPeer.capabilities), toRequestPeerCredentials(authorizedPeer.requiredCredentials)); - } - - private static CapabilitySet toCapabilities(List capabilities) { - if (capabilities == null) return CapabilitySet.all(); - if (capabilities.isEmpty()) - throw new IllegalArgumentException("\"capabilities\" array must either be not present " + - "(implies all capabilities) or contain at least one capability name"); - return CapabilitySet.fromNames(capabilities); - } - - private static List toRequestPeerCredentials(List requiredCredentials) { - return requiredCredentials.stream() - .map(TransportSecurityOptionsJsonSerializer::toRequiredPeerCredential) - .collect(toList()); - } - - private static RequiredPeerCredential toRequiredPeerCredential(RequiredCredential requiredCredential) { - if (requiredCredential.field == null) { - throw missingFieldException("field"); - } - if (requiredCredential.matchExpression == null) { - throw missingFieldException("must-match"); - } - return RequiredPeerCredential.of(toField(requiredCredential.field), requiredCredential.matchExpression); - } - - private static RequiredPeerCredential.Field toField(CredentialField field) { - switch (field) { - case CN: return RequiredPeerCredential.Field.CN; - case SAN_DNS: return RequiredPeerCredential.Field.SAN_DNS; - case SAN_URI: return RequiredPeerCredential.Field.SAN_URI; - default: throw new IllegalArgumentException("Invalid field type: " + field); - } - } - - private static TransportSecurityOptionsEntity toTransportSecurityOptionsEntity(TransportSecurityOptions options) { - TransportSecurityOptionsEntity entity = new TransportSecurityOptionsEntity(); - entity.files = new Files(); - options.getCaCertificatesFile().ifPresent(value -> entity.files.caCertificatesFile = value.toString()); - options.getCertificatesFile().ifPresent(value -> entity.files.certificatesFile = value.toString()); - options.getPrivateKeyFile().ifPresent(value -> entity.files.privateKeyFile = value.toString()); - entity.authorizedPeers = options.getAuthorizedPeers().peerPolicies().stream() - // Makes tests stable - .sorted(Comparator.comparing(PeerPolicy::policyName)) - .map(peerPolicy -> { - AuthorizedPeer authorizedPeer = new AuthorizedPeer(); - authorizedPeer.name = peerPolicy.policyName(); - authorizedPeer.requiredCredentials = new ArrayList<>(); - authorizedPeer.description = peerPolicy.description().orElse(null); - CapabilitySet caps = peerPolicy.capabilities(); - if (!caps.hasAll()) { - authorizedPeer.capabilities = List.copyOf(caps.toNames()); - } - for (RequiredPeerCredential requiredPeerCredential : peerPolicy.requiredCredentials()) { - RequiredCredential requiredCredential = new RequiredCredential(); - requiredCredential.field = toField(requiredPeerCredential.field()); - requiredCredential.matchExpression = requiredPeerCredential.pattern().asString(); - authorizedPeer.requiredCredentials.add(requiredCredential); - } - return authorizedPeer; - }) - .toList(); - if (!options.getAcceptedCiphers().isEmpty()) { - entity.acceptedCiphers = options.getAcceptedCiphers(); - } - if (!options.getAcceptedProtocols().isEmpty()) { - entity.acceptedProtocols = options.getAcceptedProtocols(); - } - if (options.isHostnameValidationDisabled()) { - entity.isHostnameValidationDisabled = true; - } - return entity; - } - - private static CredentialField toField(RequiredPeerCredential.Field field) { - switch (field) { - case CN: return CredentialField.CN; - case SAN_DNS: return CredentialField.SAN_DNS; - case SAN_URI: return CredentialField.SAN_URI; - default: throw new IllegalArgumentException("Invalid field type: " + field); - } - } - - private static IllegalArgumentException missingFieldException(String fieldName) { - return new IllegalArgumentException(String.format("'%s' missing", fieldName)); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/json/package-info.java b/security-utils/src/main/java/com/yahoo/security/tls/json/package-info.java deleted file mode 100644 index be7ec33bf04..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/json/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@ExportPackage -package com.yahoo.security.tls.json; - -import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/AuthorizedPeers.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/AuthorizedPeers.java deleted file mode 100644 index 5e49a5b341c..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/AuthorizedPeers.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Set; - -/** - * @author bjorncs - */ -public record AuthorizedPeers(Set peerPolicies) { - - private static final AuthorizedPeers EMPTY = new AuthorizedPeers(Set.of()); - - public AuthorizedPeers { - peerPolicies = verifyPeerPolicies(peerPolicies); - } - - public static AuthorizedPeers empty() { return EMPTY; } - - private static Set verifyPeerPolicies(Set peerPolicies) { - long distinctNames = peerPolicies.stream() - .map(PeerPolicy::policyName) - .distinct() - .count(); - if (distinctNames != peerPolicies.size()) { - throw new IllegalArgumentException("'authorized-peers' contains entries with duplicate names"); - } - return Set.copyOf(peerPolicies); - } - - public boolean isEmpty() { return peerPolicies.isEmpty(); } - -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/Capability.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/Capability.java deleted file mode 100644 index 09d4de37831..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/Capability.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Arrays; - -/** - * @author bjorncs - */ -public enum Capability { - CONTENT__CLUSTER_CONTROLLER__INTERNAL_STATE_API("vespa.content.cluster_controller.internal_state_api"), - CONTENT__DOCUMENT_API("vespa.content.document_api"), - CONTENT__METRICS_API("vespa.content.metrics_api"), - CONTENT__SEARCH_API("vespa.content.search_api"), - CONTENT__STATUS_PAGES("vespa.content.status_pages"), - CONTENT__STORAGE_API("vespa.content.storage_api"), - SLOBROK__API("vespa.slobrok.api"), - ; - - private final String name; - - Capability(String name) { this.name = name; } - - public String asString() { return name; } - - public static Capability fromName(String name) { - return Arrays.stream(values()) - .filter(c -> c.name.equals(name)) - .findAny().orElseThrow(() -> - new IllegalArgumentException("Cannot find predefined capability set with name '" + name + "'")); - } - -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java deleted file mode 100644 index 28e235ff672..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/CapabilitySet.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.stream.Collectors; - -/** - * @author bjorncs - */ -public class CapabilitySet { - public enum Predefined { - CONTENT_NODE("vespa.content_node", - Capability.CONTENT__STORAGE_API, Capability.CONTENT__DOCUMENT_API, Capability.SLOBROK__API), - CONTAINER_NODE("vespa.container_node", - Capability.CONTENT__DOCUMENT_API, Capability.CONTENT__SEARCH_API, Capability.SLOBROK__API), - TELEMETRY("vespa.telemetry", - Capability.CONTENT__STATUS_PAGES, Capability.CONTENT__METRICS_API), - CLUSTER_CONTROLLER_NODE("vespa.cluster_controller_node", - Capability.CONTENT__CLUSTER_CONTROLLER__INTERNAL_STATE_API, Capability.SLOBROK__API), - CONFIG_SERVER("vespa.config_server"), - ; - - private final String name; - private final EnumSet caps; - - Predefined(String name, Capability... caps) { - this.name = name; - this.caps = caps.length == 0 ? EnumSet.noneOf(Capability.class) : EnumSet.copyOf(List.of(caps)); } - - public static Optional fromName(String name) { - return Arrays.stream(values()).filter(p -> p.name.equals(name)).findAny(); - } - } - - private static final CapabilitySet ALL_CAPABILITIES = new CapabilitySet(EnumSet.allOf(Capability.class)); - private static final CapabilitySet NO_CAPABILITIES = new CapabilitySet(EnumSet.noneOf(Capability.class)); - - private final EnumSet caps; - - private CapabilitySet(EnumSet caps) { this.caps = caps; } - - public static CapabilitySet fromNames(Collection names) { - EnumSet caps = EnumSet.noneOf(Capability.class); - for (String name : names) { - Predefined predefined = Predefined.fromName(name).orElse(null); - if (predefined != null) caps.addAll(predefined.caps); - else caps.add(Capability.fromName(name)); - } - return new CapabilitySet(caps); - } - - public static CapabilitySet unionOf(Collection capSets) { - EnumSet union = EnumSet.noneOf(Capability.class); - capSets.forEach(cs -> union.addAll(cs.caps)); - return new CapabilitySet(union); - } - - public static CapabilitySet from(EnumSet caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } - public static CapabilitySet from(Collection caps) { return new CapabilitySet(EnumSet.copyOf(caps)); } - public static CapabilitySet from(Capability... caps) { return new CapabilitySet(EnumSet.copyOf(List.of(caps))); } - public static CapabilitySet all() { return ALL_CAPABILITIES; } - public static CapabilitySet none() { return NO_CAPABILITIES; } - - public boolean hasAll() { return this.caps.equals(ALL_CAPABILITIES.caps); } - public boolean hasNone() { return this.caps.equals(NO_CAPABILITIES.caps); } - public boolean has(CapabilitySet caps) { return this.caps.containsAll(caps.caps); } - public boolean has(Collection caps) { return this.caps.containsAll(caps); } - public boolean has(Capability... caps) { return this.caps.containsAll(List.of(caps)); } - - public SortedSet toNames() { - return caps.stream().map(Capability::asString).collect(Collectors.toCollection(TreeSet::new)); - } - - public Set asSet() { return Collections.unmodifiableSet(caps); } - - @Override - public String toString() { - return "CapabilitySet{" + - "caps=" + caps + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CapabilitySet that = (CapabilitySet) o; - return Objects.equals(caps, that.caps); - } - - @Override - public int hashCode() { - return Objects.hash(caps); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/GlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/GlobPattern.java deleted file mode 100644 index 46a38a77844..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/GlobPattern.java +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Arrays; -import java.util.Objects; -import java.util.regex.Pattern; - -/** - * Matching engine for glob patterns having where one ore more alternative characters acts a boundary for wildcard matching. - * - * @author bjorncs - */ -class GlobPattern { - private final String pattern; - private final char[] boundaries; - private final Pattern regexPattern; - - GlobPattern(String pattern, char[] boundaries, boolean enableSingleCharWildcard) { - this.pattern = pattern; - this.boundaries = boundaries; - this.regexPattern = toRegexPattern(pattern, boundaries, enableSingleCharWildcard); - } - - boolean matches(String value) { return regexPattern.matcher(value).matches(); } - - String asString() { return pattern; } - Pattern regexPattern() { return regexPattern; } - char[] boundaries() { return boundaries; } - - private static Pattern toRegexPattern(String pattern, char[] boundaries, boolean enableSingleCharWildcard) { - StringBuilder builder = new StringBuilder("^"); - StringBuilder precedingCharactersToQuote = new StringBuilder(); - char[] chars = pattern.toCharArray(); - for (char c : chars) { - if ((enableSingleCharWildcard && c == '?') || c == '*') { - builder.append(quotePrecedingLiteralsAndReset(precedingCharactersToQuote)); - // Note: we explicitly stop matching at a separator boundary. - // This is to make matching less vulnerable to dirty tricks (e.g dot as boundary for hostnames). - // Same applies for single chars; they should only match _within_ a boundary. - builder.append("[^").append(Pattern.quote(new String(boundaries))).append("]"); - if (c == '*') builder.append('*'); - } else { - precedingCharactersToQuote.append(c); - } - } - return Pattern.compile(builder.append(quotePrecedingLiteralsAndReset(precedingCharactersToQuote)).append('$').toString()); - } - - // Combines multiple subsequent literals inside a single quote to simplify produced regex patterns - private static String quotePrecedingLiteralsAndReset(StringBuilder literals) { - if (literals.length() > 0) { - String quoted = literals.toString(); - literals.setLength(0); - return Pattern.quote(quoted); - } - return ""; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - GlobPattern that = (GlobPattern) o; - return Objects.equals(pattern, that.pattern) && Arrays.equals(boundaries, that.boundaries); - } - - @Override - public int hashCode() { - int result = Objects.hash(pattern); - result = 31 * result + Arrays.hashCode(boundaries); - return result; - } - - @Override - public String toString() { - return "GlobPattern{" + - "pattern='" + pattern + '\'' + - ", boundaries=" + Arrays.toString(boundaries) + - ", regexPattern=" + regexPattern + - '}'; - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/HostGlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/HostGlobPattern.java deleted file mode 100644 index cb9ba13cae4..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/HostGlobPattern.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Objects; - -/** - * @author bjorncs - */ -class HostGlobPattern implements RequiredPeerCredential.Pattern { - - private final GlobPattern globPattern; - - HostGlobPattern(String pattern) { - this.globPattern = new GlobPattern(pattern, new char[] {'.'}, true); - } - - @Override - public String asString() { - return globPattern.asString(); - } - - @Override - public boolean matches(String hostString) { - return globPattern.matches(hostString); - } - - @Override - public String toString() { - return "HostGlobPattern{" + - "pattern='" + globPattern + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - HostGlobPattern that = (HostGlobPattern) o; - return Objects.equals(globPattern, that.globPattern); - } - - @Override - public int hashCode() { - return Objects.hash(globPattern); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/PeerPolicy.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/PeerPolicy.java deleted file mode 100644 index cb39e5e9c3c..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/PeerPolicy.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.List; -import java.util.Optional; - -/** - * @author bjorncs - */ -public record PeerPolicy(String policyName, Optional description, CapabilitySet capabilities, - List requiredCredentials) { - - public PeerPolicy { - requiredCredentials = List.copyOf(requiredCredentials); - } - - public PeerPolicy(String policyName, List requiredCredentials) { - this(policyName, Optional.empty(), CapabilitySet.all(), requiredCredentials); - } - - public PeerPolicy(String policyName, String description, List requiredCredentials) { - this(policyName, Optional.ofNullable(description), CapabilitySet.all(), requiredCredentials); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/RequiredPeerCredential.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/RequiredPeerCredential.java deleted file mode 100644 index 4c96a2935f8..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/RequiredPeerCredential.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Objects; - -/** - * @author bjorncs - */ -public class RequiredPeerCredential { - - public enum Field { CN, SAN_DNS, SAN_URI } - - private final Field field; - private final Pattern pattern; - - private RequiredPeerCredential(Field field, Pattern pattern) { - this.field = field; - this.pattern = pattern; - } - - public static RequiredPeerCredential of(Field field, String pattern) { - return new RequiredPeerCredential(field, createPattern(field, pattern)); - } - - private static Pattern createPattern(Field field, String pattern) { - switch (field) { - case CN: - case SAN_DNS: - return new HostGlobPattern(pattern); - case SAN_URI: - return new UriGlobPattern(pattern); - default: - throw new IllegalArgumentException("Unknown field: " + field); - } - } - - public Field field() { - return field; - } - - public Pattern pattern() { - return pattern; - } - - @Override - public String toString() { - return "RequiredPeerCredential{" + - "field=" + field + - ", pattern=" + pattern + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RequiredPeerCredential that = (RequiredPeerCredential) o; - return field == that.field && - Objects.equals(pattern, that.pattern); - } - - @Override - public int hashCode() { - return Objects.hash(field, pattern); - } - - public interface Pattern { - String asString(); - boolean matches(String fieldValue); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/UriGlobPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/UriGlobPattern.java deleted file mode 100644 index b2cc0688bb9..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/UriGlobPattern.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import java.util.Objects; - -/** - * Pattern used for matching URIs in X.509 certificate subject alternative names. - * - * @author bjorncs - */ -class UriGlobPattern implements RequiredPeerCredential.Pattern { - - private final GlobPattern globPattern; - - UriGlobPattern(String globPattern) { - this.globPattern = new GlobPattern(globPattern, new char[] {'/'}, false); - } - - @Override public String asString() { return globPattern.asString(); } - - @Override public boolean matches(String fieldValue) { return globPattern.matches(fieldValue); } - - @Override - public String toString() { - return "UriPattern{" + - "pattern='" + globPattern + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - UriGlobPattern that = (UriGlobPattern) o; - return Objects.equals(globPattern, that.globPattern); - } - - @Override - public int hashCode() { - return Objects.hash(globPattern); - } -} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/package-info.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/package-info.java deleted file mode 100644 index 61ce90654f8..00000000000 --- a/security-utils/src/main/java/com/yahoo/security/tls/policy/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@ExportPackage -package com.yahoo.security.tls.policy; - -import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file diff --git a/security-utils/src/test/java/com/yahoo/security/tls/AuthorizedPeersTest.java b/security-utils/src/test/java/com/yahoo/security/tls/AuthorizedPeersTest.java new file mode 100644 index 00000000000..e4c530dbb0b --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/AuthorizedPeersTest.java @@ -0,0 +1,26 @@ +// 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.tls.PeerPolicy; +import com.yahoo.security.tls.RequiredPeerCredential; +import org.junit.Test; + +import java.util.HashSet; + +import static com.yahoo.security.tls.RequiredPeerCredential.Field.CN; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +/** + * @author bjorncs + */ +public class AuthorizedPeersTest { + + @Test(expected = IllegalArgumentException.class) + public void throws_exception_on_peer_policies_with_duplicate_names() { + PeerPolicy peerPolicy1 = new PeerPolicy("duplicate-name", singletonList(RequiredPeerCredential.of(CN, "mycfgserver"))); + PeerPolicy peerPolicy2 = new PeerPolicy("duplicate-name", singletonList(RequiredPeerCredential.of(CN, "myclient"))); + new AuthorizedPeers(new HashSet<>(asList(peerPolicy1, peerPolicy2))); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/CapabilitySetTest.java b/security-utils/src/test/java/com/yahoo/security/tls/CapabilitySetTest.java new file mode 100644 index 00000000000..87b16dbff1f --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/CapabilitySetTest.java @@ -0,0 +1,27 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author bjorncs + */ +class CapabilitySetTest { + + @Test + void contains_all_capabilities() { + SortedSet expectedNames = Arrays.stream(Capability.values()) + .map(Capability::asString) + .collect(Collectors.toCollection(TreeSet::new)); + SortedSet actualNames = CapabilitySet.all().toNames(); + assertEquals(expectedNames, actualNames); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/DefaultTlsContextTest.java b/security-utils/src/test/java/com/yahoo/security/tls/DefaultTlsContextTest.java index 358929606cd..b6c40a0c2e1 100644 --- a/security-utils/src/test/java/com/yahoo/security/tls/DefaultTlsContextTest.java +++ b/security-utils/src/test/java/com/yahoo/security/tls/DefaultTlsContextTest.java @@ -3,9 +3,6 @@ package com.yahoo.security.tls; import com.yahoo.security.KeyUtils; import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.PeerPolicy; -import com.yahoo.security.tls.policy.RequiredPeerCredential; import org.junit.Test; import javax.net.ssl.SSLEngine; diff --git a/security-utils/src/test/java/com/yahoo/security/tls/GlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/GlobPatternTest.java new file mode 100644 index 00000000000..a93bffe6961 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/GlobPatternTest.java @@ -0,0 +1,106 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bjorncs + */ +class GlobPatternTest { + + @Test + public void glob_without_wildcards_matches_entire_string() { + assertMatches("foo", ".", "foo"); + assertNotMatches("foo", ".", "fooo"); + assertNotMatches("foo", ".", "ffoo"); + assertPatternHasRegex("foo", ".", "^\\Qfoo\\E$"); + } + + @Test + public void wildcard_glob_can_match_prefix() { + assertMatches("foo*", ".", "foo"); + assertMatches("foo*", ".", "foobar"); + assertNotMatches("foo*", ".", "ffoo"); + } + + @Test + public void wildcard_glob_can_match_suffix() { + assertMatches("*foo", ".", "foo"); + assertMatches("*foo", ".", "ffoo"); + assertNotMatches("*foo", ".", "fooo"); + } + + @Test + public void wildcard_glob_can_match_substring() { + assertMatches("f*o", ".", "fo"); + assertMatches("f*o", ".", "foo"); + assertMatches("f*o", ".", "ffoo"); + assertNotMatches("f*o", ".", "boo"); + } + + @Test + public void wildcard_glob_does_not_cross_multiple_dot_delimiter_boundaries() { + assertMatches("*.bar.baz", ".", "foo.bar.baz"); + assertMatches("*.bar.baz", ".", ".bar.baz"); + assertNotMatches("*.bar.baz", ".", "zoid.foo.bar.baz"); + assertMatches("foo.*.baz", ".", "foo.bar.baz"); + assertNotMatches("foo.*.baz", ".", "foo.bar.zoid.baz"); + + assertPatternHasRegex("*.bar.baz", ".", "^[^\\Q.\\E]*\\Q.bar.baz\\E$"); + } + + @Test + public void single_char_glob_matches_non_dot_characters() { + assertMatches("f?o", ".", "foo"); + assertNotMatches("f?o", ".", "fooo"); + assertNotMatches("f?o", ".", "ffoo"); + assertNotMatches("f?o", ".", "f.o"); + } + + @Test + public void other_regex_meta_characters_are_matched_as_literal_characters() { + String literals = "<([{\\^-=$!|]})+.>"; + assertMatches(literals, ".", literals); + assertPatternHasRegex(literals, ".", "^\\Q<([{\\^-=$!|]})+.>\\E$"); + } + + @Test + public void handles_patterns_with_multiple_alternative_boundaries() { + assertMatches("https://*.vespa.ai/", "./", "https://docs.vespa.ai/"); + assertMatches("https://vespa.ai/*.world", "./", "https://vespa.ai/hello.world"); + assertNotMatches("https://vespa.ai/*/", "./", "https://vespa.ai/hello.world/"); + assertMatches("https://vespa.ai/*/index.html", "./", "https://vespa.ai/path/index.html"); + } + + private void assertMatches(String pattern, String boundaries, String value) { + GlobPattern p = globPattern(pattern, boundaries); + assertTrue( + p.matches(value), + () -> String.format("Expected '%s' with boundaries '%s' to match '%s'", + pattern, Arrays.toString(p.boundaries()), value)); + } + + private void assertNotMatches(String pattern, String boundaries, String value) { + GlobPattern p = globPattern(pattern, boundaries); + assertFalse( + p.matches(value), + () -> String.format("Expected '%s' with boundaries '%s' to not match '%s'", + pattern, Arrays.toString(p.boundaries()), value)); + } + + private void assertPatternHasRegex(String pattern, String boundaries, String expectedPattern) { + GlobPattern p = globPattern(pattern, boundaries); + assertEquals(expectedPattern, p.regexPattern().pattern()); + } + + private static GlobPattern globPattern(String pattern, String boundaries) { + return new GlobPattern(pattern, boundaries.toCharArray(), true); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/HostGlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/HostGlobPatternTest.java new file mode 100644 index 00000000000..a5628a637f8 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/HostGlobPatternTest.java @@ -0,0 +1,69 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * @author bjorncs + */ +public class HostGlobPatternTest { + + @Test + public void glob_without_wildcards_matches_entire_string() { + assertTrue(globMatches("foo", "foo")); + assertFalse(globMatches("foo", "fooo")); + assertFalse(globMatches("foo", "ffoo")); + } + + @Test + public void wildcard_glob_can_match_prefix() { + assertTrue(globMatches("foo*", "foo")); + assertTrue(globMatches("foo*", "foobar")); + assertFalse(globMatches("foo*", "ffoo")); + } + + @Test + public void wildcard_glob_can_match_suffix() { + assertTrue(globMatches("*foo", "foo")); + assertTrue(globMatches("*foo", "ffoo")); + assertFalse(globMatches("*foo", "fooo")); + } + + @Test + public void wildcard_glob_can_match_substring() { + assertTrue(globMatches("f*o", "fo")); + assertTrue(globMatches("f*o", "foo")); + assertTrue(globMatches("f*o", "ffoo")); + assertFalse(globMatches("f*o", "boo")); + } + + @Test + public void wildcard_glob_does_not_cross_multiple_dot_delimiter_boundaries() { + assertTrue(globMatches("*.bar.baz", "foo.bar.baz")); + assertTrue(globMatches("*.bar.baz", ".bar.baz")); + assertFalse(globMatches("*.bar.baz", "zoid.foo.bar.baz")); + assertTrue(globMatches("foo.*.baz", "foo.bar.baz")); + assertFalse(globMatches("foo.*.baz", "foo.bar.zoid.baz")); + } + + @Test + public void single_char_glob_matches_non_dot_characters() { + assertTrue(globMatches("f?o", "foo")); + assertFalse(globMatches("f?o", "fooo")); + assertFalse(globMatches("f?o", "ffoo")); + assertFalse(globMatches("f?o", "f.o")); + } + + @Test + public void other_regex_meta_characters_are_matched_as_literal_characters() { + assertTrue(globMatches("<([{\\^-=$!|]})+.>", "<([{\\^-=$!|]})+.>")); + } + + private static boolean globMatches(String pattern, String value) { + return new HostGlobPattern(pattern).matches(value); + } +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/PeerAuthorizerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/PeerAuthorizerTest.java new file mode 100644 index 00000000000..42a69fd18b0 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/PeerAuthorizerTest.java @@ -0,0 +1,167 @@ +// 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.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SubjectAlternativeName.Type; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.RequiredPeerCredential.Field; +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static com.yahoo.security.tls.RequiredPeerCredential.Field.CN; +import static com.yahoo.security.tls.RequiredPeerCredential.Field.SAN_DNS; +import static com.yahoo.security.tls.RequiredPeerCredential.Field.SAN_URI; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author bjorncs + */ +public class PeerAuthorizerTest { + + private static final KeyPair KEY_PAIR = KeyUtils.generateKeypair(KeyAlgorithm.EC); + private static final String POLICY_1 = "policy-1", POLICY_2 = "policy-2"; + + @Test + public void certificate_must_match_both_san_and_cn_pattern() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); + PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanRequirement)); + + ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", asList("foo.matching.san", "foo.invalid.san"), emptyList())); + assertAuthorized(result); + assertThat(result.matchedPolicies()).containsOnly(POLICY_1); + + assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList()))); + assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", asList("foo.matching.san", "foo.invalid.san"),emptyList()))); + assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.invalid.san"), emptyList()))); + } + + @Test + public void can_match_multiple_policies() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); + + PeerAuthorizer peerAuthorizer = createPeerAuthorizer( + createPolicy(POLICY_1, cnRequirement, sanRequirement), + createPolicy(POLICY_2, cnRequirement, sanRequirement)); + + ConnectionAuthContext result = peerAuthorizer + .authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.matching.san"), emptyList())); + assertAuthorized(result); + assertThat(result.matchedPolicies()).containsOnly(POLICY_1, POLICY_2); + } + + @Test + public void can_match_subset_of_policies() { + PeerAuthorizer peerAuthorizer = createPeerAuthorizer( + createPolicy(POLICY_1, createRequiredCredential(CN, "*.matching.cn")), + createPolicy(POLICY_2, createRequiredCredential(SAN_DNS, "*.matching.san"))); + + ConnectionAuthContext result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList())); + assertAuthorized(result); + assertThat(result.matchedPolicies()).containsOnly(POLICY_2); + } + + @Test + public void must_match_all_cn_and_san_patterns() { + RequiredPeerCredential cnSuffixRequirement = createRequiredCredential(CN, "*.*.matching.suffix.cn"); + RequiredPeerCredential cnPrefixRequirement = createRequiredCredential(CN, "matching.prefix.*.*.*"); + RequiredPeerCredential sanPrefixRequirement = createRequiredCredential(SAN_DNS, "*.*.matching.suffix.san"); + RequiredPeerCredential sanSuffixRequirement = createRequiredCredential(SAN_DNS, "matching.prefix.*.*.*"); + PeerAuthorizer peerAuthorizer = createPeerAuthorizer( + createPolicy(POLICY_1, cnSuffixRequirement, cnPrefixRequirement, sanPrefixRequirement, sanSuffixRequirement)); + + assertAuthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", singletonList("matching.prefix.matching.suffix.san"), emptyList()))); + assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", singletonList("matching.prefix.invalid.suffix.san"), emptyList()))); + assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("invalid.prefix.matching.suffix.cn", singletonList("matching.prefix.matching.suffix.san"), emptyList()))); + } + + @Test + public void can_match_policy_with_san_uri_pattern() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanUriRequirement = createRequiredCredential(SAN_URI, "myscheme://my/*/uri"); + PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanUriRequirement)); + + ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.irrelevant.san"), singletonList("myscheme://my/matching/uri"))); + assertAuthorized(result); + assertThat(result.matchedPolicies()).containsOnly(POLICY_1); + + assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", emptyList(), singletonList("myscheme://my/nonmatching/url")))); + } + + @Test + public void auth_context_contains_union_of_granted_capabilities_from_policies() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); + + PeerAuthorizer peerAuthorizer = createPeerAuthorizer( + createPolicy(POLICY_1, List.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API), List.of(cnRequirement)), + createPolicy(POLICY_2, List.of(Capability.SLOBROK__API, Capability.CONTENT__SEARCH_API), List.of(sanRequirement))); + + var result = peerAuthorizer + .authorizePeer(createCertificate("foo.matching.cn", List.of("foo.matching.san"), List.of())); + assertAuthorized(result); + assertCapabiltiesGranted(result, Set.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API, Capability.CONTENT__SEARCH_API)); + } + + private static X509Certificate createCertificate(String subjectCn, List sanDns, List sanUri) { + X509CertificateBuilder builder = + X509CertificateBuilder.fromKeypair( + KEY_PAIR, + new X500Principal("CN=" + subjectCn), + Instant.EPOCH, + Instant.EPOCH.plus(100000, ChronoUnit.DAYS), + SHA256_WITH_ECDSA, + BigInteger.ONE); + sanDns.forEach(san -> builder.addSubjectAlternativeName(Type.DNS_NAME, san)); + sanUri.forEach(san -> builder.addSubjectAlternativeName(Type.UNIFORM_RESOURCE_IDENTIFIER, san)); + return builder.build(); + } + + private static RequiredPeerCredential createRequiredCredential(Field field, String pattern) { + return RequiredPeerCredential.of(field, pattern); + } + + private static PeerAuthorizer createPeerAuthorizer(PeerPolicy... policies) { + return new PeerAuthorizer(new AuthorizedPeers(Arrays.stream(policies).collect(toSet()))); + } + + private static PeerPolicy createPolicy(String name, RequiredPeerCredential... requiredCredentials) { + return new PeerPolicy(name, asList(requiredCredentials)); + } + + private static PeerPolicy createPolicy(String name, List caps, List creds) { + return new PeerPolicy(name, Optional.empty(), CapabilitySet.from(caps), creds); + } + + private static void assertAuthorized(ConnectionAuthContext result) { + assertTrue(result.authorized()); + } + + private static void assertUnauthorized(ConnectionAuthContext result) { + assertFalse(result.authorized()); + } + + private static void assertCapabiltiesGranted(ConnectionAuthContext ctx, Set expected) { + assertThat(ctx.capabilities().asSet()).containsOnly(expected.toArray(new Capability[0])); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializerTest.java new file mode 100644 index 00000000000..476ab689903 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/TransportSecurityOptionsJsonSerializerTest.java @@ -0,0 +1,99 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; + +import static com.yahoo.security.tls.RequiredPeerCredential.Field.CN; +import static com.yahoo.security.tls.RequiredPeerCredential.Field.SAN_DNS; +import static com.yahoo.security.tls.RequiredPeerCredential.Field.SAN_URI; +import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class TransportSecurityOptionsJsonSerializerTest { + + @Rule public TemporaryFolder tempDirectory = new TemporaryFolder(); + + private static final Path TEST_CONFIG_FILE = Paths.get("src/test/resources/transport-security-options.json"); + + @Test + public void can_serialize_and_deserialize_transport_security_options() throws IOException { + TransportSecurityOptions options = new TransportSecurityOptions.Builder() + .withCaCertificates(Paths.get("/path/to/ca-certs.pem")) + .withCertificates(Paths.get("/path/to/cert.pem"), Paths.get("/path/to/key.pem")) + .withHostnameValidationDisabled(false) + .withAuthorizedPeers( + new AuthorizedPeers( + new LinkedHashSet<>(Arrays.asList( + new PeerPolicy("cfgserver", "cfgserver policy description", Arrays.asList( + RequiredPeerCredential.of(CN, "mycfgserver"), + RequiredPeerCredential.of(SAN_DNS, "*.suffix.com"), + RequiredPeerCredential.of(SAN_URI, "myscheme://resource/path/"))), + new PeerPolicy("node", Optional.empty(), + CapabilitySet.from(Capability.SLOBROK__API), + Collections.singletonList(RequiredPeerCredential.of(CN, "hostname"))))))) + .build(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TransportSecurityOptionsJsonSerializer serializer = new TransportSecurityOptionsJsonSerializer(); + serializer.serialize(out, options); + TransportSecurityOptions deserializedOptions = serializer.deserialize(new ByteArrayInputStream(out.toByteArray())); + assertEquals(options, deserializedOptions); + Path expectedJsonFile = Paths.get("src/test/resources/transport-security-options-with-authz-rules.json"); + assertJsonEquals(new String(Files.readAllBytes(expectedJsonFile)), out.toString()); + } + + @Test + public void can_serialize_options_without_authorized_peers() throws IOException { + TransportSecurityOptions options = new TransportSecurityOptions.Builder() + .withCertificates(Paths.get("certs.pem"), Paths.get("myhost.key")) + .withCaCertificates(Paths.get("my_cas.pem")) + .withAcceptedCiphers(Arrays.asList("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" , "TLS_AES_256_GCM_SHA384")) + .withAcceptedProtocols(Collections.singletonList("TLSv1.2")) + .withHostnameValidationDisabled(true) + .build(); + File outputFile = tempDirectory.newFile(); + try (OutputStream out = Files.newOutputStream(outputFile.toPath())) { + new TransportSecurityOptionsJsonSerializer().serialize(out, options); + } + String expectedOutput = new String(Files.readAllBytes(TEST_CONFIG_FILE)); + String actualOutput = new String(Files.readAllBytes(outputFile.toPath())); + assertJsonEquals(expectedOutput, actualOutput); + } + + @Test + public void disable_hostname_validation_is_not_serialized_if_false() throws IOException { + TransportSecurityOptions options = new TransportSecurityOptions.Builder() + .withCertificates(Paths.get("certs.pem"), Paths.get("myhost.key")) + .withCaCertificates(Paths.get("my_cas.pem")) + .withHostnameValidationDisabled(false) + .build(); + File outputFile = tempDirectory.newFile(); + try (OutputStream out = Files.newOutputStream(outputFile.toPath())) { + new TransportSecurityOptionsJsonSerializer().serialize(out, options); + } + + String expectedOutput = new String(Files.readAllBytes( + Paths.get("src/test/resources/transport-security-options-with-disable-hostname-validation-set-to-false.json"))); + String actualOutput = new String(Files.readAllBytes(outputFile.toPath())); + assertJsonEquals(expectedOutput, actualOutput); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/UriGlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/UriGlobPatternTest.java new file mode 100644 index 00000000000..4d89d71cf85 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/UriGlobPatternTest.java @@ -0,0 +1,37 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bjorncs + */ +class UriGlobPatternTest { + + @Test + void matches_correctly() { + assertMatches("scheme://hostname/*", "scheme://hostname/mypath"); + assertMatches("scheme://hostname/*/segment2", "scheme://hostname/segment1/segment2"); + assertMatches("scheme://hostname/segment1/*", "scheme://hostname/segment1/segment2"); + assertNotMatches("scheme://hostname/*", "scheme://hostname/segment1/segment2"); + assertMatches("scheme://*/segment1/segment2", "scheme://hostname/segment1/segment2"); + assertMatches("scheme://*.name/", "scheme://host.name/"); + assertNotMatches("scheme://*", "scheme://hostname/"); + assertMatches("scheme://hostname/mypath?query=value", "scheme://hostname/mypath?query=value"); + assertNotMatches("scheme://hostname/?", "scheme://hostname/p"); + } + + private void assertMatches(String pattern, String value) { + assertTrue(new UriGlobPattern(pattern).matches(value), + () -> String.format("Expected '%s' to match '%s'", pattern, value)); + } + + private void assertNotMatches(String pattern, String value) { + assertFalse(new UriGlobPattern(pattern).matches(value), + () -> String.format("Expected '%s' to not match '%s'", pattern, value)); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java deleted file mode 100644 index 3791aed4155..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright Yahoo. 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.KeyAlgorithm; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.SubjectAlternativeName.Type; -import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.Capability; -import com.yahoo.security.tls.policy.CapabilitySet; -import com.yahoo.security.tls.policy.PeerPolicy; -import com.yahoo.security.tls.policy.RequiredPeerCredential; -import com.yahoo.security.tls.policy.RequiredPeerCredential.Field; -import org.junit.Test; - -import javax.security.auth.x500.X500Principal; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.CN; -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.SAN_DNS; -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.SAN_URI; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toSet; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author bjorncs - */ -public class PeerAuthorizerTest { - - private static final KeyPair KEY_PAIR = KeyUtils.generateKeypair(KeyAlgorithm.EC); - private static final String POLICY_1 = "policy-1", POLICY_2 = "policy-2"; - - @Test - public void certificate_must_match_both_san_and_cn_pattern() { - RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); - RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); - PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanRequirement)); - - ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", asList("foo.matching.san", "foo.invalid.san"), emptyList())); - assertAuthorized(result); - assertThat(result.matchedPolicies()).containsOnly(POLICY_1); - - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList()))); - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", asList("foo.matching.san", "foo.invalid.san"),emptyList()))); - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.invalid.san"), emptyList()))); - } - - @Test - public void can_match_multiple_policies() { - RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); - RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); - - PeerAuthorizer peerAuthorizer = createPeerAuthorizer( - createPolicy(POLICY_1, cnRequirement, sanRequirement), - createPolicy(POLICY_2, cnRequirement, sanRequirement)); - - ConnectionAuthContext result = peerAuthorizer - .authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.matching.san"), emptyList())); - assertAuthorized(result); - assertThat(result.matchedPolicies()).containsOnly(POLICY_1, POLICY_2); - } - - @Test - public void can_match_subset_of_policies() { - PeerAuthorizer peerAuthorizer = createPeerAuthorizer( - createPolicy(POLICY_1, createRequiredCredential(CN, "*.matching.cn")), - createPolicy(POLICY_2, createRequiredCredential(SAN_DNS, "*.matching.san"))); - - ConnectionAuthContext result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList())); - assertAuthorized(result); - assertThat(result.matchedPolicies()).containsOnly(POLICY_2); - } - - @Test - public void must_match_all_cn_and_san_patterns() { - RequiredPeerCredential cnSuffixRequirement = createRequiredCredential(CN, "*.*.matching.suffix.cn"); - RequiredPeerCredential cnPrefixRequirement = createRequiredCredential(CN, "matching.prefix.*.*.*"); - RequiredPeerCredential sanPrefixRequirement = createRequiredCredential(SAN_DNS, "*.*.matching.suffix.san"); - RequiredPeerCredential sanSuffixRequirement = createRequiredCredential(SAN_DNS, "matching.prefix.*.*.*"); - PeerAuthorizer peerAuthorizer = createPeerAuthorizer( - createPolicy(POLICY_1, cnSuffixRequirement, cnPrefixRequirement, sanPrefixRequirement, sanSuffixRequirement)); - - assertAuthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", singletonList("matching.prefix.matching.suffix.san"), emptyList()))); - assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", singletonList("matching.prefix.invalid.suffix.san"), emptyList()))); - assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("invalid.prefix.matching.suffix.cn", singletonList("matching.prefix.matching.suffix.san"), emptyList()))); - } - - @Test - public void can_match_policy_with_san_uri_pattern() { - RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); - RequiredPeerCredential sanUriRequirement = createRequiredCredential(SAN_URI, "myscheme://my/*/uri"); - PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, cnRequirement, sanUriRequirement)); - - ConnectionAuthContext result = authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.irrelevant.san"), singletonList("myscheme://my/matching/uri"))); - assertAuthorized(result); - assertThat(result.matchedPolicies()).containsOnly(POLICY_1); - - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", emptyList(), singletonList("myscheme://my/nonmatching/url")))); - } - - @Test - public void auth_context_contains_union_of_granted_capabilities_from_policies() { - RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); - RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); - - PeerAuthorizer peerAuthorizer = createPeerAuthorizer( - createPolicy(POLICY_1, List.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API), List.of(cnRequirement)), - createPolicy(POLICY_2, List.of(Capability.SLOBROK__API, Capability.CONTENT__SEARCH_API), List.of(sanRequirement))); - - var result = peerAuthorizer - .authorizePeer(createCertificate("foo.matching.cn", List.of("foo.matching.san"), List.of())); - assertAuthorized(result); - assertCapabiltiesGranted(result, Set.of(Capability.SLOBROK__API, Capability.CONTENT__DOCUMENT_API, Capability.CONTENT__SEARCH_API)); - } - - private static X509Certificate createCertificate(String subjectCn, List sanDns, List sanUri) { - X509CertificateBuilder builder = - X509CertificateBuilder.fromKeypair( - KEY_PAIR, - new X500Principal("CN=" + subjectCn), - Instant.EPOCH, - Instant.EPOCH.plus(100000, ChronoUnit.DAYS), - SHA256_WITH_ECDSA, - BigInteger.ONE); - sanDns.forEach(san -> builder.addSubjectAlternativeName(Type.DNS_NAME, san)); - sanUri.forEach(san -> builder.addSubjectAlternativeName(Type.UNIFORM_RESOURCE_IDENTIFIER, san)); - return builder.build(); - } - - private static RequiredPeerCredential createRequiredCredential(Field field, String pattern) { - return RequiredPeerCredential.of(field, pattern); - } - - private static PeerAuthorizer createPeerAuthorizer(PeerPolicy... policies) { - return new PeerAuthorizer(new AuthorizedPeers(Arrays.stream(policies).collect(toSet()))); - } - - private static PeerPolicy createPolicy(String name, RequiredPeerCredential... requiredCredentials) { - return new PeerPolicy(name, asList(requiredCredentials)); - } - - private static PeerPolicy createPolicy(String name, List caps, List creds) { - return new PeerPolicy(name, Optional.empty(), CapabilitySet.from(caps), creds); - } - - private static void assertAuthorized(ConnectionAuthContext result) { - assertTrue(result.authorized()); - } - - private static void assertUnauthorized(ConnectionAuthContext result) { - assertFalse(result.authorized()); - } - - private static void assertCapabiltiesGranted(ConnectionAuthContext ctx, Set expected) { - assertThat(ctx.capabilities().asSet()).containsOnly(expected.toArray(new Capability[0])); - } - -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java deleted file mode 100644 index 852d6ae94c9..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.json; - -import com.yahoo.security.tls.TransportSecurityOptions; -import com.yahoo.security.tls.policy.AuthorizedPeers; -import com.yahoo.security.tls.policy.Capability; -import com.yahoo.security.tls.policy.CapabilitySet; -import com.yahoo.security.tls.policy.PeerPolicy; -import com.yahoo.security.tls.policy.RequiredPeerCredential; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Optional; - -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.CN; -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.SAN_DNS; -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.SAN_URI; -import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; -import static org.junit.Assert.assertEquals; - -/** - * @author bjorncs - */ -public class TransportSecurityOptionsJsonSerializerTest { - - @Rule public TemporaryFolder tempDirectory = new TemporaryFolder(); - - private static final Path TEST_CONFIG_FILE = Paths.get("src/test/resources/transport-security-options.json"); - - @Test - public void can_serialize_and_deserialize_transport_security_options() throws IOException { - TransportSecurityOptions options = new TransportSecurityOptions.Builder() - .withCaCertificates(Paths.get("/path/to/ca-certs.pem")) - .withCertificates(Paths.get("/path/to/cert.pem"), Paths.get("/path/to/key.pem")) - .withHostnameValidationDisabled(false) - .withAuthorizedPeers( - new AuthorizedPeers( - new LinkedHashSet<>(Arrays.asList( - new PeerPolicy("cfgserver", "cfgserver policy description", Arrays.asList( - RequiredPeerCredential.of(CN, "mycfgserver"), - RequiredPeerCredential.of(SAN_DNS, "*.suffix.com"), - RequiredPeerCredential.of(SAN_URI, "myscheme://resource/path/"))), - new PeerPolicy("node", Optional.empty(), - CapabilitySet.from(Capability.SLOBROK__API), - Collections.singletonList(RequiredPeerCredential.of(CN, "hostname"))))))) - .build(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - TransportSecurityOptionsJsonSerializer serializer = new TransportSecurityOptionsJsonSerializer(); - serializer.serialize(out, options); - TransportSecurityOptions deserializedOptions = serializer.deserialize(new ByteArrayInputStream(out.toByteArray())); - assertEquals(options, deserializedOptions); - Path expectedJsonFile = Paths.get("src/test/resources/transport-security-options-with-authz-rules.json"); - assertJsonEquals(new String(Files.readAllBytes(expectedJsonFile)), out.toString()); - } - - @Test - public void can_serialize_options_without_authorized_peers() throws IOException { - TransportSecurityOptions options = new TransportSecurityOptions.Builder() - .withCertificates(Paths.get("certs.pem"), Paths.get("myhost.key")) - .withCaCertificates(Paths.get("my_cas.pem")) - .withAcceptedCiphers(Arrays.asList("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" , "TLS_AES_256_GCM_SHA384")) - .withAcceptedProtocols(Collections.singletonList("TLSv1.2")) - .withHostnameValidationDisabled(true) - .build(); - File outputFile = tempDirectory.newFile(); - try (OutputStream out = Files.newOutputStream(outputFile.toPath())) { - new TransportSecurityOptionsJsonSerializer().serialize(out, options); - } - String expectedOutput = new String(Files.readAllBytes(TEST_CONFIG_FILE)); - String actualOutput = new String(Files.readAllBytes(outputFile.toPath())); - assertJsonEquals(expectedOutput, actualOutput); - } - - @Test - public void disable_hostname_validation_is_not_serialized_if_false() throws IOException { - TransportSecurityOptions options = new TransportSecurityOptions.Builder() - .withCertificates(Paths.get("certs.pem"), Paths.get("myhost.key")) - .withCaCertificates(Paths.get("my_cas.pem")) - .withHostnameValidationDisabled(false) - .build(); - File outputFile = tempDirectory.newFile(); - try (OutputStream out = Files.newOutputStream(outputFile.toPath())) { - new TransportSecurityOptionsJsonSerializer().serialize(out, options); - } - - String expectedOutput = new String(Files.readAllBytes( - Paths.get("src/test/resources/transport-security-options-with-disable-hostname-validation-set-to-false.json"))); - String actualOutput = new String(Files.readAllBytes(outputFile.toPath())); - assertJsonEquals(expectedOutput, actualOutput); - } - -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/policy/AuthorizedPeersTest.java b/security-utils/src/test/java/com/yahoo/security/tls/policy/AuthorizedPeersTest.java deleted file mode 100644 index 3ad826d3996..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/policy/AuthorizedPeersTest.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import org.junit.Test; - -import java.util.HashSet; - -import static com.yahoo.security.tls.policy.RequiredPeerCredential.Field.CN; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; - -/** - * @author bjorncs - */ -public class AuthorizedPeersTest { - - @Test(expected = IllegalArgumentException.class) - public void throws_exception_on_peer_policies_with_duplicate_names() { - PeerPolicy peerPolicy1 = new PeerPolicy("duplicate-name", singletonList(RequiredPeerCredential.of(CN, "mycfgserver"))); - PeerPolicy peerPolicy2 = new PeerPolicy("duplicate-name", singletonList(RequiredPeerCredential.of(CN, "myclient"))); - new AuthorizedPeers(new HashSet<>(asList(peerPolicy1, peerPolicy2))); - } - -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/policy/CapabilitySetTest.java b/security-utils/src/test/java/com/yahoo/security/tls/policy/CapabilitySetTest.java deleted file mode 100644 index 429e5b24513..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/policy/CapabilitySetTest.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * @author bjorncs - */ -class CapabilitySetTest { - - @Test - void contains_all_capabilities() { - SortedSet expectedNames = Arrays.stream(Capability.values()) - .map(Capability::asString) - .collect(Collectors.toCollection(TreeSet::new)); - SortedSet actualNames = CapabilitySet.all().toNames(); - assertEquals(expectedNames, actualNames); - } - -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/policy/GlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/policy/GlobPatternTest.java deleted file mode 100644 index 4350aa2b0a9..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/policy/GlobPatternTest.java +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author bjorncs - */ -class GlobPatternTest { - - @Test - public void glob_without_wildcards_matches_entire_string() { - assertMatches("foo", ".", "foo"); - assertNotMatches("foo", ".", "fooo"); - assertNotMatches("foo", ".", "ffoo"); - assertPatternHasRegex("foo", ".", "^\\Qfoo\\E$"); - } - - @Test - public void wildcard_glob_can_match_prefix() { - assertMatches("foo*", ".", "foo"); - assertMatches("foo*", ".", "foobar"); - assertNotMatches("foo*", ".", "ffoo"); - } - - @Test - public void wildcard_glob_can_match_suffix() { - assertMatches("*foo", ".", "foo"); - assertMatches("*foo", ".", "ffoo"); - assertNotMatches("*foo", ".", "fooo"); - } - - @Test - public void wildcard_glob_can_match_substring() { - assertMatches("f*o", ".", "fo"); - assertMatches("f*o", ".", "foo"); - assertMatches("f*o", ".", "ffoo"); - assertNotMatches("f*o", ".", "boo"); - } - - @Test - public void wildcard_glob_does_not_cross_multiple_dot_delimiter_boundaries() { - assertMatches("*.bar.baz", ".", "foo.bar.baz"); - assertMatches("*.bar.baz", ".", ".bar.baz"); - assertNotMatches("*.bar.baz", ".", "zoid.foo.bar.baz"); - assertMatches("foo.*.baz", ".", "foo.bar.baz"); - assertNotMatches("foo.*.baz", ".", "foo.bar.zoid.baz"); - - assertPatternHasRegex("*.bar.baz", ".", "^[^\\Q.\\E]*\\Q.bar.baz\\E$"); - } - - @Test - public void single_char_glob_matches_non_dot_characters() { - assertMatches("f?o", ".", "foo"); - assertNotMatches("f?o", ".", "fooo"); - assertNotMatches("f?o", ".", "ffoo"); - assertNotMatches("f?o", ".", "f.o"); - } - - @Test - public void other_regex_meta_characters_are_matched_as_literal_characters() { - String literals = "<([{\\^-=$!|]})+.>"; - assertMatches(literals, ".", literals); - assertPatternHasRegex(literals, ".", "^\\Q<([{\\^-=$!|]})+.>\\E$"); - } - - @Test - public void handles_patterns_with_multiple_alternative_boundaries() { - assertMatches("https://*.vespa.ai/", "./", "https://docs.vespa.ai/"); - assertMatches("https://vespa.ai/*.world", "./", "https://vespa.ai/hello.world"); - assertNotMatches("https://vespa.ai/*/", "./", "https://vespa.ai/hello.world/"); - assertMatches("https://vespa.ai/*/index.html", "./", "https://vespa.ai/path/index.html"); - } - - private void assertMatches(String pattern, String boundaries, String value) { - GlobPattern p = globPattern(pattern, boundaries); - assertTrue( - p.matches(value), - () -> String.format("Expected '%s' with boundaries '%s' to match '%s'", - pattern, Arrays.toString(p.boundaries()), value)); - } - - private void assertNotMatches(String pattern, String boundaries, String value) { - GlobPattern p = globPattern(pattern, boundaries); - assertFalse( - p.matches(value), - () -> String.format("Expected '%s' with boundaries '%s' to not match '%s'", - pattern, Arrays.toString(p.boundaries()), value)); - } - - private void assertPatternHasRegex(String pattern, String boundaries, String expectedPattern) { - GlobPattern p = globPattern(pattern, boundaries); - assertEquals(expectedPattern, p.regexPattern().pattern()); - } - - private static GlobPattern globPattern(String pattern, String boundaries) { - return new GlobPattern(pattern, boundaries.toCharArray(), true); - } - -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/policy/HostGlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/policy/HostGlobPatternTest.java deleted file mode 100644 index a42eaaf74b0..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/policy/HostGlobPatternTest.java +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - - -/** - * @author bjorncs - */ -public class HostGlobPatternTest { - - @Test - public void glob_without_wildcards_matches_entire_string() { - assertTrue(globMatches("foo", "foo")); - assertFalse(globMatches("foo", "fooo")); - assertFalse(globMatches("foo", "ffoo")); - } - - @Test - public void wildcard_glob_can_match_prefix() { - assertTrue(globMatches("foo*", "foo")); - assertTrue(globMatches("foo*", "foobar")); - assertFalse(globMatches("foo*", "ffoo")); - } - - @Test - public void wildcard_glob_can_match_suffix() { - assertTrue(globMatches("*foo", "foo")); - assertTrue(globMatches("*foo", "ffoo")); - assertFalse(globMatches("*foo", "fooo")); - } - - @Test - public void wildcard_glob_can_match_substring() { - assertTrue(globMatches("f*o", "fo")); - assertTrue(globMatches("f*o", "foo")); - assertTrue(globMatches("f*o", "ffoo")); - assertFalse(globMatches("f*o", "boo")); - } - - @Test - public void wildcard_glob_does_not_cross_multiple_dot_delimiter_boundaries() { - assertTrue(globMatches("*.bar.baz", "foo.bar.baz")); - assertTrue(globMatches("*.bar.baz", ".bar.baz")); - assertFalse(globMatches("*.bar.baz", "zoid.foo.bar.baz")); - assertTrue(globMatches("foo.*.baz", "foo.bar.baz")); - assertFalse(globMatches("foo.*.baz", "foo.bar.zoid.baz")); - } - - @Test - public void single_char_glob_matches_non_dot_characters() { - assertTrue(globMatches("f?o", "foo")); - assertFalse(globMatches("f?o", "fooo")); - assertFalse(globMatches("f?o", "ffoo")); - assertFalse(globMatches("f?o", "f.o")); - } - - @Test - public void other_regex_meta_characters_are_matched_as_literal_characters() { - assertTrue(globMatches("<([{\\^-=$!|]})+.>", "<([{\\^-=$!|]})+.>")); - } - - private static boolean globMatches(String pattern, String value) { - return new HostGlobPattern(pattern).matches(value); - } -} diff --git a/security-utils/src/test/java/com/yahoo/security/tls/policy/UriGlobPatternTest.java b/security-utils/src/test/java/com/yahoo/security/tls/policy/UriGlobPatternTest.java deleted file mode 100644 index c60c782da14..00000000000 --- a/security-utils/src/test/java/com/yahoo/security/tls/policy/UriGlobPatternTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.security.tls.policy; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author bjorncs - */ -class UriGlobPatternTest { - - @Test - void matches_correctly() { - assertMatches("scheme://hostname/*", "scheme://hostname/mypath"); - assertMatches("scheme://hostname/*/segment2", "scheme://hostname/segment1/segment2"); - assertMatches("scheme://hostname/segment1/*", "scheme://hostname/segment1/segment2"); - assertNotMatches("scheme://hostname/*", "scheme://hostname/segment1/segment2"); - assertMatches("scheme://*/segment1/segment2", "scheme://hostname/segment1/segment2"); - assertMatches("scheme://*.name/", "scheme://host.name/"); - assertNotMatches("scheme://*", "scheme://hostname/"); - assertMatches("scheme://hostname/mypath?query=value", "scheme://hostname/mypath?query=value"); - assertNotMatches("scheme://hostname/?", "scheme://hostname/p"); - } - - private void assertMatches(String pattern, String value) { - assertTrue(new UriGlobPattern(pattern).matches(value), - () -> String.format("Expected '%s' to match '%s'", pattern, value)); - } - - private void assertNotMatches(String pattern, String value) { - assertFalse(new UriGlobPattern(pattern).matches(value), - () -> String.format("Expected '%s' to not match '%s'", pattern, value)); - } - -} -- cgit v1.2.3