summaryrefslogtreecommitdiffstats
path: root/security-utils
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-11-26 16:59:54 +0100
committerBjørn Christian Seime <bjorncs@oath.com>2018-11-26 17:01:48 +0100
commit1d9f0e4803429ce0e62dae0ec0031d95998381d3 (patch)
tree529973b7ceee78a260b5126b58b976bd28805b4e /security-utils
parent0dc30cceffbe24c2bbf8f6a43d0e12ad0a8a8dba (diff)
Add PeerAuthorizer
Diffstat (limited to 'security-utils')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java55
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java75
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java8
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java140
4 files changed, 278 insertions, 0 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java b/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java
new file mode 100644
index 00000000000..bcc2fa0e698
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/AuthorizationResult.java
@@ -0,0 +1,55 @@
+// Copyright 2018 Yahoo Holdings. 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.tls.policy.Role;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @author bjorncs
+ */
+public class AuthorizationResult {
+ private final Set<Role> assumedRoles;
+ private final Set<String> matchedPolicies;
+
+ public AuthorizationResult(Set<Role> assumedRoles, Set<String> matchedPolicies) {
+ this.assumedRoles = Collections.unmodifiableSet(assumedRoles);
+ this.matchedPolicies = Collections.unmodifiableSet(matchedPolicies);
+ }
+
+ public Set<Role> assumedRoles() {
+ return assumedRoles;
+ }
+
+ public Set<String> matchedPolicies() {
+ return matchedPolicies;
+ }
+
+ public boolean succeeded() {
+ return matchedPolicies.size() > 0;
+ }
+
+ @Override
+ public String toString() {
+ return "AuthorizationResult{" +
+ "assumedRoles=" + assumedRoles +
+ ", matchedPolicies=" + matchedPolicies +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AuthorizationResult that = (AuthorizationResult) o;
+ return Objects.equals(assumedRoles, that.assumedRoles) &&
+ Objects.equals(matchedPolicies, that.matchedPolicies);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(assumedRoles, matchedPolicies);
+ }
+}
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
new file mode 100644
index 00000000000..bead32fe309
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/PeerAuthorizer.java
@@ -0,0 +1,75 @@
+// Copyright 2018 Yahoo Holdings. 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.PeerPolicy;
+import com.yahoo.security.tls.policy.RequiredPeerCredential;
+import com.yahoo.security.tls.policy.Role;
+
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME;
+import static com.yahoo.security.SubjectAlternativeName.Type.IP_ADDRESS;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Uses rules from {@link AuthorizedPeers} to evaluate X509 certificates
+ *
+ * @author bjorncs
+ */
+public class PeerAuthorizer {
+ private final AuthorizedPeers authorizedPeers;
+
+ public PeerAuthorizer(AuthorizedPeers authorizedPeers) {
+ this.authorizedPeers = authorizedPeers;
+ }
+
+ public AuthorizationResult authorizePeer(X509Certificate peerCertificate) {
+ Set<Role> assumedRoles = new HashSet<>();
+ Set<String> matchedPolicies = new HashSet<>();
+ String cn = getCommonName(peerCertificate).orElse(null);
+ List<String> sans = getSubjectAlternativeNames(peerCertificate);
+ for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) {
+ if (matchesPolicy(peerPolicy, cn, sans)) {
+ assumedRoles.addAll(peerPolicy.assumedRoles());
+ matchedPolicies.add(peerPolicy.policyName());
+ }
+ }
+ return new AuthorizationResult(assumedRoles, matchedPolicies);
+ }
+
+ private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List<String> sans) {
+ return peerPolicy.requiredCredentials().stream()
+ .allMatch(requiredCredential -> matchesRequiredCredentials(requiredCredential, cn, sans));
+ }
+
+ private static boolean matchesRequiredCredentials(RequiredPeerCredential requiredCredential, String cn, List<String> sans) {
+ switch (requiredCredential.field()) {
+ case CN:
+ return cn != null && requiredCredential.pattern().matches(cn);
+ case SAN_DNS:
+ return sans.stream()
+ .anyMatch(san -> requiredCredential.pattern().matches(san));
+ default:
+ throw new RuntimeException("Unknown field: " + requiredCredential.field());
+ }
+ }
+
+ private static Optional<String> getCommonName(X509Certificate peerCertificate) {
+ return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream()
+ .findFirst();
+ }
+
+ private static List<String> getSubjectAlternativeNames(X509Certificate peerCertificate) {
+ return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream()
+ .filter(san -> san.getType() == DNS_NAME || san.getType() == IP_ADDRESS)
+ .map(SubjectAlternativeName::getValue)
+ .collect(toList());
+ }
+}
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
new file mode 100644
index 00000000000..1379e090d08
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/authz/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. 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/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java
new file mode 100644
index 00000000000..29d7c089d58
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java
@@ -0,0 +1,140 @@
+// Copyright 2018 Yahoo Holdings. 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.X509CertificateBuilder;
+import com.yahoo.security.tls.policy.AuthorizedPeers;
+import com.yahoo.security.tls.policy.HostGlobPattern;
+import com.yahoo.security.tls.policy.PeerPolicy;
+import com.yahoo.security.tls.policy.RequiredPeerCredential;
+import com.yahoo.security.tls.policy.RequiredPeerCredential.Field;
+import com.yahoo.security.tls.policy.Role;
+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.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 java.util.Collections.emptySet;
+import static java.util.stream.Collectors.toSet;
+import static org.junit.Assert.assertEquals;
+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);
+
+ @Test
+ public void certificate_must_match_both_san_and_cn_pattern() {
+ String roleName = "role";
+ String policyName = "policy";
+ RequiredPeerCredential cnRequirement = createRequiredCredential(CN, "*.matching.cn");
+ RequiredPeerCredential sanRequirement = createRequiredCredential(SAN_DNS, "*.matching.san");
+ PeerAuthorizer authorizer = createPeerAuthorizer(createPolicy(policyName, createRoles(roleName), cnRequirement, sanRequirement));
+
+ AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", "foo.matching.san", "foo.invalid.san"));
+ assertAuthorized(result);
+ assertEquals(1, result.assumedRoles().size());
+ assertEquals(roleName, result.assumedRoles().iterator().next().name());
+ assertEquals(1, result.matchedPolicies().size());
+ assertEquals(policyName, result.matchedPolicies().iterator().next());
+
+ 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")));
+ }
+
+ @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", createRoles("role-1", "role-2"), cnRequirement, sanRequirement),
+ createPolicy("policy-2", createRoles("role-2", "role-3"), cnRequirement, sanRequirement));
+
+ AuthorizationResult result = peerAuthorizer
+ .authorizePeer(createCertificate("foo.matching.cn", "foo.matching.san"));
+ assertAuthorized(result);
+ assertEquals(3, result.assumedRoles().size());
+ assertEquals(2, result.matchedPolicies().size());
+ }
+
+ @Test
+ public void can_match_subset_of_policies() {
+ PeerAuthorizer peerAuthorizer = createPeerAuthorizer(
+ 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"));
+ assertAuthorized(result);
+ assertEquals(2, result.assumedRoles().size());
+ assertEquals(1, result.matchedPolicies().size());
+ }
+
+ @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", 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")));
+ }
+
+ private static X509Certificate createCertificate(String subjectCn, String... sanCns) {
+ X509CertificateBuilder builder =
+ X509CertificateBuilder.fromKeypair(
+ KEY_PAIR,
+ new X500Principal("CN=" + subjectCn),
+ Instant.EPOCH,
+ Instant.EPOCH.plus(100000, ChronoUnit.DAYS),
+ SHA256_WITH_ECDSA,
+ BigInteger.ONE);
+ for (String sanCn : sanCns) {
+ builder.addSubjectAlternativeName(sanCn);
+ }
+ return builder.build();
+ }
+
+ private static RequiredPeerCredential createRequiredCredential(Field field, String pattern) {
+ return new RequiredPeerCredential(field, new HostGlobPattern(pattern));
+ }
+
+ private static Set<Role> createRoles(String... roleNames) {
+ return Arrays.stream(roleNames).map(Role::new).collect(toSet());
+ }
+
+ private static PeerAuthorizer createPeerAuthorizer(PeerPolicy... policies) {
+ return new PeerAuthorizer(new AuthorizedPeers(Arrays.stream(policies).collect(toSet())));
+ }
+
+ private static PeerPolicy createPolicy(String name, Set<Role> roles, RequiredPeerCredential... requiredCredentials) {
+ return new PeerPolicy(name, roles, Arrays.asList(requiredCredentials));
+ }
+
+ private static void assertAuthorized(AuthorizationResult result) {
+ assertTrue(result.succeeded());
+ }
+
+ private static void assertUnauthorized(AuthorizationResult result) {
+ assertFalse(result.succeeded());
+ }
+
+}