summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorn.christian@seime.no>2018-11-27 13:41:13 +0100
committerGitHub <noreply@github.com>2018-11-27 13:41:13 +0100
commite12e2d54042b2aeca632ee630f0d67695dfb2f1b (patch)
treebc2ad506caa02ff7ccc5f7f465ba482b38421858
parentd5f4a5f81c8e17b9c86e3dab0e1dcd0c26edd0eb (diff)
parentb0f4447a5f0601b99c612c49b8a433213355acdc (diff)
Merge pull request #7771 from vespa-engine/bjorncs/tls-certificate-validation
Add PeerAuthorizer
-rw-r--r--parent/pom.xml5
-rw-r--r--security-utils/pom.xml5
-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.java138
6 files changed, 286 insertions, 0 deletions
diff --git a/parent/pom.xml b/parent/pom.xml
index 590e89668f0..c60ed3e8d4d 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -688,6 +688,11 @@
<artifactId>tensorflow</artifactId>
<version>${tensorflow.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>3.11.1</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/security-utils/pom.xml b/security-utils/pom.xml
index 6f094f28362..450428c5c14 100644
--- a/security-utils/pom.xml
+++ b/security-utils/pom.xml
@@ -49,6 +49,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
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..ffda4fe3c2b
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/authz/PeerAuthorizerTest.java
@@ -0,0 +1,138 @@
+// 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.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 ROLE_1 = "role-1", ROLE_2 = "role-2", ROLE_3 = "role-3", 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, createRoles(ROLE_1), cnRequirement, sanRequirement));
+
+ AuthorizationResult result = authorizer.authorizePeer(createCertificate("foo.matching.cn", "foo.matching.san", "foo.invalid.san"));
+ 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")));
+ }
+
+ @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);
+ assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1, ROLE_2, ROLE_3);
+ assertThat(result.matchedPolicies()).containsOnly(POLICY_1, POLICY_2);
+ }
+
+ @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);
+ assertThat(result.assumedRoles()).extracting(Role::name).containsOnly(ROLE_1, ROLE_2);
+ 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, 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());
+ }
+
+}