diff options
Diffstat (limited to 'controller-api')
7 files changed, 206 insertions, 69 deletions
diff --git a/controller-api/pom.xml b/controller-api/pom.xml index 5ef130a22ba..543ab24999d 100644 --- a/controller-api/pom.xml +++ b/controller-api/pom.xml @@ -18,24 +18,9 @@ <dependencies> <!-- provided --> - - <dependency> - <groupId>com.yahoo.vespa</groupId> - <artifactId>component</artifactId> - <scope>provided</scope> - <version>${project.version}</version> - </dependency> - - <dependency> - <groupId>com.yahoo.vespa</groupId> - <artifactId>annotations</artifactId> - <scope>provided</scope> - <version>${project.version}</version> - </dependency> - <dependency> <groupId>com.yahoo.vespa</groupId> - <artifactId>vespajlib</artifactId> + <artifactId>container-dev</artifactId> <scope>provided</scope> <version>${project.version}</version> </dependency> @@ -54,56 +39,6 @@ <version>${project.version}</version> </dependency> - <dependency> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-annotations</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-databind</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>com.fasterxml.jackson.datatype</groupId> - <artifactId>jackson-datatype-jdk8</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>org.glassfish.jersey.media</groupId> - <artifactId>jersey-media-multipart</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>javax.servlet</groupId> - <artifactId>javax.servlet-api</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>javax.ws.rs</groupId> - <artifactId>javax.ws.rs-api</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>org.glassfish.jersey.core</groupId> - <artifactId>jersey-server</artifactId> - <version>${jersey2.version}</version> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>com.google.inject</groupId> - <artifactId>guice</artifactId> - <classifier>no_aop</classifier> - <scope>provided</scope> - </dependency> - <!-- compile --> <dependency> @@ -128,6 +63,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..bfaa6c2acda --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzIdentityVerifier.java @@ -0,0 +1,41 @@ +// 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.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(cert); + return allowedIdentities.contains(certificateIdentity); + } catch (SSLPeerUnverifiedException e) { + log.log(Level.WARNING, "Unverified client: " + hostname); + return false; + } + } + +} + diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzPrincipal.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzPrincipal.java index 8279edcd8e6..b31cb4a26bb 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzPrincipal.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzPrincipal.java @@ -5,6 +5,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain; import java.security.Principal; import java.util.Objects; +import java.util.Optional; /** * @author bjorncs @@ -14,6 +15,10 @@ 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; @@ -33,8 +38,8 @@ public class AthenzPrincipal implements Principal { return athenzIdentity.getDomain(); } - public NToken getNToken() { - return nToken; + public Optional<NToken> getNToken() { + return Optional.ofNullable(nToken); } @Override diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtils.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtils.java index 0ed5d86dd7e..04ec0b61614 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtils.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtils.java @@ -4,6 +4,10 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.identifiers.UserId; +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; +import java.security.cert.X509Certificate; + /** * @author bjorncs */ @@ -23,4 +27,40 @@ public class AthenzUtils { } } + public static AthenzIdentity createAthenzIdentity(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 createAthenzIdentity(domain, identityName); + } + + public static AthenzIdentity createAthenzIdentity(X509Certificate certificate) { + String commonName = getCommonName(certificate); + if (isAthenzRoleIdentity(commonName)) { + throw new IllegalArgumentException("Athenz role certificate not supported"); + } + return createAthenzIdentity(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/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/InvalidTokenException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/InvalidTokenException.java index 1df1746b02e..967af1c553f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/InvalidTokenException.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/InvalidTokenException.java @@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.athenz; /** * @author bjorncs */ -public class InvalidTokenException extends Exception { +public class InvalidTokenException extends RuntimeException { public InvalidTokenException(String message) { super(message); } 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 diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtilsTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtilsTest.java new file mode 100644 index 00000000000..f2db74a4c3d --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzUtilsTest.java @@ -0,0 +1,21 @@ +package com.yahoo.vespa.hosted.controller.api.integration.athenz; + +import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class AthenzUtilsTest { + + @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 = AthenzUtils.createAthenzIdentity(fullName); + assertEquals(expectedIdentity, actualIdentity); + } + +}
\ No newline at end of file |