diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 6866074d47..245263768f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -8,12 +8,14 @@ import net.corda.core.internal.* import net.corda.core.utilities.days import net.corda.core.utilities.millis import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x509.* import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.bc.BcX509ExtensionUtils +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.operator.ContentSigner @@ -149,18 +151,25 @@ $trustedRoot""", e, certPath, e.index) /** * Build a partial X.509 certificate ready for signing. * + * @param certificateType type of the certificate. * @param issuer name of the issuing entity. + * @param issuerPublicKey public key of the issuing entity. * @param subject name of the certificate subject. * @param subjectPublicKey public key of the certificate subject. * @param validityWindow the time period the certificate is valid for. * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + * @param crlDistPoint CRL distribution point. + * @param crlIssuer X500Name of the CRL issuer. */ - fun createPartialCertificate(certificateType: CertificateType, + private fun createPartialCertificate(certificateType: CertificateType, issuer: X500Principal, + issuerPublicKey: PublicKey, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509v3CertificateBuilder { + nameConstraints: NameConstraints? = null, + crlDistPoint: String? = null, + crlIssuer: X500Name? = null): X509v3CertificateBuilder { val serial = BigInteger.valueOf(random63BitValue()) val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded)) @@ -171,10 +180,12 @@ $trustedRoot""", e, certPath, e.index) .addExtension(Extension.basicConstraints, true, BasicConstraints(certificateType.isCA)) .addExtension(Extension.keyUsage, false, certificateType.keyUsage) .addExtension(Extension.extendedKeyUsage, false, keyPurposes) + .addExtension(Extension.authorityKeyIdentifier, false, JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerPublicKey)) if (role != null) { builder.addExtension(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), false, role) } + addCrlInfo(builder, crlDistPoint, crlIssuer) if (nameConstraints != null) { builder.addExtension(Extension.nameConstraints, true, nameConstraints) } @@ -185,11 +196,15 @@ $trustedRoot""", e, certPath, e.index) /** * Create a X509 v3 certificate using the given issuer certificate and key pair. * + * @param certificateType type of the certificate. * @param issuerCertificate The Public certificate of the root CA above this used to sign it. * @param issuerKeyPair The KeyPair of the root CA above this used to sign it. * @param subject subject of the generated certificate. * @param subjectPublicKey subject's public key. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + * @param crlDistPoint CRL distribution point. + * @param crlIssuer X500Name of the CRL issuer. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates. */ @@ -200,7 +215,9 @@ $trustedRoot""", e, certPath, e.index) subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, - nameConstraints: NameConstraints? = null): X509Certificate { + nameConstraints: NameConstraints? = null, + crlDistPoint: String? = null, + crlIssuer: X500Name? = null): X509Certificate { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate) return createCertificate( certificateType, @@ -209,28 +226,36 @@ $trustedRoot""", e, certPath, e.index) subject, subjectPublicKey, window, - nameConstraints + nameConstraints, + crlDistPoint, + crlIssuer ) } /** * Build and sign an X.509 certificate with the given signer. * + * @param certificateType type of the certificate. * @param issuer name of the issuing entity. * @param issuerSigner content signer to sign the certificate with. * @param subject name of the certificate subject. * @param subjectPublicKey public key of the certificate subject. * @param validityWindow the time period the certificate is valid for. * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + * @param crlDistPoint CRL distribution point. + * @param crlIssuer X500Name of the CRL issuer. */ fun createCertificate(certificateType: CertificateType, issuer: X500Principal, + issuerPublicKey: PublicKey, issuerSigner: ContentSigner, subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509Certificate { - val builder = createPartialCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) + nameConstraints: NameConstraints? = null, + crlDistPoint: String? = null, + crlIssuer: X500Name? = null): X509Certificate { + val builder = createPartialCertificate(certificateType, issuer, issuerPublicKey, subject, subjectPublicKey, validityWindow, nameConstraints, crlDistPoint, crlIssuer) return builder.build(issuerSigner).run { require(isValidOn(Date())) toJca() @@ -240,6 +265,7 @@ $trustedRoot""", e, certPath, e.index) /** * Build and sign an X.509 certificate with CA cert private key. * + * @param certificateType type of the certificate. * @param issuer name of the issuing entity. * @param issuerKeyPair the public & private key to sign the certificate with. * @param subject name of the certificate subject. @@ -253,11 +279,21 @@ $trustedRoot""", e, certPath, e.index) subject: X500Principal, subjectPublicKey: PublicKey, validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509Certificate { + nameConstraints: NameConstraints? = null, + crlDistPoint: String? = null, + crlIssuer: X500Name? = null): X509Certificate { val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) val provider = Crypto.findProvider(signatureScheme.providerName) val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) - val builder = createPartialCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) + val builder = createPartialCertificate( + certificateType, + issuer, + issuerKeyPair.public, + subject, subjectPublicKey, + validityWindow, + nameConstraints, + crlDistPoint, + crlIssuer) return builder.build(signer).run { require(isValidOn(Date())) require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))) @@ -302,6 +338,21 @@ $trustedRoot""", e, certPath, e.index) fun buildCertPath(certificates: List): CertPath { return X509CertificateFactory().generateCertPath(certificates) } + + private fun addCrlInfo(builder: X509v3CertificateBuilder, crlDistPoint: String?, crlIssuer: X500Name?) { + if (crlDistPoint != null) { + val distPointName = DistributionPointName(GeneralNames(GeneralName(GeneralName.uniformResourceIdentifier, crlDistPoint))) + val crlIssuerGeneralNames = crlIssuer?.let { + GeneralNames(GeneralName(crlIssuer)) + } + // The second argument is flag that allows you to define what reason of certificate revocation is served by this distribution point see [ReasonFlags]. + // The idea is that you have different revocation per revocation reason. Since we won't go into such a granularity, we can skip that parameter. + // The third argument allows you to specify the name of the CRL issuer, it needs to be consistent with the crl (IssuingDistributionPoint) extension and the idp argument. + // If idp == true, set it, if idp == false, leave it null as done here. + val distPoint = DistributionPoint(distPointName, null, crlIssuerGeneralNames) + builder.addExtension(Extension.cRLDistributionPoints, false, CRLDistPoint(arrayOf(distPoint))) + } + } } // Assuming cert type to role is 1:1 diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 8e5aa1c776..1fbd9e1dad 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -20,9 +20,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.TestIdentity import net.corda.testing.internal.createDevIntermediateCaCertPath import org.assertj.core.api.Assertions.assertThat -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.* import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -103,6 +101,28 @@ class X509UtilitiesTest { } } + @Test + fun `create valid server certificate chain includes CRL info`() { + val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), caKey) + val caSubjectKeyIdentifier = SubjectKeyIdentifier.getInstance(caCert.toBc().getExtension(Extension.subjectKeyIdentifier).parsedValue) + val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val crlDistPoint = "http://test.com" + val serverCert = X509Utilities.createCertificate( + CertificateType.TLS, + caCert, + caKey, + X500Principal("CN=Server Cert,O=R3 Ltd,L=London,C=GB"), + keyPair.public, + crlDistPoint = crlDistPoint) + serverCert.toBc().run { + val certCrlDistPoint = CRLDistPoint.getInstance(getExtension(Extension.cRLDistributionPoints).parsedValue) + assertTrue(certCrlDistPoint.distributionPoints.first().distributionPoint.toString().contains(crlDistPoint)) + val certCaAuthorityKeyIdentifier = AuthorityKeyIdentifier.getInstance(getExtension(Extension.authorityKeyIdentifier).parsedValue) + assertTrue(Arrays.equals(caSubjectKeyIdentifier.keyIdentifier, certCaAuthorityKeyIdentifier.keyIdentifier)) + } + } + @Test fun `storing EdDSA key in java keystore`() { val tmpKeyStore = tempFile("keystore.jks") diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 1db8b49e02..6cef78b051 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -40,6 +40,7 @@ fun freshCertificate(identityService: IdentityService, val ourCertificate = X509Utilities.createCertificate( CertificateType.CONFIDENTIAL_LEGAL_IDENTITY, issuerCert.subjectX500Principal, + issuerCert.publicKey, issuerSigner, issuer.name.x500Principal, subjectPublicKey,