diff -Nru canl-java-2.8.2/debian/changelog canl-java-2.8.3/debian/changelog --- canl-java-2.8.2/debian/changelog 2022-10-17 16:36:45.000000000 +0000 +++ canl-java-2.8.3/debian/changelog 2023-06-15 06:30:22.000000000 +0000 @@ -1,3 +1,9 @@ +canl-java (2.8.3-1) unstable; urgency=medium + + * Update to version 2.8.3 + + -- Mattias Ellert Thu, 15 Jun 2023 08:30:22 +0200 + canl-java (2.8.2-2) unstable; urgency=medium * Ignore additional tests that require network (Closes: #1021552) diff -Nru canl-java-2.8.2/debian/control canl-java-2.8.3/debian/control --- canl-java-2.8.2/debian/control 2022-09-16 06:03:30.000000000 +0000 +++ canl-java-2.8.3/debian/control 2023-06-15 06:30:22.000000000 +0000 @@ -11,7 +11,7 @@ libbcpkix-java (>= 1.69), libbcprov-java (>= 1.69), libcommons-io-java (>= 2.6) -Standards-Version: 4.6.1 +Standards-Version: 4.6.2 Section: java Vcs-Browser: https://salsa.debian.org/ellert/canl-java Vcs-Git: https://salsa.debian.org/ellert/canl-java.git diff -Nru canl-java-2.8.2/debian/copyright canl-java-2.8.3/debian/copyright --- canl-java-2.8.2/debian/copyright 2022-09-16 06:00:49.000000000 +0000 +++ canl-java-2.8.3/debian/copyright 2023-06-15 06:30:22.000000000 +0000 @@ -3,7 +3,7 @@ Source: https://github.com/eu-emi/canl-java/archive/canl-2.8.2.tar.gz Files: * -Copyright: 2010-2022 ICM Uniwersytet Warszawski +Copyright: 2010-2023 ICM Uniwersytet Warszawski License: BSD-3-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -158,7 +158,7 @@ License: Apache-2.0 Files: debian/* -Copyright: 2016-2022 Mattias Ellert +Copyright: 2016-2023 Mattias Ellert License: Apache-2.0 License: Apache-2.0 diff -Nru canl-java-2.8.2/pom.xml canl-java-2.8.3/pom.xml --- canl-java-2.8.2/pom.xml 2022-09-13 09:59:04.000000000 +0000 +++ canl-java-2.8.3/pom.xml 2023-03-15 16:52:39.000000000 +0000 @@ -4,7 +4,7 @@ eu.eu-emi.security canl jar - 2.8.2 + 2.8.3 canl EMI Common X.509 Authentication Library @@ -75,7 +75,7 @@ scm:git:git://github.com/eu-emi/canl-java.git https://github.com/eu-emi/canl-java scm:git:ssh://git@github.com/eu-emi/canl-java.git - canl-2.8.2 + canl-2.8.3 @@ -109,6 +109,12 @@ 3.23.1 test + + org.hamcrest + hamcrest-all + 1.3 + test + diff -Nru canl-java-2.8.2/src/main/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidator.java canl-java-2.8.3/src/main/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidator.java --- canl-java-2.8.2/src/main/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidator.java 2022-09-13 09:59:04.000000000 +0000 +++ canl-java-2.8.3/src/main/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidator.java 2023-03-15 16:52:39.000000000 +0000 @@ -39,6 +39,7 @@ */ public class OpensslCertChainValidator extends AbstractValidator { + private static final X509Certificate[] EMPTY_CERT_ARRAY = new X509Certificate[0]; private OpensslTrustAnchorStore trustStore; private AbstractCRLStoreSPI crlStore; private final NamespaceCheckingMode namespaceMode; @@ -226,14 +227,19 @@ } ValidationResult result = super.validate(certChain, anchors); - + validateNamespaces(certChain, result); + return result; + } + + private void validateNamespaces(X509Certificate[] certChain, ValidationResult result) + { NamespaceChecker checker = new NamespaceChecker(namespaceMode, trustStore.getPmaNsStore(), trustStore.getGlobusNsStore()); - List errors = checker.check(certChain); + + X509Certificate[] certChainToValidate = result.isValid() ? result.getValidChain().toArray(EMPTY_CERT_ARRAY) : certChain; + List errors = checker.check(certChainToValidate); processErrorList(errors); result.addErrors(errors); - - return result; } } diff -Nru canl-java-2.8.2/src/test/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidatorTest.java canl-java-2.8.3/src/test/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidatorTest.java --- canl-java-2.8.2/src/test/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidatorTest.java 1970-01-01 00:00:00.000000000 +0000 +++ canl-java-2.8.3/src/test/java/eu/emi/security/authn/x509/impl/OpensslCertChainValidatorTest.java 2023-03-15 16:52:39.000000000 +0000 @@ -0,0 +1,662 @@ +/* + * Copyright 2023 Deutsches Elektronen-Synchrotron (DESY) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.emi.security.authn.x509.impl; + +import static java.time.Duration.of; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import eu.emi.security.authn.x509.CrlCheckingMode; +import eu.emi.security.authn.x509.NamespaceCheckingMode; +import eu.emi.security.authn.x509.OCSPCheckingMode; +import eu.emi.security.authn.x509.OCSPParametes; +import eu.emi.security.authn.x509.ProxySupport; +import eu.emi.security.authn.x509.RevocationParameters; +import eu.emi.security.authn.x509.ValidationResult; +import eu.emi.security.authn.x509.helpers.trust.OpensslTruststoreHelper; + +/** + * A set of unit-tests to verify correct behaviour of + * OpensslCertChainValidator that follow the BDD style. + */ +public class OpensslCertChainValidatorTest +{ + private OpensslCertChainValidator validator; + private Path trustStore; + + @Before + public void setup() throws IOException { + validator = null; + trustStore = Paths.get("target/openssl-trust-stores/" + + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE)); + Files.createDirectories(trustStore); + } + + @After + public void tearDown() throws IOException { + if (Files.exists(trustStore)) { + Files.walk(trustStore) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + + if (validator != null) { + validator.dispose(); + } + } + + @Test + public void shouldValidateRootIssuedEEC() throws Exception { + CA rootCA = given(aCertificateAuthority() + .selfSigned() + .withName("DC=org, DC=example, CN=root CA")); + + given(anOpensslTrustStore() + .withNamespacesFiles() + .withSigningPolicyFiles() + .trustingCA(rootCA) + .authorising("/DC=org/DC=example")); + + given(anOpensslCertChainValidator() + .with(OCSPCheckingMode.IGNORE) + .with(CrlCheckingMode.REQUIRE) + .with(ProxySupport.ALLOW) + .with(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS_REQUIRE) + .withUpdateInterval(of(2, MINUTES)) + .withLazyLoading()); + + X509Certificate serviceCertificate = given(anEEC() + .withSubject("DC=org, DC=example, CN=remote host") + .signedBy(rootCA)); + + ValidationResult result = whenValidating(serviceCertificate); + + assertThat(result.isValid(), is(equalTo(true))); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getUnresolvedCriticalExtensions(), is(empty())); + } + + @Test + public void shouldValidateIntermediateCaIssuedEEC() throws Exception { + CA rootCA = given(aCertificateAuthority() + .selfSigned() + .withName("DC=org, DC=example, CN=root CA")); + CA interCA = given(aCertificateAuthority() + .signedBy(rootCA) + .withName("DC=org, DC=example, CN=intermediate CA 1")); + + given(anOpensslTrustStore() + .withNamespacesFiles() + .withSigningPolicyFiles() + .trustingCA(rootCA) + .authorising("/DC=org/DC=example") + .andTrustingCA(interCA) + .authorising("/DC=org/DC=example")); + + given(anOpensslCertChainValidator() + .with(OCSPCheckingMode.IGNORE) + .with(CrlCheckingMode.REQUIRE) + .with(ProxySupport.ALLOW) + .with(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS_REQUIRE) + .withUpdateInterval(of(2, MINUTES)) + .withLazyLoading()); + + X509Certificate serviceCertificate = given(anEEC() + .withSubject("DC=org, DC=example, CN=remote host") + .signedBy(interCA)); + + ValidationResult result = whenValidating(serviceCertificate); + + assertThat(result.isValid(), is(equalTo(true))); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getUnresolvedCriticalExtensions(), is(empty())); + } + + @Test + public void shouldIgnoreIrrelevantCAWithWrongSubject() throws Exception { + CA root = given(aCertificateAuthority() + .selfSigned() + .withName("DC=org, DC=example, CN=first root")); + CA inter1 = given(aCertificateAuthority() + .signedBy(root) + .withName("DC=org, DC=example, CN=first intermediate")); + CA inter2 = given(aCertificateAuthority() + .signedBy(root) + .withName("DC=ch, DC=cern, CN=second intermediate")); + + given(anOpensslTrustStore() + .withNamespacesFiles() + .withSigningPolicyFiles() + .trustingCA(root) + .authorising("/DC=org/DC=example") + .andTrustingCA(inter1) + .authorising("/DC=org/DC=example")); + + given(anOpensslCertChainValidator() + .with(OCSPCheckingMode.IGNORE) + .with(CrlCheckingMode.REQUIRE) + .with(ProxySupport.ALLOW) + .with(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS_REQUIRE) + .withUpdateInterval(of(2, MINUTES)) + .withLazyLoading()); + + X509Certificate serviceCertificate = given(anEEC() + .withSubject("DC=org, DC=example, CN=remote host") + .signedBy(inter1)); + + ValidationResult result = whenValidating( + serviceCertificate, + inter1.getCertificate(), + inter2.getCertificate(), + root.getCertificate() + ); + + assertThat(result.toString(), result.isValid(), is(equalTo(true))); + assertThat(result.getErrors(), is(empty())); + assertThat(result.getUnresolvedCriticalExtensions(), is(empty())); + } + + private ValidationResult whenValidating(X509Certificate... certificates) { + return validator.validate(certificates); + } + + private OpensslCertChainValidatorBuilder anOpensslCertChainValidator() { + return new OpensslCertChainValidatorBuilder(); + } + + private CABuilder aCertificateAuthority() { + return new CABuilder(); + } + + private CertificateBuilder anEEC() { + return new CertificateBuilder().asEEC().ofLostCredental(); + } + + private OpensslTrustStoreBuilder anOpensslTrustStore() throws IOException { + return new OpensslTrustStoreBuilder(); + } + + private void given(OpensslCertChainValidatorBuilder builder) { + validator = builder.build(); + } + + private void given(OpensslTrustStoreBuilder.TrustBuilder builder) throws IOException { + builder.and().build(); + } + + private CA given(CABuilder builder) throws OperatorCreationException, CertIOException, CertificateException { + return builder.build(); + } + + private X509Certificate given(CertificateBuilder builder) throws OperatorCreationException, CertIOException, CertificateException { + return builder.build(); + } + + /** + * Builder pattern class for creating and configuring an + * OpensslCertChainValidator instance. + */ + private class OpensslCertChainValidatorBuilder { + private OCSPCheckingMode ocspMode; + private CrlCheckingMode crlCheckingMode; + private ProxySupport proxySupport; + private NamespaceCheckingMode namespaceCheckingMode; + private Duration updateInterval; + private boolean isLazy; + + public OpensslCertChainValidatorBuilder with(OCSPCheckingMode mode) { + ocspMode = requireNonNull(mode); + return this; + } + + public OpensslCertChainValidatorBuilder with(CrlCheckingMode mode) { + crlCheckingMode = requireNonNull(mode); + return this; + } + + public OpensslCertChainValidatorBuilder with(ProxySupport mode) { + proxySupport = requireNonNull(mode); + return this; + } + + public OpensslCertChainValidatorBuilder with(NamespaceCheckingMode mode) { + namespaceCheckingMode = requireNonNull(mode); + return this; + } + + public OpensslCertChainValidatorBuilder withUpdateInterval(Duration interval) { + updateInterval = requireNonNull(interval); + return this; + } + + public OpensslCertChainValidatorBuilder withLazyLoading() { + this.isLazy = true; + return this; + } + + public OpensslCertChainValidator build() { + assertThat(ocspMode, not(nullValue())); + assertThat(crlCheckingMode, not(nullValue())); + assertThat(proxySupport, not(nullValue())); + assertThat(namespaceCheckingMode, not(nullValue())); + assertThat(updateInterval, not(nullValue())); + + OCSPParametes ocspParameters = new OCSPParametes(ocspMode); + RevocationParameters revocationParams = + new RevocationParameters(crlCheckingMode, ocspParameters); + ValidatorParams validatorParams = new ValidatorParams(revocationParams, + proxySupport); + + return new OpensslCertChainValidator(trustStore.toString(), true, + namespaceCheckingMode, updateInterval.toMillis(), + validatorParams, isLazy); + } + } + + /** + * Builder pattern class for creating the OpenSSL trust store. + */ + private class OpensslTrustStoreBuilder { + private final List trusts = new ArrayList<>(); + private boolean writeNamespacesFiles; + private boolean writeSigningPolicyFiles; + + public OpensslTrustStoreBuilder() throws IOException { + Files.createDirectories(trustStore); + } + + public OpensslTrustStoreBuilder withNamespacesFiles() { + writeNamespacesFiles = true; + return this; + } + + public OpensslTrustStoreBuilder withSigningPolicyFiles() { + writeSigningPolicyFiles = true; + return this; + } + + public TrustBuilder trustingCA(CA ca) { + TrustBuilder trust = new TrustBuilder(ca, writeNamespacesFiles, + writeSigningPolicyFiles); + trusts.add(trust); + return trust; + } + + public void build() throws IOException { + for (TrustBuilder tb : trusts) { + tb.build(); + } + } + + /** + * Builder pattern class for configuring trust of a specific CA. + */ + private class TrustBuilder { + private final CA ca; + private final String hash; + private final List authorisedNames = new ArrayList<>(); + private final boolean writeNamespacesFiles; + private final boolean writeSigningPolicyFiles; + + private TrustBuilder(CA ca, boolean namespaces, boolean signingpolicy) { + this.ca = ca; + writeNamespacesFiles = namespaces; + writeSigningPolicyFiles = signingpolicy; + + hash = OpensslTruststoreHelper.getOpenSSLCAHash(ca.getSubject(), true); + } + + private void writeHashFile(String suffix, String contents) throws IOException { + Path filePath = trustStore.resolve(hash + suffix); + Files.write(filePath, contents.getBytes(StandardCharsets.UTF_8)); + } + + private TrustBuilder authorising(String... distinguishedNames) throws IOException { + assertTrue("You need to enable either namespaces, signing_policy (or both) files", + writeNamespacesFiles || writeSigningPolicyFiles); + authorisedNames.addAll(asList(distinguishedNames)); + return this; + } + + private void writeNamespaces() throws IOException { + StringWriter stringWriter = new StringWriter(); + PrintWriter pw = new PrintWriter(stringWriter); + for (String dn : authorisedNames) { + pw.println("TO Issuer \"" + ca.getOldDn() + "\" \\"); + pw.println(" PERMIT Subject \"" + dn + "/.*\""); + pw.println(); + } + + pw.flush(); + stringWriter.flush(); + + writeHashFile(".namespaces", stringWriter.toString()); + } + + private void writeSigningPolicy() throws IOException { + StringWriter stringWriter = new StringWriter(); + PrintWriter pw = new PrintWriter(stringWriter); + pw.println("access_id_CA X509 '" + ca.getOldDn() + "'"); + pw.println("pos_rights globus CA:sign"); + pw.println(authorisedNames.stream() + .map(dn -> "\"" + dn + "/*\"") + .collect(Collectors.joining(" ", "cond_subjects globus '", "'"))); + pw.flush(); + stringWriter.flush(); + writeHashFile(".signing_policy", stringWriter.toString()); + } + + private OpensslTrustStoreBuilder and() { + return OpensslTrustStoreBuilder.this; + } + + private TrustBuilder andTrustingCA(CA ca) throws IOException { + return and().trustingCA(ca); + } + + private void build() throws IOException { + writeHashFile(".0", ca.buildPemCertificate()); + writeHashFile(".r0", ca.buildPemCrl()); + + if (writeNamespacesFiles) { + writeNamespaces(); + } + + if (writeSigningPolicyFiles) { + writeSigningPolicy(); + } + } + } + } + + /** + * A class that represents a certificate authority. The CA may be either a + * root CA or intermediate CA. + */ + private static class CA { + private final X509Certificate certificate; + private final PrivateKey privateKey; + + public CA(X509Certificate certificate, PrivateKey privateKey) { + this.certificate = requireNonNull(certificate); + this.privateKey = requireNonNull(privateKey); + } + + public X509Certificate getCertificate() { + return certificate; + } + + private String pemEncode(Object input) { + try { + StringWriter stringWriter = new StringWriter(); + JcaPEMWriter writer = new JcaPEMWriter(stringWriter); + writer.writeObject(input); + writer.flush(); + return stringWriter.toString(); + } catch (IOException e) { + throw new RuntimeException("Unexpected IOException " + e, e); + } + } + + public String buildPemCertificate() { + return pemEncode(certificate); + } + + public String buildPemCrl() { + Instant validFrom = Instant.now().minus(10, MINUTES); + X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(certificate, Date.from(validFrom)); + builder.setNextUpdate(Date.from(validFrom.plus(7, DAYS))); + try { + ContentSigner signer = new JcaContentSignerBuilder("SHA256WITHRSAENCRYPTION") + .setProvider("BC") + .build(privateKey); + X509CRL crl = new JcaX509CRLConverter().setProvider("BC").getCRL(builder.build(signer)); + return pemEncode(crl); + } catch (CRLException | OperatorCreationException e) { + throw new RuntimeException("Unexpected exception " + e, e); + } + } + + public void sign(CertificateBuilder builder) { + builder.withIssuer(getDn()); + builder.signedBy(privateKey); + } + + public X500Principal getSubject() { + return certificate.getSubjectX500Principal(); + } + + public String getDn() { + return X500Name.getInstance(getSubject().getEncoded()).toString(); + } + + public String getOldDn() { + String rfc2253 = getSubject().getName(); + return OpensslNameUtils.convertFromRfc2253(rfc2253, true); + } + } + + /** + * A builder pattern class for creating a new CA. + */ + private static class CABuilder { + private String name; + private Optional signedBy = Optional.empty(); + private final PublicKey publicKey; + private final PrivateKey privateKey; + + public CABuilder() { + KeyPair kp = buildKeyPair(); + publicKey = kp.getPublic(); + privateKey = kp.getPrivate(); + } + + public CABuilder withName(String name) { + this.name = name; + return this; + } + + public CABuilder selfSigned() { + signedBy = Optional.empty(); + return this; + } + + public CABuilder signedBy(CA ca) { + signedBy = Optional.of(ca); + return this; + } + + private KeyPair buildKeyPair() { + KeyPairGenerator keyGen; + try { + keyGen = KeyPairGenerator.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("RSA not supported: " + e.getMessage(), e); + } + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } + + private X509Certificate buildCertificate() throws OperatorCreationException, CertIOException, CertificateException { + CertificateBuilder certBuilder = new CertificateBuilder() + .withPublicKey(publicKey) + .withSubject(name) + .asCA(); + CertificateBuilder certBuilderWithSigner = signedBy + .map(ca -> certBuilder.signedBy(ca)) + .orElseGet(() -> certBuilder.withIssuer(name).signedBy(privateKey)); + X509Certificate certificate = certBuilderWithSigner.build(); + return certificate; + } + + public CA build() throws OperatorCreationException, OperatorCreationException, CertIOException, CertificateException { + assertThat(name, not(nullValue())); + + X509Certificate certificate = buildCertificate(); + + return new CA(certificate, privateKey); + } + } + + /** + * A builder pattern class for creating a certificate. It can do this from + * either an existing public key or by generating a fresh public/private + * key-pair and discarding the private key. + */ + private static class CertificateBuilder { + private PrivateKey signingKey; + private PublicKey publicKey; + private X500Name subject; + private X500Name issuer; + private Instant notBefore = Instant.now().minus(2, HOURS); + private Instant notAfter = Instant.now().plus(2, HOURS); + private BigInteger serial = new BigInteger(Long.toString(Instant.now().getEpochSecond())); + private String algorithm = "SHA256WithRSA"; + private boolean isCA; + + public CertificateBuilder signedBy(CA ca) { + ca.sign(this); + return this; + } + + public CertificateBuilder signedBy(PrivateKey key) { + signingKey = key; + return this; + } + + public CertificateBuilder withSubject(String dn) { + this.subject = new X500Name(dn); + return this; + } + + public CertificateBuilder withIssuer(String dn) { + try { + X500Principal p = X500NameUtils.getX500Principal(dn); + this.issuer = new X500Name(p.getName()); + return this; + } catch (IOException e) { + throw new RuntimeException(e.toString(), e); + } + } + + public CertificateBuilder asCA() { + isCA = true; + return this; + } + + public CertificateBuilder asEEC() { + isCA = false; + return this; + } + + public CertificateBuilder withPublicKey(PublicKey key) { + publicKey = requireNonNull(key); + return this; + } + + public CertificateBuilder ofLostCredental() { + KeyPairGenerator keyGen; + try { + keyGen = KeyPairGenerator.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("RSA not supported: " + e.getMessage(), e); + } + keyGen.initialize(2048); + KeyPair kp = keyGen.generateKeyPair(); + publicKey = kp.getPublic(); + return this; // Whoopsie, we just lost the private key. + } + + public X509Certificate build() throws OperatorCreationException, + CertIOException, CertificateException { + assertThat(publicKey, not(nullValue())); + assertThat(signingKey, not(nullValue())); + assertThat(subject, not(nullValue())); + assertThat(issuer, not(nullValue())); + + ContentSigner contentSigner = new JcaContentSignerBuilder(algorithm).build(signingKey); + JcaX509v3CertificateBuilder certBuilder = + new JcaX509v3CertificateBuilder(issuer, serial, + Date.from(notBefore), Date.from(notAfter), subject, + publicKey); + BasicConstraints basicConstraints = new BasicConstraints(isCA); + certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, + basicConstraints); + X509CertificateHolder holder = certBuilder.build(contentSigner); + return new JcaX509CertificateConverter().setProvider("BC") + .getCertificate(holder); + } + } +}