summaryrefslogtreecommitdiffstats
path: root/controller-api/src
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2017-12-14 15:55:02 +0100
committerBjørn Christian Seime <bjorncs@oath.com>2017-12-15 11:25:35 +0100
commitb9eee31ea876a8909c90162294a869bd3478d702 (patch)
treef8eaef4c8ae1dc9046ea343a80fc00e6bccf89a4 /controller-api/src
parent25e335782b31cb20ef19a36fc4c87e64411756e7 (diff)
Add hostname verifier that verifies CN in Athenz x509 certificates
Diffstat (limited to 'controller-api/src')
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifier.java55
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifierTest.java82
2 files changed, 137 insertions, 0 deletions
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