From 96f5cb0fe8b72b5c322f6d8b022a51ec4ef8788d Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Tue, 16 Jan 2018 16:14:26 +0100 Subject: Move Athenz types from controller-api to vespa-athenz --- .../com/yahoo/vespa/athenz/api/AthenzIdentity.java | 14 ++++ .../athenz/api/AthenzIdentityCertificate.java | 27 +++++++ .../yahoo/vespa/athenz/api/AthenzPrincipal.java | 62 ++++++++++++++++ .../yahoo/vespa/athenz/api/AthenzPublicKey.java | 49 +++++++++++++ .../vespa/athenz/api/AthenzRoleCertificate.java | 27 +++++++ .../com/yahoo/vespa/athenz/api/AthenzService.java | 52 ++++++++++++++ .../com/yahoo/vespa/athenz/api/AthenzUser.java | 51 +++++++++++++ .../java/com/yahoo/vespa/athenz/api/NToken.java | 36 ++++++++++ .../java/com/yahoo/vespa/athenz/api/ZToken.java | 36 ++++++++++ .../yahoo/vespa/athenz/utils/AthenzIdentities.java | 67 +++++++++++++++++ .../vespa/athenz/utils/AthenzIdentityVerifier.java | 45 ++++++++++++ .../com/yahoo/vespa/athenz/utils/package-info.java | 9 +++ .../yahoo/vespa/athenz/api/AthenzDomainTest.java | 56 +++++++++++++++ .../vespa/athenz/utils/AthenzIdentitiesTest.java | 23 ++++++ .../athenz/utils/AthenzIdentityVerifierTest.java | 84 ++++++++++++++++++++++ 15 files changed, 638 insertions(+) create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentity.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleCertificate.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzService.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzUser.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/NToken.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentities.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifier.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/package-info.java create mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzDomainTest.java create mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentitiesTest.java create mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifierTest.java (limited to 'vespa-athenz/src') diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentity.java new file mode 100644 index 00000000000..a02653fbda7 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentity.java @@ -0,0 +1,14 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + + +/** + * @author bjorncs + */ +public interface AthenzIdentity { + AthenzDomain getDomain(); + String getName(); + default String getFullName() { + return getDomain().getName() + "." + getName(); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java new file mode 100644 index 00000000000..0e9e9432790 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzIdentityCertificate.java @@ -0,0 +1,27 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * @author bjorncs + */ +public class AthenzIdentityCertificate { + + private final X509Certificate certificate; + private final PrivateKey privateKey; + + public AthenzIdentityCertificate(X509Certificate certificate, PrivateKey privateKey) { + this.certificate = certificate; + this.privateKey = privateKey; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java new file mode 100644 index 00000000000..e96f5bd72d4 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPrincipal.java @@ -0,0 +1,62 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.security.Principal; +import java.util.Objects; +import java.util.Optional; + +/** + * @author bjorncs + */ +public class AthenzPrincipal implements Principal { + + private final AthenzIdentity athenzIdentity; + private final NToken nToken; + + public AthenzPrincipal(AthenzIdentity athenzIdentity) { + this(athenzIdentity, null); + } + + public AthenzPrincipal(AthenzIdentity athenzIdentity, + NToken nToken) { + this.athenzIdentity = athenzIdentity; + this.nToken = nToken; + } + + public AthenzIdentity getIdentity() { + return athenzIdentity; + } + + @Override + public String getName() { + return athenzIdentity.getFullName(); + } + + public AthenzDomain getDomain() { + return athenzIdentity.getDomain(); + } + + public Optional getNToken() { + return Optional.ofNullable(nToken); + } + + @Override + public String toString() { + return "AthenzPrincipal{" + + "athenzIdentity=" + athenzIdentity + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AthenzPrincipal principal = (AthenzPrincipal) o; + return Objects.equals(athenzIdentity, principal.athenzIdentity); + } + + @Override + public int hashCode() { + return Objects.hash(athenzIdentity); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java new file mode 100644 index 00000000000..1c810e3e9c9 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java @@ -0,0 +1,49 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.security.PublicKey; +import java.util.Objects; + +/** + * @author bjorncs + */ +public class AthenzPublicKey { + + private final PublicKey publicKey; + private final String keyId; + + public AthenzPublicKey(PublicKey publicKey, String keyId) { + this.publicKey = publicKey; + this.keyId = keyId; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public String getKeyId() { + return keyId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AthenzPublicKey that = (AthenzPublicKey) o; + return Objects.equals(publicKey, that.publicKey) && + Objects.equals(keyId, that.keyId); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, keyId); + } + + @Override + public String toString() { + return "AthenzPublicKey{" + + "publicKey=" + publicKey + + ", keyId='" + keyId + '\'' + + '}'; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleCertificate.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleCertificate.java new file mode 100644 index 00000000000..ec19e08dc8d --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzRoleCertificate.java @@ -0,0 +1,27 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * @author bjorncs + */ +public class AthenzRoleCertificate { + + private final X509Certificate certificate; + private final PrivateKey privateKey; + + public AthenzRoleCertificate(X509Certificate certificate, PrivateKey privateKey) { + this.certificate = certificate; + this.privateKey = privateKey; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzService.java new file mode 100644 index 00000000000..c566d4fe4af --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzService.java @@ -0,0 +1,52 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class AthenzService implements AthenzIdentity { + + private final AthenzDomain domain; + private final String serviceName; + + public AthenzService(AthenzDomain domain, String serviceName) { + this.domain = domain; + this.serviceName = serviceName; + } + + public AthenzService(String domain, String serviceName) { + this(new AthenzDomain(domain), serviceName); + } + + + @Override + public AthenzDomain getDomain() { + return domain; + } + + @Override + public String getName() { + return serviceName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AthenzService that = (AthenzService) o; + return Objects.equals(domain, that.domain) && + Objects.equals(serviceName, that.serviceName); + } + + @Override + public int hashCode() { + return Objects.hash(domain, serviceName); + } + + @Override + public String toString() { + return String.format("AthenzService(%s)", getFullName()); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzUser.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzUser.java new file mode 100644 index 00000000000..720a5289454 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzUser.java @@ -0,0 +1,51 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import com.yahoo.vespa.athenz.utils.AthenzIdentities; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class AthenzUser implements AthenzIdentity { + private final String userId; + + public AthenzUser(String userId) { + this.userId = userId; + } + + public static AthenzUser fromUserId(String userId) { + return new AthenzUser(userId); + } + + @Override + public AthenzDomain getDomain() { + return AthenzIdentities.USER_PRINCIPAL_DOMAIN; + } + + @Override + public String getName() { + return userId; + } + + @Override + public String toString() { + return "AthenzUser{" + + "userId=" + userId + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AthenzUser that = (AthenzUser) o; + return Objects.equals(userId, that.userId); + } + + @Override + public int hashCode() { + return Objects.hash(userId); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/NToken.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/NToken.java new file mode 100644 index 00000000000..27fc0b553a0 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/NToken.java @@ -0,0 +1,36 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.util.Objects; + +/** + * Represents an Athenz NToken (principal token) + * + * @author bjorncs + */ +public class NToken { + + private final String rawToken; + + public NToken(String rawToken) { + this.rawToken = rawToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NToken nToken = (NToken) o; + return Objects.equals(rawToken, nToken.rawToken); + } + + public String getRawToken() { + return rawToken; + } + + @Override + public int hashCode() { + return Objects.hash(rawToken); + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java new file mode 100644 index 00000000000..ae520e66429 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/ZToken.java @@ -0,0 +1,36 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.api; + +import java.util.Objects; + +/** + * Represents an Athenz ZToken (role token) + * + * @author bjorncs + */ +public class ZToken { + + private final String rawToken; + + public ZToken(String rawToken) { + this.rawToken = rawToken; + } + + public String getRawToken() { + return rawToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ZToken zToken = (ZToken) o; + return Objects.equals(rawToken, zToken.rawToken); + } + + @Override + public int hashCode() { + return Objects.hash(rawToken); + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentities.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentities.java new file mode 100644 index 00000000000..fd34263e387 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentities.java @@ -0,0 +1,67 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.utils; + +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.AthenzUser; + +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; +import java.security.cert.X509Certificate; + +/** + * @author bjorncs + */ +public class AthenzIdentities { + + private AthenzIdentities() {} + + public static final AthenzDomain USER_PRINCIPAL_DOMAIN = new AthenzDomain("user"); + public static final AthenzService ZMS_ATHENZ_SERVICE = new AthenzService("sys.auth", "zms"); + + public static AthenzIdentity from(AthenzDomain domain, String identityName) { + if (domain.equals(USER_PRINCIPAL_DOMAIN)) { + return AthenzUser.fromUserId(identityName); + } else { + return new AthenzService(domain, identityName); + } + } + + public static AthenzIdentity from(String fullName) { + int domainIdentityNameSeparatorIndex = fullName.lastIndexOf('.'); + if (domainIdentityNameSeparatorIndex == -1 + || domainIdentityNameSeparatorIndex == 0 + || domainIdentityNameSeparatorIndex == fullName.length() - 1) { + throw new IllegalArgumentException("Invalid Athenz identity: " + fullName); + } + AthenzDomain domain = new AthenzDomain(fullName.substring(0, domainIdentityNameSeparatorIndex)); + String identityName = fullName.substring(domainIdentityNameSeparatorIndex + 1, fullName.length()); + return from(domain, identityName); + } + + public static AthenzIdentity from(X509Certificate certificate) { + String commonName = getCommonName(certificate); + if (isAthenzRoleIdentity(commonName)) { + throw new IllegalArgumentException("Athenz role certificate not supported"); + } + return from(commonName); + } + + private static boolean isAthenzRoleIdentity(String commonName) { + return commonName.contains(":role."); + } + + private static String getCommonName(X509Certificate certificate) { + try { + String subjectPrincipal = certificate.getSubjectX500Principal().getName(); + return new LdapName(subjectPrincipal).getRdns().stream() + .filter(rdn -> rdn.getType().equalsIgnoreCase("cn")) + .map(rdn -> rdn.getValue().toString()) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Could not find CN in certificate: " + subjectPrincipal)); + } catch (NamingException e) { + throw new IllegalArgumentException("Invalid CN: " + e, e); + } + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifier.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifier.java new file mode 100644 index 00000000000..a73bbb7ed8c --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifier.java @@ -0,0 +1,45 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.utils; + +import com.yahoo.vespa.athenz.api.AthenzIdentity; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link HostnameVerifier} that validates Athenz x509 certificates using the identity in the Common Name attribute. + * + * @author bjorncs + */ +public class AthenzIdentityVerifier implements HostnameVerifier { + + private static final Logger log = Logger.getLogger(AthenzIdentityVerifier.class.getName()); + + private final Set allowedIdentities; + + public AthenzIdentityVerifier(Set allowedIdentities) { + this.allowedIdentities = allowedIdentities; + } + + @Override + public boolean verify(String hostname, SSLSession session) { + try { + X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0]; + return isTrusted(AthenzIdentities.from(cert)); + } catch (SSLPeerUnverifiedException e) { + log.log(Level.WARNING, "Unverified client: " + hostname); + return false; + } + } + + public boolean isTrusted(AthenzIdentity identity) { + return allowedIdentities.contains(identity); + } + +} + diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/package-info.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/package-info.java new file mode 100644 index 00000000000..07fe46a4d2c --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/package-info.java @@ -0,0 +1,9 @@ +// 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.vespa.athenz.utils; + +import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzDomainTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzDomainTest.java new file mode 100644 index 00000000000..c3fa7396569 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/api/AthenzDomainTest.java @@ -0,0 +1,56 @@ +package com.yahoo.vespa.athenz.api; + +import org.junit.Test; + +import java.util.function.Supplier; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author bjorncs + */ +public class AthenzDomainTest { + + @Test + public void domain_can_be_constructed_from_valid_string() { + new AthenzDomain("home.john.my-app"); + } + + @Test + public void invalid_domain_throws_exception() { + assertInvalid(() -> new AthenzDomain("endswithdot.")); + assertInvalid(() -> new AthenzDomain(".startswithdot")); + } + + @Test + public void parent_domain_is_without_name_suffix() { + assertEquals(new AthenzDomain("home.john"), new AthenzDomain("home.john.myapp").getParent()); + } + + @Test + public void domain_name_suffix_is_the_suffix_after_last_dot() { + assertEquals("myapp", new AthenzDomain("home.john.myapp").getNameSuffix()); + } + + @Test + public void domain_without_dot_is_toplevel() { + assertTrue(new AthenzDomain("toplevel").isTopLevelDomain()); + assertFalse(new AthenzDomain("not.toplevel").isTopLevelDomain()); + } + + private static void assertInvalid(Supplier domainCreator) { + try { + AthenzDomain domain = domainCreator.get(); + fail("Expected IllegalArgumentException for domain: " + domain.getName()); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), startsWith("Not a valid domain name")); + } + } + + +} \ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentitiesTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentitiesTest.java new file mode 100644 index 00000000000..5dcc853da5a --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentitiesTest.java @@ -0,0 +1,23 @@ +package com.yahoo.vespa.athenz.utils; + +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzService; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class AthenzIdentitiesTest { + + @Test + public void athenz_identity_is_parsed_from_dot_separated_string() { + AthenzIdentity expectedIdentity = new AthenzService(new AthenzDomain("my.subdomain"), "myservicename"); + String fullName = expectedIdentity.getFullName(); + AthenzIdentity actualIdentity = AthenzIdentities.from(fullName); + assertEquals(expectedIdentity, actualIdentity); + } + +} \ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifierTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifierTest.java new file mode 100644 index 00000000000..dabfc16b024 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/AthenzIdentityVerifierTest.java @@ -0,0 +1,84 @@ +package com.yahoo.vespa.athenz.utils; + +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzService; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Test; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; + +import static java.util.Collections.singleton; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author bjorncs + */ +public class AthenzIdentityVerifierTest { + + @Test + public void verifies_certificate_with_athenz_service_as_common_name() throws Exception { + AthenzIdentity trustedIdentity = new AthenzService("mydomain", "alice"); + AthenzIdentity unknownIdentity = new AthenzService("mydomain", "mallory"); + KeyPair keyPair = createKeyPair(); + AthenzIdentityVerifier verifier = new AthenzIdentityVerifier(singleton(trustedIdentity)); + assertTrue(verifier.verify("hostname", createSslSessionMock(createSelfSignedCertificate(keyPair, trustedIdentity)))); + assertFalse(verifier.verify("hostname", createSslSessionMock(createSelfSignedCertificate(keyPair, unknownIdentity)))); + } + + private static KeyPair createKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(512); + return keyGen.generateKeyPair(); + } + + private static X509Certificate createSelfSignedCertificate(KeyPair keyPair, AthenzIdentity identity) + throws OperatorCreationException, CertIOException, CertificateException { + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); + X500Name x500Name = new X500Name("CN="+ identity.getFullName()); + Instant now = Instant.now(); + Date notBefore = Date.from(now); + Date notAfter = Date.from(now.plus(Duration.ofDays(30))); + + X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder( + x500Name, BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, x500Name, keyPair.getPublic() + ) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()) + .getCertificate(certificateBuilder.build(contentSigner)); + + } + + private static SSLSession createSslSessionMock(X509Certificate certificate) throws SSLPeerUnverifiedException { + SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenReturn(new Certificate[]{certificate}); + return sslSession; + } + +} \ No newline at end of file -- cgit v1.2.3