diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2018-03-21 11:37:21 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-21 11:37:21 +0100 |
commit | de021c008d317b75b59e1d76624cbcb14fe18f27 (patch) | |
tree | 80e0003130e28500d2b7746aa165eeac548094cb | |
parent | db9aff7fd0015b37b9cb034c6c798e90f231fa5f (diff) | |
parent | 8d38ad85f091a88abb8269bae8372ca0444dcabf (diff) |
Merge pull request #5386 from vespa-engine/bjorncs/certificate-builder
Bjorncs/certificate builder
40 files changed, 1007 insertions, 498 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslTrustStoreConfigurator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslTrustStoreConfigurator.java index 376dd2ed4ac..3091321c47a 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslTrustStoreConfigurator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslTrustStoreConfigurator.java @@ -8,32 +8,18 @@ import com.yahoo.jdisc.http.ssl.SslTrustStoreContext; import com.yahoo.log.LogLevel; import com.yahoo.vespa.athenz.tls.KeyStoreBuilder; import com.yahoo.vespa.athenz.tls.KeyStoreType; +import com.yahoo.vespa.athenz.tls.SignatureAlgorithm; +import com.yahoo.vespa.athenz.tls.X509CertificateBuilder; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; -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 javax.security.auth.x500.X500Principal; import java.io.File; -import java.io.IOException; -import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; -import java.security.Provider; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; -import java.util.Date; import java.util.logging.Logger; /** @@ -44,7 +30,6 @@ public class AthenzSslTrustStoreConfigurator implements SslTrustStoreConfigurato private static final Logger log = Logger.getLogger(AthenzSslTrustStoreConfigurator.class.getName()); private static final String CERTIFICATE_ALIAS = "cfgselfsigned"; - private static final Provider provider = new BouncyCastleProvider(); private final KeyStore trustStore; @Inject @@ -89,29 +74,20 @@ public class AthenzSslTrustStoreConfigurator implements SslTrustStoreConfigurato return keyProvider.getKeyPair(zoneConfig.secretVersion()); } - private static X509Certificate createSelfSignedCertificate(KeyPair keyPair, ConfigserverConfig config) - throws IOException, CertificateException, OperatorCreationException { - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); - X500Name x500Name = new X500Name("CN="+ config.loadBalancerAddress()); + private static X509Certificate createSelfSignedCertificate(KeyPair keyPair, ConfigserverConfig config) { + X500Principal subject = new X500Principal("CN="+ config.loadBalancerAddress()); Instant now = Instant.now(); - Date notBefore = Date.from(now); - Date notAfter = Date.from(now.plus(Duration.ofDays(30))); - - GeneralNames generalNames = new GeneralNames( - config.zookeeperserver().stream() - .map(server -> new GeneralName(GeneralName.dNSName, server.hostname())) - .toArray(GeneralName[]::new)); - - X509v3CertificateBuilder certificateBuilder = - new JcaX509v3CertificateBuilder( - x500Name, BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, x500Name, keyPair.getPublic() - ) - .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) - .addExtension(Extension.subjectAlternativeName, false, generalNames); - - return new JcaX509CertificateConverter() - .setProvider(provider) - .getCertificate(certificateBuilder.build(contentSigner)); + X509CertificateBuilder builder = X509CertificateBuilder + .fromKeypair( + keyPair, + subject, + now, + now.plus(Duration.ofDays(30)), + SignatureAlgorithm.SHA256_WITH_RSA, + now.toEpochMilli()) + .setBasicConstraints(true, true); + config.zookeeperserver().forEach(server -> builder.addSubjectAlternativeName(server.hostname())); + return builder.build(); } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java index 25733bf0075..cfef2bc0e33 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSerializedPayload.java @@ -7,12 +7,9 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.util.io.pem.PemObject; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import java.io.IOException; -import java.io.StringWriter; -import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; /** @@ -56,13 +53,7 @@ public class CertificateSerializedPayload { @Override public void serialize( X509Certificate certificate, JsonGenerator gen, SerializerProvider serializers) throws IOException { - try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { - pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded())); - pemWriter.flush(); - gen.writeString(stringWriter.toString()); - } catch (CertificateEncodingException e) { - throw new RuntimeException("Failed to encode X509Certificate", e); - } + gen.writeString(X509CertificateUtils.toPem(certificate)); } } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java index f6f6bb1dbca..7b4a599d5dd 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSigner.java @@ -6,41 +6,23 @@ import com.google.inject.Inject; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.provision.Zone; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.athenz.tls.Extension; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.SignatureAlgorithm; +import com.yahoo.vespa.athenz.tls.X509CertificateBuilder; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERUTF8String; -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.x500.AttributeTypeAndValue; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.Extensions; -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.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; - -import java.math.BigInteger; + +import javax.security.auth.x500.X500Principal; import java.security.PrivateKey; -import java.security.Provider; -import java.security.PublicKey; import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.Enumeration; +import java.time.Instant; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; -import java.util.stream.Stream; import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig; @@ -56,16 +38,13 @@ public class CertificateSigner { private static final Logger log = Logger.getLogger(CertificateSigner.class.getName()); - static final String SIGNER_ALGORITHM = "SHA256withRSA"; + static final SignatureAlgorithm SIGNER_ALGORITHM = SignatureAlgorithm.SHA256_WITH_RSA; static final Duration CERTIFICATE_EXPIRATION = Duration.ofDays(30); - private static final List<ASN1ObjectIdentifier> ILLEGAL_EXTENSIONS = ImmutableList.of( - Extension.basicConstraints, Extension.subjectAlternativeName); - - private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); - private final Provider provider = new BouncyCastleProvider(); + private static final List<Extension> ILLEGAL_EXTENSIONS = ImmutableList.of( + Extension.BASIC_CONSTRAINS, Extension.SUBJECT_ALTERNATIVE_NAMES); private final PrivateKey caPrivateKey; - private final X500Name issuer; + private final X500Principal issuer; private final Clock clock; @Inject @@ -78,7 +57,7 @@ public class CertificateSigner { CertificateSigner(PrivateKey caPrivateKey, String loadBalancerAddress, Clock clock) { this.caPrivateKey = caPrivateKey; - this.issuer = new X500Name("CN=" + loadBalancerAddress); + this.issuer = new X500Principal("CN=" + loadBalancerAddress); this.clock = clock; } @@ -89,46 +68,28 @@ public class CertificateSigner { * <li>CSR does not contain any any of the extensions in {@code ILLEGAL_EXTENSIONS}</li> * </ul> */ - X509Certificate generateX509Certificate(PKCS10CertificationRequest certReq, String remoteHostname) { - verifyCertificateCommonName(certReq.getSubject(), remoteHostname); - verifyCertificateExtensions(certReq); - - Date notBefore = Date.from(clock.instant()); - Date notAfter = Date.from(clock.instant().plus(CERTIFICATE_EXPIRATION)); + X509Certificate generateX509Certificate(Pkcs10Csr csr, String remoteHostname) { + verifyCertificateCommonName(csr.getSubject(), remoteHostname); + verifyCertificateExtensions(csr); + Instant now = clock.instant(); try { - PublicKey publicKey = new JcaPKCS10CertificationRequest(certReq).getPublicKey(); - X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder( - issuer, BigInteger.valueOf(clock.millis()), notBefore, notAfter, certReq.getSubject(), publicKey) - - // Set Basic Constraints to false - .addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); - - ContentSigner caSigner = new JcaContentSignerBuilder(SIGNER_ALGORITHM) - .setProvider(provider) - .build(caPrivateKey); - - return certificateConverter - .setProvider(provider) - .getCertificate(caBuilder.build(caSigner)); + return X509CertificateBuilder.fromCsr(csr, issuer, now, now.plus(CERTIFICATE_EXPIRATION), caPrivateKey, SIGNER_ALGORITHM, now.toEpochMilli()) + .setBasicConstraints(true, false) + .build(); } catch (Exception ex) { log.log(LogLevel.ERROR, "Failed to generate X509 Certificate", ex); - throw new RuntimeException("Failed to generate X509 Certificate"); + throw new RuntimeException("Failed to generate X509 Certificate", ex); } } - static void verifyCertificateCommonName(X500Name subject, String remoteHostname) { - List<AttributeTypeAndValue> attributesAndValues = Arrays.stream(subject.getRDNs()) - .flatMap(rdn -> rdn.isMultiValued() ? - Stream.of(rdn.getTypesAndValues()) : Stream.of(rdn.getFirst())) - .filter(attr -> attr.getType() == BCStyle.CN) - .collect(Collectors.toList()); - - if (attributesAndValues.size() != 1) { + static void verifyCertificateCommonName(X500Principal subject, String remoteHostname) { + List<String> commonNames = X509CertificateUtils.getCommonNames(subject); + if (commonNames.size() != 1) { throw new IllegalArgumentException("Only 1 common name should be set"); } - String actualCommonName = DERUTF8String.getInstance(attributesAndValues.get(0).getValue()).getString(); + String actualCommonName = commonNames.get(0); if (! actualCommonName.equals(remoteHostname)) { throw new IllegalArgumentException("Remote hostname " + remoteHostname + " does not match common name " + actualCommonName); @@ -136,15 +97,12 @@ public class CertificateSigner { } @SuppressWarnings("unchecked") - static void verifyCertificateExtensions(PKCS10CertificationRequest request) { - List<String> illegalExt = Arrays - .stream(request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) - .map(attribute -> Extensions.getInstance(attribute.getAttrValues().getObjectAt(0))) - .flatMap(ext -> Collections.list((Enumeration<ASN1ObjectIdentifier>) ext.oids()).stream()) - .filter(ILLEGAL_EXTENSIONS::contains) - .map(ASN1ObjectIdentifier::getId) + static void verifyCertificateExtensions(Pkcs10Csr csr) { + List<String> extensionOIds = csr.getExtensionOIds(); + List<String> illegalExt = ILLEGAL_EXTENSIONS.stream() + .map(Extension::getOId) + .filter(extensionOIds::contains) .collect(Collectors.toList()); - if (! illegalExt.isEmpty()) { throw new IllegalArgumentException("CSR contains illegal extensions: " + String.join(", ", illegalExt)); } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java index 0c6199efdcb..1dd452866a5 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerResource.java @@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; import com.google.inject.Inject; import com.yahoo.container.jaxrs.annotation.Component; import com.yahoo.log.LogLevel; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BadRequestException; @@ -45,7 +45,7 @@ public class CertificateSignerResource { try { InetAddress addr = InetAddress.getByName(req.getRemoteAddr()); String remoteHostname = addr.getHostName(); - PKCS10CertificationRequest csr = csrPayload.csr; + Pkcs10Csr csr = csrPayload.csr; log.log(LogLevel.DEBUG, "Certification request from " + remoteHostname + ": " + csr); X509Certificate certificate = certificateSigner.generateX509Certificate(csr, remoteHostname); return new CertificateSerializedPayload(certificate); diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java index f56214513aa..375a4c3e17d 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CsrSerializedPayload.java @@ -7,11 +7,10 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; import java.io.IOException; -import java.io.StringReader; /** * Contains PEM formatted Certificate Signing Request (CSR) @@ -20,11 +19,11 @@ import java.io.StringReader; */ public class CsrSerializedPayload { - @JsonProperty("csr") public final PKCS10CertificationRequest csr; + @JsonProperty("csr") public final Pkcs10Csr csr; @JsonCreator public CsrSerializedPayload(@JsonProperty("csr") @JsonDeserialize(using = CertificateRequestDeserializer.class) - PKCS10CertificationRequest csr) { + Pkcs10Csr csr) { this.csr = csr; } @@ -50,13 +49,11 @@ public class CsrSerializedPayload { '}'; } - public static class CertificateRequestDeserializer extends JsonDeserializer<PKCS10CertificationRequest> { + public static class CertificateRequestDeserializer extends JsonDeserializer<Pkcs10Csr> { @Override - public PKCS10CertificationRequest deserialize( + public Pkcs10Csr deserialize( JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - try (PEMParser pemParser = new PEMParser(new StringReader(jsonParser.getValueAsString()))) { - return (PKCS10CertificationRequest) pemParser.readObject(); - } + return Pkcs10CsrUtils.fromPem(jsonParser.getValueAsString()); } } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java index ca5c776bf3c..35b483affae 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/AthenzCertificateClient.java @@ -1,10 +1,10 @@ // 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.athenz.instanceproviderservice.impl; -import com.yahoo.athenz.auth.util.Crypto; import com.yahoo.athenz.zts.InstanceRefreshRequest; import com.yahoo.athenz.zts.ZTSClient; import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; import javax.net.ssl.SSLContext; @@ -34,7 +34,7 @@ public class AthenzCertificateClient { req.setKeyId(Integer.toString(zoneConfig.secretVersion())); String pemEncoded = ztsClient.postInstanceRefreshRequest(zoneConfig.domain(), zoneConfig.serviceName(), req) .getCertificate(); - return Crypto.loadX509Certificate(pemEncoded); + return X509CertificateUtils.fromPem(pemEncoded); } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CkmsKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CkmsKeyProvider.java index 2f2cd5a8495..41aee29d761 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CkmsKeyProvider.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/CkmsKeyProvider.java @@ -2,9 +2,9 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; import com.google.inject.Inject; -import com.yahoo.athenz.auth.util.Crypto; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.Ckms; +import com.yahoo.vespa.athenz.tls.KeyUtils; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; @@ -58,10 +58,9 @@ public class CkmsKeyProvider implements KeyProvider { } } - // TODO: Consider moving to cryptoutils private KeyPair readKeyPair(int version) { - PrivateKey privateKey = Crypto.loadPrivateKey(ckms.getSecret(secretName, version)); - PublicKey publicKey = Crypto.extractPublicKey(privateKey); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(ckms.getSecret(secretName, version)); + PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); return new KeyPair(publicKey, privateKey); } } diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java index 594bbf77fce..6c624eb1da0 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ca/CertificateSignerTest.java @@ -2,23 +2,16 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice.ca; import com.yahoo.test.ManualClock; -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.asn1.x509.ExtensionsGenerator; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import com.yahoo.vespa.athenz.tls.Extension; +import com.yahoo.vespa.athenz.tls.KeyAlgorithm; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrBuilder; import org.junit.Test; +import javax.security.auth.x500.X500Principal; import java.math.BigInteger; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.cert.X509Certificate; import java.time.Instant; import java.util.Collection; @@ -35,8 +28,6 @@ import static org.junit.Assert.fail; */ public class CertificateSignerTest { - private final KeyPair clientKeyPair = getKeyPair(); - private final long startTime = 1234567890000L; private final KeyPair caKeyPair = getKeyPair(); private final String cfgServerHostname = "cfg1.us-north-1.vespa.domain.tld"; @@ -47,22 +38,21 @@ public class CertificateSignerTest { @Test public void test_signing() throws Exception { - ExtensionsGenerator extGen = new ExtensionsGenerator(); - String subject = "C=NO,OU=Vespa,CN=" + requestersHostname; - PKCS10CertificationRequest request = makeRequest(subject, extGen.generate()); + String subject = String.format("CN=%s,OU=Vespa,C=NO", requestersHostname); + Pkcs10Csr csr = createCsrBuilder(subject).build(); - X509Certificate certificate = signer.generateX509Certificate(request, requestersHostname); - assertCertificate(certificate, subject, Collections.singleton(Extension.basicConstraints.getId())); + X509Certificate certificate = signer.generateX509Certificate(csr, requestersHostname); + assertCertificate(certificate, subject, Collections.singleton(Extension.BASIC_CONSTRAINS.getOId())); } @Test public void common_name_test() throws Exception { CertificateSigner.verifyCertificateCommonName( - new X500Name("CN=" + requestersHostname), requestersHostname); + new X500Principal("CN=" + requestersHostname), requestersHostname); CertificateSigner.verifyCertificateCommonName( - new X500Name("C=NO,OU=Vespa,CN=" + requestersHostname), requestersHostname); + new X500Principal("C=NO,OU=Vespa,CN=" + requestersHostname), requestersHostname); CertificateSigner.verifyCertificateCommonName( - new X500Name("C=NO+OU=org,CN=" + requestersHostname), requestersHostname); + new X500Principal("C=NO+OU=org,CN=" + requestersHostname), requestersHostname); assertCertificateCommonNameException("C=NO", "Only 1 common name should be set"); assertCertificateCommonNameException("C=US+CN=abc123.domain.tld,C=NO+CN=" + requestersHostname, "Only 1 common name should be set"); @@ -72,26 +62,15 @@ public class CertificateSignerTest { @Test(expected = IllegalArgumentException.class) public void extensions_test_subject_alternative_names() throws Exception { - ExtensionsGenerator extGen = new ExtensionsGenerator(); - extGen.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName[] { - new GeneralName(GeneralName.dNSName, "some.other.domain.tld")})); - PKCS10CertificationRequest request = makeRequest("OU=Vespa", extGen.generate()); - - CertificateSigner.verifyCertificateExtensions(request); - } - - @Test - public void extensions_allowed() throws Exception { - ExtensionsGenerator extGen = new ExtensionsGenerator(); - extGen.addExtension(Extension.certificateIssuer, true, new byte[0]); - PKCS10CertificationRequest request = makeRequest("OU=Vespa", extGen.generate()); - - CertificateSigner.verifyCertificateExtensions(request); + Pkcs10Csr csr = createCsrBuilder("OU=Vespa") + .addSubjectAlternativeName("some.other.domain.tld") + .build(); + CertificateSigner.verifyCertificateExtensions(csr); } private void assertCertificateCommonNameException(String subject, String expectedMessage) { try { - CertificateSigner.verifyCertificateCommonName(new X500Name(subject), requestersHostname); + CertificateSigner.verifyCertificateCommonName(new X500Principal(subject), requestersHostname); fail("Expected to fail"); } catch (IllegalArgumentException e) { assertEquals(expectedMessage, e.getMessage()); @@ -103,8 +82,8 @@ public class CertificateSignerTest { assertEquals(BigInteger.valueOf(startTime), certificate.getSerialNumber()); assertEquals(startTime, certificate.getNotBefore().getTime()); assertEquals(startTime + CertificateSigner.CERTIFICATE_EXPIRATION.toMillis(), certificate.getNotAfter().getTime()); - assertEquals(CertificateSigner.SIGNER_ALGORITHM, certificate.getSigAlgName()); - assertEquals(expectedSubjectName, certificate.getSubjectDN().getName()); + assertEquals(CertificateSigner.SIGNER_ALGORITHM.getAlgorithmName(), certificate.getSigAlgName()); + assertEquals(new X500Principal(expectedSubjectName), certificate.getSubjectX500Principal()); assertEquals("CN=" + cfgServerHostname, certificate.getIssuerX500Principal().getName()); Set<String> extensions = Stream.of(certificate.getNonCriticalExtensionOIDs(), @@ -116,20 +95,11 @@ public class CertificateSignerTest { certificate.verify(caKeyPair.getPublic()); } - private PKCS10CertificationRequest makeRequest(String subject, Extensions extensions) throws Exception { - PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder( - new X500Name(subject), clientKeyPair.getPublic()); - builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensions); - - ContentSigner signGen = new JcaContentSignerBuilder(CertificateSigner.SIGNER_ALGORITHM).build(caKeyPair.getPrivate()); - return builder.build(signGen); + private Pkcs10CsrBuilder createCsrBuilder(String subject) { + return Pkcs10CsrBuilder.fromKeypair(new X500Principal(subject), caKeyPair, CertificateSigner.SIGNER_ALGORITHM); } private static KeyPair getKeyPair() { - try { - return KeyPairGenerator.getInstance("RSA").genKeyPair(); - } catch (Exception e) { - throw new RuntimeException(e); - } + return KeyUtils.generateKeypair(KeyAlgorithm.RSA); } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java index 110dbe9c9b3..bdd4a8c0b9d 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java @@ -7,11 +7,9 @@ import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresher; import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import java.security.Security; import java.util.Collections; import java.util.Optional; @@ -28,8 +26,6 @@ public class SslConfigServerApiImpl implements ConfigServerApi { private final Optional<ConfigServerKeyStoreRefresher> keyStoreRefresher; public SslConfigServerApiImpl(Environment environment) { - Security.addProvider(new BouncyCastleProvider()); - this.environment = environment; // At this point we don't know the state of the keystore, it may not exist at all, or the keystore diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java index e7148754fde..94bff3c9633 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java @@ -7,13 +7,9 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.openssl.PEMParser; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import java.io.IOException; -import java.io.StringReader; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; /** @@ -58,12 +54,7 @@ public class CertificateSerializedPayload { @Override public X509Certificate deserialize( JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - try (PEMParser pemParser = new PEMParser(new StringReader(jsonParser.getValueAsString()))) { - X509CertificateHolder x509CertificateHolder = (X509CertificateHolder) pemParser.readObject(); - return new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); - } catch (CertificateException e) { - throw new RuntimeException("Failed to deserialize X509Certificate", e); - } + return X509CertificateUtils.fromPem(jsonParser.getValueAsString()); } } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java index a9db96c2a77..beb1acd9e5a 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java @@ -3,24 +3,19 @@ package com.yahoo.vespa.hosted.node.admin.configserver.certificate; import com.yahoo.log.LogLevel; import com.yahoo.net.HostName; +import com.yahoo.vespa.athenz.tls.KeyAlgorithm; import com.yahoo.vespa.athenz.tls.KeyStoreBuilder; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrBuilder; +import com.yahoo.vespa.athenz.tls.SignatureAlgorithm; import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; - -import java.io.IOException; + +import javax.security.auth.x500.X500Principal; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Clock; import java.util.concurrent.Executors; @@ -40,7 +35,7 @@ public class ConfigServerKeyStoreRefresher { private static final Logger logger = Logger.getLogger(ConfigServerKeyStoreRefresher.class.getName()); private static final String KEY_STORE_ALIAS = "alias"; static final long MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY = 3600; - static final String SIGNER_ALGORITHM = "SHA256withRSA"; + static final SignatureAlgorithm SIGNER_ALGORITHM = SignatureAlgorithm.SHA256_WITH_RSA; static final String CONFIG_SERVER_CERTIFICATE_SIGNING_PATH = "/athenz/v1/provider/sign"; private final ScheduledExecutorService executor; @@ -101,12 +96,11 @@ public class ConfigServerKeyStoreRefresher { } while (!executor.isTerminated()); } - public boolean refreshKeyStoreIfNeeded() throws - IOException, NoSuchAlgorithmException, OperatorCreationException, CertificateException, KeyStoreException, NoSuchProviderException { + public boolean refreshKeyStoreIfNeeded() { if (!shouldRefreshCertificate()) return false; KeyPair keyPair = generateKeyPair(); - PKCS10CertificationRequest csr = generateCsr(keyPair, hostname); + Pkcs10Csr csr = generateCsr(keyPair, hostname); X509Certificate certificate = sendCsr(csr); storeCertificate(keyPair, certificate); @@ -141,8 +135,7 @@ public class ConfigServerKeyStoreRefresher { * well before the certificate actually expires so that we have enough time to retry without * overloading config server. */ - private long getSecondsUntilCertificateShouldBeRefreshed() - throws NoSuchAlgorithmException, CertificateException, NoSuchProviderException, KeyStoreException, IOException { + private long getSecondsUntilCertificateShouldBeRefreshed() throws KeyStoreException{ X509Certificate cert = getConfigServerCertificate(); long notBefore = cert.getNotBefore().getTime() / 1000; long notAfter = cert.getNotAfter().getTime() / 1000; @@ -152,12 +145,11 @@ public class ConfigServerKeyStoreRefresher { return Math.max(0, notBefore + thirdOfLifetime - now); } - X509Certificate getConfigServerCertificate() throws NoSuchAlgorithmException, CertificateException, NoSuchProviderException, KeyStoreException, IOException { + X509Certificate getConfigServerCertificate() throws KeyStoreException { return (X509Certificate) keyStoreOptions.loadKeyStore().getCertificate(KEY_STORE_ALIAS); } - private void storeCertificate(KeyPair keyPair, X509Certificate certificate) - throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, NoSuchProviderException { + private void storeCertificate(KeyPair keyPair, X509Certificate certificate) { keyStoreOptions.path.getParent().toFile().mkdirs(); KeyStore keyStore = KeyStoreBuilder.withType(keyStoreOptions.keyStoreType) @@ -167,7 +159,7 @@ public class ConfigServerKeyStoreRefresher { keyStoreOptions.storeKeyStore(keyStore); } - private X509Certificate sendCsr(PKCS10CertificationRequest csr) { + private X509Certificate sendCsr(Pkcs10Csr csr) { CertificateSerializedPayload certificateSerializedPayload = configServerApi.post( CONFIG_SERVER_CERTIFICATE_SIGNING_PATH, new CsrSerializedPayload(csr), @@ -176,17 +168,12 @@ public class ConfigServerKeyStoreRefresher { return certificateSerializedPayload.certificate; } - static KeyPair generateKeyPair() throws NoSuchAlgorithmException { - KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); - rsa.initialize(2048); - return rsa.genKeyPair(); + static KeyPair generateKeyPair() { + return KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); } - private static PKCS10CertificationRequest generateCsr(KeyPair keyPair, String commonName) - throws NoSuchAlgorithmException, OperatorCreationException { - ContentSigner signer = new JcaContentSignerBuilder(SIGNER_ALGORITHM).build(keyPair.getPrivate()); - - return new JcaPKCS10CertificationRequestBuilder(new X500Name("CN=" + commonName), keyPair.getPublic()) - .build(signer); + private static Pkcs10Csr generateCsr(KeyPair keyPair, String commonName) { + return Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=" + commonName), keyPair, SIGNER_ALGORITHM) + .build(); } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java index aa83fdc9e22..34b190b546c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java @@ -7,12 +7,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.util.io.pem.PemObject; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; import java.io.IOException; -import java.io.StringWriter; /** * Contains PEM formatted Certificate Signing Request (CSR) @@ -23,10 +21,10 @@ import java.io.StringWriter; public class CsrSerializedPayload { @JsonProperty("csr") @JsonSerialize(using = CertificateRequestSerializer.class) - public final PKCS10CertificationRequest csr; + public final Pkcs10Csr csr; @JsonCreator - public CsrSerializedPayload(@JsonProperty("csr") PKCS10CertificationRequest csr) { + public CsrSerializedPayload(@JsonProperty("csr") Pkcs10Csr csr) { this.csr = csr; } @@ -52,15 +50,10 @@ public class CsrSerializedPayload { '}'; } - public static class CertificateRequestSerializer extends JsonSerializer<PKCS10CertificationRequest> { + public static class CertificateRequestSerializer extends JsonSerializer<Pkcs10Csr> { @Override - public void serialize( - PKCS10CertificationRequest csr, JsonGenerator gen, SerializerProvider serializers) throws IOException { - try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { - pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded())); - pemWriter.flush(); - gen.writeString(stringWriter.toString()); - } + public void serialize(Pkcs10Csr csr, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Pkcs10CsrUtils.toPem(csr)); } } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java index 85684ea3bd4..69aaad210a4 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java @@ -2,24 +2,19 @@ package com.yahoo.vespa.hosted.node.admin.configserver.certificate; import com.yahoo.test.ManualClock; +import com.yahoo.vespa.athenz.tls.X509CertificateBuilder; import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; -import org.bouncycastle.asn1.x500.X500Name; -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.jcajce.JcaContentSignerBuilder; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import java.math.BigInteger; +import javax.security.auth.x500.X500Principal; import java.security.KeyPair; import java.security.cert.X509Certificate; import java.time.Duration; -import java.util.Date; +import java.time.Instant; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -141,22 +136,13 @@ public class ConfigServerKeyStoreRefresherTest { .thenThrow(exception); } - private X509Certificate makeCertificate(int serial) throws Exception { - try { - KeyPair keyPair = ConfigServerKeyStoreRefresher.generateKeyPair(); - X500Name subject = new X500Name("CN=" + commonName); - Date notBefore = Date.from(clock.instant()); - Date notAfter = Date.from(clock.instant().plus(certificateExpiration)); - - JcaX509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(subject, - BigInteger.valueOf(serial), notBefore, notAfter, subject, keyPair.getPublic()); - ContentSigner sigGen = new JcaContentSignerBuilder(ConfigServerKeyStoreRefresher.SIGNER_ALGORITHM) - .build(keyPair.getPrivate()); - return new JcaX509CertificateConverter() - .setProvider(new BouncyCastleProvider()) - .getCertificate(certGen.build(sigGen)); - } catch (Exception e) { - throw new RuntimeException(e); - } + private X509Certificate makeCertificate(int serial) { + KeyPair keyPair = ConfigServerKeyStoreRefresher.generateKeyPair(); + X500Principal subject = new X500Principal("CN=" + commonName); + Instant notBefore = clock.instant(); + Instant notAfter = clock.instant().plus(certificateExpiration); + + return X509CertificateBuilder.fromKeypair(keyPair, subject, notBefore, notAfter, ConfigServerKeyStoreRefresher.SIGNER_ALGORITHM, serial) + .build(); } }
\ No newline at end of file diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java index 892a4f266ae..62ac722759a 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.identity; -import com.yahoo.athenz.auth.util.Crypto; import com.yahoo.component.AbstractComponent; import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; import com.yahoo.vespa.athenz.api.AthenzDomain; @@ -9,19 +8,25 @@ import com.yahoo.vespa.athenz.api.AthenzIdentityCertificate; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.tls.AthenzSslContextBuilder; import com.yahoo.vespa.athenz.tls.KeyStoreType; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import javax.net.ssl.SSLContext; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Paths; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import static java.util.stream.Collectors.joining; + /** * @author mortent */ @@ -63,13 +68,21 @@ public class SiaIdentityProvider extends AbstractComponent implements AthenzIden } private SSLContext createIdentitySslContext() { - X509Certificate certificate = Crypto.loadX509Certificate(Paths.get(path, "certs", String.format("%s.%s.cert.pem", getDomain(),getService())).toFile()); - PrivateKey privateKey = Crypto.loadPrivateKey(Paths.get(path, "keys", String.format("%s.%s.key.pem", getDomain(),getService())).toFile()); + try { + String certPem = Files.lines(Paths.get(path, "certs", String.format("%s.%s.cert.pem", getDomain(), getService()))) + .collect(joining()); + X509Certificate certificate = X509CertificateUtils.fromPem(certPem); + String keyPem = Files.lines(Paths.get(path, "keys", String.format("%s.%s.key.pem", getDomain(), getService()))) + .collect(joining()); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(keyPem); - return new AthenzSslContextBuilder() - .withTrustStore(new File(trustStorePath), KeyStoreType.JKS) - .withIdentityCertificate(new AthenzIdentityCertificate(certificate, privateKey)) - .build(); + return new AthenzSslContextBuilder() + .withTrustStore(new File(trustStorePath), KeyStoreType.JKS) + .withIdentityCertificate(new AthenzIdentityCertificate(certificate, privateKey)) + .build(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } private void reloadSslContext() { diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/AthenzCredentialsService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/AthenzCredentialsService.java index b9fb7e94782..e73cd2f2bdd 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/AthenzCredentialsService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/AthenzCredentialsService.java @@ -3,8 +3,14 @@ package com.yahoo.vespa.athenz.identityprovider; import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.container.core.identity.IdentityConfig; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import com.yahoo.vespa.athenz.tls.KeyAlgorithm; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrBuilder; +import com.yahoo.vespa.athenz.tls.Pkcs10CsrUtils; +import com.yahoo.vespa.athenz.tls.SignatureAlgorithm; +import javax.security.auth.x500.X500Principal; import java.io.IOException; import java.io.UncheckedIOException; import java.security.KeyPair; @@ -34,20 +40,20 @@ class AthenzCredentialsService { } AthenzCredentials registerInstance() { - KeyPair keyPair = CryptoUtils.createKeyPair(); + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); String rawDocument = identityDocumentService.getSignedIdentityDocument(); SignedIdentityDocument document = parseSignedIdentityDocument(rawDocument); - PKCS10CertificationRequest csr = CryptoUtils.createCSR(identityConfig.domain(), - identityConfig.service(), - document.dnsSuffix, - document.providerUniqueId, - keyPair); + Pkcs10Csr csr = createCSR(identityConfig.domain(), + identityConfig.service(), + document.dnsSuffix, + document.providerUniqueId, + keyPair); InstanceRegisterInformation instanceRegisterInformation = new InstanceRegisterInformation(document.providerService, identityConfig.domain(), identityConfig.service(), rawDocument, - CryptoUtils.toPem(csr)); + Pkcs10CsrUtils.toPem(csr)); InstanceIdentity instanceIdentity = athenzService.sendInstanceRegisterRequest(instanceRegisterInformation, document.ztsEndpoint); return toAthenzCredentials(instanceIdentity, keyPair, document); @@ -55,13 +61,13 @@ class AthenzCredentialsService { AthenzCredentials updateCredentials(AthenzCredentials currentCredentials) { SignedIdentityDocument document = currentCredentials.getIdentityDocument(); - KeyPair newKeyPair = CryptoUtils.createKeyPair(); - PKCS10CertificationRequest csr = CryptoUtils.createCSR(identityConfig.domain(), - identityConfig.service(), - document.dnsSuffix, - document.providerUniqueId, - newKeyPair); - InstanceRefreshInformation refreshInfo = new InstanceRefreshInformation(CryptoUtils.toPem(csr)); + KeyPair newKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); + Pkcs10Csr csr = createCSR(identityConfig.domain(), + identityConfig.service(), + document.dnsSuffix, + document.providerUniqueId, + newKeyPair); + InstanceRefreshInformation refreshInfo = new InstanceRefreshInformation(Pkcs10CsrUtils.toPem(csr)); InstanceIdentity instanceIdentity = athenzService.sendInstanceRefreshRequest(document.providerService, identityConfig.domain(), @@ -90,4 +96,22 @@ class AthenzCredentialsService { } } + private static Pkcs10Csr createCSR(String identityDomain, + String identityService, + String dnsSuffix, + String providerUniqueId, + KeyPair keyPair) { + X500Principal subject = new X500Principal(String.format("CN=%s.%s", identityDomain, identityService)); + // Add SAN dnsname <service>.<domain-with-dashes>.<provider-dnsname-suffix> + // and SAN dnsname <provider-unique-instance-id>.instanceid.athenz.<provider-dnsname-suffix> + return Pkcs10CsrBuilder.fromKeypair(subject, keyPair, SignatureAlgorithm.SHA256_WITH_RSA) + .addSubjectAlternativeName(String.format("%s.%s.%s", + identityService, + identityDomain.replace(".", "-"), + dnsSuffix)) + .addSubjectAlternativeName(String.format("%s.instanceid.athenz.%s", + providerUniqueId, + dnsSuffix)) + .build(); + } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtils.java deleted file mode 100644 index 6e74d3bc8b1..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtils.java +++ /dev/null @@ -1,113 +0,0 @@ -// 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.identityprovider; - -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.ExtensionsGenerator; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; -import org.bouncycastle.util.io.pem.PemObject; - -import javax.security.auth.x500.X500Principal; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.UncheckedIOException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/** - * @author bjorncs - */ -class CryptoUtils { - - private static final BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider(); - - private CryptoUtils() {} - - static KeyPair createKeyPair() { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - static PKCS10CertificationRequest createCSR(String identityDomain, - String identityService, - String dnsSuffix, - String providerUniqueId, - KeyPair keyPair) { - try { - // Add SAN dnsname <service>.<domain-with-dashes>.<provider-dnsname-suffix> - // and SAN dnsname <provider-unique-instance-id>.instanceid.athenz.<provider-dnsname-suffix> - GeneralNames subjectAltNames = new GeneralNames(new GeneralName[]{ - new GeneralName(GeneralName.dNSName, String.format("%s.%s.%s", - identityService, - identityDomain.replace(".", "-"), - dnsSuffix)), - new GeneralName(GeneralName.dNSName, String.format("%s.instanceid.athenz.%s", - providerUniqueId, - dnsSuffix)) - }); - - ExtensionsGenerator extGen = new ExtensionsGenerator(); - extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); - - X500Principal subject = new X500Principal( - String.format("CN=%s.%s", identityDomain, identityService)); - - PKCS10CertificationRequestBuilder requestBuilder = - new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic()); - requestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()); - return requestBuilder.build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate())); - } catch (OperatorCreationException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - static String toPem(PKCS10CertificationRequest csr) { - try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { - pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded())); - pemWriter.flush(); - return stringWriter.toString(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - static X509Certificate parseCertificate(String pemEncodedCertificate) { - try (PEMParser parser = new PEMParser(new StringReader(pemEncodedCertificate))) { - Object pemObject = parser.readObject(); - if (pemObject instanceof X509Certificate) { - return (X509Certificate) pemObject; - } - if (pemObject instanceof X509CertificateHolder) { - return new JcaX509CertificateConverter() - .setProvider(bouncyCastleProvider) - .getCertificate((X509CertificateHolder) pemObject); - } - throw new IllegalArgumentException("Invalid type of PEM object: " + pemObject); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } -} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/InstanceIdentity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/InstanceIdentity.java index b90ce56ca7e..899ff7a95d7 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/InstanceIdentity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/InstanceIdentity.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import java.io.IOException; import java.security.cert.X509Certificate; @@ -41,7 +42,7 @@ public class InstanceIdentity { public static class X509CertificateDeserializer extends JsonDeserializer<X509Certificate> { @Override public X509Certificate deserialize(JsonParser parser, DeserializationContext context) throws IOException { - return CryptoUtils.parseCertificate(parser.getValueAsString()); + return X509CertificateUtils.fromPem(parser.getValueAsString()); } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BasicConstraintsExtension.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BasicConstraintsExtension.java new file mode 100644 index 00000000000..008268dbfe0 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BasicConstraintsExtension.java @@ -0,0 +1,14 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +/** + * @author bjorncs + */ +class BasicConstraintsExtension { + final boolean isCritical, isCertAuthorityCertificate; + + BasicConstraintsExtension(boolean isCritical, boolean isCertAuthorityCertificate) { + this.isCritical = isCritical; + this.isCertAuthorityCertificate = isCertAuthorityCertificate; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BouncyCastleProviderHolder.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BouncyCastleProviderHolder.java new file mode 100644 index 00000000000..03049961dc0 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/BouncyCastleProviderHolder.java @@ -0,0 +1,14 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * @author bjorncs + */ +class BouncyCastleProviderHolder { + + private static final BouncyCastleProvider bcProvider = new BouncyCastleProvider(); + + static BouncyCastleProvider getInstance() { return bcProvider; } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Extension.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Extension.java new file mode 100644 index 00000000000..18403669c4d --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Extension.java @@ -0,0 +1,22 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; + +/** + * @author bjorncs + */ +public enum Extension { + BASIC_CONSTRAINS(org.bouncycastle.asn1.x509.Extension.basicConstraints), + SUBJECT_ALTERNATIVE_NAMES(org.bouncycastle.asn1.x509.Extension.subjectAlternativeName); + + final ASN1ObjectIdentifier extensionOId; + + Extension(ASN1ObjectIdentifier extensionOId) { + this.extensionOId = extensionOId; + } + + public String getOId() { + return extensionOId.getId(); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyAlgorithm.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyAlgorithm.java new file mode 100644 index 00000000000..4c4198adaac --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyAlgorithm.java @@ -0,0 +1,19 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +/** + * @author bjorncs + */ +public enum KeyAlgorithm { + RSA("RSA"); + + private final String algorithmName; + + KeyAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + String getAlgorithmName() { + return algorithmName; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyStoreType.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyStoreType.java index a5c35549540..6c08a60ff5b 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyStoreType.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyStoreType.java @@ -17,10 +17,8 @@ public enum KeyStoreType { } }, PKCS12 { - private final BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider(); - KeyStore createKeystore() throws KeyStoreException { - return KeyStore.getInstance("PKCS12", bouncyCastleProvider); + return KeyStore.getInstance("PKCS12", BouncyCastleProviderHolder.getInstance()); } }; abstract KeyStore createKeystore() throws GeneralSecurityException; diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyUtils.java new file mode 100644 index 00000000000..f49e1324ba5 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/KeyUtils.java @@ -0,0 +1,41 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import com.yahoo.athenz.auth.util.Crypto; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @author bjorncs + */ +public class KeyUtils { + private KeyUtils() {} + + public static KeyPair generateKeypair(KeyAlgorithm algorithm, int keySize) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm.getAlgorithmName()); + if (keySize != -1) { + keyGen.initialize(keySize); + } + return keyGen.genKeyPair(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public static KeyPair generateKeypair(KeyAlgorithm algorithm) { + return generateKeypair(algorithm, -1); + } + + public static PublicKey extractPublicKey(PrivateKey privateKey) { + return Crypto.extractPublicKey(privateKey); + } + + public static PrivateKey fromPemEncodedPrivateKey(String pem) { + return Crypto.loadPrivateKey(pem); + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java new file mode 100644 index 00000000000..d9cd3141f19 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10Csr.java @@ -0,0 +1,90 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; + +import javax.security.auth.x500.X500Principal; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +/** + * @author bjorncs + */ +public class Pkcs10Csr { + + private final PKCS10CertificationRequest csr; + + Pkcs10Csr(PKCS10CertificationRequest csr) { + this.csr = csr; + } + + PKCS10CertificationRequest getBcCsr() { + return csr; + } + + public X500Principal getSubject() { + return new X500Principal(csr.getSubject().toString()); + } + + public List<String> getSubjectAlternativeNames() { + return getExtensions() + .map(extensions -> GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName)) + .map(generalNames -> Arrays.stream(generalNames.getNames()) + .map(Pkcs10Csr::toString) + .collect(toList())) + .orElse(emptyList()); + } + + /** + * @return If basic constraints extension is present: returns true if CA cert, false otherwise. Returns empty if the extension is not present. + */ + public Optional<Boolean> getBasicConstraints() { + return getExtensions() + .map(BasicConstraints::fromExtensions) + .map(BasicConstraints::isCA); + } + + public List<String> getExtensionOIds() { + return getExtensions() + .map(extensions -> Arrays.stream(extensions.getExtensionOIDs()) + .map(ASN1ObjectIdentifier::getId) + .collect(toList())) + .orElse(emptyList()); + + } + + private Optional<Extensions> getExtensions() { + return Optional.of(csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) + .filter(attributes -> attributes.length > 0) + .map(attributes -> attributes[0]) + .map(attribute -> Extensions.getInstance(attribute.getAttrValues().getObjectAt(0))); + } + + private static String toString(GeneralName generalName) { + ASN1Encodable name = generalName.getName(); + switch (generalName.getTagNo()) { + case GeneralName.rfc822Name: + case GeneralName.dNSName: + case GeneralName.uniformResourceIdentifier: + return DERIA5String.getInstance(name).getString(); + case GeneralName.directoryName: + return X500Name.getInstance(name).toString(); + default: + return name.toString(); + } + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java new file mode 100644 index 00000000000..9c2cd9d4d9f --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilder.java @@ -0,0 +1,89 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.ExtensionsGenerator; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.List; + +/** + * @author bjorncs + */ +public class Pkcs10CsrBuilder { + + private final X500Principal subject; + private final KeyPair keyPair; + private final List<String> subjectAlternativeNames = new ArrayList<>(); + private final SignatureAlgorithm signatureAlgorithm; + private BasicConstraintsExtension basicConstraintsExtension; + + private Pkcs10CsrBuilder(X500Principal subject, + KeyPair keyPair, + SignatureAlgorithm signatureAlgorithm) { + this.subject = subject; + this.keyPair = keyPair; + this.signatureAlgorithm = signatureAlgorithm; + } + + public static Pkcs10CsrBuilder fromKeypair(X500Principal subject, + KeyPair keyPair, + SignatureAlgorithm signatureAlgorithm) { + return new Pkcs10CsrBuilder(subject, keyPair, signatureAlgorithm); + } + + public Pkcs10CsrBuilder addSubjectAlternativeName(String san) { + this.subjectAlternativeNames.add(san); + return this; + } + + public Pkcs10CsrBuilder setBasicConstraints(boolean isCritical, boolean isCertAuthorityCertificate) { + this.basicConstraintsExtension = new BasicConstraintsExtension(isCritical, isCertAuthorityCertificate); + return this; + } + + public Pkcs10Csr build() { + try { + PKCS10CertificationRequestBuilder requestBuilder = + new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic()); + ExtensionsGenerator extGen = new ExtensionsGenerator(); + if (basicConstraintsExtension != null) { + extGen.addExtension( + Extension.basicConstraints, + basicConstraintsExtension.isCritical, + new BasicConstraints(basicConstraintsExtension.isCertAuthorityCertificate)); + } + if (!subjectAlternativeNames.isEmpty()) { + GeneralNames generalNames = new GeneralNames( + subjectAlternativeNames.stream() + .map(san -> new GeneralName(GeneralName.dNSName, san)) + .toArray(GeneralName[]::new)); + extGen.addExtension(Extension.subjectAlternativeName, false, generalNames); + } + requestBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()); + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm.getAlgorithmName()) + .setProvider(BouncyCastleProviderHolder.getInstance()) + .build(keyPair.getPrivate()); + return new Pkcs10Csr(requestBuilder.build(contentSigner)); + } catch (OperatorCreationException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtils.java new file mode 100644 index 00000000000..2289c9ac0ee --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtils.java @@ -0,0 +1,38 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.util.io.pem.PemObject; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UncheckedIOException; + +/** + * @author bjorncs + */ +public class Pkcs10CsrUtils { + + private Pkcs10CsrUtils() {} + + public static Pkcs10Csr fromPem(String pem) { + try (PEMParser pemParser = new PEMParser(new StringReader(pem))) { + return new Pkcs10Csr((PKCS10CertificationRequest) pemParser.readObject()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static String toPem(Pkcs10Csr csr) { + try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getBcCsr().getEncoded())); + pemWriter.flush(); + return stringWriter.toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/SignatureAlgorithm.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/SignatureAlgorithm.java new file mode 100644 index 00000000000..3d85a12f714 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/SignatureAlgorithm.java @@ -0,0 +1,19 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +/** + * @author bjorncs + */ +public enum SignatureAlgorithm { + SHA256_WITH_RSA("SHA256withRSA"); + + private final String algorithmName; + + SignatureAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + public String getAlgorithmName() { + return algorithmName; + } +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java new file mode 100644 index 00000000000..ba22af13fd2 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilder.java @@ -0,0 +1,145 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; + +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.sql.Date; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * @author bjorncs + */ +public class X509CertificateBuilder { + + private final long serialNumber; + private final SignatureAlgorithm signingAlgorithm; + private final PrivateKey caPrivateKey; + private final Instant notBefore; + private final Instant notAfter; + private final List<String> subjectAlternativeNames = new ArrayList<>(); + private final X500Principal issuer; + private final X500Principal subject; + private final PublicKey certPublicKey; + private BasicConstraintsExtension basicConstraintsExtension; + + private X509CertificateBuilder(X500Principal issuer, + X500Principal subject, + Instant notBefore, + Instant notAfter, + PublicKey certPublicKey, + PrivateKey caPrivateKey, + SignatureAlgorithm signingAlgorithm, + long serialNumber) { + this.issuer = issuer; + this.subject = subject; + this.notBefore = notBefore; + this.notAfter = notAfter; + this.certPublicKey = certPublicKey; + this.caPrivateKey = caPrivateKey; + this.signingAlgorithm = signingAlgorithm; + this.serialNumber = serialNumber; + } + + public static X509CertificateBuilder fromCsr(Pkcs10Csr csr, + X500Principal caIssuer, + Instant notBefore, + Instant notAfter, + PrivateKey caPrivateKey, + SignatureAlgorithm signingAlgorithm, + long serialNumber) { + try { + PKCS10CertificationRequest bcCsr = csr.getBcCsr(); + PublicKey publicKey = new JcaPKCS10CertificationRequest(bcCsr).getPublicKey(); + return new X509CertificateBuilder(caIssuer, + new X500Principal(bcCsr.getSubject().getEncoded()), + notBefore, + notAfter, + publicKey, + caPrivateKey, + signingAlgorithm, + serialNumber); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static X509CertificateBuilder fromKeypair(KeyPair keyPair, + X500Principal subject, + Instant notBefore, + Instant notAfter, + SignatureAlgorithm signingAlgorithm, + long serialNumber) { + return new X509CertificateBuilder(subject, + subject, + notBefore, + notAfter, + keyPair.getPublic(), + keyPair.getPrivate(), + signingAlgorithm, + serialNumber); + } + + public X509CertificateBuilder addSubjectAlternativeName(String san) { + this.subjectAlternativeNames.add(san); + return this; + } + + public X509CertificateBuilder setBasicConstraints(boolean isCritical, boolean isCertAuthorityCertificate) { + this.basicConstraintsExtension = new BasicConstraintsExtension(isCritical, isCertAuthorityCertificate); + return this; + } + + public X509Certificate build() { + try { + JcaX509v3CertificateBuilder jcaCertBuilder = new JcaX509v3CertificateBuilder( + issuer, BigInteger.valueOf(serialNumber), Date.from(notBefore), Date.from(notAfter), subject, certPublicKey); + if (basicConstraintsExtension != null) { + jcaCertBuilder.addExtension( + Extension.basicConstraints, + basicConstraintsExtension.isCritical, + new BasicConstraints(basicConstraintsExtension.isCertAuthorityCertificate)); + } + if (!subjectAlternativeNames.isEmpty()) { + GeneralNames generalNames = new GeneralNames( + subjectAlternativeNames.stream() + .map(san -> new GeneralName(GeneralName.dNSName, san)) + .toArray(GeneralName[]::new)); + jcaCertBuilder.addExtension(Extension.subjectAlternativeName, false, generalNames); + } + ContentSigner contentSigner = new JcaContentSignerBuilder(signingAlgorithm.getAlgorithmName()) + .setProvider(BouncyCastleProviderHolder.getInstance()) + .build(caPrivateKey); + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProviderHolder.getInstance()) + .getCertificate(jcaCertBuilder.build(contentSigner)); + } catch (OperatorException | GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java new file mode 100644 index 00000000000..3e1bd3eb31c --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/X509CertificateUtils.java @@ -0,0 +1,78 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.tls; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.util.io.pem.PemObject; + +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author bjorncs + */ +public class X509CertificateUtils { + + private X509CertificateUtils() {} + + public static X509Certificate fromPem(String pem) { + try (PEMParser parser = new PEMParser(new StringReader(pem))) { + Object pemObject = parser.readObject(); + if (pemObject instanceof X509Certificate) { + return (X509Certificate) pemObject; + } + if (pemObject instanceof X509CertificateHolder) { + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProviderHolder.getInstance()) + .getCertificate((X509CertificateHolder) pemObject); + } + throw new IllegalArgumentException("Invalid type of PEM object: " + pemObject); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + public static String toPem(X509Certificate certificate) { + try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded())); + pemWriter.flush(); + return stringWriter.toString(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static List<String> getCommonNames(X509Certificate certificate) { + return getCommonNames(certificate.getSubjectX500Principal()); + } + + public static List<String> getCommonNames(X500Principal subject) { + try { + String subjectPrincipal = subject.getName(); + return new LdapName(subjectPrincipal).getRdns().stream() + .filter(rdn -> rdn.getType().equalsIgnoreCase("cn")) + .map(rdn -> rdn.getValue().toString()) + .collect(toList()); + } catch (NamingException e) { + throw new IllegalArgumentException("Invalid CN: " + e, e); + } + + } +} 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 index fd34263e387..ad2428cc292 100644 --- 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 @@ -5,10 +5,10 @@ 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 com.yahoo.vespa.athenz.tls.X509CertificateUtils; -import javax.naming.NamingException; -import javax.naming.ldap.LdapName; import java.security.cert.X509Certificate; +import java.util.List; /** * @author bjorncs @@ -53,15 +53,11 @@ public class AthenzIdentities { } 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); + List<String> commonNames = X509CertificateUtils.getCommonNames(certificate); + if (commonNames.size() != 1) { + String subjectName = certificate.getSubjectX500Principal().getName(); + throw new IllegalArgumentException("Expected single CN in certificate's subject: " + subjectName); } + return commonNames.get(0); } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtilsTest.java deleted file mode 100644 index 353c5d3c504..00000000000 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/CryptoUtilsTest.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.athenz.identityprovider; - -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.junit.Test; - -import java.io.IOException; -import java.security.KeyPair; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertThat; - -/** - * @author bjorncs - */ -public class CryptoUtilsTest { - - @Test - public void certificate_signing_request_is_correct_and_can_be_serialized_to_pem() throws IOException { - KeyPair keyPair = CryptoUtils.createKeyPair(); - PKCS10CertificationRequest csr = CryptoUtils.createCSR( - "identity-domain", "identity-service", "vespa.cloud.com", "unique.instance.id", keyPair); - String pem = CryptoUtils.toPem(csr); - assertThat(pem, containsString("BEGIN CERTIFICATE REQUEST")); - assertThat(pem, containsString("END CERTIFICATE REQUEST")); - } - -} diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/AthenzSslContextBuilderTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/AthenzSslContextBuilderTest.java index 20ac8791863..cf1b2168dc8 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/AthenzSslContextBuilderTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/AthenzSslContextBuilderTest.java @@ -10,7 +10,6 @@ import java.security.KeyPair; import java.security.cert.X509Certificate; import static com.yahoo.vespa.athenz.tls.TestUtils.createCertificate; -import static com.yahoo.vespa.athenz.tls.TestUtils.createKeyPair; import static com.yahoo.vespa.athenz.tls.TestUtils.createKeystore; import static com.yahoo.vespa.athenz.tls.TestUtils.createKeystoreFile; @@ -48,7 +47,7 @@ public class AthenzSslContextBuilderTest { @Test public void can_build_sslcontext_with_keystore_from_private_key_and_certificate() throws Exception { - KeyPair keyPair = createKeyPair(); + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); X509Certificate certificate = createCertificate(keyPair); new AthenzSslContextBuilder() .withKeyStore(keyPair.getPrivate(), certificate) diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyStoreBuilderTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyStoreBuilderTest.java index 1b6fa8bcbf1..6060f6f3521 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyStoreBuilderTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyStoreBuilderTest.java @@ -9,7 +9,6 @@ import java.security.KeyPair; import java.security.cert.X509Certificate; import static com.yahoo.vespa.athenz.tls.TestUtils.createCertificate; -import static com.yahoo.vespa.athenz.tls.TestUtils.createKeyPair; import static com.yahoo.vespa.athenz.tls.TestUtils.createKeystoreFile; /** @@ -24,7 +23,7 @@ public class KeyStoreBuilderTest { @Test public void can_create_jks_keystore_from_privatekey_and_certificate() throws Exception { - KeyPair keyPair = createKeyPair(); + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 4096); X509Certificate certificate = createCertificate(keyPair); KeyStoreBuilder.withType(KeyStoreType.JKS) .withKeyEntry("key", keyPair.getPrivate(), certificate) diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyUtilsTest.java new file mode 100644 index 00000000000..a8730a31838 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/KeyUtilsTest.java @@ -0,0 +1,22 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import java.security.KeyPair; +import java.security.PublicKey; + +import static org.junit.Assert.assertNotNull; + +/** + * @author bjorncs + */ +public class KeyUtilsTest { + + @Test + public void can_extract_public_key_from_private() { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); + PublicKey publicKey = KeyUtils.extractPublicKey(keyPair.getPrivate()); + assertNotNull(publicKey); + } + +}
\ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilderTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilderTest.java new file mode 100644 index 00000000000..e3aaba66efe --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrBuilderTest.java @@ -0,0 +1,27 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; + +import java.security.KeyPair; + +import static org.junit.Assert.*; + +/** + * @author bjorncs + */ +public class Pkcs10CsrBuilderTest { + + @Test + public void can_build_csr_with_sans() { + X500Principal subject = new X500Principal("CN=subject"); + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA) + .addSubjectAlternativeName("san1.com") + .addSubjectAlternativeName("san2.com") + .build(); + assertEquals(subject, csr.getSubject()); + } + +}
\ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java new file mode 100644 index 00000000000..bb2e80ba705 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrTest.java @@ -0,0 +1,55 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author bjorncs + */ +public class Pkcs10CsrTest { + + @Test + public void can_read_subject_alternative_names() { + X500Principal subject = new X500Principal("CN=subject"); + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + String san1 = "san1.com"; + String san2 = "san2.com"; + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA) + .addSubjectAlternativeName(san1) + .addSubjectAlternativeName(san2) + .build(); + assertEquals(Arrays.asList(san1, san2), csr.getSubjectAlternativeNames()); + } + + @Test + public void can_read_basic_constraints() { + X500Principal subject = new X500Principal("CN=subject"); + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA) + .setBasicConstraints(true, true) + .build(); + assertTrue(csr.getBasicConstraints().isPresent()); + assertTrue(csr.getBasicConstraints().get()); + } + + @Test + public void can_read_extensions() { + X500Principal subject = new X500Principal("CN=subject"); + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA) + .addSubjectAlternativeName("san") + .setBasicConstraints(true, true) + .build(); + List<String> expected = Arrays.asList(Extension.BASIC_CONSTRAINS.getOId(), Extension.SUBJECT_ALTERNATIVE_NAMES.getOId()); + List<String> actual = csr.getExtensionOIds(); + assertEquals(expected, actual); + } + +}
\ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtilsTest.java new file mode 100644 index 00000000000..5b5a57f1fcc --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/Pkcs10CsrUtilsTest.java @@ -0,0 +1,29 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.security.KeyPair; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * @author bjorncs + */ +public class Pkcs10CsrUtilsTest { + + @Test + public void can_deserialize_serialized_pem_csr() { + X500Principal subject = new X500Principal("CN=subject"); + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, keypair, SignatureAlgorithm.SHA256_WITH_RSA).build(); + String pem = Pkcs10CsrUtils.toPem(csr); + Pkcs10Csr deserializedCsr = Pkcs10CsrUtils.fromPem(pem); + assertThat(pem, containsString("BEGIN CERTIFICATE REQUEST")); + assertThat(pem, containsString("END CERTIFICATE REQUEST")); + assertEquals(subject, deserializedCsr.getSubject()); + } + +}
\ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java index 54601c04514..64f15408313 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/TestUtils.java @@ -1,23 +1,13 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.tls; -import com.yahoo.athenz.auth.util.Crypto; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; - -import java.io.BufferedOutputStream; +import javax.security.auth.x500.X500Principal; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import static com.yahoo.vespa.athenz.tls.KeyStoreUtils.writeKeyStoreToFile; @@ -26,32 +16,22 @@ import static com.yahoo.vespa.athenz.tls.KeyStoreUtils.writeKeyStoreToFile; */ class TestUtils { - static KeyStore createKeystore(KeyStoreType type, char[] password) - throws GeneralSecurityException, IOException, OperatorCreationException { - KeyPair keyPair = createKeyPair(); - KeyStore keystore = type.createKeystore(); - keystore.load(null); - keystore.setKeyEntry("entry-name", keyPair.getPrivate(), password, new Certificate[]{createCertificate(keyPair)}); - return keystore; - } - - static X509Certificate createCertificate(KeyPair keyPair) - throws OperatorCreationException, IOException { - String x500Principal = "CN=mysubject"; - PKCS10CertificationRequest csr = - Crypto.getPKCS10CertRequest( - Crypto.generateX509CSR(keyPair.getPrivate(), x500Principal, null)); - return Crypto.generateX509Certificate(csr, keyPair.getPrivate(), new X500Name(x500Principal), 3600, false); + static KeyStore createKeystore(KeyStoreType type, char[] password) { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 4096); + return KeyStoreBuilder.withType(type) + .withKeyEntry("entry-name", keyPair.getPrivate(), password, createCertificate(keyPair)) + .build(); } - static KeyPair createKeyPair() throws NoSuchAlgorithmException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); - keyGen.initialize(4096); - return keyGen.genKeyPair(); + static X509Certificate createCertificate(KeyPair keyPair) { + X500Principal subject = new X500Principal("CN=mysubject"); + return X509CertificateBuilder + .fromKeypair( + keyPair, subject, Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA256_WITH_RSA, 1) + .build(); } - static void createKeystoreFile(File file, KeyStoreType type, char[] password) - throws IOException, GeneralSecurityException, OperatorCreationException { + static void createKeystoreFile(File file, KeyStoreType type, char[] password) { writeKeyStoreToFile(createKeystore(type, password), file, password); } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilderTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilderTest.java new file mode 100644 index 00000000000..81ff4fdb208 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateBuilderTest.java @@ -0,0 +1,58 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class X509CertificateBuilderTest { + + @Test + public void can_build_self_signed_certificate() throws NoSuchAlgorithmException { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + X500Principal subject = new X500Principal("CN=myservice"); + X509Certificate cert = + X509CertificateBuilder.fromKeypair( + keyPair, + subject, + Instant.now(), + Instant.now().plus(1, ChronoUnit.DAYS), + SignatureAlgorithm.SHA256_WITH_RSA, + 1) + .setBasicConstraints(true, true) + .build(); + assertEquals(subject, cert.getSubjectX500Principal()); + } + + @Test + public void can_build_certificate_from_csr() { + X500Principal subject = new X500Principal("CN=subject"); + X500Principal issuer = new X500Principal("CN=issuer"); + KeyPair csrKeypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(subject, csrKeypair, SignatureAlgorithm.SHA256_WITH_RSA).build(); + KeyPair caKeypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + X509Certificate cert = X509CertificateBuilder + .fromCsr( + csr, + issuer, + Instant.now(), + Instant.now().plus(1, ChronoUnit.DAYS), + caKeypair.getPrivate(), + SignatureAlgorithm.SHA256_WITH_RSA, + 1) + .addSubjectAlternativeName("subject1.alt") + .addSubjectAlternativeName("subject2.alt") + .build(); + assertEquals(subject, cert.getSubjectX500Principal()); + } + +}
\ No newline at end of file diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java new file mode 100644 index 00000000000..3ba011b1efb --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/tls/X509CertificateUtilsTest.java @@ -0,0 +1,36 @@ +package com.yahoo.vespa.athenz.tls; + +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class X509CertificateUtilsTest { + @Test + public void can_deserialize_serialized_pem_certificate() { + KeyPair keypair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + X500Principal subject = new X500Principal("CN=myservice"); + X509Certificate cert = X509CertificateBuilder + .fromKeypair( + keypair, + subject, + Instant.now(), + Instant.now().plus(1, ChronoUnit.DAYS), + SignatureAlgorithm.SHA256_WITH_RSA, + 1) + .build(); + assertEquals(subject, cert.getSubjectX500Principal()); + X509Certificate deserializedCert = X509CertificateUtils.fromPem(X509CertificateUtils.toPem(cert)); + assertEquals(subject, deserializedCert.getSubjectX500Principal()); + } + + +}
\ No newline at end of file |