diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2017-12-14 15:55:02 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2017-12-15 11:25:35 +0100 |
commit | b9eee31ea876a8909c90162294a869bd3478d702 (patch) | |
tree | f8eaef4c8ae1dc9046ea343a80fc00e6bccf89a4 | |
parent | 25e335782b31cb20ef19a36fc4c87e64411756e7 (diff) |
Add hostname verifier that verifies CN in Athenz x509 certificates
3 files changed, 150 insertions, 0 deletions
diff --git a/controller-api/pom.xml b/controller-api/pom.xml index 5ef130a22ba..ff084810301 100644 --- a/controller-api/pom.xml +++ b/controller-api/pom.xml @@ -128,6 +128,19 @@ <scope>test</scope> </dependency> + <!-- Required for AthenzIdentityVerifierTest --> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> <build> diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifier.java new file mode 100644 index 00000000000..9eacbb48ddc --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifier.java @@ -0,0 +1,55 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.athenz; + +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; +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 + */ +// TODO Move to dedicated Athenz bundle +public class AthenzIdentityVerifier implements HostnameVerifier { + + private static final Logger log = Logger.getLogger(AthenzIdentityVerifier.class.getName()); + + private final Set<AthenzIdentity> allowedIdentities; + + public AthenzIdentityVerifier(Set<AthenzIdentity> allowedIdentities) { + this.allowedIdentities = allowedIdentities; + } + + @Override + public boolean verify(String hostname, SSLSession session) { + try { + X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0]; + AthenzIdentity certificateIdentity = AthenzUtils.createAthenzIdentity(getCommonName(cert)); + return allowedIdentities.contains(certificateIdentity); + } catch (SSLPeerUnverifiedException e) { + log.log(Level.WARNING, "Unverified client: " + hostname); + return false; + } + } + + 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/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifierTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifierTest.java new file mode 100644 index 00000000000..88da28fb273 --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifierTest.java @@ -0,0 +1,82 @@ +package com.yahoo.vespa.hosted.controller.api.integration.athenz; + +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 |