summaryrefslogtreecommitdiffstats
path: root/security-utils
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2020-11-26 09:57:06 +0100
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2020-11-26 09:57:06 +0100
commit3488082ed378f300c40328891e0ce8dcdd8c4475 (patch)
treef317ca272d4f5ea3ebe7e3801cf1d27fac956cd3 /security-utils
parent340042594187f907968bac445bf2ae085fdb9d45 (diff)
Support SAN URI based rules in authorization policies
Diffstat (limited to 'security-utils')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java4
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsEntity.java2
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializer.java2
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/policy/RequiredPeerCredential.java4
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/policy/UriPattern.java46
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java45
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/json/TransportSecurityOptionsJsonSerializerTest.java8
-rw-r--r--security-utils/src/test/resources/transport-security-options-with-authz-rules.json29
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