diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-11-26 09:57:06 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-11-26 09:57:06 +0100 |
commit | 3488082ed378f300c40328891e0ce8dcdd8c4475 (patch) | |
tree | f317ca272d4f5ea3ebe7e3801cf1d27fac956cd3 /security-utils | |
parent | 340042594187f907968bac445bf2ae085fdb9d45 (diff) |
Support SAN URI based rules in authorization policies
Diffstat (limited to 'security-utils')
8 files changed, 121 insertions, 19 deletions
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 index a40813be96f..1d74f0a170f 100644 --- 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 @@ -17,6 +17,7 @@ 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; /** @@ -59,6 +60,7 @@ public class PeerAuthorizer { 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: @@ -73,7 +75,7 @@ public class PeerAuthorizer { private static List<String> getSubjectAlternativeNames(X509Certificate peerCertificate) { return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream() - .filter(san -> san.getType() == DNS_NAME || san.getType() == IP_ADDRESS) + .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/json/TransportSecurityOptionsEntity.java b/security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java index 83742950dbc..35eef68cef2 100644 --- 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 @@ -44,5 +44,5 @@ class TransportSecurityOptionsEntity { @JsonProperty("must-match") String matchExpression; } - enum CredentialField { CN, SAN_DNS } + 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 index 49cae9aa7fb..75134e20b68 100644 --- 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 @@ -125,6 +125,7 @@ public class TransportSecurityOptionsJsonSerializer { 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); } } @@ -171,6 +172,7 @@ public class TransportSecurityOptionsJsonSerializer { 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); } } 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 index 1eef3a67521..3ae886fef61 100644 --- 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 @@ -8,7 +8,7 @@ import java.util.Objects; */ public class RequiredPeerCredential { - public enum Field { CN, SAN_DNS } + public enum Field { CN, SAN_DNS, SAN_URI } private final Field field; private final Pattern pattern; @@ -27,6 +27,8 @@ public class RequiredPeerCredential { case CN: case SAN_DNS: return new HostGlobPattern(pattern); + case SAN_URI: + return new UriPattern(pattern); default: throw new IllegalArgumentException("Unknown field: " + field); } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/policy/UriPattern.java b/security-utils/src/main/java/com/yahoo/security/tls/policy/UriPattern.java new file mode 100644 index 00000000000..18f5c0ce2de --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/policy/UriPattern.java @@ -0,0 +1,46 @@ +// Copyright Verizon Media. 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 UriPattern implements RequiredPeerCredential.Pattern { + + private final String pattern; + + UriPattern(String pattern) { + this.pattern = pattern; + } + + @Override public String asString() { return pattern; } + + @Override + public boolean matches(String fieldValue) { + // Only exact match is supported (unlike for host names) + return fieldValue.equals(pattern); + } + + @Override + public String toString() { + return "UriPattern{" + + "pattern='" + pattern + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UriPattern that = (UriPattern) o; + return Objects.equals(pattern, that.pattern); + } + + @Override + public int hashCode() { + return Objects.hash(pattern); + } +} 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 index 2530bfcfb45..4440b964096 100644 --- 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 @@ -3,6 +3,7 @@ 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.PeerPolicy; @@ -18,12 +19,17 @@ 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.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.emptySet; +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; @@ -43,14 +49,14 @@ public class PeerAuthorizerTest { RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san"); PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, createRoles(ROLE_1), cnRequirement, sanRequirement)); - AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", "foo.matching.san", "foo.invalid.san")); + AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", asList("foo.matching.san", "foo.invalid.san"), emptyList())); assertAuthorized(result); assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1); assertThat(result.matchedPolicies()).containsOnly(POLICY_1); - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", "foo.matching.san"))); - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.invalid.cn", "foo.matching.san", "foo.invalid.san"))); - assertUnauthorized(authorizer.authorizePeer(createCertificate("foo.matching.cn", "foo.invalid.san"))); + 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 @@ -63,7 +69,7 @@ public class PeerAuthorizerTest { createPolicy(POLICY_2, createRoles(ROLE_2, ROLE_3), cnRequirement, sanRequirement)); AuthorizationResult result = peerAuthorizer - .authorizePeer(createCertificate("foo.matching.cn", "foo.matching.san")); + .authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.matching.san"), emptyList())); assertAuthorized(result); assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1, ROLE_2, ROLE_3); assertThat(result.matchedPolicies()).containsOnly(POLICY_1, POLICY_2); @@ -75,7 +81,7 @@ public class PeerAuthorizerTest { createPolicy(POLICY_1, createRoles(ROLE_1), createRequiredCredential(CN, "*.matching.cn")), createPolicy(POLICY_2, createRoles(ROLE_1, ROLE_2), createRequiredCredential(SAN_DNS, "*.matching.san"))); - AuthorizationResult result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", "foo.matching.san")); + AuthorizationResult result = peerAuthorizer.authorizePeer(createCertificate("foo.invalid.cn", singletonList("foo.matching.san"), emptyList())); assertAuthorized(result); assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1, ROLE_2); assertThat(result.matchedPolicies()).containsOnly(POLICY_2); @@ -90,12 +96,24 @@ public class PeerAuthorizerTest { PeerAuthorizer peerAuthorizer = createPeerAuthorizer( createPolicy(POLICY_1, emptySet(), cnSuffixRequirement, cnPrefixRequirement, sanPrefixRequirement, sanSuffixRequirement)); - assertAuthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", "matching.prefix.matching.suffix.san"))); - assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("matching.prefix.matching.suffix.cn", "matching.prefix.invalid.suffix.san"))); - assertUnauthorized(peerAuthorizer.authorizePeer(createCertificate("invalid.prefix.matching.suffix.cn", "matching.prefix.matching.suffix.san"))); + 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()))); } - private static X509Certificate createCertificate(String subjectCn, String... sanCns) { + @Test + public void can_exact_match_policy_with_san_uri_pattern() { + RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn"); + RequiredPeerCredential sanUriRequirement = createRequiredCredential(SAN_URI, "myscheme://my/exact/uri"); + PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(POLICY_1, createRoles(ROLE_1), cnRequirement, sanUriRequirement)); + + AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", singletonList("foo.irrelevant.san"), singletonList("myscheme://my/exact/uri"))); + assertAuthorized(result); + assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1); + assertThat(result.matchedPolicies()).containsOnly(POLICY_1); + } + + private static X509Certificate createCertificate(String subjectCn, List<String> sanDns, List<String> sanUri) { X509CertificateBuilder builder = X509CertificateBuilder.fromKeypair( KEY_PAIR, @@ -104,9 +122,8 @@ public class PeerAuthorizerTest { Instant.EPOCH.plus(100000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE); - for (String sanCn : sanCns) { - builder.addSubjectAlternativeName(sanCn); - } + sanDns.forEach(san -> builder.addSubjectAlternativeName(Type.DNS_NAME, san)); + sanUri.forEach(san -> builder.addSubjectAlternativeName(Type.UNIFORM_RESOURCE_IDENTIFIER, san)); return builder.build(); } @@ -123,7 +140,7 @@ public class PeerAuthorizerTest { } private static PeerPolicy createPolicy(String name, Set<Role> roles, RequiredPeerCredential... requiredCredentials) { - return new PeerPolicy(name, roles, Arrays.asList(requiredCredentials)); + return new PeerPolicy(name, roles, asList(requiredCredentials)); } private static void assertAuthorized(AuthorizationResult result) { 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 index 22df35cedfb..ee1fa12b15f 100644 --- 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 @@ -24,6 +24,7 @@ import java.util.HashSet; 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 java.util.Collections.singleton; import static org.junit.Assert.assertEquals; @@ -38,7 +39,7 @@ public class TransportSecurityOptionsJsonSerializerTest { 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() { + 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")) @@ -48,7 +49,8 @@ public class TransportSecurityOptionsJsonSerializerTest { new HashSet<>(Arrays.asList( new PeerPolicy("cfgserver", "cfgserver policy description", singleton(new Role("myrole")), Arrays.asList( RequiredPeerCredential.of(CN, "mycfgserver"), - RequiredPeerCredential.of(SAN_DNS, "*.suffix.com"))), + RequiredPeerCredential.of(SAN_DNS, "*.suffix.com"), + RequiredPeerCredential.of(SAN_URI, "myscheme://resource/path/"))), new PeerPolicy("node", singleton(new Role("anotherrole")), Collections.singletonList(RequiredPeerCredential.of(CN, "hostname"))))))) .build(); @@ -57,6 +59,8 @@ public class TransportSecurityOptionsJsonSerializerTest { 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 diff --git a/security-utils/src/test/resources/transport-security-options-with-authz-rules.json b/security-utils/src/test/resources/transport-security-options-with-authz-rules.json new file mode 100644 index 00000000000..ea0bee38c8a --- /dev/null +++ b/security-utils/src/test/resources/transport-security-options-with-authz-rules.json @@ -0,0 +1,29 @@ +{ + "files" : { + "private-key" : "/path/to/key.pem", + "certificates" : "/path/to/cert.pem", + "ca-certificates" : "/path/to/ca-certs.pem" + }, + "authorized-peers" : [ { + "required-credentials" : [ { + "field" : "CN", + "must-match" : "mycfgserver" + }, { + "field" : "SAN_DNS", + "must-match" : "*.suffix.com" + }, { + "field" : "SAN_URI", + "must-match" : "myscheme://resource/path/" + } ], + "name" : "cfgserver", + "description" : "cfgserver policy description", + "roles" : [ "myrole" ] + }, { + "required-credentials" : [ { + "field" : "CN", + "must-match" : "hostname" + } ], + "name" : "node", + "roles" : [ "anotherrole" ] + } ] +}
\ No newline at end of file |