diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 6e1a2fec15..1f5f7693e2 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -14,7 +14,10 @@ import org.bouncycastle.asn1.bc.BCObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x9.X9ObjectIdentifiers import org.bouncycastle.cert.bc.BcX509ExtensionUtils import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter @@ -558,26 +561,26 @@ object Crypto { /** * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key. */ - fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair, + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, subject: X500Name, subjectPublicKey: PublicKey, - keyUsage: KeyUsage, purposes: List, validityWindow: Pair, - pathLength: Int? = null, subjectAlternativeName: List? = null): X509Certificate { + nameConstraints: NameConstraints? = null): X509Certificate { val signatureScheme = findSignatureScheme(issuerKeyPair.private) val provider = providerMap[signatureScheme.providerName] val serial = BigInteger.valueOf(random63BitValue()) - val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } }) + val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded))) - .addExtension(Extension.basicConstraints, pathLength != null, if (pathLength == null) BasicConstraints(false) else BasicConstraints(pathLength)) - .addExtension(Extension.keyUsage, false, keyUsage) + .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) + .addExtension(Extension.keyUsage, false, certificateType.keyUsage) .addExtension(Extension.extendedKeyUsage, false, keyPurposes) - if (subjectAlternativeName != null && subjectAlternativeName.isNotEmpty()) { - builder.addExtension(Extension.subjectAlternativeName, false, DERSequence(subjectAlternativeName.toTypedArray())) + if (nameConstraints != null) { + builder.addExtension(Extension.nameConstraints, true, nameConstraints) } + val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply { checkValidity(Date()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index 73a7192afa..f8a3ec7c5d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -5,23 +5,19 @@ import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.* import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.openssl.jcajce.JcaPEMWriter -import org.bouncycastle.util.IPAddress import org.bouncycastle.util.io.pem.PemReader import java.io.FileReader import java.io.FileWriter import java.io.InputStream -import java.net.InetAddress import java.nio.file.Path -import java.security.InvalidAlgorithmParameterException import java.security.KeyPair import java.security.KeyStore import java.security.PublicKey import java.security.cert.* +import java.security.cert.Certificate import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -31,20 +27,12 @@ object X509Utilities { val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 // Aliases for private keys and certificates. - val CORDA_ROOT_CA_PRIVATE_KEY = "cordarootcaprivatekey" val CORDA_ROOT_CA = "cordarootca" - val CORDA_INTERMEDIATE_CA_PRIVATE_KEY = "cordaintermediatecaprivatekey" val CORDA_INTERMEDIATE_CA = "cordaintermediateca" - val CORDA_CLIENT_CA_PRIVATE_KEY = "cordaclientcaprivatekey" + val CORDA_CLIENT_TLS = "cordaclienttls" val CORDA_CLIENT_CA = "cordaclientca" - private val CA_KEY_USAGE = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign) - private val CLIENT_KEY_USAGE = KeyUsage(KeyUsage.digitalSignature) - private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage) - private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth) - private val DEFAULT_VALIDITY_WINDOW = Pair(Duration.ofMillis(0), Duration.ofDays(365 * 10)) - /** * Helper function to return the latest out of an instant and an optional date. */ @@ -110,101 +98,59 @@ object X509Utilities { } /* - * Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. - * @param subject the cert Subject will be populated with the domain string. - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. - * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. - * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates. + * Create a de novo root self-signed X509 v3 CA cert. */ @JvmStatic - fun createSelfSignedCACert(subject: X500Name, - keyPair: KeyPair, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { + fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) - val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2) - return CertificateAndKeyPair(cert, keyPair) + val cert = Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) + return cert } - @JvmStatic - fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair - = createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow) - /** - * Create a de novo root intermediate X509 v3 CA cert and KeyPair. + * Create a X509 v3 cert. + * @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 ca The Public certificate and KeyPair of the root CA certificate above this used to sign it. - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. + * @param subjectPublicKey subject 's public key. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @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. */ @JvmStatic - fun createIntermediateCert(subject: X500Name, - ca: CertificateAndKeyPair, - signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { - val keyPair = generateKeyPair(signatureScheme) - val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) - val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1) - return CertificateAndKeyPair(cert, keyPair) - } - - /** - * Create an X509v3 certificate suitable for use in TLS roles. - * @param subject The contents to put in the subject field of the certificate. - * @param publicKey The PublicKey to be wrapped in the certificate. - * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate. - * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes. - * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes. - * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. - * @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS. - * This certificate is not marked as a CA cert to be similar in nature to commercial certificates. - */ - @JvmStatic - fun createTlsServerCert(subject: X500Name, publicKey: PublicKey, - ca: CertificateAndKeyPair, - subjectAlternativeNameDomains: List, - subjectAlternativeNameIps: List, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { - - val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) - val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) } - val ipAddresses = subjectAlternativeNameIps.filter { - IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it) - }.map { GeneralName(GeneralName.iPAddress, it) } - return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses) + fun createCertificate(certificateType: CertificateType, + issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, + nameConstraints: NameConstraints? = null): X509Certificate { + val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate) + val cert = Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) + return cert } /** * Build a certificate path from a trusted root certificate to a target certificate. This will always return a path - * directly from the root to the target, with no intermediate certificates (presuming that path is valid). + * directly from the target to the root. * - * @param rootCertAndKey trusted root certificate that will be the start of the path. - * @param targetCertAndKey certificate the path ends at. + * @param trustedRoot trusted root certificate that will be the start of the path. + * @param certificates certificates in the path. * @param revocationEnabled whether revocation of certificates in the path should be checked. */ - fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair, - targetCertAndKey: X509Certificate, - revocationEnabled: Boolean): CertPathBuilderResult { - val intermediateCertificates = setOf(targetCertAndKey) - val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates)) - val certPathFactory = CertPathBuilder.getInstance("PKIX") - val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null) - val certPathParameters = try { - PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply { - certificate = targetCertAndKey - }) - } catch (ex: InvalidAlgorithmParameterException) { - throw RuntimeException(ex) - }.apply { - addCertStore(certStore) - isRevocationEnabled = revocationEnabled - } - return certPathFactory.build(certPathParameters) + fun createCertificatePath(trustedRoot: X509Certificate, vararg certificates: X509Certificate, revocationEnabled: Boolean): CertPath { + val certFactory = CertificateFactory.getInstance("X509") + val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) + params.isRevocationEnabled = revocationEnabled + return certFactory.generateCertPath(certificates.toList()) + } + + fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) { + require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } + val certFactory = CertificateFactory.getInstance("X509") + val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(certificates.toList()) + val pathValidator = CertPathValidator.getInstance("PKIX") + pathValidator.validate(certPath, params) } /** @@ -237,7 +183,8 @@ object X509Utilities { /** * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine. - * @param keyStoreFilePath KeyStore path to save output to. + * @param sslKeyStorePath KeyStore path to save ssl key and cert to. + * @param clientCAKeystorePath KeyStore path to save client CA key and cert to. * @param storePassword access password for KeyStore. * @param keyPassword PrivateKey access password for the generated keys. * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. @@ -245,57 +192,66 @@ object X509Utilities { * @param caKeyPassword password to unlock private keys in the CA KeyStore. * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications. */ - fun createKeystoreForSSL(keyStoreFilePath: Path, - storePassword: String, - keyPassword: String, - caKeyStore: KeyStore, - caKeyPassword: String, - commonName: X500Name, - signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore { + fun createKeystoreForCordaNode(sslKeyStorePath: Path, + clientCAKeystorePath: Path, + storePassword: String, + keyPassword: String, + caKeyStore: KeyStore, + caKeyPassword: String, + legalName: X500Name, + signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) { - val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) - val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) + val rootCACert = caKeyStore.getX509Certificate(CORDA_ROOT_CA) + val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA, caKeyPassword) - val serverKey = generateKeyPair(signatureScheme) - val host = InetAddress.getLocalHost() - val serverCert = createTlsServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress)) + val clientKey = generateKeyPair(signatureScheme) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName))), arrayOf()) + val clientCACert = createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, legalName, clientKey.public, nameConstraints = nameConstraints) + + val tlsKey = generateKeyPair(signatureScheme) + val clientTLSCert = createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) val keyPass = keyPassword.toCharArray() - val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) - keyStore.addOrReplaceKey( - CORDA_CLIENT_CA_PRIVATE_KEY, - serverKey.private, + val clientCAKeystore = KeyStoreUtilities.loadOrCreateKeyStore(clientCAKeystorePath, storePassword) + clientCAKeystore.addOrReplaceKey( + CORDA_CLIENT_CA, + clientKey.private, keyPass, - arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate)) - keyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, serverCert) - keyStore.save(keyStoreFilePath, storePassword) - return keyStore + arrayOf(clientCACert, intermediateCACert, rootCACert)) + clientCAKeystore.save(clientCAKeystorePath, storePassword) + + val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeyStorePath, storePassword) + tlsKeystore.addOrReplaceKey( + CORDA_CLIENT_TLS, + tlsKey.private, + keyPass, + arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)) + tlsKeystore.save(sslKeyStorePath, storePassword) } fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) = Crypto.createCertificateSigningRequest(subject, keyPair, signatureScheme) } /** - * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an - * exception. + * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } /** * Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this * adds one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } /** * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. * * @param mutator a function to generate the new value from the previous one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name { val builder = X500NameBuilder(BCStyle.INSTANCE) var matched = false @@ -319,6 +275,7 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() +val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject class CertificateStream(val input: InputStream) { private val certificateFactory = CertificateFactory.getInstance("X.509") @@ -327,3 +284,11 @@ class CertificateStream(val input: InputStream) { } data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair) + +enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { + ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false), + IDENTITY(KeyUsage(KeyUsage.digitalSignature), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false) +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt new file mode 100644 index 0000000000..2d687df267 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -0,0 +1,124 @@ +package net.corda.core.crypto + +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Test +import java.security.KeyStore +import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateFactory +import java.security.cert.PKIXParameters +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class X509NameConstraintsTest { + + private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { + val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Root CA"), rootKeys) + + val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getDevX509Name("Corda Intermediate CA"), intermediateCAKeyPair.public) + + val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getDevX509Name("Corda Client CA"), clientCAKeyPair.public, nameConstraints = nameConstraints) + + val keyPass = "password" + val trustStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) + trustStore.load(null, keyPass.toCharArray()) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) + + val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public) + + val keyStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) + keyStore.load(null, keyPass.toCharArray()) + keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(), arrayOf(tlsCert, clientCACert, intermediateCACert, rootCACert)) + return Pair(keyStore, trustStore) + } + + @Test + fun `illegal common name`() { + val acceptableNames = listOf("CN=Bank A TLS, O=Bank A", "CN=Bank A") + .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() + + val nameConstraints = NameConstraints(acceptableNames, arrayOf()) + val pathValidator = CertPathValidator.getInstance("PKIX") + val certFactory = CertificateFactory.getInstance("X509") + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, O=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + } + + @Test + fun `x500 name with correct cn and extra attribute`() { + val acceptableNames = listOf("CN=Bank A TLS, UID=", "O=Bank A") + .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() + + val nameConstraints = NameConstraints(acceptableNames, arrayOf()) + val certFactory = CertificateFactory.getInstance("X509") + Crypto.ECDSA_SECP256R1_SHA256 + val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME) + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A, UID=12345"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, UID=, E=me@email.com, C=UK"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("O=Bank A, UID=, E=me@email.com, C=UK"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt index 4612349134..2a25d8c60f 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -1,10 +1,13 @@ package net.corda.core.crypto +import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 +import net.corda.core.crypto.Crypto.generateKeyPair +import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME +import net.corda.core.crypto.X509Utilities.createSelfSignedCACertificate import net.corda.core.div import net.corda.testing.MEGA_CORP import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.GeneralName import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -33,61 +36,53 @@ class X509UtilitiesTest { @Test fun `create valid self-signed CA certificate`() { - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test Cert")) - assertTrue { caCertAndKey.certificate.subjectDN.name.contains("CN=Test Cert") } // using our subject common name - assertEquals(caCertAndKey.certificate.issuerDN, caCertAndKey.certificate.subjectDN) //self-signed - caCertAndKey.certificate.checkValidity(Date()) // throws on verification problems - caCertAndKey.certificate.verify(caCertAndKey.keyPair.public) // throws on verification problems - assertTrue { caCertAndKey.certificate.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) - assertTrue { caCertAndKey.certificate.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + assertTrue { caCert.subjectDN.name.contains("CN=Test Cert") } // using our subject common name + assertEquals(caCert.issuerDN, caCert.subjectDN) //self-signed + caCert.checkValidity(Date()) // throws on verification problems + caCert.verify(caKey.public) // throws on verification problems + assertTrue { caCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) + assertTrue { caCert.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate } @Test fun `load and save a PEM file certificate`() { val tmpCertificateFile = tempFile("cacert.pem") - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test Cert")) - X509Utilities.saveCertificateAsPEMFile(caCertAndKey.certificate, tmpCertificateFile) + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) - assertEquals(caCertAndKey.certificate, readCertificate) + assertEquals(caCert, readCertificate) } @Test fun `create valid server certificate chain`() { - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert")) + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) val subjectDN = getTestX509Name("Server Cert") - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val serverCert = X509Utilities.createTlsServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) + val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subjectDN, keyPair.public) assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name - assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert + assertEquals(caCert.issuerDN, serverCert.issuerDN) // Issued by our CA cert serverCert.checkValidity(Date()) // throws on verification problems - serverCert.verify(caCertAndKey.keyPair.public) // throws on verification problems + serverCert.verify(caKey.public) // throws on verification problems assertFalse { serverCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) assertTrue { serverCert.basicConstraints == -1 } // This returns the signing path length should be -1 for non-CA certificate - assertEquals(2, serverCert.subjectAlternativeNames.size) - var foundAliasDnsName = false - for (entry in serverCert.subjectAlternativeNames) { - val typeId = entry[0] as Int - val value = entry[1] as String - if (typeId == GeneralName.iPAddress) { - assertEquals("10.0.0.54", value) - } else if (value == "alias name") { - foundAliasDnsName = true - } - } - assertTrue(foundAliasDnsName) } @Test fun `storing EdDSA key in java keystore`() { val tmpKeyStore = tempFile("keystore.jks") - val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), Crypto.EDDSA_ED25519_SHA512) + val keyPair = generateKeyPair(EDDSA_ED25519_SHA512) + val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) - assertEquals(selfSignCert.certificate.publicKey, selfSignCert.keyPair.public) + assertEquals(selfSignCert.publicKey, keyPair.public) // Save the EdDSA private key with self sign cert in the keystore. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", selfSignCert.keyPair.private, "password".toCharArray(), arrayOf(selfSignCert.certificate)) + keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), arrayOf(selfSignCert)) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -97,20 +92,21 @@ class X509UtilitiesTest { assertNotNull(pubKey) assertNotNull(privateKey) - assertEquals(selfSignCert.keyPair.public, pubKey) - assertEquals(selfSignCert.keyPair.private, privateKey) + assertEquals(keyPair.public, pubKey) + assertEquals(keyPair.private, privateKey) } @Test fun `signing EdDSA key with EcDSA certificate`() { val tmpKeyStore = tempFile("keystore.jks") - val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test")) - val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") - val edDSACert = X509Utilities.createTlsServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54")) + val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val ecDSACert = createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey) + val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512) + val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, X500Name("CN=TestEdDSA"), edDSAKeypair.public) // Save the EdDSA private key with cert chains. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert.certificate, edDSACert)) + keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert, edDSACert)) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -138,8 +134,8 @@ class X509UtilitiesTest { // Load back generated root CA Cert and private key from keystore and check against copy in truststore val keyStore = KeyStoreUtilities.loadKeyStore(tmpKeyStore, "keystorepass") val trustStore = KeyStoreUtilities.loadKeyStore(tmpTrustStore, "trustpass") - val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY) as X509Certificate - val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey + val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate + val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate assertEquals(rootCaCert, rootCaFromTrustStore) rootCaCert.checkValidity(Date()) @@ -147,24 +143,25 @@ class X509UtilitiesTest { // Now sign something with private key and verify against certificate public key val testData = "12345".toByteArray() - val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } + val caSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } // Load back generated intermediate CA Cert and private key - val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) as X509Certificate - val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey + val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate + val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey intermediateCaCert.checkValidity(Date()) intermediateCaCert.verify(rootCaCert.publicKey) // Now sign something with private key and verify against certificate public key - val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } + val intermediateSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } } @Test fun `create server certificate in keystore for SSL`() { val tmpCAKeyStore = tempFile("keystore.jks") val tmpTrustStore = tempFile("truststore.jks") + val tmpSSLKeyStore = tempFile("sslkeystore.jks") val tmpServerKeyStore = tempFile("serverkeystore.jks") // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store @@ -176,30 +173,39 @@ class X509UtilitiesTest { // Load signing intermediate CA cert val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") - val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") + val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass") // Generate server cert and private key and populate another keystore suitable for SSL - X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) + X509Utilities.createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) // Load back server certificate val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") - val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") + val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass") serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) assertTrue { serverCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } + // Load back server certificate + val sslKeyStore = KeyStoreUtilities.loadKeyStore(tmpSSLKeyStore, "serverstorepass") + val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass") + + sslCertAndKey.certificate.checkValidity(Date()) + sslCertAndKey.certificate.verify(serverCertAndKey.certificate.publicKey) + + assertTrue { sslCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() - val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.certificate.publicKey, signature, testData) } + val signature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.certificate.publicKey, signature, testData) } } @Test fun `create server cert and use in SSL socket`() { val tmpCAKeyStore = tempFile("keystore.jks") val tmpTrustStore = tempFile("truststore.jks") + val tmpSSLKeyStore = tempFile("sslkeystore.jks") val tmpServerKeyStore = tempFile("serverkeystore.jks") // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store @@ -210,7 +216,8 @@ class X509UtilitiesTest { "trustpass") // Generate server cert and private key and populate another keystore suitable for SSL - val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name) + X509Utilities.createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name) + val keyStore = KeyStoreUtilities.loadKeyStore(tmpSSLKeyStore, "serverstorepass") val trustStore = KeyStoreUtilities.loadKeyStore(tmpTrustStore, "trustpass") val context = SSLContext.getInstance("TLS") @@ -235,7 +242,7 @@ class X509UtilitiesTest { arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true - serverParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking + serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. serverSocket.sslParameters = serverParams serverSocket.useClientMode = false @@ -247,7 +254,7 @@ class X509UtilitiesTest { "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"), arrayOf("TLSv1.2")) - clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking + clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. clientSocket.sslParameters = clientParams clientSocket.useClientMode = true // We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind @@ -284,8 +291,7 @@ class X509UtilitiesTest { val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal val x500name = X500Name(peerX500Principal.name) assertEquals(MEGA_CORP.name, x500name) - - + X509Utilities.validateCertificateChain(trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA), *peerChain) val output = DataOutputStream(clientSocket.outputStream) output.writeUTF("Hello World") var timeout = 0 @@ -324,36 +330,40 @@ class X509UtilitiesTest { trustStoreFilePath: Path, trustStorePassword: String ): KeyStore { - val rootCA = X509Utilities.createSelfSignedCACert(X509Utilities.getDevX509Name("Corda Node Root CA")) - val intermediateCA = X509Utilities.createIntermediateCert(X509Utilities.getDevX509Name("Corda Node Intermediate CA"), rootCA) + val rootCAKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Node Root CA"), rootCAKey) + + val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getDevX509Name("Corda Node Intermediate CA"), intermediateCAKeyPair.public) val keyPass = keyPassword.toCharArray() val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) - keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, rootCA.keyPair.private, keyPass, arrayOf(rootCA.certificate)) + keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf(rootCACert)) - keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, - intermediateCA.keyPair.private, + keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, + intermediateCAKeyPair.private, keyPass, - arrayOf(intermediateCA.certificate, rootCA.certificate)) + arrayOf(intermediateCACert, rootCACert)) keyStore.save(keyStoreFilePath, storePassword) val trustStore = KeyStoreUtilities.loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCA.certificate) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCA.certificate) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert) trustStore.save(trustStoreFilePath, trustStorePassword) return keyStore } + @Test fun `Get correct private key type from Keystore`() { - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair) + val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") - keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate)) + keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert)) val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 4309ba3521..521e0f2b20 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -143,7 +143,7 @@ class KryoTests { @Test fun `serialize - deserialize X509Certififcate`() { - val expected = X509Utilities.createSelfSignedCACert(ALICE.name).certificate + val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) val serialized = expected.serialize(kryo).bytes val actual: X509Certificate = serialized.deserialize(kryo) assertEquals(expected, actual) @@ -151,9 +151,10 @@ class KryoTests { @Test fun `serialize - deserialize X509CertPath`() { - val rootCA = X509Utilities.createSelfSignedCACert(ALICE.name) - val certificate = X509Utilities.createTlsServerCert(BOB.name, BOB_PUBKEY, rootCA, emptyList(), emptyList()) - val expected = X509Utilities.createCertificatePath(rootCA, certificate, false).certPath + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) + val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY) + val expected = X509Utilities.createCertificatePath(rootCACert, certificate, revocationEnabled = false) val serialized = expected.serialize(kryo).bytes val actual: CertPath = serialized.deserialize(kryo) assertEquals(expected, actual) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 59d866917b..446dc4a903 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -73,6 +73,11 @@ UNRELEASED point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards compatibility of CorDapp flows. +* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain. + * The corda node will now be issued a restricted client CA for identity/transaction key signing. + * TLS certificate are now stored in `sslkeystore.jks` and identity keys are stored in `nodekeystore.jks` +.. warning:: The old keystore will need to be removed when upgrading to this version. + Milestone 11.1 -------------- diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index b56f52239a..d77de310db 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -107,8 +107,10 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { */ fun checkStorePasswords() { val config = config ?: return - config.keyStoreFile.read { - KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray()) + arrayOf(config.sslKeystore, config.nodeKeystore).forEach { + it.read { + KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray()) + } } config.trustStoreFile.read { KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 2366f26cfd..cdd0074a1b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -53,14 +53,14 @@ class ArtemisTcpTransport { ) if (config != null && enableSSL) { - config.keyStoreFile.expectedOnDefaultFileSystem() + config.sslKeystore.expectedOnDefaultFileSystem() config.trustStoreFile.expectedOnDefaultFileSystem() - val tlsOptions = mapOf( + val tlsOptions = mapOf( // Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake // and AES encryption TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStoreFile, + TransportConstants.KEYSTORE_PATH_PROP_NAME to config.sslKeystore, TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt index 2832f783db..8bc6644dcf 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt @@ -7,6 +7,7 @@ interface SSLConfiguration { val keyStorePassword: String val trustStorePassword: String val certificatesDirectory: Path - val keyStoreFile: Path get() = certificatesDirectory / "sslkeystore.jks" + val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" + val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 64a3ee71a6..f32f267a82 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -1,14 +1,26 @@ package net.corda.services.messaging +import net.corda.core.copyTo +import net.corda.core.createDirectories +import net.corda.core.crypto.* +import net.corda.core.exists import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.config.SSLConfiguration +import net.corda.testing.MEGA_CORP +import net.corda.testing.MINI_CORP import net.corda.testing.messaging.SimpleMQClient import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException +import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.junit.Test +import java.nio.file.Files /** * Runs the security tests with the attacker pretending to be a node on the network. @@ -66,4 +78,56 @@ class MQSecurityAsNodeTest : MQSecurityTest() { attacker.start(PEER_USER, PEER_USER, enableSSL = false) // Login as a peer } } + + @Test + fun `login with invalid certificate chain`() { + val sslConfig = object : SSLConfiguration { + override val certificatesDirectory = Files.createTempDirectory("certs") + override val keyStorePassword: String get() = "cordacadevpass" + override val trustStorePassword: String get() = "trustpass" + + init { + val legalName = MEGA_CORP.name + certificatesDirectory.createDirectories() + if (!trustStoreFile.exists()) { + javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) + } + + val caKeyStore = KeyStoreUtilities.loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + + val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) + val intermediateCA = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") + val clientKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + + // Set name constrain to the legal name. + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName))), arrayOf()) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCA.certificate, intermediateCA.keyPair, legalName, clientKey.public, nameConstraints = nameConstraints) + val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Using different x500 name in the TLS cert which is not allowed in the name constraints. + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, MINI_CORP.name, tlsKey.public) + val keyPass = keyStorePassword.toCharArray() + val clientCAKeystore = KeyStoreUtilities.loadOrCreateKeyStore(nodeKeystore, keyStorePassword) + clientCAKeystore.addOrReplaceKey( + X509Utilities.CORDA_CLIENT_CA, + clientKey.private, + keyPass, + arrayOf(clientCACert, intermediateCA.certificate, rootCACert)) + clientCAKeystore.save(nodeKeystore, keyStorePassword) + + val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeystore, keyStorePassword) + tlsKeystore.addOrReplaceKey( + X509Utilities.CORDA_CLIENT_TLS, + tlsKey.private, + keyPass, + arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert)) + tlsKeystore.save(sslKeystore, keyStorePassword) + } + } + + val attacker = clientTo(alice.configuration.p2pAddress, sslConfig) + + assertThatExceptionOfType(ActiveMQNotConnectedException::class.java).isThrownBy { + attacker.start(PEER_USER, PEER_USER) + } + } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2ec1b77ce0..348699c273 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -391,7 +391,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun makeServiceEntries(): List { return advertisedServices.map { val serviceId = it.type.id - val serviceName = it.name ?: configuration.myLegalName.replaceCommonName(serviceId) + val serviceName = it.name ?: X500Name("${configuration.myLegalName},OU=$serviceId") val identity = obtainKeyPair(serviceId, serviceName).first ServiceEntry(it, identity) } @@ -401,16 +401,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun acceptableLiveFiberCountOnStop(): Int = 0 private fun hasSSLCertificates(): Boolean { - val keyStore = try { + val (sslKeystore, keystore) = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - KeyStoreUtilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword) + Pair(KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) } catch (e: IOException) { - null + return false } catch (e: KeyStoreException) { log.warn("Certificate key store found but key store password does not match configuration.") - null + return false } - return keyStore?.containsAlias(X509Utilities.CORDA_CLIENT_CA) ?: false + return sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && keystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } // Specific class so that MockNode can catch it. @@ -600,12 +600,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // the legal name is actually validated in some way. // TODO: Integrate with Key management service? - val keystore = KeyStoreUtilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword) + val keystore = KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) + val clientCA = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, configuration.keyStorePassword) val privateKeyAlias = "$serviceId-private-key" val privKeyFile = configuration.baseDirectory / privateKeyAlias val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" - val identityAndKey = if (configuration.keyStoreFile.exists() && keystore.containsAlias(privateKeyAlias)) { + val identityAndKey = if (configuration.nodeKeystore.exists() && keystore.containsAlias(privateKeyAlias)) { // Get keys from keystore. val (cert, keyPair) = keystore.getCertificateAndKeyPair(privateKeyAlias, configuration.keyStorePassword) val loadedServiceName = X509CertificateHolder(cert.encoded).subject @@ -626,19 +627,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "$serviceName vs ${myIdentity.name}") // Load the private key. val keyPair = privKeyFile.readAll().deserialize() - // TODO: Use a proper certificate chain. - val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) - keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) - keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) + keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) Pair(myIdentity, keyPair) } else { // Create new keys and store in keystore. log.info("Identity key not found, generating fresh key!") val keyPair: KeyPair = generateKeyPair() - val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) - keystore.addOrReplaceKey(privateKeyAlias, selfSignCert.keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) - keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) - Pair(Party(serviceName, selfSignCert.keyPair.public), selfSignCert.keyPair) + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) + keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) + Pair(Party(serviceName, keyPair.public), keyPair) } partyKeys += identityAndKey.second return identityAndKey diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index ded16966be..8fdae16bdb 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -54,8 +54,8 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: X500Name) { if (!trustStoreFile.exists()) { javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) } - if (!keyStoreFile.exists()) { + if (!sslKeystore.exists() || !nodeKeystore.exists()) { val caKeyStore = KeyStoreUtilities.loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") - X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) + X509Utilities.createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 9df1d8f2ed..23c6e1a33a 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -6,7 +6,7 @@ import com.google.common.util.concurrent.SettableFuture import io.netty.handler.ssl.SslHandler import net.corda.core.* import net.corda.core.crypto.* -import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -30,10 +30,7 @@ import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory +import org.apache.activemq.artemis.core.remoting.impl.netty.* import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl @@ -46,8 +43,11 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal +import org.apache.activemq.artemis.utils.ConfigurationHelper import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import rx.Subscription +import sun.security.x509.X509CertImpl import java.io.IOException import java.math.BigInteger import java.security.KeyStore @@ -259,9 +259,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { - val keyStore = KeyStoreUtilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword) + val keyStore = KeyStoreUtilities.loadKeyStore(config.sslKeystore, config.keyStorePassword) val trustStore = KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword) - val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_CA) + val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_TLS) val ourSubjectDN = X500Name(ourCertificate.subjectDN.name) // This is a sanity check and should not fail unless things have been misconfigured @@ -423,9 +423,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort" // This is called on one of Artemis' background threads - internal fun hostVerificationFail(peerLegalName: X500Name, expectedLegalName: X500Name) { - log.error("Peer has wrong CN - expected $expectedLegalName but got $peerLegalName. This is either a fatal " + - "misconfiguration by the remote peer or an SSL man-in-the-middle attack!") + internal fun hostVerificationFail(expectedLegalName: X500Name, errorMsg: String?) { + log.error(errorMsg) if (expectedLegalName == config.networkMapService?.legalName) { // If the peer that failed host verification was the network map node then we're in big trouble and need to bail! _networkMapConnectionFuture!!.setException(IOException("${config.networkMapService} failed host verification check")) @@ -466,7 +465,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, } class VerifyingNettyConnectorFactory : NettyConnectorFactory() { - override fun createConnector(configuration: MutableMap?, + override fun createConnector(configuration: MutableMap, handler: BufferHandler?, listener: ClientConnectionLifeCycleListener?, closeExecutor: Executor?, @@ -478,7 +477,7 @@ class VerifyingNettyConnectorFactory : NettyConnectorFactory() { } } -private class VerifyingNettyConnector(configuration: MutableMap?, +private class VerifyingNettyConnector(configuration: MutableMap, handler: BufferHandler?, listener: ClientConnectionLifeCycleListener?, closeExecutor: Executor?, @@ -486,27 +485,37 @@ private class VerifyingNettyConnector(configuration: MutableMap?, scheduledThreadPool: ScheduledExecutorService?, protocolManager: ClientProtocolManager?) : NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) { - private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer - private val expecteLegalName: X500Name? = configuration?.get(ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME) as X500Name? + private val server = configuration[ArtemisMessagingServer::class.java.name] as ArtemisMessagingServer + private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration) override fun createConnection(): Connection? { - val connection = super.createConnection() as NettyConnection? - if (connection != null && expecteLegalName != null) { - val peerLegalName: X500Name = connection - .channel - .pipeline() - .get(SslHandler::class.java) - .engine() - .session - .peerPrincipal - .name - .let(::X500Name) - if (peerLegalName != expecteLegalName) { + val connection = super.createConnection() as? NettyConnection + if (sslEnabled && connection != null) { + val expectedLegalName = configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] as X500Name + try { + val session = connection.channel + .pipeline() + .get(SslHandler::class.java) + .engine() + .session + // Checks the peer name is the one we are expecting. + val peerLegalName = session.peerPrincipal.name.let(::X500Name) + require(peerLegalName == expectedLegalName) { + "Peer has wrong CN - expected $expectedLegalName but got $peerLegalName. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + // Make sure certificate has the same name. + val peerCertificate = X509CertificateHolder(session.peerCertificateChain.first().encoded) + require(peerCertificate.subject == expectedLegalName) { + "Peer has wrong subject name in the certificate - expected $expectedLegalName but got ${peerCertificate.subject}. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + X509Utilities.validateCertificateChain(X509CertImpl(session.localCertificates.last().encoded), *session.peerCertificates) + server.onTcpConnection(peerLegalName) + } catch (e: IllegalArgumentException) { connection.close() - server!!.hostVerificationFail(peerLegalName, expecteLegalName) - return null // Artemis will keep trying to reconnect until it's told otherwise - } else { - server!!.onTcpConnection(peerLegalName) + server.hostVerificationFail(expectedLegalName, e.message) + return null } } return connection @@ -547,7 +556,7 @@ sealed class CertificateChainCheckPolicy { object LeafMustMatch : CertificateChainCheckPolicy() { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { - val ourPublicKey = keyStore.getCertificate(CORDA_CLIENT_CA).publicKey + val ourPublicKey = keyStore.getCertificate(CORDA_CLIENT_TLS).publicKey return object : Check { override fun checkCertificateChain(theirChain: Array) { val theirLeaf = theirChain.first().publicKey diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 67fab935ce..87a09a22e3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -127,7 +127,7 @@ class RaftUniquenessProvider( return NettyTransport.builder() .withSsl() .withSslProtocol(SslProtocol.TLSv1_2) - .withKeyStorePath(config.keyStoreFile.toString()) + .withKeyStorePath(config.sslKeystore.toString()) .withKeyStorePassword(config.keyStorePassword) .withTrustStorePath(config.trustStoreFile.toString()) .withTrustStorePassword(config.trustStorePassword) diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index aa7b7ff2c0..54343b58a3 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -3,6 +3,7 @@ package net.corda.node.utilities.registration import net.corda.core.* import net.corda.core.crypto.* import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.node.services.config.NodeConfiguration import org.bouncycastle.openssl.jcajce.JcaPEMWriter @@ -31,15 +32,16 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: fun buildKeystore() { config.certificatesDirectory.createDirectories() - val caKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.keyStoreFile, keystorePassword) + val caKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.nodeKeystore, keystorePassword) if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (!caKeyStore.containsAlias(SELF_SIGNED_PRIVATE_KEY)) { - val selfSignCert = X509Utilities.createSelfSignedCACert(config.myLegalName) + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName, keyPair) // Save to the key store. - caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, selfSignCert.keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate)) - caKeyStore.save(config.keyStoreFile, keystorePassword) + caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert)) + caKeyStore.save(config.nodeKeystore, keystorePassword) } val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) val requestId = submitOrResumeCertificateSigningRequest(keyPair) @@ -58,13 +60,22 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: // Save private key and certificate chain to the key store. caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA, keyPair.private, privateKeyPassword.toCharArray(), certificates) caKeyStore.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - caKeyStore.save(config.keyStoreFile, keystorePassword) + caKeyStore.save(config.nodeKeystore, keystorePassword) // Save root certificates to trust store. val trustStore = KeyStoreUtilities.loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword) // Assumes certificate chain always starts with client certificate and end with root certificate. trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last()) trustStore.save(config.trustStoreFile, config.trustStorePassword) - println("Certificate and private key stored in ${config.keyStoreFile}.") + println("Node private key and certificate stored in ${config.nodeKeystore}.") + + println("Generating SSL certificate for node messaging service.") + val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA) + val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, caCert.subject, sslKey.public) + val sslKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.sslKeystore, keystorePassword) + sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), arrayOf(sslCert, *certificates)) + sslKeyStore.save(config.sslKeystore, config.keyStorePassword) + println("SSL private key and certificate stored in ${config.sslKeystore}.") // All done, clean up temp files. requestIdStore.deleteIfExists() } else { @@ -72,6 +83,7 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: } } + /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks b/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks index 77a6bb5db5..af5ff8fce8 100644 Binary files a/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks and b/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks differ diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks b/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks index f21f91eb2f..dd2c81122c 100644 Binary files a/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks and b/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks differ diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index f6057b11e1..ce8c330e1d 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -1,7 +1,6 @@ package net.corda.node.services.network -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.IdentityService @@ -66,13 +65,13 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert unknown anonymous key is unrecognised`() { - val rootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) - val txCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, rootCertAndKey) + val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) + val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val service = InMemoryIdentityService() - val rootKey = rootCertAndKey.keyPair // TODO: Generate certificate with an EdDSA key rather than ECDSA - val identity = Party(rootCertAndKey) - val txIdentity = AnonymousParty(txCertAndKey.keyPair.public) + val identity = Party(CertificateAndKeyPair(rootCert, rootKey)) + val txIdentity = AnonymousParty(txKey.public) assertFailsWith { service.assertOwnership(identity, txIdentity) @@ -85,20 +84,26 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert ownership`() { - val aliceRootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) - val aliceTxCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, aliceRootCertAndKey) - val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCertAndKey, aliceTxCertAndKey.certificate, false).certPath - val bobRootCertAndKey = X509Utilities.createSelfSignedCACert(BOB.name) - val bobTxCertAndKey = X509Utilities.createIntermediateCert(BOB.name, bobRootCertAndKey) - val bobCertPath = X509Utilities.createCertificatePath(bobRootCertAndKey, bobTxCertAndKey.certificate, false).certPath - val service = InMemoryIdentityService() - val alice = Party(aliceRootCertAndKey) - val anonymousAlice = AnonymousParty(aliceTxCertAndKey.keyPair.public) - val bob = Party(bobRootCertAndKey) - val anonymousBob = AnonymousParty(bobTxCertAndKey.keyPair.public) + val aliceRootKey = Crypto.generateKeyPair() + val aliceRootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, aliceRootKey) + val aliceTxKey = Crypto.generateKeyPair() + val aliceTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, aliceRootCert, aliceRootKey, ALICE.name, aliceTxKey.public) + val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCert, aliceTxCert, revocationEnabled = false) - service.registerPath(aliceRootCertAndKey.certificate, anonymousAlice, aliceCertPath) - service.registerPath(bobRootCertAndKey.certificate, anonymousBob, bobCertPath) + val bobRootKey = Crypto.generateKeyPair() + val bobRootCert = X509Utilities.createSelfSignedCACertificate(BOB.name, bobRootKey) + val bobTxKey = Crypto.generateKeyPair() + val bobTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, bobRootCert, bobRootKey, BOB.name, bobTxKey.public) + val bobCertPath = X509Utilities.createCertificatePath(bobRootCert, bobTxCert, revocationEnabled = false) + + val service = InMemoryIdentityService() + val alice = Party(CertificateAndKeyPair(aliceRootCert, aliceRootKey)) + val anonymousAlice = AnonymousParty(aliceTxKey.public) + val bob = Party(CertificateAndKeyPair(bobRootCert, bobRootKey)) + val anonymousBob = AnonymousParty(bobTxKey.public) + + service.registerPath(aliceRootCert, anonymousAlice, aliceCertPath) + service.registerPath(bobRootCert, anonymousBob, bobCertPath) // Verify that paths are verified service.assertOwnership(alice, anonymousAlice) diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index d2d9a8348d..c1d47292f8 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -3,14 +3,12 @@ package net.corda.node.utilities.registration import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock -import net.corda.core.crypto.KeyStoreUtilities -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.* import net.corda.core.exists import net.corda.core.utilities.ALICE import net.corda.testing.TestNodeConfiguration import net.corda.testing.getTestX509Name -import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -31,7 +29,7 @@ class NetworkRegistrationHelperTest { "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA") .map { getTestX509Name(it) } - val certs = identities.map { X509Utilities.createSelfSignedCACert(it).certificate } + val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } .toTypedArray() val certService: NetworkRegistrationService = mock { @@ -44,32 +42,45 @@ class NetworkRegistrationHelperTest { myLegalName = ALICE.name, networkMapService = null) - assertFalse(config.keyStoreFile.exists()) + assertFalse(config.nodeKeystore.exists()) + assertFalse(config.sslKeystore.exists()) assertFalse(config.trustStoreFile.exists()) NetworkRegistrationHelper(config, certService).buildKeystore() - assertTrue(config.keyStoreFile.exists()) + assertTrue(config.nodeKeystore.exists()) + assertTrue(config.sslKeystore.exists()) assertTrue(config.trustStoreFile.exists()) - KeyStoreUtilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword).run { - assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)) - val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - assertEquals(3, certificateChain.size) + val nodeKeystore = KeyStoreUtilities.loadKeyStore(config.nodeKeystore, config.keyStorePassword) + val sslKeystore = KeyStoreUtilities.loadKeyStore(config.sslKeystore, config.keyStorePassword) + val trustStore = KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword) + + + nodeKeystore.run { assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)) + assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + assertEquals(3, certificateChain.size) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) } - KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword).run { - assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)) + sslKeystore.run { + assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) + assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) + assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) + assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + assertEquals(4, certificateChain.size) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + } + + trustStore.run { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)) } } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 45090b343b..35ae8093b2 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -217,7 +217,6 @@ fun getTestX509Name(commonName: String): X500Name { val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) nameBuilder.addRDN(BCStyle.CN, commonName) nameBuilder.addRDN(BCStyle.O, "R3") - nameBuilder.addRDN(BCStyle.OU, "Corda QA Department") nameBuilder.addRDN(BCStyle.L, "New York") nameBuilder.addRDN(BCStyle.C, "US") return nameBuilder.build() diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index a4755f6a0a..247db93a88 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -1,7 +1,6 @@ package net.corda.testing.messaging import com.google.common.net.HostAndPort -import net.corda.core.crypto.X509Utilities import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection @@ -37,6 +36,10 @@ class SimpleMQClient(val target: HostAndPort, fun createMessage(): ClientMessage = session.createMessage(false) fun stop() { - sessionFactory.close() + try { + sessionFactory.close() + } catch (e: Exception) { + // sessionFactory might not have initialised. + } } } \ No newline at end of file diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index a0ce5b7d6b..958465bdc5 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -79,7 +79,7 @@ class NodeWebServer(val config: WebServerConfig) { httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.addCustomizer(SecureRequestCustomizer()) val sslContextFactory = SslContextFactory() - sslContextFactory.keyStorePath = config.keyStoreFile.toString() + sslContextFactory.keyStorePath = config.sslKeystore.toString() sslContextFactory.setKeyStorePassword(config.keyStorePassword) sslContextFactory.setKeyManagerPassword(config.keyStorePassword) sslContextFactory.setTrustStorePath(config.trustStoreFile.toString())