diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2018-11-27 13:41:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-27 13:41:13 +0100 |
commit | e12e2d54042b2aeca632ee630f0d67695dfb2f1b (patch) | |
tree | bc2ad506caa02ff7ccc5f7f465ba482b38421858 | |
parent | d5f4a5f81c8e17b9c86e3dab0e1dcd0c26edd0eb (diff) | |
parent | b0f4447a5f0601b99c612c49b8a433213355acdc (diff) |
Merge pull request #7771 from vespa-engine/bjorncs/tls-certificate-validation
Add PeerAuthorizer
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()); + } + +} |