diff --git a/build.gradle b/build.gradle index 60472fe83c..51cb668740 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ buildscript { ext.jolokia_version = '2.0.0-M1' ext.assertj_version = '3.5.1' ext.log4j_version = '2.6.2' + ext.bouncycastle_version = '1.54' repositories { mavenCentral() diff --git a/core/build.gradle b/core/build.gradle index d289d4c60c..85102ed784 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -69,6 +69,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/WhitelistTrustManager.kt b/core/src/main/kotlin/com/r3corda/core/crypto/WhitelistTrustManager.kt new file mode 100644 index 0000000000..afc1e61e15 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/crypto/WhitelistTrustManager.kt @@ -0,0 +1,178 @@ +package com.r3corda.core.crypto + +import sun.security.util.HostnameChecker +import java.net.InetAddress +import java.net.Socket +import java.net.UnknownHostException +import java.security.KeyStore +import java.security.Provider +import java.security.Security +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.concurrent.ConcurrentHashMap +import javax.net.ssl.* + +/** + * Call this to change the default verification algorithm and this use the WhitelistTrustManager + * implementation. This is a work around to the fact that ArtemisMQ and probably many other libraries + * don't correctly configure the SSLParameters with setEndpointIdentificationAlgorithm and thus don't check + * that the certificate matches with the DNS entry requested. This exposes us to man in the middle attacks. + * The issue has been raised with ArtemisMQ: https://issues.apache.org/jira/browse/ARTEMIS-656 + */ +fun registerWhitelistTrustManager() { + if (Security.getProvider("WhitelistTrustManager") == null) { + Security.addProvider(WhitelistTrustManagerProvider) + } +} + +/** + * Custom Security Provider that forces the TrustManagerFactory to be our custom one. + * Also holds the identity of the original TrustManager algorithm so + * that we can delegate most of the checking to the proper Java code. We simply add some more checks. + * + * The whitelist automatically includes the local server DNS name and IP address + * + */ +object WhitelistTrustManagerProvider : Provider("WhitelistTrustManager", + 1.0, + "Provider for custom trust manager that always validates certificate names") { + + val originalTrustProviderAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm") + + private val _whitelist = ConcurrentHashMap.newKeySet() + val whitelist: Set get() = _whitelist.toSet() // The acceptable IP and DNS names for clients and servers. + + init { + // Add ourselves to whitelist as currently we have to connect to a local ArtemisMQ broker + val host = InetAddress.getLocalHost() + addWhitelistEntry(host.hostName) + + // Register our custom TrustManagerFactorySpi + put("TrustManagerFactory.whitelistTrustManager", "com.r3corda.core.crypto.WhitelistTrustManagerSpi") + + // Forcibly change the TrustManagerFactory defaultAlgorithm to be us + // This will apply to all code using TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + // Which includes the standard HTTPS implementation and most other SSL code + // TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)) will + // allow access to the original implementation which is normally "PKIX" + Security.setProperty("ssl.TrustManagerFactory.algorithm", "whitelistTrustManager") + } + + /** + * Adds an extra name to the whitelist if not already present + * If this is a new entry it will internally request a DNS lookup which may block the calling thread. + */ + fun addWhitelistEntry(serverName: String) { + if (!_whitelist.contains(serverName)) { // Safe as we never delete from the set + addWhitelistEntries(listOf(serverName)) + } + } + + /** + * Adds a list of servers to the whitelist and also adds their fully resolved name/ip address after DNS lookup + * If the server name is not an actual DNS name this is silently ignored. + * The DNS request may block the calling thread. + */ + fun addWhitelistEntries(serverNames: List) { + _whitelist.addAll(serverNames) + for (name in serverNames) { + try { + val addresses = InetAddress.getAllByName(name).toList() + _whitelist.addAll(addresses.map { y -> y.canonicalHostName }) + _whitelist.addAll(addresses.map { y -> y.hostAddress }) + } catch (ex: UnknownHostException) { + // Ignore if the server name is not resolvable e.g. for wildcard addresses, or addresses that can only be resolved externally + } + } + } +} + +/** + * Registered TrustManagerFactorySpi + */ +class WhitelistTrustManagerSpi : TrustManagerFactorySpi() { + // Get the original implementation to delegate to (can't use Kotlin delegation on abstract classes unfortunately). + val originalProvider = TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm) + + override fun engineInit(keyStore: KeyStore?) { + originalProvider.init(keyStore) + } + + override fun engineInit(managerFactoryParameters: ManagerFactoryParameters?) { + originalProvider.init(managerFactoryParameters) + } + + override fun engineGetTrustManagers(): Array { + val parent = originalProvider.trustManagers.first() as X509ExtendedTrustManager + // Wrap original provider in ours and return + return arrayOf(WhitelistTrustManager(parent)) + } +} + +/** + * Our TrustManager extension takes the standard certificate checker and first delegates all the + * chain checking to that. If everything is well formed we then simply add a check against our whitelist + */ +class WhitelistTrustManager(val originalProvider: X509ExtendedTrustManager) : X509ExtendedTrustManager() { + // Use same Helper class as standard HTTPS library validator + val checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS) + + private fun checkIdentity(hostname: String?, cert: X509Certificate) { + // Based on standard code in sun.security.ssl.X509TrustManagerImpl.checkIdentity + // if IPv6 strip off the "[]" + if ((hostname != null) && hostname.startsWith("[") && hostname.endsWith("]")) { + checker.match(hostname.substring(1, hostname.length - 1), cert) + } else { + checker.match(hostname, cert) + } + } + + /** + * scan whitelist and confirm the certificate matches at least one entry + */ + private fun checkWhitelist(cert: X509Certificate) { + for (whiteListEntry in WhitelistTrustManagerProvider.whitelist) { + try { + checkIdentity(whiteListEntry, cert) + return // if we get here without throwing we had a match + } catch(ex: CertificateException) { + // Ignore and check the next entry until we find a match, or exhaust the whitelist + } + } + throw CertificateException("Certificate not on whitelist ${cert.subjectDN}") + } + + override fun checkClientTrusted(chain: Array, authType: String, socket: Socket?) { + originalProvider.checkClientTrusted(chain, authType, socket) + checkWhitelist(chain[0]) + } + + override fun checkClientTrusted(chain: Array, authType: String, engine: SSLEngine?) { + originalProvider.checkClientTrusted(chain, authType, engine) + checkWhitelist(chain[0]) + } + + override fun checkClientTrusted(chain: Array, authType: String) { + originalProvider.checkClientTrusted(chain, authType) + checkWhitelist(chain[0]) + } + + override fun checkServerTrusted(chain: Array, authType: String, socket: Socket?) { + originalProvider.checkServerTrusted(chain, authType, socket) + checkWhitelist(chain[0]) + } + + override fun checkServerTrusted(chain: Array, authType: String, engine: SSLEngine?) { + originalProvider.checkServerTrusted(chain, authType, engine) + checkWhitelist(chain[0]) + } + + override fun checkServerTrusted(chain: Array, authType: String) { + originalProvider.checkServerTrusted(chain, authType) + checkWhitelist(chain[0]) + } + + override fun getAcceptedIssuers(): Array { + return originalProvider.acceptedIssuers + } +} 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..1d16a20b0f --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt @@ -0,0 +1,556 @@ +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 + * Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date + * Thus we avoid too many round trip conversions. + */ + 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 getDevX509Name(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) + val output = FileOutputStream(keyStoreFilePath.toFile()) + output.use { + keyStore.store(output, 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 = getDevX509Name(domain) + val serial = BigInteger.valueOf(random63BitValue()) + val subject = issuer + val pubKey = keyPair.public + + // Ten year certificate validity + // TODO how do we manage certificate expiry, revocation and loss + 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 = getDevX509Name(domain) + val pubKey = keyPair.public + + // Ten year certificate validity + // TODO how do we manage certificate expiry, revocation and loss + val window = getCertificateValidityWindow(0, 365*10, 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()) + + // Ten year certificate validity + // TODO how do we manage certificate expiry, revocation and loss + val window = getCertificateValidityWindow(0, 365*10, 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 = getDevX509Name(host.canonicalHostName) + val serverCert = X509Utilities.createServerCert(subject, + serverKey.public, + intermediateCA, + if(host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName), + 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/WhitelistTrustManagerTest.kt b/core/src/test/kotlin/com/r3corda/core/crypto/WhitelistTrustManagerTest.kt new file mode 100644 index 0000000000..a32e55e4ff --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/crypto/WhitelistTrustManagerTest.kt @@ -0,0 +1,203 @@ +package com.r3corda.core.crypto + +import org.junit.BeforeClass +import org.junit.Test +import java.net.Socket +import java.security.KeyStore +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLEngine +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedTrustManager +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class WhitelistTrustManagerTest { + companion object { + @BeforeClass + @JvmStatic + fun registerTrustManager() { + // Validate original factory + assertEquals("PKIX", TrustManagerFactory.getDefaultAlgorithm()) + + //register for all tests + registerWhitelistTrustManager() + } + } + + private fun getTrustmanagerAndCert(whitelist: String, certificateName: String): Pair { + WhitelistTrustManagerProvider.addWhitelistEntry(whitelist) + + val caCertAndKey = X509Utilities.createSelfSignedCACert(certificateName) + + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null, null) + keyStore.setCertificateEntry("cacert", caCertAndKey.certificate) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(keyStore) + + return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, caCertAndKey.certificate) + } + + private fun getTrustmanagerAndUntrustedChainCert(): Pair { + WhitelistTrustManagerProvider.addWhitelistEntry("test.r3corda.com") + + val otherCaCertAndKey = X509Utilities.createSelfSignedCACert("bad root") + + val caCertAndKey = X509Utilities.createSelfSignedCACert("good root") + + val subject = X509Utilities.getDevX509Name("test.r3corda.com") + val serverKey = X509Utilities.generateECDSAKeyPairForSSL() + val serverCert = X509Utilities.createServerCert(subject, + serverKey.public, + otherCaCertAndKey, + listOf(), + listOf()) + + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null, null) + keyStore.setCertificateEntry("cacert", caCertAndKey.certificate) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(keyStore) + + return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, serverCert) + } + + + @Test + fun `getDefaultAlgorithm TrustManager is WhitelistTrustManager`() { + registerWhitelistTrustManager() // Check double register is safe + + assertEquals("whitelistTrustManager", TrustManagerFactory.getDefaultAlgorithm()) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + trustManagerFactory.init(null as KeyStore?) + + val trustManagers = trustManagerFactory.trustManagers + + assertTrue { trustManagers.all { it is WhitelistTrustManager } } + } + + @Test + fun `check certificate works for whitelisted certificate and specific domain`() { + val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.r3corda.com") + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + } + + @Test + fun `check certificate works for specific certificate and wildcard permitted domain`() { + val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com") + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + } + + @Test + fun `check certificate works for wildcard certificate and non wildcard domain`() { + val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com") + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) + + trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) + } + + @Test + fun `check unknown certificate rejected`() { + val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.notr3.com") + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + } + + @Test + fun `check unknown wildcard certificate rejected`() { + val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "*.notr3.com") + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + } + + @Test + fun `check unknown certificate rejected against mismatched wildcard`() { + val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.notr3.com") + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + } + + @Test + fun `check certificate signed by untrusted root is still rejected, despite matched name`() { + val (trustManager, cert) = getTrustmanagerAndUntrustedChainCert() + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) } + + assertFailsWith { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) } + } +} \ 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..ff4168c6ce --- /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.getDevX509Name("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.canonicalHostName) } + + // 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().canonicalHostName + 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 9d1ea59c8c..ad538b6953 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -52,6 +52,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..489d116eda 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/Node.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/Node.kt @@ -67,11 +67,14 @@ class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort, // when our process shuts down, but we try in stop() anyway just to be nice. private var nodeFileLock: FileLock? = null - override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, serverThread) + override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, configuration, serverThread) override fun startMessagingService() { // Start up the MQ service. - (net as ArtemisMessagingService).start() + (net as ArtemisMessagingService).apply { + configureWithDevSSLCertificate() // TODO Create proper certificate provisioning process + start() + } } private fun initWebServer(): Server { diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt index cdb5699b91..ce8b0eae3a 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt @@ -119,6 +119,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override val myLegalName: String = legalName ?: "Mock Company $id" override val exportJMXto: String = "" override val nearestCity: String = "Atlantis" + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } val node = nodeFactory.create(path, config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair) if (start) { diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/Simulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/Simulation.kt index 8227c1b885..72ce1bb6fe 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/Simulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/Simulation.kt @@ -59,6 +59,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val myLegalName: String = "Bank $letter" override val exportJMXto: String = "" override val nearestCity: String = city + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) } @@ -77,6 +79,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val myLegalName: String = "Network coordination center" override val exportJMXto: String = "" override val nearestCity: String = "Amsterdam" + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {} @@ -91,6 +95,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val myLegalName: String = "Notary Service" override val exportJMXto: String = "" override val nearestCity: String = "Zurich" + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) } @@ -104,6 +110,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val myLegalName: String = "Rates Service Provider" override val exportJMXto: String = "" override val nearestCity: String = "Madrid" + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) { @@ -122,6 +130,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override val myLegalName: String = "Regulator A" override val exportJMXto: String = "" override val nearestCity: String = "Paris" + override val keyStorePassword: String = "dummy" + override val trustStorePassword: String = "trustpass" } val n = object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) { diff --git a/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt index 3cd30b29a8..4eb104aca6 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/config/NodeConfiguration.kt @@ -8,6 +8,8 @@ interface NodeConfiguration { val myLegalName: String val exportJMXto: String val nearestCity: String + val keyStorePassword: String + val trustStorePassword: String } // Allow the use of "String by config" syntax. TODO: Make it more flexible. @@ -17,4 +19,6 @@ class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : N override val myLegalName: String by config override val exportJMXto: String by config override val nearestCity: String by config + override val keyStorePassword: String by config + override val trustStorePassword: String by config } \ No newline at end of file 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..b38f6c0fc6 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,11 +3,13 @@ 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 import com.r3corda.core.utilities.loggerFor import com.r3corda.node.internal.Node +import com.r3corda.node.services.config.NodeConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.* @@ -15,12 +17,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 +27,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 +37,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 AMQP/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 @@ -51,11 +50,13 @@ import javax.annotation.concurrent.ThreadSafe * * @param directory A place where Artemis can stash its message journal and other files. * @param myHostPort What host and port to bind to for receiving inbound connections. + * @param config The config object is used to pass in the passwords for the certificate KeyStore and TrustStore * @param defaultExecutor This will be used as the default executor to run message handlers on, if no other is specified. */ @ThreadSafe class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort, + val config: NodeConfiguration, val defaultExecutor: Executor = RunOnCallerThread) : SingletonSerializeAsToken(), MessagingService { // In future: can contain onion routing info, etc. @@ -98,6 +99,21 @@ 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") + + // Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher. + // Our self-generated certificates all use ECDSA for handshakes, but we allow classical RSA certificates to work + // in case we need to use keytool certificates in some demos + private val CIPHER_SUITES = listOf( + "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") + init { require(directory.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" } } @@ -138,9 +154,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 +319,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 +331,47 @@ 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 protocol 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 messages e.g. topology and heartbeats + // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications + 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 config.keyStorePassword, // TODO proper management of keystores and password + TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", + TRUSTSTORE_PATH_PROP_NAME to trustStorePath, + TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword, + ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), + 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. Then provision KeyStores into certificates folder under node path. + */ + 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, config.keyStorePassword, config.keyStorePassword, 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..e2cd96e023 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingServiceTests.kt @@ -2,6 +2,7 @@ package com.r3corda.node.services import com.r3corda.core.messaging.Message import com.r3corda.core.testing.freeLocalHostAndPort +import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.messaging.ArtemisMessagingService import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -55,7 +56,16 @@ class ArtemisMessagingServiceTests { } private fun createMessagingService(): ArtemisMessagingService { - return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply { + val config = object: NodeConfiguration { + override val myLegalName: String = "me" + override val exportJMXto: String = "" + override val nearestCity: String = "London" + override val keyStorePassword: String = "testpass" + override val trustStorePassword: String = "trustpass" + + } + return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort, config).apply { + configureWithDevSSLCertificate() messagingNetwork = this } } diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index 08960dbfa2..99037c5957 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -76,6 +76,8 @@ fun main(args: Array) { override val myLegalName: String = "Rate fix demo node" override val exportJMXto: String = "http" override val nearestCity: String = "Atlantis" + override val keyStorePassword: String = "cordacadevpass" + override val trustStorePassword: String = "trustpass" } val apiAddr = HostAndPort.fromParts(myNetAddr.hostText, myNetAddr.port + 1) diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 65c0d10700..27851335e1 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -1,3 +1,5 @@ myLegalName = "Vast Global MegaCorp, Ltd" exportJMXto = "http" -nearestCity = "The Moon" \ No newline at end of file +nearestCity = "The Moon" +keyStorePassword = "cordacadevpass" +trustStorePassword = "trustpass" \ No newline at end of file