diff --git a/build.gradle b/build.gradle index 0f15cbaa33..a13a246a4b 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ buildscript { ext.jolokia_version = '2.0.0-M1' ext.slf4j_version = '1.7.21' ext.assertj_version = '3.5.1' + ext.bouncycastle_version = '1.54' repositories { mavenCentral() diff --git a/core/build.gradle b/core/build.gradle index 33e81bafc6..76f42028a3 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -60,6 +60,10 @@ dependencies { // Java ed25519 implementation. See https://github.com/str4d/ed25519-java/ compile 'net.i2p.crypto:eddsa:0.1.0' + + // Bouncy castle support needed for X509 certificate manipulation + compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" + compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" } quasarScan.dependsOn('classes') diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt new file mode 100644 index 0000000000..60d8b40329 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt @@ -0,0 +1,542 @@ +package com.r3corda.core.crypto + +import com.r3corda.core.random63BitValue +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.X500NameBuilder +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x509.* +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.bc.BcX509ExtensionUtils +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.util.IPAddress +import org.bouncycastle.util.io.pem.PemReader +import java.io.* +import java.math.BigInteger +import java.net.InetAddress +import java.nio.file.Files +import java.nio.file.Path +import java.security.* +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* + +object X509Utilities { + + val SIGNATURE_ALGORITHM = "SHA256withECDSA" + val KEY_GENERATION_ALGORITHM = "ECDSA" + val ECDSA_CURVE = "secp256k1" // TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys + + val KEYSTORE_TYPE = "JKS" + val CA_CERT_ALIAS = "CA Cert" + val CERT_PRIVATE_KEY_ALIAS = "Server Private Key" + val ROOT_CA_CERT_PRIVATE_KEY_ALIAS = "Root CA Private Key" + val INTERMEDIATE_CA_PRIVATE_KEY_ALIAS = "Intermediate CA Private Key" + + init { + Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates + } + + /** + * Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range + * @param daysBefore number of days to roll back returned start date relative to current date + * @param daysAfter number of days to roll forward returned end date relative to current date + * @param parentNotBefore if provided is used to lower bound the date interval returned + * @param parentNotAfter if provided is used to upper bound the date interval returned + */ + private fun GetCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair { + val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS) + + var notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS)) + if (parentNotBefore != null) { + if (parentNotBefore.after(notBefore)) { + notBefore = parentNotBefore + } + } + + var notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS)) + if (parentNotAfter != null) { + if (parentNotAfter.after(notAfter)) { + notAfter = parentNotAfter + } + } + + return Pair(notBefore, notAfter) + } + + /** + * Encode provided public key in correct format for inclusion in certificate issuer/subject fields + */ + private fun createSubjectKeyIdentifier(key: Key): SubjectKeyIdentifier { + val info = SubjectPublicKeyInfo.getInstance(key.encoded) + return BcX509ExtensionUtils().createSubjectKeyIdentifier(info) + } + + /** + * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key + */ + private fun signCertificate(certificateBuilder: X509v3CertificateBuilder, signedWithPrivateKey: PrivateKey): X509Certificate { + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(signedWithPrivateKey) + return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateBuilder.build(signer)) + } + + /** + * Helper method to create Subject field contents + */ + fun GetX509Name(domain: String): X500Name { + val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) + nameBuilder.addRDN(BCStyle.CN, domain) + nameBuilder.addRDN(BCStyle.O, "R3") + nameBuilder.addRDN(BCStyle.OU, "corda") + nameBuilder.addRDN(BCStyle.L, "London") + nameBuilder.addRDN(BCStyle.C, "UK") + return nameBuilder.build() + } + + /** + * Helper method to either open an existing keystore for modification, or create a new blank keystore + * @param keyStoreFilePath location of KeyStore file + * @param storePassword password to open the store. This does not have to be the same password as any keys stored, + * but for SSL purposes this is recommended. + * @return returns the KeyStore opened/created + */ + fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore { + val pass = storePassword.toCharArray() + val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) + if (Files.exists(keyStoreFilePath)) { + val input = FileInputStream(keyStoreFilePath.toFile()) + input.use { + keyStore.load(input, pass) + } + } else { + keyStore.load(null, pass) + } + return keyStore + } + + /** + * Helper method to open an existing keystore for modification/read + * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException + * @param storePassword password to open the store. This does not have to be the same password as any keys stored, + * but for SSL purposes this is recommended. + * @return returns the KeyStore opened + */ + fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore { + val pass = storePassword.toCharArray() + val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) + val input = FileInputStream(keyStoreFilePath.toFile()) + input.use { + keyStore.load(input, pass) + } + return keyStore + } + + /** + * Helper method to open an existing keystore for modification/read + * @param input stream containing a KeyStore e.g. loaded from a resource file + * @param storePassword password to open the store. This does not have to be the same password as any keys stored, + * but for SSL purposes this is recommended. + * @return returns the KeyStore opened + */ + fun loadKeyStore(input: InputStream, storePassword: String): KeyStore { + val pass = storePassword.toCharArray() + val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) + input.use { + keyStore.load(input, pass) + } + return keyStore + } + + /** + * Helper method save KeyStore to storage + * @param keyStore the KeyStore to persist + * @param keyStoreFilePath the file location to save to + * @param storePassword password to access the store in future. This does not have to be the same password as any keys stored, + * but for SSL purposes this is recommended. + */ + fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) { + val pass = storePassword.toCharArray() + val output = FileOutputStream(keyStoreFilePath.toFile()) + output.use { + keyStore.store(output, pass) + } + } + + /** + * Helper extension method to add, or overwrite any key data in store + * @param alias name to record the private key and certificate chain under + * @param key cryptographic key to store + * @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored, + * but for SSL purposes this is recommended. + * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert + */ + private fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) { + try { + this.deleteEntry(alias) + } catch (kse: KeyStoreException) { + // ignore as may not exist in keystore yet + } + this.setKeyEntry(alias, key, password, chain) + } + + /** + * Helper extension method to add, or overwrite any public certificate data in store + * @param alias name to record the public certificate under + * @param cert certificate to store + */ + private fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { + try { + this.deleteEntry(alias) + } catch (kse: KeyStoreException) { + // ignore as may not exist in keystore yet + } + this.setCertificateEntry(alias, cert) + } + + + /** + * Generate a standard curve ECDSA KeyPair suitable for TLS, although the rest of Corda uses newer curves. + * @return The generated Public/Private KeyPair + */ + fun generateECDSAKeyPairForSSL(): KeyPair { + val keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM, BouncyCastleProvider.PROVIDER_NAME) + val ecSpec = ECGenParameterSpec(ECDSA_CURVE) // Force named curve, because TLS implementations don't support many curves + keyGen.initialize(ecSpec, SecureRandom()) + return keyGen.generateKeyPair() + } + + /** + * Helper data class to pass around public certificate and KeyPair entities when using CA certs + */ + data class CACertAndKey(val certificate: X509Certificate, val keypair: KeyPair) + + + /** + * Create a de novo root self-signed X509 v3 CA cert and KeyPair. + * @param domain The Common (CN) field of the cert Subject will be populated with the domain string + * @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 + */ + fun createSelfSignedCACert(domain: String): CACertAndKey { + val keyPair = generateECDSAKeyPairForSSL() + + val issuer = GetX509Name(domain) + val serial = BigInteger.valueOf(random63BitValue()) + val subject = issuer + val pubKey = keyPair.public + val window = GetCertificateValidityWindow(0, 365 * 10) + + val builder = JcaX509v3CertificateBuilder( + issuer, serial, window.first, window.second, subject, pubKey) + + builder.addExtension(Extension.subjectKeyIdentifier, false, + createSubjectKeyIdentifier(pubKey)) + builder.addExtension(Extension.basicConstraints, true, + BasicConstraints(2)) + + val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign) + builder.addExtension(Extension.keyUsage, false, usage) + + val purposes = ASN1EncodableVector() + purposes.add(KeyPurposeId.id_kp_serverAuth) + purposes.add(KeyPurposeId.id_kp_clientAuth) + purposes.add(KeyPurposeId.anyExtendedKeyUsage) + builder.addExtension(Extension.extendedKeyUsage, false, + DERSequence(purposes)) + + val cert = signCertificate(builder, keyPair.private) + + cert.checkValidity(Date()) + cert.verify(pubKey) + + return CACertAndKey(cert, keyPair) + } + + /** + * Create a de novo root intermediate X509 v3 CA cert and KeyPair. + * @param domain The Common (CN) field of the cert Subject will be populated with the domain string + * @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it + * @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 + */ + fun createIntermediateCert(domain: String, + certificateAuthority: CACertAndKey): CACertAndKey { + val keyPair = generateECDSAKeyPairForSSL() + + val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject + val serial = BigInteger.valueOf(random63BitValue()) + val subject = GetX509Name(domain) + val pubKey = keyPair.public + // One year certificate validity + val window = GetCertificateValidityWindow(0, 365, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter) + + val builder = JcaX509v3CertificateBuilder( + issuer, serial, window.first, window.second, subject, pubKey) + + builder.addExtension(Extension.subjectKeyIdentifier, false, + createSubjectKeyIdentifier(pubKey)) + builder.addExtension(Extension.basicConstraints, true, + BasicConstraints(1)) + + val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign) + builder.addExtension(Extension.keyUsage, false, usage) + + val purposes = ASN1EncodableVector() + purposes.add(KeyPurposeId.id_kp_serverAuth) + purposes.add(KeyPurposeId.id_kp_clientAuth) + purposes.add(KeyPurposeId.anyExtendedKeyUsage) + builder.addExtension(Extension.extendedKeyUsage, false, + DERSequence(purposes)) + + val cert = signCertificate(builder, certificateAuthority.keypair.private) + + cert.checkValidity(Date()) + cert.verify(certificateAuthority.keypair.public) + + return CACertAndKey(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 certificateAuthority 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 + * @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. + */ + fun createServerCert(subject: X500Name, + publicKey: PublicKey, + certificateAuthority: CACertAndKey, + subjectAlternativeNameDomains: List, + subjectAlternativeNameIps: List): X509Certificate { + + val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject + val serial = BigInteger.valueOf(random63BitValue()) + val window = GetCertificateValidityWindow(0, 365, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter) + + val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, publicKey) + builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey)) + builder.addExtension(Extension.basicConstraints, false, BasicConstraints(false)) + + val usage = KeyUsage(KeyUsage.digitalSignature) + builder.addExtension(Extension.keyUsage, false, usage) + + val purposes = ASN1EncodableVector() + purposes.add(KeyPurposeId.id_kp_serverAuth) + purposes.add(KeyPurposeId.id_kp_clientAuth) + builder.addExtension(Extension.extendedKeyUsage, false, + DERSequence(purposes)) + + val subjectAlternativeNames = ArrayList() + subjectAlternativeNames.add(GeneralName(GeneralName.dNSName, subject.getRDNs(BCStyle.CN).first().first.value)) + + for (subjectAlternativeNameDomain in subjectAlternativeNameDomains) { + subjectAlternativeNames.add(GeneralName(GeneralName.dNSName, subjectAlternativeNameDomain)) + } + + for (subjectAlternativeNameIp in subjectAlternativeNameIps) { + if (IPAddress.isValidIPv6WithNetmask(subjectAlternativeNameIp) + || IPAddress.isValidIPv6(subjectAlternativeNameIp) + || IPAddress.isValidIPv4WithNetmask(subjectAlternativeNameIp) + || IPAddress.isValidIPv4(subjectAlternativeNameIp)) { + subjectAlternativeNames.add(GeneralName(GeneralName.iPAddress, subjectAlternativeNameIp)) + } + } + + val subjectAlternativeNamesExtension = DERSequence(subjectAlternativeNames.toTypedArray()) + builder.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNamesExtension) + + val cert = signCertificate(builder, certificateAuthority.keypair.private) + + cert.checkValidity(Date()) + cert.verify(certificateAuthority.keypair.public) + + return cert + } + + /** + * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection + * @param x509Certificate certificate to save + * @param filename Target filename + */ + fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) { + val fileWriter = FileWriter(filename.toFile()) + var jcaPEMWriter: JcaPEMWriter? = null + try { + jcaPEMWriter = JcaPEMWriter(fileWriter) + jcaPEMWriter.writeObject(x509Certificate) + } finally { + jcaPEMWriter?.close() + fileWriter.close() + } + } + + /** + * Helper method to load back a .pem/.cer format file copy of a certificate + * @param filename Source filename + * @return The X509Certificate that was encoded in the file + */ + fun loadCertificateFromPEMFile(filename: Path): X509Certificate { + val reader = PemReader(FileReader(filename.toFile())) + val pemObject = reader.readPemObject() + val certFact = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME) + val inputStream = ByteArrayInputStream(pemObject.content) + try { + val cert = certFact.generateCertificate(inputStream) as X509Certificate + cert.checkValidity() + return cert + } finally { + inputStream.close() + } + } + + /** + * Extract public and private keys from a KeyStore file assuming storage alias is know + * @param keyStoreFilePath Path to load KeyStore from + * @param storePassword Password to unlock the KeyStore + * @param keyPassword Password to unlock the private key entries + * @param alias The name to lookup the Key and Certificate chain from + * @return The KeyPair found in the KeyStore under the specified alias + */ + fun loadKeyPairFromKeyStore(keyStoreFilePath: Path, + storePassword: String, + keyPassword: String, + alias: String): KeyPair { + val keyStore = loadKeyStore(keyStoreFilePath, storePassword) + val keyEntry = keyStore.getKey(alias, keyPassword.toCharArray()) as PrivateKey + val certificate = keyStore.getCertificate(alias) as X509Certificate + return KeyPair(certificate.publicKey, keyEntry) + } + + /** + * Extract public X509 certificate from a KeyStore file assuming storage alias is know + * @param keyStoreFilePath Path to load KeyStore from + * @param storePassword Password to unlock the KeyStore + * @param alias The name to lookup the Key and Certificate chain from + * @return The X509Certificate found in the KeyStore under the specified alias + */ + fun loadCertificateFromKeyStore(keyStoreFilePath: Path, + storePassword: String, + alias: String): X509Certificate { + val keyStore = loadKeyStore(keyStoreFilePath, storePassword) + return keyStore.getCertificate(alias) as X509Certificate + } + + /** + * All in one wrapper to manufacture a root CA cert and an Intermediate CA cert. + * Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs + * @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into. + * @param storePassword The storage password to protect access to the generated KeyStore and public certificates + * @param keyPassword The password that protects the CA private keys. + * Unlike the SSL libraries that tend to assume the password is the same as the keystore password. + * These CA private keys should be protected more effectively with a distinct password. + * @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore + * @param trustStorePassword The password to protect the truststore + * @return The KeyStore object that was saved to file + */ + fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path, + storePassword: String, + keyPassword: String, + trustStoreFilePath: Path, + trustStorePassword: String + ): KeyStore { + val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA") + val intermediateCA = X509Utilities.createIntermediateCert("Corda Node Intermediate CA", rootCA) + + val keypass = keyPassword.toCharArray() + val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) + + keyStore.addOrReplaceKey(ROOT_CA_CERT_PRIVATE_KEY_ALIAS, rootCA.keypair.private, keypass, arrayOf(rootCA.certificate)) + + keyStore.addOrReplaceKey(INTERMEDIATE_CA_PRIVATE_KEY_ALIAS, + intermediateCA.keypair.private, + keypass, + arrayOf(intermediateCA.certificate, rootCA.certificate)) + + saveKeyStore(keyStore, keyStoreFilePath, storePassword) + + val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword) + + trustStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate) + + saveKeyStore(trustStore, trustStoreFilePath, trustStorePassword) + + return keyStore + } + + /** + * Helper method to load a Certificate and KeyPair from their KeyStore. + * The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys. + * @param keyStore Source KeyStore to look in for the data + * @param keyPassword The password for the PrivateKey (not the store access password) + * @param alias The name to search for the data. Typically if generated with the methods here this will be one of + * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above + */ + fun loadCertificateAndKey(keyStore: KeyStore, + keyPassword: String, + alias: String): CACertAndKey { + val keypass = keyPassword.toCharArray() + val key = keyStore.getKey(alias, keypass) as PrivateKey + val cert = keyStore.getCertificate(alias) as X509Certificate + return CACertAndKey(cert, KeyPair(cert.publicKey, key)) + } + + /** + * 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 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. + * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore + * @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): KeyStore { + val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore, + caKeyPassword, + X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS) + val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore, + caKeyPassword, + X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) + + val serverKey = X509Utilities.generateECDSAKeyPairForSSL() + val host = InetAddress.getLocalHost() + val subject = GetX509Name(host.canonicalHostName) + val serverCert = X509Utilities.createServerCert(subject, + serverKey.public, + intermediateCA, + listOf(), + listOf(host.hostAddress)) + + val keypass = keyPassword.toCharArray() + val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) + + keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS, + serverKey.private, + keypass, + arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate)) + + keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate) + + saveKeyStore(keyStore, keyStoreFilePath, storePassword) + + return keyStore + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt new file mode 100644 index 0000000000..65cb19c455 --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt @@ -0,0 +1,285 @@ +package com.r3corda.core.crypto + +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x509.GeneralName +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.nio.file.Paths +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.Signature +import java.security.cert.X509Certificate +import java.util.* +import javax.net.ssl.* +import kotlin.concurrent.thread +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class X509UtilitiesTest { + @Rule + @JvmField + val tempFolder: TemporaryFolder = TemporaryFolder() + + @Test + fun `create valid self-signed CA certificate`() { + val caCertAndKey = X509Utilities.createSelfSignedCACert("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 + } + + + @Test + fun `load and save a PEM file certificate`() { + val tmpCertificateFile = tempFolder.root.toPath().resolve("cacert.pem") + + val caCertAndKey = X509Utilities.createSelfSignedCACert("Test Cert") + X509Utilities.saveCertificateAsPEMFile(caCertAndKey.certificate, tmpCertificateFile) + val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) + assertEquals(caCertAndKey.certificate, readCertificate) + } + + + @Test + fun `create valid server certificate chain`() { + val caCertAndKey = X509Utilities.createSelfSignedCACert("Test CA Cert") + val subjectDN = X509Utilities.GetX509Name("Server Cert") + val keypair = X509Utilities.generateECDSAKeyPairForSSL() + val serverCert = X509Utilities.createServerCert(subjectDN, keypair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) + assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name + assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert + serverCert.checkValidity(Date()) // throws on verification problems + serverCert.verify(caCertAndKey.keypair.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(3, serverCert.subjectAlternativeNames.size) + var foundMainDnsName = false + 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(typeId == GeneralName.dNSName) { + if(value == "Server Cert") { + foundMainDnsName = true + } else if (value == "alias name") { + foundAliasDnsName = true + } + } + } + assertTrue(foundMainDnsName) + assertTrue(foundAliasDnsName) + } + + @Test + fun `create full CA keystore`() { + val tmpKeyStore = tempFolder.root.toPath().resolve("keystore.jks") + val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks") + + // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store + X509Utilities.createCAKeyStoreAndTrustStore(tmpKeyStore, + "keystorepass", + "keypass", + tmpTrustStore, + "trustpass") + + // Load back generated root CA Cert and private key from keystore and check against copy in truststore + val keyStore = X509Utilities.loadKeyStore(tmpKeyStore, "keystorepass") + val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass") + val rootCaCert = keyStore.getCertificate(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS) as X509Certificate + val rootCaPrivateKey = keyStore.getKey(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey + val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CA_CERT_ALIAS) as X509Certificate + assertEquals(rootCaCert, rootCaFromTrustStore) + rootCaCert.checkValidity(Date()) + rootCaCert.verify(rootCaCert.publicKey) + + // Now sign something with private key and verify against certificate public key + val testData = "12345".toByteArray() + val caSigner = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + caSigner.initSign(rootCaPrivateKey) + caSigner.update(testData) + val caSignature = caSigner.sign() + val caVerifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + caVerifier.initVerify(rootCaCert.publicKey) + caVerifier.update(testData) + assertTrue { caVerifier.verify(caSignature) } + + // Load back generated intermediate CA Cert and private key + val intermediateCaCert = keyStore.getCertificate(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) as X509Certificate + val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey + intermediateCaCert.checkValidity(Date()) + intermediateCaCert.verify(rootCaCert.publicKey) + + // Now sign something with private key and verify against certificate public key + val intermediateSigner = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + intermediateSigner.initSign(intermediateCaCertPrivateKey) + intermediateSigner.update(testData) + val intermediateSignature = intermediateSigner.sign() + val intermediateVerifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + intermediateVerifier.initVerify(intermediateCaCert.publicKey) + intermediateVerifier.update(testData) + assertTrue { intermediateVerifier.verify(intermediateSignature) } + } + + + @Test + fun `create server certificate in keystore for SSL`() { + val tmpCAKeyStore = tempFolder.root.toPath().resolve("keystore.jks") + val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks") + val tmpServerKeyStore = tempFolder.root.toPath().resolve("serverkeystore.jks") + + // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store + X509Utilities.createCAKeyStoreAndTrustStore(tmpCAKeyStore, + "cakeystorepass", + "cakeypass", + tmpTrustStore, + "trustpass") + + // Load signing intermediate CA cert + val caKeyStore = X509Utilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") + val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) + + // Generate server cert and private key and populate another keystore suitable for SSL + X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass") + + // Load back server certificate + val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") + val serverCertAndKey = X509Utilities.loadCertificateAndKey(serverKeyStore, "serverkeypass", X509Utilities.CERT_PRIVATE_KEY_ALIAS) + + serverCertAndKey.certificate.checkValidity(Date()) + serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) + val host = InetAddress.getLocalHost() + + assertTrue { serverCertAndKey.certificate.subjectDN.name.contains("CN=" + host.hostName) } + + // Now sign something with private key and verify against certificate public key + val testData = "123456".toByteArray() + val signer = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + signer.initSign(serverCertAndKey.keypair.private) + signer.update(testData) + val signature = signer.sign() + val verifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM) + verifier.initVerify(serverCertAndKey.certificate.publicKey) + verifier.update(testData) + assertTrue { verifier.verify(signature) } + } + + @Test + fun `create server cert and use in SSL socket`() { + val tmpCAKeyStore = tempFolder.root.toPath().resolve("keystore.jks") + val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks") + val tmpServerKeyStore = tempFolder.root.toPath().resolve("serverkeystore.jks") + + // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store + val caKeyStore = X509Utilities.createCAKeyStoreAndTrustStore(tmpCAKeyStore, + "cakeystorepass", + "cakeypass", + tmpTrustStore, + "trustpass") + + // Generate server cert and private key and populate another keystore suitable for SSL + val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass") + val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass") + + val context = SSLContext.getInstance("TLS") + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + keyManagerFactory.init(keyStore, "serverstorepass".toCharArray()) + val keyManagers = keyManagerFactory.getKeyManagers() + val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustMgrFactory.init(trustStore) + val trustManagers = trustMgrFactory.trustManagers + context.init(keyManagers, trustManagers, SecureRandom()) + + val serverSocketFactory = context.serverSocketFactory + val clientSocketFactory = context.socketFactory + + val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket + val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"), + arrayOf("TLSv1.2")) + serverParams.wantClientAuth = true + serverParams.needClientAuth = true + serverParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking + serverSocket.sslParameters = serverParams + serverSocket.useClientMode = false + + + val clientSocket = clientSocketFactory.createSocket() as SSLSocket + val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "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 + clientSocket.sslParameters = clientParams + clientSocket.useClientMode = true + + val lock = Object() + var done = false + var serverError = false + + val serverThread = thread() { + try { + val sslServerSocket = serverSocket.accept() + assert(sslServerSocket.isConnected) + val serverInput = DataInputStream(sslServerSocket.inputStream) + val receivedString = serverInput.readUTF() + assertEquals("Hello World", receivedString) + synchronized(lock) { + done = true + lock.notifyAll() + } + sslServerSocket.close() + } catch (ex: Throwable) { + serverError = true + } + } + + clientSocket.connect(InetSocketAddress(InetAddress.getLocalHost(), serverSocket.localPort)) + assertTrue(clientSocket.isConnected) + + // Double check hostname manually + val peerChain = clientSocket.session.peerCertificates + val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal + val x500name = X500Name(peerX500Principal.name) + val cn = x500name.getRDNs(BCStyle.CN).first().first.value.toString() + val hostname = InetAddress.getLocalHost().hostName + assertEquals(hostname, cn) + + + val output = DataOutputStream(clientSocket.outputStream) + output.writeUTF("Hello World") + var timeout = 0 + synchronized(lock) { + while (!done) { + timeout++ + if (timeout > 10) throw IOException("Timed out waiting for server to complete") + lock.wait(1000) + } + } + + clientSocket.close() + serverThread.join(1000) + assertFalse { serverError } + serverSocket.close() + assertTrue(done) + } +} \ No newline at end of file diff --git a/node/build.gradle b/node/build.gradle index 8a289161b5..313a852bbc 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -41,6 +41,7 @@ dependencies { // Artemis: for reliable p2p message queues. compile "org.apache.activemq:artemis-server:${artemis_version}" compile "org.apache.activemq:artemis-core-client:${artemis_version}" + runtime "org.apache.activemq:artemis-amqp-protocol:${artemis_version}" // JAnsi: for drawing things to the terminal in nicely coloured ways. compile "org.fusesource.jansi:jansi:1.13" diff --git a/node/src/main/kotlin/com/r3corda/node/internal/Node.kt b/node/src/main/kotlin/com/r3corda/node/internal/Node.kt index 75b217bc62..2171d1515c 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/Node.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/Node.kt @@ -71,7 +71,10 @@ class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort, override fun startMessagingService() { // Start up the MQ service. - (net as ArtemisMessagingService).start() + (net as ArtemisMessagingService).apply { + configureWithDevSSLCertificate() //Provision a dev certificate and private key if required + start() + } } private fun initWebServer(): Server { diff --git a/node/src/main/kotlin/com/r3corda/node/services/messaging/ArtemisMessagingService.kt b/node/src/main/kotlin/com/r3corda/node/services/messaging/ArtemisMessagingService.kt index 8756e40bb0..9eb2c56552 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/messaging/ArtemisMessagingService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/messaging/ArtemisMessagingService.kt @@ -3,6 +3,7 @@ package com.r3corda.node.services.messaging import com.google.common.net.HostAndPort import com.r3corda.core.RunOnCallerThread import com.r3corda.core.ThreadBox +import com.r3corda.core.crypto.X509Utilities import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.messaging.* import com.r3corda.core.serialization.SingletonSerializeAsToken @@ -15,12 +16,9 @@ import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.config.Configuration 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.invm.InVMAcceptorFactory -import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory -import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.HOST_PROP_NAME -import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.PORT_PROP_NAME +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.* 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 @@ -28,6 +26,7 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule import java.math.BigInteger import java.nio.file.FileSystems +import java.nio.file.Files import java.nio.file.Path import java.time.Instant import java.util.* @@ -37,13 +36,12 @@ import javax.annotation.concurrent.ThreadSafe // TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman. // TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later) -// TODO: SSL /** * This class implements the [MessagingService] API using Apache Artemis, the successor to their ActiveMQ product. * Artemis is a message queue broker and here, we embed the entire server inside our own process. Nodes communicate - * with each other using (by default) an Artemis specific protocol, but it supports other protocols like AQMP/1.0 - * as well. + * with each other using an Artemis specific protocol, but it supports other protocols like AQMP/1.0 + * as well for interop. * * The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must * be able to receive TCP connections in order to receive messages). It is good enough for local communication within @@ -98,6 +96,10 @@ class ArtemisMessagingService(val directory: Path, // TODO: This is not robust and needs to be replaced by more intelligently using the message queue server. private val undeliveredMessages = CopyOnWriteArrayList() + private val keyStorePath = directory.resolve("certificates").resolve("sslkeystore.jks") + private val trustStorePath = directory.resolve("certificates").resolve("truststore.jks") + private val KEYSTORE_PASSWORD = "cordacadevpass" // TODO we need a proper way of managing keystores and passwords + init { require(directory.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" } } @@ -138,9 +140,9 @@ class ArtemisMessagingService(val directory: Path, activeMQServer.registerActivationFailureListener { exception -> throw exception } activeMQServer.start() - // Connect to our in-memory server. + // Connect to our server. clientFactory = ActiveMQClient.createServerLocatorWithoutHA( - TransportConfiguration(InVMConnectorFactory::class.java.name)).createSessionFactory() + tcpTransport(ConnectionDirection.OUTBOUND, myHostPort.hostText, myHostPort.port)).createSessionFactory() // Create a queue on which to receive messages and set up the handler. val session = clientFactory.createSession() @@ -303,8 +305,7 @@ class ArtemisMessagingService(val directory: Path, setConfigDirectories(config, directory) // We will be talking to our server purely in memory. config.acceptorConfigurations = setOf( - tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port), - TransportConfiguration(InVMAcceptorFactory::class.java.name) + tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port) ) return config } @@ -316,9 +317,46 @@ class ArtemisMessagingService(val directory: Path, ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name }, mapOf( + // Basic TCP target details HOST_PROP_NAME to host, - PORT_PROP_NAME to port.toInt() + PORT_PROP_NAME to port.toInt(), + + // Turn on AMQP support, which needs the protoclo jar on the classpath. + // Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop + // It does not use AMQP messages for its own + PROTOCOLS_PROP_NAME to "CORE,AMQP", + + // Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake + // and AES encryption + SSL_ENABLED_PROP_NAME to true, + KEYSTORE_PROVIDER_PROP_NAME to "JKS", + KEYSTORE_PATH_PROP_NAME to keyStorePath, + KEYSTORE_PASSWORD_PROP_NAME to KEYSTORE_PASSWORD, // TODO proper management of keystores and password + TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", + TRUSTSTORE_PATH_PROP_NAME to trustStorePath, + TRUSTSTORE_PASSWORD_PROP_NAME to "trustpass", + ENABLED_CIPHER_SUITES_PROP_NAME to "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_DSS_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2", + NEED_CLIENT_AUTH_PROP_NAME to true ) ) + /** + * Strictly for dev only automatically construct a server certificate\private key signed from + * the CA certs in Node resources. + */ + fun configureWithDevSSLCertificate() { + Files.createDirectories(directory.resolve("certificates")) + if (!Files.exists(trustStorePath)) { + Files.copy(javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordatruststore.jks"), + trustStorePath) + } + if (!Files.exists(keyStorePath)) { + val caKeyStore = X509Utilities.loadKeyStore( + javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordadevcakeys.jks"), + "cordacadevpass") + X509Utilities.createKeystoreForSSL(keyStorePath, KEYSTORE_PASSWORD, KEYSTORE_PASSWORD, caKeyStore, "cordacadevkeypass") + } + } + } diff --git a/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks b/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks new file mode 100644 index 0000000000..c7aee7e249 Binary files /dev/null and b/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks differ diff --git a/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks b/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks new file mode 100644 index 0000000000..af011804e3 Binary files /dev/null and b/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks differ diff --git a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt index 917b2ae9d1..92479eba53 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt @@ -56,6 +56,7 @@ class ArtemisMessagingServiceTests { private fun createMessagingService(): ArtemisMessagingService { return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply { + configureWithDevSSLCertificate() messagingNetwork = this } }