diff --git a/.gitignore b/.gitignore index 68b8472e9b..03f02c5ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ lib/dokka.jar .idea/libraries .idea/shelf .idea/dataSources -/gradle-plugins/.idea +/gradle-plugins/.idea/ # Include the -parameters compiler option by default in IntelliJ required for serialization. !.idea/compiler.xml @@ -84,8 +84,10 @@ crashlytics-build.properties docs/virtualenv/ # bft-smart -node/bft-smart-config/currentView -node/config/currentView +config/currentView + +# vim +*.swp # Files you may find useful to have in your working directory. PLAN diff --git a/.idea/compiler.xml b/.idea/compiler.xml index acd527bc35..d37c3b63c3 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -61,10 +61,10 @@ + + - - @@ -98,4 +98,4 @@ - \ No newline at end of file + diff --git a/constants.properties b/constants.properties index 74d2be1352..29575b5763 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.12.1 +gradlePluginsVersion=0.12.2 kotlinVersion=1.1.2 guavaVersion=21.0 bouncycastleVersion=1.56 diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index d8160d70cc..4e73dac456 100644 --- a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -25,7 +25,8 @@ public class CordformNode { public List advertisedServices = emptyList(); /** - * If running a distributed notary, a list of node addresses for joining the Raft cluster + * If running a Raft notary cluster, the address of at least one node in the cluster, or leave blank to start a new cluster. + * If running a BFT notary cluster, the addresses of all nodes in the cluster. */ public List notaryClusterAddresses = emptyList(); /** @@ -82,11 +83,18 @@ public class CordformNode { } /** - * Set the port which to bind the Copycat (Raft) node to + * Set the port which to bind the Copycat (Raft) node to. * * @param notaryPort The Raft port. */ public void notaryNodePort(Integer notaryPort) { config = config.withValue("notaryNodeAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + notaryPort)); } + + /** + * @param id The (0-based) BFT replica ID. + */ + public void bftReplicaId(Integer id) { + config = config.withValue("bftReplicaId", ConfigValueFactory.fromAnyRef(id)); + } } diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index c4218c1784..89fd25e6e9 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -110,8 +110,9 @@ infix fun ListenableFuture.failure(body: (Throwable) -> Unit): Listenable infix fun ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, { (mapper as (F?) -> T)(it) }) infix fun ListenableFuture.flatMap(mapper: (F) -> ListenableFuture): ListenableFuture = Futures.transformAsync(this) { mapper(it!!) } -inline fun Collection.mapToArray(transform: (T) -> R) = run { - val iterator = iterator() +inline fun Collection.mapToArray(transform: (T) -> R) = mapToArray(transform, iterator(), size) +inline fun IntProgression.mapToArray(transform: (Int) -> R) = mapToArray(transform, iterator(), 1 + (last - first) / step) +inline fun mapToArray(transform: (T) -> R, iterator: Iterator, size: Int) = run { var expected = 0 Array(size) { expected++ == it || throw UnsupportedOperationException("Array constructor is non-sequential!") diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 6e1a2fec15..1f5f7693e2 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -14,7 +14,10 @@ import org.bouncycastle.asn1.bc.BCObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x9.X9ObjectIdentifiers import org.bouncycastle.cert.bc.BcX509ExtensionUtils import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter @@ -558,26 +561,26 @@ object Crypto { /** * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key. */ - fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair, + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, subject: X500Name, subjectPublicKey: PublicKey, - keyUsage: KeyUsage, purposes: List, validityWindow: Pair, - pathLength: Int? = null, subjectAlternativeName: List? = null): X509Certificate { + nameConstraints: NameConstraints? = null): X509Certificate { val signatureScheme = findSignatureScheme(issuerKeyPair.private) val provider = providerMap[signatureScheme.providerName] val serial = BigInteger.valueOf(random63BitValue()) - val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } }) + val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded))) - .addExtension(Extension.basicConstraints, pathLength != null, if (pathLength == null) BasicConstraints(false) else BasicConstraints(pathLength)) - .addExtension(Extension.keyUsage, false, keyUsage) + .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) + .addExtension(Extension.keyUsage, false, certificateType.keyUsage) .addExtension(Extension.extendedKeyUsage, false, keyPurposes) - if (subjectAlternativeName != null && subjectAlternativeName.isNotEmpty()) { - builder.addExtension(Extension.subjectAlternativeName, false, DERSequence(subjectAlternativeName.toTypedArray())) + if (nameConstraints != null) { + builder.addExtension(Extension.nameConstraints, true, nameConstraints) } + val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply { checkValidity(Date()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index 73a7192afa..f8a3ec7c5d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -5,23 +5,19 @@ import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.* import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.openssl.jcajce.JcaPEMWriter -import org.bouncycastle.util.IPAddress import org.bouncycastle.util.io.pem.PemReader import java.io.FileReader import java.io.FileWriter import java.io.InputStream -import java.net.InetAddress import java.nio.file.Path -import java.security.InvalidAlgorithmParameterException import java.security.KeyPair import java.security.KeyStore import java.security.PublicKey import java.security.cert.* +import java.security.cert.Certificate import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -31,20 +27,12 @@ object X509Utilities { val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 // Aliases for private keys and certificates. - val CORDA_ROOT_CA_PRIVATE_KEY = "cordarootcaprivatekey" val CORDA_ROOT_CA = "cordarootca" - val CORDA_INTERMEDIATE_CA_PRIVATE_KEY = "cordaintermediatecaprivatekey" val CORDA_INTERMEDIATE_CA = "cordaintermediateca" - val CORDA_CLIENT_CA_PRIVATE_KEY = "cordaclientcaprivatekey" + val CORDA_CLIENT_TLS = "cordaclienttls" val CORDA_CLIENT_CA = "cordaclientca" - private val CA_KEY_USAGE = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign) - private val CLIENT_KEY_USAGE = KeyUsage(KeyUsage.digitalSignature) - private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage) - private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth) - private val DEFAULT_VALIDITY_WINDOW = Pair(Duration.ofMillis(0), Duration.ofDays(365 * 10)) - /** * Helper function to return the latest out of an instant and an optional date. */ @@ -110,101 +98,59 @@ object X509Utilities { } /* - * Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. - * @param subject the cert Subject will be populated with the domain string. - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. - * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. - * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates. + * Create a de novo root self-signed X509 v3 CA cert. */ @JvmStatic - fun createSelfSignedCACert(subject: X500Name, - keyPair: KeyPair, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { + fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) - val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2) - return CertificateAndKeyPair(cert, keyPair) + val cert = Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) + return cert } - @JvmStatic - fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair - = createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow) - /** - * Create a de novo root intermediate X509 v3 CA cert and KeyPair. + * Create a X509 v3 cert. + * @param issuerCertificate The Public certificate of the root CA above this used to sign it. + * @param issuerKeyPair The KeyPair of the root CA above this used to sign it. * @param subject subject of the generated certificate. - * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it. - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. + * @param subjectPublicKey subject 's public key. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates. */ @JvmStatic - fun createIntermediateCert(subject: X500Name, - ca: CertificateAndKeyPair, - signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { - val keyPair = generateKeyPair(signatureScheme) - val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) - val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1) - return CertificateAndKeyPair(cert, keyPair) - } - - /** - * Create an X509v3 certificate suitable for use in TLS roles. - * @param subject The contents to put in the subject field of the certificate. - * @param publicKey The PublicKey to be wrapped in the certificate. - * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate. - * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes. - * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes. - * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. - * @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS. - * This certificate is not marked as a CA cert to be similar in nature to commercial certificates. - */ - @JvmStatic - fun createTlsServerCert(subject: X500Name, publicKey: PublicKey, - ca: CertificateAndKeyPair, - subjectAlternativeNameDomains: List, - subjectAlternativeNameIps: List, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { - - val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) - val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) } - val ipAddresses = subjectAlternativeNameIps.filter { - IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it) - }.map { GeneralName(GeneralName.iPAddress, it) } - return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses) + fun createCertificate(certificateType: CertificateType, + issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, + nameConstraints: NameConstraints? = null): X509Certificate { + val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate) + val cert = Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) + return cert } /** * Build a certificate path from a trusted root certificate to a target certificate. This will always return a path - * directly from the root to the target, with no intermediate certificates (presuming that path is valid). + * directly from the target to the root. * - * @param rootCertAndKey trusted root certificate that will be the start of the path. - * @param targetCertAndKey certificate the path ends at. + * @param trustedRoot trusted root certificate that will be the start of the path. + * @param certificates certificates in the path. * @param revocationEnabled whether revocation of certificates in the path should be checked. */ - fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair, - targetCertAndKey: X509Certificate, - revocationEnabled: Boolean): CertPathBuilderResult { - val intermediateCertificates = setOf(targetCertAndKey) - val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates)) - val certPathFactory = CertPathBuilder.getInstance("PKIX") - val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null) - val certPathParameters = try { - PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply { - certificate = targetCertAndKey - }) - } catch (ex: InvalidAlgorithmParameterException) { - throw RuntimeException(ex) - }.apply { - addCertStore(certStore) - isRevocationEnabled = revocationEnabled - } - return certPathFactory.build(certPathParameters) + fun createCertificatePath(trustedRoot: X509Certificate, vararg certificates: X509Certificate, revocationEnabled: Boolean): CertPath { + val certFactory = CertificateFactory.getInstance("X509") + val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) + params.isRevocationEnabled = revocationEnabled + return certFactory.generateCertPath(certificates.toList()) + } + + fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) { + require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } + val certFactory = CertificateFactory.getInstance("X509") + val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(certificates.toList()) + val pathValidator = CertPathValidator.getInstance("PKIX") + pathValidator.validate(certPath, params) } /** @@ -237,7 +183,8 @@ object X509Utilities { /** * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine. - * @param keyStoreFilePath KeyStore path to save output to. + * @param sslKeyStorePath KeyStore path to save ssl key and cert to. + * @param clientCAKeystorePath KeyStore path to save client CA key and cert to. * @param storePassword access password for KeyStore. * @param keyPassword PrivateKey access password for the generated keys. * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. @@ -245,57 +192,66 @@ object X509Utilities { * @param caKeyPassword password to unlock private keys in the CA KeyStore. * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications. */ - fun createKeystoreForSSL(keyStoreFilePath: Path, - storePassword: String, - keyPassword: String, - caKeyStore: KeyStore, - caKeyPassword: String, - commonName: X500Name, - signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore { + fun createKeystoreForCordaNode(sslKeyStorePath: Path, + clientCAKeystorePath: Path, + storePassword: String, + keyPassword: String, + caKeyStore: KeyStore, + caKeyPassword: String, + legalName: X500Name, + signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) { - val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) - val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) + val rootCACert = caKeyStore.getX509Certificate(CORDA_ROOT_CA) + val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA, caKeyPassword) - val serverKey = generateKeyPair(signatureScheme) - val host = InetAddress.getLocalHost() - val serverCert = createTlsServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress)) + val clientKey = generateKeyPair(signatureScheme) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName))), arrayOf()) + val clientCACert = createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, legalName, clientKey.public, nameConstraints = nameConstraints) + + val tlsKey = generateKeyPair(signatureScheme) + val clientTLSCert = createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) val keyPass = keyPassword.toCharArray() - val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) - keyStore.addOrReplaceKey( - CORDA_CLIENT_CA_PRIVATE_KEY, - serverKey.private, + val clientCAKeystore = KeyStoreUtilities.loadOrCreateKeyStore(clientCAKeystorePath, storePassword) + clientCAKeystore.addOrReplaceKey( + CORDA_CLIENT_CA, + clientKey.private, keyPass, - arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate)) - keyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, serverCert) - keyStore.save(keyStoreFilePath, storePassword) - return keyStore + arrayOf(clientCACert, intermediateCACert, rootCACert)) + clientCAKeystore.save(clientCAKeystorePath, storePassword) + + val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeyStorePath, storePassword) + tlsKeystore.addOrReplaceKey( + CORDA_CLIENT_TLS, + tlsKey.private, + keyPass, + arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)) + tlsKeystore.save(sslKeyStorePath, storePassword) } fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) = Crypto.createCertificateSigningRequest(subject, keyPair, signatureScheme) } /** - * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an - * exception. + * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } /** * Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this * adds one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } /** * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. * * @param mutator a function to generate the new value from the previous one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. */ -@Throws(IllegalArgumentException::class) private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name { val builder = X500NameBuilder(BCStyle.INSTANCE) var matched = false @@ -319,6 +275,7 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() +val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject class CertificateStream(val input: InputStream) { private val certificateFactory = CertificateFactory.getInstance("X.509") @@ -327,3 +284,11 @@ class CertificateStream(val input: InputStream) { } data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair) + +enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { + ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), + TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false), + IDENTITY(KeyUsage(KeyUsage.digitalSignature), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt b/core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt new file mode 100644 index 0000000000..63811d1ba0 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt @@ -0,0 +1,26 @@ +package net.corda.core.internal + +interface ShutdownHook { + /** + * Safe to call from the block passed into [addShutdownHook]. + */ + fun cancel() +} + +/** + * The given block will run on most kinds of termination including SIGTERM, but not on SIGKILL. + * @return An object via which you can cancel the hook. + */ +fun addShutdownHook(block: () -> Unit): ShutdownHook { + val hook = Thread { block() } + val runtime = Runtime.getRuntime() + runtime.addShutdownHook(hook) + return object : ShutdownHook { + override fun cancel() { + // Allow the block to call cancel without causing IllegalStateException in the shutdown case: + if (Thread.currentThread() != hook) { + runtime.removeShutdownHook(hook) + } + } + } +} diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt new file mode 100644 index 0000000000..2d687df267 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -0,0 +1,124 @@ +package net.corda.core.crypto + +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Test +import java.security.KeyStore +import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateFactory +import java.security.cert.PKIXParameters +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class X509NameConstraintsTest { + + private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { + val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Root CA"), rootKeys) + + val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getDevX509Name("Corda Intermediate CA"), intermediateCAKeyPair.public) + + val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getDevX509Name("Corda Client CA"), clientCAKeyPair.public, nameConstraints = nameConstraints) + + val keyPass = "password" + val trustStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) + trustStore.load(null, keyPass.toCharArray()) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) + + val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public) + + val keyStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) + keyStore.load(null, keyPass.toCharArray()) + keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(), arrayOf(tlsCert, clientCACert, intermediateCACert, rootCACert)) + return Pair(keyStore, trustStore) + } + + @Test + fun `illegal common name`() { + val acceptableNames = listOf("CN=Bank A TLS, O=Bank A", "CN=Bank A") + .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() + + val nameConstraints = NameConstraints(acceptableNames, arrayOf()) + val pathValidator = CertPathValidator.getInstance("PKIX") + val certFactory = CertificateFactory.getInstance("X509") + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, O=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + } + + @Test + fun `x500 name with correct cn and extra attribute`() { + val acceptableNames = listOf("CN=Bank A TLS, UID=", "O=Bank A") + .map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray() + + val nameConstraints = NameConstraints(acceptableNames, arrayOf()) + val certFactory = CertificateFactory.getInstance("X509") + Crypto.ECDSA_SECP256R1_SHA256 + val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME) + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertFailsWith(CertPathValidatorException::class) { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A, UID=12345"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, UID=, E=me@email.com, C=UK"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + assertTrue { + val (keystore, trustStore) = makeKeyStores(X500Name("O=Bank A, UID=, E=me@email.com, C=UK"), nameConstraints) + val params = PKIXParameters(trustStore) + params.isRevocationEnabled = false + val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) + pathValidator.validate(certPath, params) + true + } + + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt index 4612349134..2a25d8c60f 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -1,10 +1,13 @@ package net.corda.core.crypto +import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 +import net.corda.core.crypto.Crypto.generateKeyPair +import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME +import net.corda.core.crypto.X509Utilities.createSelfSignedCACertificate import net.corda.core.div import net.corda.testing.MEGA_CORP import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.GeneralName import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -33,61 +36,53 @@ class X509UtilitiesTest { @Test fun `create valid self-signed CA certificate`() { - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test Cert")) - assertTrue { caCertAndKey.certificate.subjectDN.name.contains("CN=Test Cert") } // using our subject common name - assertEquals(caCertAndKey.certificate.issuerDN, caCertAndKey.certificate.subjectDN) //self-signed - caCertAndKey.certificate.checkValidity(Date()) // throws on verification problems - caCertAndKey.certificate.verify(caCertAndKey.keyPair.public) // throws on verification problems - assertTrue { caCertAndKey.certificate.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) - assertTrue { caCertAndKey.certificate.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + assertTrue { caCert.subjectDN.name.contains("CN=Test Cert") } // using our subject common name + assertEquals(caCert.issuerDN, caCert.subjectDN) //self-signed + caCert.checkValidity(Date()) // throws on verification problems + caCert.verify(caKey.public) // throws on verification problems + assertTrue { caCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) + assertTrue { caCert.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate } @Test fun `load and save a PEM file certificate`() { val tmpCertificateFile = tempFile("cacert.pem") - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test Cert")) - X509Utilities.saveCertificateAsPEMFile(caCertAndKey.certificate, tmpCertificateFile) + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) - assertEquals(caCertAndKey.certificate, readCertificate) + assertEquals(caCert, readCertificate) } @Test fun `create valid server certificate chain`() { - val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert")) + val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) val subjectDN = getTestX509Name("Server Cert") - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val serverCert = X509Utilities.createTlsServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) + val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subjectDN, keyPair.public) assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name - assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert + assertEquals(caCert.issuerDN, serverCert.issuerDN) // Issued by our CA cert serverCert.checkValidity(Date()) // throws on verification problems - serverCert.verify(caCertAndKey.keyPair.public) // throws on verification problems + serverCert.verify(caKey.public) // throws on verification problems assertFalse { serverCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) assertTrue { serverCert.basicConstraints == -1 } // This returns the signing path length should be -1 for non-CA certificate - assertEquals(2, serverCert.subjectAlternativeNames.size) - var foundAliasDnsName = false - for (entry in serverCert.subjectAlternativeNames) { - val typeId = entry[0] as Int - val value = entry[1] as String - if (typeId == GeneralName.iPAddress) { - assertEquals("10.0.0.54", value) - } else if (value == "alias name") { - foundAliasDnsName = true - } - } - assertTrue(foundAliasDnsName) } @Test fun `storing EdDSA key in java keystore`() { val tmpKeyStore = tempFile("keystore.jks") - val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), Crypto.EDDSA_ED25519_SHA512) + val keyPair = generateKeyPair(EDDSA_ED25519_SHA512) + val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) - assertEquals(selfSignCert.certificate.publicKey, selfSignCert.keyPair.public) + assertEquals(selfSignCert.publicKey, keyPair.public) // Save the EdDSA private key with self sign cert in the keystore. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", selfSignCert.keyPair.private, "password".toCharArray(), arrayOf(selfSignCert.certificate)) + keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), arrayOf(selfSignCert)) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -97,20 +92,21 @@ class X509UtilitiesTest { assertNotNull(pubKey) assertNotNull(privateKey) - assertEquals(selfSignCert.keyPair.public, pubKey) - assertEquals(selfSignCert.keyPair.private, privateKey) + assertEquals(keyPair.public, pubKey) + assertEquals(keyPair.private, privateKey) } @Test fun `signing EdDSA key with EcDSA certificate`() { val tmpKeyStore = tempFile("keystore.jks") - val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test")) - val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") - val edDSACert = X509Utilities.createTlsServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54")) + val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val ecDSACert = createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey) + val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512) + val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, X500Name("CN=TestEdDSA"), edDSAKeypair.public) // Save the EdDSA private key with cert chains. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert.certificate, edDSACert)) + keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert, edDSACert)) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -138,8 +134,8 @@ class X509UtilitiesTest { // Load back generated root CA Cert and private key from keystore and check against copy in truststore val keyStore = KeyStoreUtilities.loadKeyStore(tmpKeyStore, "keystorepass") val trustStore = KeyStoreUtilities.loadKeyStore(tmpTrustStore, "trustpass") - val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY) as X509Certificate - val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey + val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate + val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate assertEquals(rootCaCert, rootCaFromTrustStore) rootCaCert.checkValidity(Date()) @@ -147,24 +143,25 @@ class X509UtilitiesTest { // Now sign something with private key and verify against certificate public key val testData = "12345".toByteArray() - val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } + val caSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } // Load back generated intermediate CA Cert and private key - val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) as X509Certificate - val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey + val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate + val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey intermediateCaCert.checkValidity(Date()) intermediateCaCert.verify(rootCaCert.publicKey) // Now sign something with private key and verify against certificate public key - val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } + val intermediateSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } } @Test fun `create server certificate in keystore for SSL`() { val tmpCAKeyStore = tempFile("keystore.jks") val tmpTrustStore = tempFile("truststore.jks") + val tmpSSLKeyStore = tempFile("sslkeystore.jks") val tmpServerKeyStore = tempFile("serverkeystore.jks") // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store @@ -176,30 +173,39 @@ class X509UtilitiesTest { // Load signing intermediate CA cert val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") - val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") + val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass") // Generate server cert and private key and populate another keystore suitable for SSL - X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) + X509Utilities.createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) // Load back server certificate val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") - val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") + val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass") serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) assertTrue { serverCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } + // Load back server certificate + val sslKeyStore = KeyStoreUtilities.loadKeyStore(tmpSSLKeyStore, "serverstorepass") + val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass") + + sslCertAndKey.certificate.checkValidity(Date()) + sslCertAndKey.certificate.verify(serverCertAndKey.certificate.publicKey) + + assertTrue { sslCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() - val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) - assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.certificate.publicKey, signature, testData) } + val signature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.certificate.publicKey, signature, testData) } } @Test fun `create server cert and use in SSL socket`() { val tmpCAKeyStore = tempFile("keystore.jks") val tmpTrustStore = tempFile("truststore.jks") + val tmpSSLKeyStore = tempFile("sslkeystore.jks") val tmpServerKeyStore = tempFile("serverkeystore.jks") // Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store @@ -210,7 +216,8 @@ class X509UtilitiesTest { "trustpass") // Generate server cert and private key and populate another keystore suitable for SSL - val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name) + X509Utilities.createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name) + val keyStore = KeyStoreUtilities.loadKeyStore(tmpSSLKeyStore, "serverstorepass") val trustStore = KeyStoreUtilities.loadKeyStore(tmpTrustStore, "trustpass") val context = SSLContext.getInstance("TLS") @@ -235,7 +242,7 @@ class X509UtilitiesTest { arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true - serverParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking + serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. serverSocket.sslParameters = serverParams serverSocket.useClientMode = false @@ -247,7 +254,7 @@ class X509UtilitiesTest { "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"), arrayOf("TLSv1.2")) - clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking + clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator. clientSocket.sslParameters = clientParams clientSocket.useClientMode = true // We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind @@ -284,8 +291,7 @@ class X509UtilitiesTest { val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal val x500name = X500Name(peerX500Principal.name) assertEquals(MEGA_CORP.name, x500name) - - + X509Utilities.validateCertificateChain(trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA), *peerChain) val output = DataOutputStream(clientSocket.outputStream) output.writeUTF("Hello World") var timeout = 0 @@ -324,36 +330,40 @@ class X509UtilitiesTest { trustStoreFilePath: Path, trustStorePassword: String ): KeyStore { - val rootCA = X509Utilities.createSelfSignedCACert(X509Utilities.getDevX509Name("Corda Node Root CA")) - val intermediateCA = X509Utilities.createIntermediateCert(X509Utilities.getDevX509Name("Corda Node Intermediate CA"), rootCA) + val rootCAKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Node Root CA"), rootCAKey) + + val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getDevX509Name("Corda Node Intermediate CA"), intermediateCAKeyPair.public) val keyPass = keyPassword.toCharArray() val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) - keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, rootCA.keyPair.private, keyPass, arrayOf(rootCA.certificate)) + keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf(rootCACert)) - keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, - intermediateCA.keyPair.private, + keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, + intermediateCAKeyPair.private, keyPass, - arrayOf(intermediateCA.certificate, rootCA.certificate)) + arrayOf(intermediateCACert, rootCACert)) keyStore.save(keyStoreFilePath, storePassword) val trustStore = KeyStoreUtilities.loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCA.certificate) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCA.certificate) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert) trustStore.save(trustStoreFilePath, trustStorePassword) return keyStore } + @Test fun `Get correct private key type from Keystore`() { - val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair) + val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") - keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate)) + keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert)) val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 4309ba3521..521e0f2b20 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -143,7 +143,7 @@ class KryoTests { @Test fun `serialize - deserialize X509Certififcate`() { - val expected = X509Utilities.createSelfSignedCACert(ALICE.name).certificate + val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) val serialized = expected.serialize(kryo).bytes val actual: X509Certificate = serialized.deserialize(kryo) assertEquals(expected, actual) @@ -151,9 +151,10 @@ class KryoTests { @Test fun `serialize - deserialize X509CertPath`() { - val rootCA = X509Utilities.createSelfSignedCACert(ALICE.name) - val certificate = X509Utilities.createTlsServerCert(BOB.name, BOB_PUBKEY, rootCA, emptyList(), emptyList()) - val expected = X509Utilities.createCertificatePath(rootCA, certificate, false).certPath + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) + val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY) + val expected = X509Utilities.createCertificatePath(rootCACert, certificate, revocationEnabled = false) val serialized = expected.serialize(kryo).bytes val actual: CertPath = serialized.deserialize(kryo) assertEquals(expected, actual) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 59d866917b..446dc4a903 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -73,6 +73,11 @@ UNRELEASED point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards compatibility of CorDapp flows. +* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain. + * The corda node will now be issued a restricted client CA for identity/transaction key signing. + * TLS certificate are now stored in `sslkeystore.jks` and identity keys are stored in `nodekeystore.jks` +.. warning:: The old keystore will need to be removed when upgrading to this version. + Milestone 11.1 -------------- diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 3bf72a1fe5..206a68fc48 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -107,33 +107,38 @@ To run from the command line in Windows: 4. Run ``gradlew samples:attachment-demo:runSender`` in another terminal window to send the attachment. Now look at the other windows to see the output of the demo -Raft Notary demo ----------------- +Notary demo +----------- -This demo shows a party getting transactions notarised by a distributed `Raft `_-based notary service. -The demo will start three distributed notary nodes, and two counterparty nodes. One of the counterparties will generate transactions -that transfer a self-issued asset to the other party and submit them for notarisation. +This demo shows a party getting transactions notarised by either a single-node or a distributed notary service. +All versions of the demo start two counterparty nodes. +One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation. +The `Raft `_ version of the demo will start three distributed notary nodes. +The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary, every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement. +In the BFT SMaRt distributed notary, three signatures are required. You will notice that successive transactions get signed by different members of the cluster (usually allocated in a random order). -To run from the command line in Unix: +To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:raft-notary-demo:deployNodes``, which will create node directories with configs under ``samples/raft-notary-demo/build/nodes``. -2. Run ``./samples/raft-notary-demo/build/nodes/runnodes``, which will start the nodes in separate terminal windows/tabs. +1. Run ``./gradlew samples:notary-demo:deployNodesRaft``, which will create node directories with configs under ``samples/notary-demo/build/nodes``. +2. Run ``./samples/notary-demo/build/nodes/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals -3. Run ``./gradlew samples:raft-notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests +3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys To run from the command line in Windows: -1. Run ``gradlew samples:raft-notary-demo:deployNodes``, which will create node directories with configs under ``samples\raft-notary-demo\build\nodes``. -2. Run ``samples\raft-notary-demo\build\nodes\runnodes``, which will start the nodes in separate terminal windows/tabs. +1. Run ``gradlew samples:notary-demo:deployNodesRaft``, which will create node directories with configs under ``samples\notary-demo\build\nodes``. +2. Run ``samples\notary-demo\build\nodes\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals -3. Run ``gradlew samples:raft-notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests +3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys +To run the BFT SMaRt notary demo, use ``deployNodesBFT`` instead of ``deployNodesRaft``. For a single notary node, use ``deployNodesSingle``. + Notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores by using the H2 web console: diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index 865164241b..84299dee0f 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -11,15 +11,15 @@ import net.corda.core.crypto.* import net.corda.core.crypto.KeyStoreUtilities.loadKeyStore import net.corda.core.crypto.KeyStoreUtilities.loadOrCreateKeyStore import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA -import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY -import net.corda.core.crypto.X509Utilities.createIntermediateCert -import net.corda.core.crypto.X509Utilities.createTlsServerCert +import net.corda.core.crypto.X509Utilities.createCertificate import net.corda.core.seconds import net.corda.core.utilities.loggerFor import net.corda.node.utilities.configureDatabase import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector @@ -83,8 +83,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAnd for (id in storage.getApprovedRequestIds()) { storage.approveRequest(id) { val request = JcaPKCS10CertificationRequest(request) - createTlsServerCert(request.subject, request.publicKey, caCertAndKey, - if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) + createCertificate(CertificateType.CLIENT_CA, caCertAndKey.certificate, caCertAndKey.keyPair, request.subject, request.publicKey, nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), arrayOf())) } logger.info("Approved request $id") serverStatus.lastApprovalTime = Instant.now() @@ -137,19 +136,20 @@ private fun DoormanParameters.generateRootKeyPair() { val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword) val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") - if (rootStore.containsAlias(CORDA_ROOT_CA_PRIVATE_KEY)) { - val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey - println("Key $CORDA_ROOT_CA_PRIVATE_KEY already exists in keystore, process will now terminate.") + if (rootStore.containsAlias(CORDA_ROOT_CA)) { + val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey + println("Key $CORDA_ROOT_CA already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } - val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name(CORDA_ROOT_CA)) - rootStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, selfSignCert.keyPair.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate)) + val selfSignKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name(CORDA_ROOT_CA), selfSignKey) + rootStore.addOrReplaceKey(CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) rootStore.save(rootStorePath, rootKeystorePassword) println("Root CA keypair and certificate stored in $rootStorePath.") - println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey) + println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey) } private fun DoormanParameters.generateCAKeyPair() { @@ -159,7 +159,7 @@ private fun DoormanParameters.generateCAKeyPair() { val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword) - val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) + val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(rootPrivateKeyPassword, CORDA_ROOT_CA) val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") @@ -167,19 +167,20 @@ private fun DoormanParameters.generateCAKeyPair() { keystorePath.parent.createDirectories() val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) - if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) { - val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey - println("Key $CORDA_INTERMEDIATE_CA_PRIVATE_KEY already exists in keystore, process will now terminate.") + if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA)) { + val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey + println("Key $CORDA_INTERMEDIATE_CA already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } - val intermediateKeyAndCert = createIntermediateCert(X500Name(CORDA_INTERMEDIATE_CA), rootKeyAndCert) - keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, intermediateKeyAndCert.keyPair.private, - caPrivateKeyPassword.toCharArray(), arrayOf(intermediateKeyAndCert.certificate, rootKeyAndCert.certificate)) + val intermediateKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCert = createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, X500Name(CORDA_INTERMEDIATE_CA), intermediateKey.public) + keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA, intermediateKey.private, + caPrivateKeyPassword.toCharArray(), arrayOf(intermediateCert, rootKeyAndCert.certificate)) keyStore.save(keystorePath, keystorePassword) println("Intermediate CA keypair and certificate stored in $keystorePath.") - println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey) + println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey) } private fun DoormanParameters.startDoorman() { @@ -189,8 +190,8 @@ private fun DoormanParameters.startDoorman() { val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") val keystore = loadOrCreateKeyStore(keystorePath, keystorePassword) - val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last() - val caCertAndKey = keystore.getCertificateAndKeyPair(caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY) + val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA).last() + val caCertAndKey = keystore.getCertificateAndKeyPair(caPrivateKeyPassword, CORDA_INTERMEDIATE_CA) // Create DB connection. val (datasource, database) = configureDatabase(dataSourceProperties) diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt index a05648adab..bce54f85fa 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt @@ -5,10 +5,7 @@ import com.nhaarman.mockito_kotlin.* import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestStorage -import net.corda.core.crypto.CertificateStream -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.* import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat @@ -29,12 +26,14 @@ import javax.ws.rs.core.MediaType import kotlin.test.assertEquals class DoormanServiceTest { - private val rootCA = X509Utilities.createSelfSignedCACert(X500Name("CN=Corda Node Root CA,L=London")) - private val intermediateCA = X509Utilities.createSelfSignedCACert(X500Name("CN=Corda Node Intermediate CA,L=London")) + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val intermediateCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey) private lateinit var doormanServer: DoormanServer private fun startSigningServer(storage: CertificationRequestStorage) { - doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), intermediateCA, rootCA.certificate, storage) + doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), CertificateAndKeyPair(intermediateCACert, intermediateCAKey), rootCACert, storage) doormanServer.start() } @@ -90,8 +89,7 @@ class DoormanServiceTest { storage.approveRequest(id) { JcaPKCS10CertificationRequest(request).run { - X509Utilities.createTlsServerCert(subject, publicKey, intermediateCA, - if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) + X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey) } } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index 3c73cdb555..d19f230a0b 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -3,6 +3,7 @@ package com.r3.corda.doorman.internal.persistence import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.DBCertificateRequestStorage +import net.corda.core.crypto.CertificateType import net.corda.core.crypto.Crypto import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME @@ -21,7 +22,8 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue class DBCertificateRequestStorageTest { - private val intermediateCA = X509Utilities.createSelfSignedCACert(X500Name("CN=Corda Node Intermediate CA")) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val intermediateCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Intermediate CA"), intermediateCAKey) private var closeDb: Closeable? = null private lateinit var storage: DBCertificateRequestStorage @@ -137,12 +139,12 @@ class DBCertificateRequestStorageTest { private fun approveRequest(requestId: String) { storage.approveRequest(requestId) { JcaPKCS10CertificationRequest(request).run { - X509Utilities.createTlsServerCert( + X509Utilities.createCertificate( + CertificateType.TLS, + intermediateCACert, + intermediateCAKey, subject, - publicKey, - intermediateCA, - if (ipAddress == hostName) listOf() else listOf(hostName), - listOf(ipAddress)) + publicKey) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index b56f52239a..d77de310db 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -107,8 +107,10 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { */ fun checkStorePasswords() { val config = config ?: return - config.keyStoreFile.read { - KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray()) + arrayOf(config.sslKeystore, config.nodeKeystore).forEach { + it.read { + KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray()) + } } config.trustStoreFile.read { KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 2366f26cfd..cdd0074a1b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -53,14 +53,14 @@ class ArtemisTcpTransport { ) if (config != null && enableSSL) { - config.keyStoreFile.expectedOnDefaultFileSystem() + config.sslKeystore.expectedOnDefaultFileSystem() config.trustStoreFile.expectedOnDefaultFileSystem() - val tlsOptions = mapOf( + val tlsOptions = mapOf( // Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake // and AES encryption TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStoreFile, + TransportConstants.KEYSTORE_PATH_PROP_NAME to config.sslKeystore, TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt index 2832f783db..8bc6644dcf 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt @@ -7,6 +7,7 @@ interface SSLConfiguration { val keyStorePassword: String val trustStorePassword: String val certificatesDirectory: Path - val keyStoreFile: Path get() = certificatesDirectory / "sslkeystore.jks" + val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" + val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" } \ No newline at end of file diff --git a/node/bft-smart-config/hosts.config b/node/bft-smart-config/hosts.config deleted file mode 100644 index a30ed09c18..0000000000 --- a/node/bft-smart-config/hosts.config +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2007-2013 Alysson Bessani, Eduardo Alchieri, Paulo Sousa, and the authors indicated in the @author tags -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file defines the replicas ids, IPs and ports. -# It is used by the replicas and clients to find connection info -# to the initial replicas. -# The ports defined here are the ports used by clients to communicate -# with the replicas. Additional connections are opened by replicas to -# communicate with each other. This additional connection is opened in the -# next port defined here. For an example, consider the line "0 127.0.0.1 11000". -# That means that clients will open a communication channel to replica 0 in -# IP 127.0.0.1 and port 11000. On startup, replicas with id different than 0 -# will open a communication channel to replica 0 in port 11001. -# The same holds for replicas 1, 2, 3 ... N. - -#server id, address and port (the ids from 0 to n-1 are the service replicas) -0 127.0.0.1 11000 -1 127.0.0.1 11010 -2 127.0.0.1 11020 -3 127.0.0.1 11030 -4 127.0.0.1 11040 -5 127.0.0.1 11050 -6 127.0.0.1 11060 -7 127.0.0.1 11070 -7001 127.0.0.1 11100 diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 92250649a3..737251f4d9 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -40,7 +40,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { // javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] systemProperties['visualvm.display.name'] = 'Corda' - systemProperties['jdk.serialFilter'] = 'maxbytes=0' minJavaVersion = '1.8.0' minUpdateVersion['1.8'] = java8_minUpdateVersion caplets = ['CordaCaplet'] diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 0884300518..30f1cf38a5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -53,7 +53,6 @@ class BootTests { class ObjectInputStreamFlow : FlowLogic() { @Suspendable override fun call() { - System.clearProperty("jdk.serialFilter") // This checks that the node has already consumed the property. val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray() ObjectInputStream(data.inputStream()).use { it.readObject() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index c85ce319b1..155fe9c43c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -5,19 +5,19 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType import net.corda.core.crypto.appendToCommonName -import net.corda.core.crypto.commonName -import net.corda.core.div import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.ALICE +import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode import net.corda.node.internal.Node import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction import net.corda.testing.node.NodeBasedTest @@ -28,71 +28,55 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class BFTNotaryServiceTests : NodeBasedTest() { - private companion object { - val notaryCommonName = X500Name("CN=BFT Notary Server,O=R3,OU=corda,L=Zurich,C=CH") - - fun buildNodeName(it: Int, notaryName: X500Name): X500Name { - return notaryName.appendToCommonName("-$it") - } - } - @Test fun `detect double spend`() { - val masterNode = startBFTNotaryCluster(notaryCommonName, 4, BFTNonValidatingNotaryService.type).first() + val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") + startBFTNotaryCluster(clusterName, 4, BFTNonValidatingNotaryService.type) val alice = startNode(ALICE.name).getOrThrow() - - val notaryParty = alice.netMapCache.getNotary(notaryCommonName)!! - + val notaryParty = alice.netMapCache.getNotary(clusterName)!! val inputState = issueState(alice, notaryParty) - val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) - - val firstSpend = alice.services.startFlow(NotaryFlow.Client(firstSpendTx)) - firstSpend.resultFuture.getOrThrow() - - val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).run { - val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity) - addOutputState(dummyState) - this + alice.services.startFlow(NotaryFlow.Client(firstSpendTx)).resultFuture.getOrThrow() + val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).also { + it.addOutputState(DummyContract.SingleOwnerState(0, alice.info.legalIdentity)) } val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) - - val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } + val ex = assertFailsWith(NotaryException::class) { + secondSpend.resultFuture.getOrThrow() + } val error = ex.error as NotaryError.Conflict assertEquals(error.txId, secondSpendTx.id) } - private fun issueState(node: AbstractNode, notary: Party): StateAndRef<*> { - return node.database.transaction { - val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) - val stx = node.services.signInitialTransaction(builder) - node.services.recordTransactions(listOf(stx)) + private fun issueState(node: AbstractNode, notary: Party) = node.run { + database.transaction { + val builder = DummyContract.generateInitial(Random().nextInt(), notary, info.legalIdentity.ref(0)) + val stx = services.signInitialTransaction(builder) + services.recordTransactions(listOf(stx)) StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) } } - private fun startBFTNotaryCluster(notaryName: X500Name, + private fun startBFTNotaryCluster(clusterName: X500Name, clusterSize: Int, - serviceType: ServiceType): List { + serviceType: ServiceType) { require(clusterSize > 0) - val quorum = (2 * clusterSize + 1) / 3 + val replicaNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } ServiceIdentityGenerator.generateToDisk( - (0 until clusterSize).map { tempFolder.root.toPath() / "${notaryName.commonName}-$it" }, + replicaNames.map { baseDirectory(it) }, serviceType.id, - notaryName, - quorum) - - val serviceInfo = ServiceInfo(serviceType, notaryName) - val nodes = (0 until clusterSize).map { + clusterName, + minCorrectReplicas(clusterSize)) + val serviceInfo = ServiceInfo(serviceType, clusterName) + val notaryClusterAddresses = (0 until clusterSize).map { "localhost:${11000 + it * 10}" } + (0 until clusterSize).forEach { startNode( - buildNodeName(it, notaryName), + replicaNames[it], advertisedServices = setOf(serviceInfo), - configOverrides = mapOf("notaryNodeId" to it) + configOverrides = mapOf("bftReplicaId" to it, "notaryClusterAddresses" to notaryClusterAddresses) ).getOrThrow() } - - return nodes } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 64a3ee71a6..f32f267a82 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -1,14 +1,26 @@ package net.corda.services.messaging +import net.corda.core.copyTo +import net.corda.core.createDirectories +import net.corda.core.crypto.* +import net.corda.core.exists import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.config.SSLConfiguration +import net.corda.testing.MEGA_CORP +import net.corda.testing.MINI_CORP import net.corda.testing.messaging.SimpleMQClient import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException +import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.junit.Test +import java.nio.file.Files /** * Runs the security tests with the attacker pretending to be a node on the network. @@ -66,4 +78,56 @@ class MQSecurityAsNodeTest : MQSecurityTest() { attacker.start(PEER_USER, PEER_USER, enableSSL = false) // Login as a peer } } + + @Test + fun `login with invalid certificate chain`() { + val sslConfig = object : SSLConfiguration { + override val certificatesDirectory = Files.createTempDirectory("certs") + override val keyStorePassword: String get() = "cordacadevpass" + override val trustStorePassword: String get() = "trustpass" + + init { + val legalName = MEGA_CORP.name + certificatesDirectory.createDirectories() + if (!trustStoreFile.exists()) { + javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) + } + + val caKeyStore = KeyStoreUtilities.loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") + + val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) + val intermediateCA = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") + val clientKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + + // Set name constrain to the legal name. + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName))), arrayOf()) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCA.certificate, intermediateCA.keyPair, legalName, clientKey.public, nameConstraints = nameConstraints) + val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Using different x500 name in the TLS cert which is not allowed in the name constraints. + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, MINI_CORP.name, tlsKey.public) + val keyPass = keyStorePassword.toCharArray() + val clientCAKeystore = KeyStoreUtilities.loadOrCreateKeyStore(nodeKeystore, keyStorePassword) + clientCAKeystore.addOrReplaceKey( + X509Utilities.CORDA_CLIENT_CA, + clientKey.private, + keyPass, + arrayOf(clientCACert, intermediateCA.certificate, rootCACert)) + clientCAKeystore.save(nodeKeystore, keyStorePassword) + + val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeystore, keyStorePassword) + tlsKeystore.addOrReplaceKey( + X509Utilities.CORDA_CLIENT_TLS, + tlsKey.private, + keyPass, + arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert)) + tlsKeystore.save(sslKeystore, keyStorePassword) + } + } + + val attacker = clientTo(alice.configuration.p2pAddress, sslConfig) + + assertThatExceptionOfType(ActiveMQNotConnectedException::class.java).isThrownBy { + attacker.start(PEER_USER, PEER_USER) + } + } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 312b7f3cbd..b4332d56b9 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -3,8 +3,6 @@ package net.corda.services.messaging import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.commonName import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.DEFAULT_SESSION_ID @@ -64,10 +62,8 @@ class P2PMessagingTest : NodeBasedTest() { // TODO Use a dummy distributed service @Test fun `communicating with a distributed service which the network map node is part of`() { - - val root = tempFolder.root.toPath() ServiceIdentityGenerator.generateToDisk( - listOf(root / DUMMY_MAP.name.commonName, root / SERVICE_2_NAME.commonName), + listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) }, RaftValidatingNotaryService.type.id, DISTRIBUTED_SERVICE_NAME) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index cbe6cb973d..95fb7c9b23 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -2,8 +2,6 @@ package net.corda.services.messaging import com.google.common.util.concurrent.ListenableFuture import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.commonName -import net.corda.core.div import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.NodeInfo @@ -60,7 +58,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun startSimpleNode(legalName: X500Name): SimpleNode { val config = TestNodeConfiguration( - baseDirectory = tempFolder.root.toPath() / legalName.commonName, + baseDirectory = baseDirectory(legalName), myLegalName = legalName, networkMapService = NetworkMapInfo(networkMapNode.configuration.p2pAddress, networkMapNode.info.legalIdentity.name)) config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 88920dea15..f293552a3c 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -10,10 +10,10 @@ import net.corda.core.crypto.commonName import net.corda.core.crypto.orgName import net.corda.core.node.VersionInfo import net.corda.core.utilities.Emoji -import net.corda.core.utilities.LogHelper.withLevel import net.corda.node.internal.Node import net.corda.node.internal.enforceSingleNodeIsRunning import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper @@ -21,7 +21,6 @@ import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole import org.slf4j.LoggerFactory import org.slf4j.bridge.SLF4JBridgeHandler -import java.io.* import java.lang.management.ManagementFactory import java.net.InetAddress import java.nio.file.Paths @@ -72,8 +71,6 @@ fun main(args: Array) { enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory) initLogging(cmdlineOptions) - disableJavaDeserialization() // Should be after initLogging to avoid TMI. - // Manifest properties are only available if running from the corda jar fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null @@ -107,7 +104,7 @@ fun main(args: Array) { println("Unable to load the configuration file: ${e.rootCause.message}") exitProcess(2) } - + SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) if (cmdlineOptions.isRegistration) { println() println("******************************************************************") @@ -208,29 +205,12 @@ private fun assertCanNormalizeEmptyPath() { } } -private fun failStartUp(message: String): Nothing { +internal fun failStartUp(message: String): Nothing { println(message) println("Corda will now exit...") exitProcess(1) } -private fun disableJavaDeserialization() { - // ObjectInputFilter and friends are in java.io in Java 9 but sun.misc in backports, so we are using the system property interface for portability. - // This property has already been set in the Capsule. Anywhere else may be too late, but we'll repeat it here for developers. - System.setProperty("jdk.serialFilter", "maxbytes=0") - // Attempt at deserialization so that ObjectInputFilter (permanently) inits itself: - val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray() - try { - withLevel("java.io.serialization", "WARN") { - ObjectInputStream(data.inputStream()).use { it.readObject() } // Logs REJECTED at INFO, which we don't want users to see. - } - // JDK 8u121 is the earliest JDK8 JVM that supports this functionality. - failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121 and set system property 'jdk.serialFilter' to 'maxbytes=0' when booting Corda.") - } catch (e: InvalidClassException) { - // Good, our system property is honoured. - } -} - private fun printPluginsAndServices(node: Node) { node.configuration.extraAdvertisedServiceIds.let { if (it.isNotEmpty()) printBasicNodeInfo("Providing network services", it.joinToString()) diff --git a/node/src/main/kotlin/net/corda/node/SerialFilter.kt b/node/src/main/kotlin/net/corda/node/SerialFilter.kt new file mode 100644 index 0000000000..8ff753f42b --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/SerialFilter.kt @@ -0,0 +1,62 @@ +package net.corda.node + +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Proxy + +internal object SerialFilter { + private val filterInterface: Class<*> + private val serialClassGetter: Method + private val undecided: Any + private val rejected: Any + private val serialFilterLock: Any + private val serialFilterField: Field + + init { + // ObjectInputFilter and friends are in java.io in Java 9 but sun.misc in backports: + fun getFilterInterface(packageName: String): Class<*>? { + return try { + Class.forName("$packageName.ObjectInputFilter") + } catch (e: ClassNotFoundException) { + null + } + } + // JDK 8u121 is the earliest JDK8 JVM that supports this functionality. + filterInterface = getFilterInterface("java.io") + ?: getFilterInterface("sun.misc") + ?: failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121.") + serialClassGetter = Class.forName("${filterInterface.name}\$FilterInfo").getMethod("serialClass") + val statusEnum = Class.forName("${filterInterface.name}\$Status") + undecided = statusEnum.getField("UNDECIDED").get(null) + rejected = statusEnum.getField("REJECTED").get(null) + val configClass = Class.forName("${filterInterface.name}\$Config") + serialFilterLock = configClass.getDeclaredField("serialFilterLock").also { it.isAccessible = true }.get(null) + serialFilterField = configClass.getDeclaredField("serialFilter").also { it.isAccessible = true } + } + + internal fun install(acceptClass: (Class<*>) -> Boolean) { + val filter = Proxy.newProxyInstance(javaClass.classLoader, arrayOf(filterInterface)) { _, _, args -> + val serialClass = serialClassGetter.invoke(args[0]) as Class<*>? + if (applyPredicate(acceptClass, serialClass)) { + undecided + } else { + rejected + } + } + // Can't simply use the setter as in non-trampoline mode Capsule has inited the filter in premain: + synchronized(serialFilterLock) { + serialFilterField.set(null, filter) + } + } + + internal fun applyPredicate(acceptClass: (Class<*>) -> Boolean, serialClass: Class<*>?): Boolean { + // Similar logic to jdk.serialFilter, our concern is side-effects at deserialisation time: + if (null == serialClass) return true + var componentType: Class<*> = serialClass + while (componentType.isArray) componentType = componentType.componentType + if (componentType.isPrimitive) return true + return acceptClass(componentType) + } +} + +internal fun defaultSerialFilter(@Suppress("UNUSED_PARAMETER") clazz: Class<*>) = false diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 80b51deed6..65ae64bd26 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -32,6 +32,8 @@ import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs import net.corda.cordform.CordformNode import net.corda.cordform.CordformContext +import net.corda.core.internal.ShutdownHook +import net.corda.core.internal.addShutdownHook import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name @@ -236,22 +238,19 @@ fun genericD coerce: (D) -> DI, dsl: DI.() -> A ): A { - var shutdownHook: Thread? = null + var shutdownHook: ShutdownHook? = null try { driverDsl.start() - shutdownHook = Thread({ + shutdownHook = addShutdownHook { driverDsl.shutdown() - }) - Runtime.getRuntime().addShutdownHook(shutdownHook) + } return dsl(coerce(driverDsl)) } catch (exception: Throwable) { log.error("Driver shutting down because of exception", exception) throw exception } finally { driverDsl.shutdown() - if (shutdownHook != null) { - Runtime.getRuntime().removeShutdownHook(shutdownHook) - } + shutdownHook?.cancel() } } @@ -558,21 +557,19 @@ class DriverDSL( verifierType: VerifierType, rpcUsers: List ): ListenableFuture>> { - val nodeNames = (1..clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(it.toString()) } + val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val paths = nodeNames.map { baseDirectory(it) } ServiceIdentityGenerator.generateToDisk(paths, type.id, notaryName) - - val serviceInfo = ServiceInfo(type, notaryName) - val advertisedService = setOf(serviceInfo) + val advertisedServices = setOf(ServiceInfo(type, notaryName)) val notaryClusterAddress = portAllocation.nextHostAndPort() // Start the first node that will bootstrap the cluster - val firstNotaryFuture = startNode(nodeNames.first(), advertisedService, rpcUsers, verifierType, mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) + val firstNotaryFuture = startNode(nodeNames.first(), advertisedServices, rpcUsers, verifierType, mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) // All other nodes will join the cluster val restNotaryFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString())) - startNode(it, advertisedService, rpcUsers, verifierType, configOverride) + startNode(it, advertisedServices, rpcUsers, verifierType, configOverride) } return firstNotaryFuture.flatMap { firstNotary -> diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index dc99af9e4c..348699c273 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -61,6 +61,7 @@ import org.jetbrains.exposed.sql.Database import org.slf4j.Logger import java.io.IOException import java.lang.reflect.Modifier.* +import java.net.InetAddress import java.net.URL import java.nio.file.FileAlreadyExistsException import java.nio.file.Path @@ -390,7 +391,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun makeServiceEntries(): List { return advertisedServices.map { val serviceId = it.type.id - val serviceName = it.name ?: configuration.myLegalName.replaceCommonName(serviceId) + val serviceName = it.name ?: X500Name("${configuration.myLegalName},OU=$serviceId") val identity = obtainKeyPair(serviceId, serviceName).first ServiceEntry(it, identity) } @@ -400,16 +401,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun acceptableLiveFiberCountOnStop(): Int = 0 private fun hasSSLCertificates(): Boolean { - val keyStore = try { + val (sslKeystore, keystore) = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - KeyStoreUtilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword) + Pair(KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) } catch (e: IOException) { - null + return false } catch (e: KeyStoreException) { log.warn("Certificate key store found but key store password does not match configuration.") - null + return false } - return keyStore?.containsAlias(X509Utilities.CORDA_CLIENT_CA) ?: false + return sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && keystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } // Specific class so that MockNode can catch it. @@ -518,10 +519,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timestampChecker, uniquenessProvider as RaftUniquenessProvider) RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timestampChecker, uniquenessProvider as RaftUniquenessProvider) BFTNonValidatingNotaryService.type -> with(configuration as FullNodeConfiguration) { - val nodeId = notaryNodeId ?: throw IllegalArgumentException("notaryNodeId value must be specified in the configuration") - val client = BFTSMaRt.Client(nodeId) - tokenizableServices += client - BFTNonValidatingNotaryService(services, timestampChecker, nodeId, database, client) + val replicaId = bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") + BFTSMaRtConfig(notaryClusterAddresses).use { config -> + val client = BFTSMaRt.Client(config, replicaId).also { tokenizableServices += it } // (Ab)use replicaId for clientId. + BFTNonValidatingNotaryService(config, services, timestampChecker, replicaId, database, client) + } } else -> { throw IllegalArgumentException("Notary type ${type.id} is not handled by makeNotaryService.") @@ -598,12 +600,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // the legal name is actually validated in some way. // TODO: Integrate with Key management service? - val keystore = KeyStoreUtilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword) + val keystore = KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) + val clientCA = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, configuration.keyStorePassword) val privateKeyAlias = "$serviceId-private-key" val privKeyFile = configuration.baseDirectory / privateKeyAlias val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" - val identityAndKey = if (configuration.keyStoreFile.exists() && keystore.containsAlias(privateKeyAlias)) { + val identityAndKey = if (configuration.nodeKeystore.exists() && keystore.containsAlias(privateKeyAlias)) { // Get keys from keystore. val (cert, keyPair) = keystore.getCertificateAndKeyPair(privateKeyAlias, configuration.keyStorePassword) val loadedServiceName = X509CertificateHolder(cert.encoded).subject @@ -624,19 +627,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "$serviceName vs ${myIdentity.name}") // Load the private key. val keyPair = privKeyFile.readAll().deserialize() - // TODO: Use a proper certificate chain. - val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) - keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) - keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) + keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) Pair(myIdentity, keyPair) } else { // Create new keys and store in keystore. log.info("Identity key not found, generating fresh key!") val keyPair: KeyPair = generateKeyPair() - val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) - keystore.addOrReplaceKey(privateKeyAlias, selfSignCert.keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) - keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) - Pair(Party(serviceName, selfSignCert.keyPair.public), selfSignCert.keyPair) + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) + keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) + Pair(Party(serviceName, keyPair.public), keyPair) } partyKeys += identityAndKey.second return identityAndKey diff --git a/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt b/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt index 0b09e2953c..c04f898a3b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt +++ b/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt @@ -1,7 +1,7 @@ package net.corda.node.internal +import net.corda.core.internal.addShutdownHook import net.corda.core.div -import net.corda.core.utilities.loggerFor import java.io.RandomAccessFile import java.lang.management.ManagementFactory import java.nio.file.Path @@ -26,9 +26,9 @@ fun enforceSingleNodeIsRunning(baseDirectory: Path) { } // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us // when our process shuts down, but we try in stop() anyway just to be nice. - Runtime.getRuntime().addShutdownHook(Thread { + addShutdownHook { pidFileLock.release() - }) + } val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0] pidFileRw.setLength(0) pidFileRw.write(ourProcessID.toByteArray()) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index c2847fc740..5795aa8627 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -5,16 +5,15 @@ import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import net.corda.core.flatMap +import net.corda.core.* +import net.corda.core.internal.ShutdownHook +import net.corda.core.internal.addShutdownHook import net.corda.core.messaging.RPCOps -import net.corda.core.minutes import net.corda.core.node.ServiceHub import net.corda.core.node.VersionInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.node.services.UniquenessProvider -import net.corda.core.seconds -import net.corda.core.success import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import net.corda.node.printBasicNodeInfo @@ -47,7 +46,6 @@ import java.io.IOException import java.time.Clock import java.util.* import javax.management.ObjectName -import kotlin.concurrent.thread /** * A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub], @@ -112,7 +110,7 @@ class Node(override val configuration: FullNodeConfiguration, var messageBroker: ArtemisMessagingServer? = null - private var shutdownThread: Thread? = null + private var shutdownHook: ShutdownHook? = null private lateinit var userService: RPCUserService @@ -295,12 +293,9 @@ class Node(override val configuration: FullNodeConfiguration, (startupComplete as SettableFuture).set(Unit) } - - shutdownThread = thread(start = false) { + shutdownHook = addShutdownHook { stop() } - Runtime.getRuntime().addShutdownHook(shutdownThread) - return this } @@ -322,12 +317,9 @@ class Node(override val configuration: FullNodeConfiguration, synchronized(this) { if (shutdown) return shutdown = true - // Unregister shutdown hook to prevent any unnecessary second calls to stop - if ((shutdownThread != null) && (Thread.currentThread() != shutdownThread)) { - Runtime.getRuntime().removeShutdownHook(shutdownThread) - shutdownThread = null - } + shutdownHook?.cancel() + shutdownHook = null } printBasicNodeInfo("Shutting down ...") diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index ded16966be..8fdae16bdb 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -54,8 +54,8 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: X500Name) { if (!trustStoreFile.exists()) { javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) } - if (!keyStoreFile.exists()) { + if (!sslKeystore.exists() || !nodeKeystore.exists()) { val caKeyStore = KeyStoreUtilities.loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") - X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) + X509Utilities.createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) } } diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index f0db112828..1ee3b213b0 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -62,7 +62,7 @@ data class FullNodeConfiguration( // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: HostAndPort?, val extraAdvertisedServiceIds: List, - val notaryNodeId: Int?, + val bftReplicaId: Int?, val notaryNodeAddress: HostAndPort?, val notaryClusterAddresses: List, override val certificateChainCheckPolicies: List, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 9df1d8f2ed..23c6e1a33a 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -6,7 +6,7 @@ import com.google.common.util.concurrent.SettableFuture import io.netty.handler.ssl.SslHandler import net.corda.core.* import net.corda.core.crypto.* -import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -30,10 +30,7 @@ import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector -import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory +import org.apache.activemq.artemis.core.remoting.impl.netty.* import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl @@ -46,8 +43,11 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal +import org.apache.activemq.artemis.utils.ConfigurationHelper import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import rx.Subscription +import sun.security.x509.X509CertImpl import java.io.IOException import java.math.BigInteger import java.security.KeyStore @@ -259,9 +259,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { - val keyStore = KeyStoreUtilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword) + val keyStore = KeyStoreUtilities.loadKeyStore(config.sslKeystore, config.keyStorePassword) val trustStore = KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword) - val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_CA) + val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_TLS) val ourSubjectDN = X500Name(ourCertificate.subjectDN.name) // This is a sanity check and should not fail unless things have been misconfigured @@ -423,9 +423,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort" // This is called on one of Artemis' background threads - internal fun hostVerificationFail(peerLegalName: X500Name, expectedLegalName: X500Name) { - log.error("Peer has wrong CN - expected $expectedLegalName but got $peerLegalName. This is either a fatal " + - "misconfiguration by the remote peer or an SSL man-in-the-middle attack!") + internal fun hostVerificationFail(expectedLegalName: X500Name, errorMsg: String?) { + log.error(errorMsg) if (expectedLegalName == config.networkMapService?.legalName) { // If the peer that failed host verification was the network map node then we're in big trouble and need to bail! _networkMapConnectionFuture!!.setException(IOException("${config.networkMapService} failed host verification check")) @@ -466,7 +465,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, } class VerifyingNettyConnectorFactory : NettyConnectorFactory() { - override fun createConnector(configuration: MutableMap?, + override fun createConnector(configuration: MutableMap, handler: BufferHandler?, listener: ClientConnectionLifeCycleListener?, closeExecutor: Executor?, @@ -478,7 +477,7 @@ class VerifyingNettyConnectorFactory : NettyConnectorFactory() { } } -private class VerifyingNettyConnector(configuration: MutableMap?, +private class VerifyingNettyConnector(configuration: MutableMap, handler: BufferHandler?, listener: ClientConnectionLifeCycleListener?, closeExecutor: Executor?, @@ -486,27 +485,37 @@ private class VerifyingNettyConnector(configuration: MutableMap?, scheduledThreadPool: ScheduledExecutorService?, protocolManager: ClientProtocolManager?) : NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) { - private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer - private val expecteLegalName: X500Name? = configuration?.get(ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME) as X500Name? + private val server = configuration[ArtemisMessagingServer::class.java.name] as ArtemisMessagingServer + private val sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration) override fun createConnection(): Connection? { - val connection = super.createConnection() as NettyConnection? - if (connection != null && expecteLegalName != null) { - val peerLegalName: X500Name = connection - .channel - .pipeline() - .get(SslHandler::class.java) - .engine() - .session - .peerPrincipal - .name - .let(::X500Name) - if (peerLegalName != expecteLegalName) { + val connection = super.createConnection() as? NettyConnection + if (sslEnabled && connection != null) { + val expectedLegalName = configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] as X500Name + try { + val session = connection.channel + .pipeline() + .get(SslHandler::class.java) + .engine() + .session + // Checks the peer name is the one we are expecting. + val peerLegalName = session.peerPrincipal.name.let(::X500Name) + require(peerLegalName == expectedLegalName) { + "Peer has wrong CN - expected $expectedLegalName but got $peerLegalName. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + // Make sure certificate has the same name. + val peerCertificate = X509CertificateHolder(session.peerCertificateChain.first().encoded) + require(peerCertificate.subject == expectedLegalName) { + "Peer has wrong subject name in the certificate - expected $expectedLegalName but got ${peerCertificate.subject}. This is either a fatal " + + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" + } + X509Utilities.validateCertificateChain(X509CertImpl(session.localCertificates.last().encoded), *session.peerCertificates) + server.onTcpConnection(peerLegalName) + } catch (e: IllegalArgumentException) { connection.close() - server!!.hostVerificationFail(peerLegalName, expecteLegalName) - return null // Artemis will keep trying to reconnect until it's told otherwise - } else { - server!!.onTcpConnection(peerLegalName) + server.hostVerificationFail(expectedLegalName, e.message) + return null } } return connection @@ -547,7 +556,7 @@ sealed class CertificateChainCheckPolicy { object LeafMustMatch : CertificateChainCheckPolicy() { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { - val ourPublicKey = keyStore.getCertificate(CORDA_CLIENT_CA).publicKey + val ourPublicKey = keyStore.getCertificate(CORDA_CLIENT_TLS).publicKey return object : Check { override fun checkCertificateChain(theirChain: Array) { val theirLeaf = theirChain.first().publicKey diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 233b367b0b..fdc85c84d0 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -14,6 +14,7 @@ import net.corda.core.utilities.unwrap import net.corda.flows.NotaryException import net.corda.node.services.api.ServiceHubInternal import org.jetbrains.exposed.sql.Database +import java.nio.file.Path import kotlin.concurrent.thread /** @@ -21,14 +22,18 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and timestamp validity. */ -class BFTNonValidatingNotaryService(services: ServiceHubInternal, +class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, + services: ServiceHubInternal, timestampChecker: TimestampChecker, serverId: Int, db: Database, - val client: BFTSMaRt.Client) : NotaryService { + private val client: BFTSMaRt.Client) : NotaryService { init { + val configHandle = config.handle() thread(name = "BFTSmartServer-$serverId", isDaemon = true) { - Server(serverId, db, "bft_smart_notary_committed_states", services, timestampChecker) + configHandle.use { + Server(configHandle.path, serverId, db, "bft_smart_notary_committed_states", services, timestampChecker) + } } } @@ -62,11 +67,12 @@ class BFTNonValidatingNotaryService(services: ServiceHubInternal, } } - private class Server(id: Int, + private class Server(configHome: Path, + id: Int, db: Database, tableName: String, services: ServiceHubInternal, - timestampChecker: TimestampChecker) : BFTSMaRt.Server(id, db, tableName, services, timestampChecker) { + timestampChecker: TimestampChecker) : BFTSMaRt.Server(configHome, id, db, tableName, services, timestampChecker) { override fun executeCommand(command: ByteArray): ByteArray { val request = command.deserialize() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 90a69ce161..bc352301be 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -32,6 +32,7 @@ import net.corda.node.services.transactions.BFTSMaRt.Server import net.corda.node.utilities.JDBCHashMap import net.corda.node.utilities.transaction import org.jetbrains.exposed.sql.Database +import java.nio.file.Path import java.util.* /** @@ -66,13 +67,17 @@ object BFTSMaRt { data class Signatures(val txSignatures: List) : ClusterResponse() } - class Client(val id: Int) : SingletonSerializeAsToken() { + class Client(config: BFTSMaRtConfig, private val clientId: Int) : SingletonSerializeAsToken() { + private val configHandle = config.handle() + companion object { private val log = loggerFor() } /** A proxy for communicating with the BFT cluster */ - private val proxy: ServiceProxy by lazy { buildProxy() } + private val proxy: ServiceProxy by lazy { + configHandle.use { buildProxy(it.path) } + } /** * Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every @@ -86,10 +91,10 @@ object BFTSMaRt { return response } - private fun buildProxy(): ServiceProxy { + private fun buildProxy(configHome: Path): ServiceProxy { val comparator = buildResponseComparator() val extractor = buildExtractor() - return ServiceProxy(id, "bft-smart-config", comparator, extractor) + return ServiceProxy(clientId, configHome.toString(), comparator, extractor) } /** A comparator to check if replies from two replicas are the same. */ @@ -111,7 +116,7 @@ object BFTSMaRt { val accepted = responses.filterIsInstance() val rejected = responses.filterIsInstance() - log.debug { "BFT Client $id: number of replicas accepted the commit: ${accepted.size}, rejected: ${rejected.size}" } + log.debug { "BFT Client $clientId: number of replicas accepted the commit: ${accepted.size}, rejected: ${rejected.size}" } // TODO: only return an aggregate if the majority of signatures are replies // TODO: return an error reported by the majority and not just the first one @@ -137,7 +142,8 @@ object BFTSMaRt { * The validation logic can be specified by implementing the [executeCommand] method. */ @Suppress("LeakingThis") - abstract class Server(val id: Int, + abstract class Server(configHome: Path, + val replicaId: Int, val db: Database, tableName: String, val services: ServiceHubInternal, @@ -152,7 +158,7 @@ object BFTSMaRt { init { // TODO: Looks like this statement is blocking. Investigate the bft-smart node startup. - ServiceReplica(id, "bft-smart-config", this, this, null, DefaultReplier()) + ServiceReplica(replicaId, configHome.toString(), this, this, null, DefaultReplier()) } override fun appExecuteUnordered(command: ByteArray, msgCtx: MessageContext): ByteArray? { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt new file mode 100644 index 0000000000..f5f2c72931 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt @@ -0,0 +1,61 @@ +package net.corda.node.services.transactions + +import com.google.common.net.HostAndPort +import net.corda.core.div +import java.io.FileWriter +import java.io.PrintWriter +import java.net.InetAddress +import java.nio.file.Files + +/** + * BFT SMaRt can only be configured via files in a configHome directory. + * Each instance of this class creates such a configHome, accessible via [path]. + * The files are deleted on [close] typically via [use], see [PathManager] for details. + */ +class BFTSMaRtConfig(replicaAddresses: List) : PathManager(Files.createTempDirectory("bft-smart-config")) { + companion object { + internal val portIsClaimedFormat = "Port %s is claimed by another replica: %s" + } + + init { + val claimedPorts = mutableSetOf() + replicaAddresses.map { it.port }.forEach { base -> + // Each replica claims the configured port and the next one: + (0..1).map { base + it }.forEach { port -> + claimedPorts.add(port) || throw IllegalArgumentException(portIsClaimedFormat.format(port, claimedPorts)) + } + } + configWriter("hosts.config") { + replicaAddresses.forEachIndexed { index, address -> + // The documentation strongly recommends IP addresses: + println("${index} ${InetAddress.getByName(address.host).hostAddress} ${address.port}") + } + } + val n = replicaAddresses.size + val systemConfig = String.format(javaClass.getResource("system.config.printf").readText(), n, maxFaultyReplicas(n)) + configWriter("system.config") { + print(systemConfig) + } + } + + private fun configWriter(name: String, block: PrintWriter.() -> Unit) { + // Default charset, consistent with loaders: + FileWriter((path / name).toFile()).use { + PrintWriter(it).use { + it.run(block) + } + } + } +} + +fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 +fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3 +fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1 + +fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { + it.startsWith("bftsmart.") + || it.startsWith("java.security.") + || it.startsWith("java.util.") + || it.startsWith("java.lang.") + || it.startsWith("java.net.") +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt new file mode 100644 index 0000000000..886222ea7a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt @@ -0,0 +1,43 @@ +package net.corda.node.services.transactions + +import net.corda.core.internal.addShutdownHook +import java.io.Closeable +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicInteger + +internal class DeleteOnExitPath(internal val path: Path) { + private val shutdownHook = addShutdownHook { dispose() } + internal fun dispose() { + path.toFile().deleteRecursively() + shutdownHook.cancel() + } +} + +open class PathHandle internal constructor(private val deleteOnExitPath: DeleteOnExitPath, private val handleCounter: AtomicInteger) : Closeable { + val path + get(): Path { + val path = deleteOnExitPath.path + check(handleCounter.get() != 0) { "Defunct path: $path" } + return path + } + + init { + handleCounter.incrementAndGet() + } + + fun handle() = PathHandle(deleteOnExitPath, handleCounter) + + override fun close() { + if (handleCounter.decrementAndGet() == 0) { + deleteOnExitPath.dispose() + } + } +} + +/** + * An instance of this class is a handle on a temporary [path]. + * If necessary, additional handles on the same path can be created using the [handle] method. + * The path is (recursively) deleted when [close] is called on the last handle, typically at the end of a [use] expression. + * The value of eager cleanup of temporary files is that there are cases when shutdown hooks don't run e.g. SIGKILL. + */ +open class PathManager(path: Path) : PathHandle(DeleteOnExitPath(path), AtomicInteger()) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 67fab935ce..87a09a22e3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -127,7 +127,7 @@ class RaftUniquenessProvider( return NettyTransport.builder() .withSsl() .withSslProtocol(SslProtocol.TLSv1_2) - .withKeyStorePath(config.keyStoreFile.toString()) + .withKeyStorePath(config.sslKeystore.toString()) .withKeyStorePassword(config.keyStorePassword) .withTrustStorePath(config.trustStoreFile.toString()) .withTrustStorePassword(config.trustStorePassword) diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index aa7b7ff2c0..54343b58a3 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -3,6 +3,7 @@ package net.corda.node.utilities.registration import net.corda.core.* import net.corda.core.crypto.* import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.node.services.config.NodeConfiguration import org.bouncycastle.openssl.jcajce.JcaPEMWriter @@ -31,15 +32,16 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: fun buildKeystore() { config.certificatesDirectory.createDirectories() - val caKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.keyStoreFile, keystorePassword) + val caKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.nodeKeystore, keystorePassword) if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (!caKeyStore.containsAlias(SELF_SIGNED_PRIVATE_KEY)) { - val selfSignCert = X509Utilities.createSelfSignedCACert(config.myLegalName) + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName, keyPair) // Save to the key store. - caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, selfSignCert.keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate)) - caKeyStore.save(config.keyStoreFile, keystorePassword) + caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert)) + caKeyStore.save(config.nodeKeystore, keystorePassword) } val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) val requestId = submitOrResumeCertificateSigningRequest(keyPair) @@ -58,13 +60,22 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: // Save private key and certificate chain to the key store. caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA, keyPair.private, privateKeyPassword.toCharArray(), certificates) caKeyStore.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - caKeyStore.save(config.keyStoreFile, keystorePassword) + caKeyStore.save(config.nodeKeystore, keystorePassword) // Save root certificates to trust store. val trustStore = KeyStoreUtilities.loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword) // Assumes certificate chain always starts with client certificate and end with root certificate. trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last()) trustStore.save(config.trustStoreFile, config.trustStorePassword) - println("Certificate and private key stored in ${config.keyStoreFile}.") + println("Node private key and certificate stored in ${config.nodeKeystore}.") + + println("Generating SSL certificate for node messaging service.") + val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA) + val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, caCert.subject, sslKey.public) + val sslKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.sslKeystore, keystorePassword) + sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), arrayOf(sslCert, *certificates)) + sslKeyStore.save(config.sslKeystore, config.keyStorePassword) + println("SSL private key and certificate stored in ${config.sslKeystore}.") // All done, clean up temp files. requestIdStore.deleteIfExists() } else { @@ -72,6 +83,7 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: } } + /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks b/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks index 77a6bb5db5..af5ff8fce8 100644 Binary files a/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks and b/node/src/main/resources/net/corda/node/internal/certificates/cordadevcakeys.jks differ diff --git a/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks b/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks index f21f91eb2f..dd2c81122c 100644 Binary files a/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks and b/node/src/main/resources/net/corda/node/internal/certificates/cordatruststore.jks differ diff --git a/node/bft-smart-config/system.config b/node/src/main/resources/net/corda/node/services/transactions/system.config.printf similarity index 98% rename from node/bft-smart-config/system.config rename to node/src/main/resources/net/corda/node/services/transactions/system.config.printf index 46b730be18..a8388b3c22 100644 --- a/node/bft-smart-config/system.config +++ b/node/src/main/resources/net/corda/node/services/transactions/system.config.printf @@ -32,10 +32,10 @@ system.communication.defaultkeys = true ############################################ #Number of servers in the group -system.servers.num = 4 +system.servers.num = %s #Maximum number of faulty replicas -system.servers.f = 1 +system.servers.f = %s #Timeout to asking for a client request system.totalordermulticast.timeout = 2000 diff --git a/node/src/test/kotlin/net/corda/node/SerialFilterTests.kt b/node/src/test/kotlin/net/corda/node/SerialFilterTests.kt new file mode 100644 index 0000000000..590929715a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/SerialFilterTests.kt @@ -0,0 +1,31 @@ +package net.corda.node + +import org.junit.Test +import java.io.IOException +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.test.fail + +class SerialFilterTests { + @Test + fun `null and primitives are accepted and arrays are unwrapped`() { + val acceptClass = { _: Class<*> -> fail("Should not be invoked.") } + listOf(null, Byte::class.javaPrimitiveType, IntArray::class.java, Array::class.java).forEach { + assertTrue(SerialFilter.applyPredicate(acceptClass, it)) + } + } + + @Test + fun `the predicate is applied to the componentType`() { + val classes = mutableListOf>() + val acceptClass = { clazz: Class<*> -> + classes.add(clazz) + false + } + listOf(String::class.java, Array::class.java, Array>::class.java).forEach { + assertFalse(SerialFilter.applyPredicate(acceptClass, it)) + } + assertEquals(listOf>(String::class.java, Unit::class.java, IOException::class.java), classes) + } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index 8c472f2f81..38a239f949 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.getOrThrow import net.corda.core.utilities.ALICE +import net.corda.core.utilities.WHITESPACE import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -12,11 +13,8 @@ import org.junit.Test class NodeTest : NodeBasedTest() { @Test fun `empty plugins directory`() { - val baseDirectory = tempFolder.root.toPath() / ALICE.name.commonName + val baseDirectory = baseDirectory(ALICE.name) (baseDirectory / "plugins").createDirectories() - val node = startNode(ALICE.name).getOrThrow() - // Make sure we created the plugins dir in the correct place - assertThat(baseDirectory).isEqualTo(node.configuration.baseDirectory) - + startNode(ALICE.name).getOrThrow() } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index f6057b11e1..ce8c330e1d 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -1,7 +1,6 @@ package net.corda.node.services.network -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.IdentityService @@ -66,13 +65,13 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert unknown anonymous key is unrecognised`() { - val rootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) - val txCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, rootCertAndKey) + val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) + val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val service = InMemoryIdentityService() - val rootKey = rootCertAndKey.keyPair // TODO: Generate certificate with an EdDSA key rather than ECDSA - val identity = Party(rootCertAndKey) - val txIdentity = AnonymousParty(txCertAndKey.keyPair.public) + val identity = Party(CertificateAndKeyPair(rootCert, rootKey)) + val txIdentity = AnonymousParty(txKey.public) assertFailsWith { service.assertOwnership(identity, txIdentity) @@ -85,20 +84,26 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert ownership`() { - val aliceRootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) - val aliceTxCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, aliceRootCertAndKey) - val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCertAndKey, aliceTxCertAndKey.certificate, false).certPath - val bobRootCertAndKey = X509Utilities.createSelfSignedCACert(BOB.name) - val bobTxCertAndKey = X509Utilities.createIntermediateCert(BOB.name, bobRootCertAndKey) - val bobCertPath = X509Utilities.createCertificatePath(bobRootCertAndKey, bobTxCertAndKey.certificate, false).certPath - val service = InMemoryIdentityService() - val alice = Party(aliceRootCertAndKey) - val anonymousAlice = AnonymousParty(aliceTxCertAndKey.keyPair.public) - val bob = Party(bobRootCertAndKey) - val anonymousBob = AnonymousParty(bobTxCertAndKey.keyPair.public) + val aliceRootKey = Crypto.generateKeyPair() + val aliceRootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, aliceRootKey) + val aliceTxKey = Crypto.generateKeyPair() + val aliceTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, aliceRootCert, aliceRootKey, ALICE.name, aliceTxKey.public) + val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCert, aliceTxCert, revocationEnabled = false) - service.registerPath(aliceRootCertAndKey.certificate, anonymousAlice, aliceCertPath) - service.registerPath(bobRootCertAndKey.certificate, anonymousBob, bobCertPath) + val bobRootKey = Crypto.generateKeyPair() + val bobRootCert = X509Utilities.createSelfSignedCACertificate(BOB.name, bobRootKey) + val bobTxKey = Crypto.generateKeyPair() + val bobTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, bobRootCert, bobRootKey, BOB.name, bobTxKey.public) + val bobCertPath = X509Utilities.createCertificatePath(bobRootCert, bobTxCert, revocationEnabled = false) + + val service = InMemoryIdentityService() + val alice = Party(CertificateAndKeyPair(aliceRootCert, aliceRootKey)) + val anonymousAlice = AnonymousParty(aliceTxKey.public) + val bob = Party(CertificateAndKeyPair(bobRootCert, bobRootKey)) + val anonymousBob = AnonymousParty(bobTxKey.public) + + service.registerPath(aliceRootCert, anonymousAlice, aliceCertPath) + service.registerPath(bobRootCert, anonymousBob, bobCertPath) // Verify that paths are verified service.assertOwnership(alice, anonymousAlice) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt new file mode 100644 index 0000000000..c18fe75d9e --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt @@ -0,0 +1,40 @@ +package net.corda.node.services.transactions + +import com.google.common.net.HostAndPort +import net.corda.node.services.transactions.BFTSMaRtConfig.Companion.portIsClaimedFormat +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class BFTSMaRtConfigTests { + @Test + fun `replica arithmetic`() { + (1..20).forEach { n -> + assertEquals(n, maxFaultyReplicas(n) + minCorrectReplicas(n)) + } + (1..3).forEach { n -> assertEquals(0, maxFaultyReplicas(n)) } + (4..6).forEach { n -> assertEquals(1, maxFaultyReplicas(n)) } + (7..9).forEach { n -> assertEquals(2, maxFaultyReplicas(n)) } + 10.let { n -> assertEquals(3, maxFaultyReplicas(n)) } + } + + @Test + fun `min cluster size`() { + assertEquals(1, minClusterSize(0)) + assertEquals(4, minClusterSize(1)) + assertEquals(7, minClusterSize(2)) + assertEquals(10, minClusterSize(3)) + } + + @Test + fun `overlapping port ranges are rejected`() { + fun addresses(vararg ports: Int) = ports.map { HostAndPort.fromParts("localhost", it) } + assertFailsWith(IllegalArgumentException::class, portIsClaimedFormat.format(11001, setOf(11000, 11001))) { + BFTSMaRtConfig(addresses(11000, 11001)).use {} + } + assertFailsWith(IllegalArgumentException::class, portIsClaimedFormat.format(11001, setOf(11001, 11002))) { + BFTSMaRtConfig(addresses(11001, 11000)).use {} + } + BFTSMaRtConfig(addresses(11000, 11002)).use {} // Non-overlapping. + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt new file mode 100644 index 0000000000..979a4f794a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt @@ -0,0 +1,32 @@ +package net.corda.node.services.transactions + +import net.corda.core.exists +import org.junit.Test +import java.nio.file.Files +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class PathManagerTests { + @Test + fun `path deleted when manager closed`() { + val manager = PathManager(Files.createTempFile(javaClass.simpleName, null)) + val leakedPath = manager.use { + it.path.also { assertTrue(it.exists()) } + } + assertFalse(leakedPath.exists()) + assertFailsWith(IllegalStateException::class) { manager.path } + } + + @Test + fun `path deleted when handle closed`() { + val handle = PathManager(Files.createTempFile(javaClass.simpleName, null)).use { + it.handle() + } + val leakedPath = handle.use { + it.path.also { assertTrue(it.exists()) } + } + assertFalse(leakedPath.exists()) + assertFailsWith(IllegalStateException::class) { handle.path } + } +} diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index d2d9a8348d..c1d47292f8 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -3,14 +3,12 @@ package net.corda.node.utilities.registration import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock -import net.corda.core.crypto.KeyStoreUtilities -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.* import net.corda.core.exists import net.corda.core.utilities.ALICE import net.corda.testing.TestNodeConfiguration import net.corda.testing.getTestX509Name -import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -31,7 +29,7 @@ class NetworkRegistrationHelperTest { "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA") .map { getTestX509Name(it) } - val certs = identities.map { X509Utilities.createSelfSignedCACert(it).certificate } + val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } .toTypedArray() val certService: NetworkRegistrationService = mock { @@ -44,32 +42,45 @@ class NetworkRegistrationHelperTest { myLegalName = ALICE.name, networkMapService = null) - assertFalse(config.keyStoreFile.exists()) + assertFalse(config.nodeKeystore.exists()) + assertFalse(config.sslKeystore.exists()) assertFalse(config.trustStoreFile.exists()) NetworkRegistrationHelper(config, certService).buildKeystore() - assertTrue(config.keyStoreFile.exists()) + assertTrue(config.nodeKeystore.exists()) + assertTrue(config.sslKeystore.exists()) assertTrue(config.trustStoreFile.exists()) - KeyStoreUtilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword).run { - assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)) - val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - assertEquals(3, certificateChain.size) + val nodeKeystore = KeyStoreUtilities.loadKeyStore(config.nodeKeystore, config.keyStorePassword) + val sslKeystore = KeyStoreUtilities.loadKeyStore(config.sslKeystore, config.keyStorePassword) + val trustStore = KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword) + + + nodeKeystore.run { assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)) + assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + assertEquals(3, certificateChain.size) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) } - KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword).run { - assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)) + sslKeystore.run { + assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) + assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) + assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) + assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + assertEquals(4, certificateChain.size) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + } + + trustStore.run { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA)) - assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)) } } } diff --git a/samples/README.md b/samples/README.md index fd33b5e052..49255bfe41 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,5 +7,5 @@ Please refer to `README.md` in the individual project folders. There are the fo * **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.** * **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo. * **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio. -* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary. -* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash) \ No newline at end of file +* **notary-demo** A simple demonstration of a node getting multiple transactions notarised by a single or distributed (Raft or BFT SMaRt) notary. +* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash) diff --git a/samples/raft-notary-demo/README.md b/samples/notary-demo/README.md similarity index 60% rename from samples/raft-notary-demo/README.md rename to samples/notary-demo/README.md index ca94e70b0d..3abafa3d81 100644 --- a/samples/raft-notary-demo/README.md +++ b/samples/notary-demo/README.md @@ -1,5 +1,5 @@ -# Distributed Notary (Raft) Demo +# Distributed Notary Demo -This program is a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary. +This program is a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary. Please see docs/build/html/running-the-demos.html to learn how to use this demo. diff --git a/samples/raft-notary-demo/build.gradle b/samples/notary-demo/build.gradle similarity index 91% rename from samples/raft-notary-demo/build.gradle rename to samples/notary-demo/build.gradle index 9e1f6d0817..09842d2694 100644 --- a/samples/raft-notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -41,7 +41,7 @@ publishing { publications { jarAndSources(MavenPublication) { from components.java - artifactId 'raftnotarydemo' + artifactId 'notarydemo' artifact sourceJar artifact javadocJar @@ -57,6 +57,10 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' } +task deployNodesBFT(type: Cordform, dependsOn: 'jar') { + definitionClass = 'net.corda.notarydemo.BFTNotaryCordform' +} + task notarise(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.notarydemo.NotariseKt' diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt b/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt similarity index 87% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt rename to samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt index 973162de73..9f67b21a0e 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt @@ -6,8 +6,6 @@ import net.corda.node.driver.driver import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode -fun CordformDefinition.node(configure: CordformNode.() -> Unit) = addNode { cordformNode -> cordformNode.configure() } - fun CordformDefinition.clean() { System.err.println("Deleting: $driverDirectory") driverDirectory.toFile().deleteRecursively() diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt new file mode 100644 index 0000000000..45d25fdb43 --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt @@ -0,0 +1,26 @@ +package net.corda.demorun.util + +import com.google.common.net.HostAndPort +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import net.corda.core.node.services.ServiceInfo +import net.corda.nodeapi.User +import org.bouncycastle.asn1.x500.X500Name + +fun CordformDefinition.node(configure: CordformNode.() -> Unit) { + addNode { cordformNode -> cordformNode.configure() } +} + +fun CordformNode.name(name: X500Name) = name(name.toString()) + +fun CordformNode.rpcUsers(vararg users: User) { + rpcUsers = users.map { it.toMap() } +} + +fun CordformNode.advertisedServices(vararg services: ServiceInfo) { + advertisedServices = services.map { it.toString() } +} + +fun CordformNode.notaryClusterAddresses(vararg addresses: HostAndPort) { + notaryClusterAddresses = addresses.map { it.toString() } +} diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt new file mode 100644 index 0000000000..06a198c41d --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -0,0 +1,69 @@ +package net.corda.notarydemo + +import com.google.common.net.HostAndPort +import net.corda.core.div +import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.demorun.util.* +import net.corda.demorun.runNodes +import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformNode +import net.corda.core.mapToArray +import net.corda.node.services.transactions.minCorrectReplicas +import org.bouncycastle.asn1.x500.X500Name + +fun main(args: Array) = BFTNotaryCordform.runNodes() + +private val clusterSize = 4 // Minimum size thats tolerates a faulty replica. +private val notaryNames = createNotaryNames(clusterSize) + +object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { + private val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") + private val advertisedService = ServiceInfo(BFTNonValidatingNotaryService.type, clusterName) + + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + val clusterAddresses = (0 until clusterSize).mapToArray { HostAndPort.fromParts("localhost", 11000 + it * 10) } + fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { + name(notaryNames[replicaId]) + advertisedServices(advertisedService) + notaryClusterAddresses(*clusterAddresses) + bftReplicaId(replicaId) + configure() + } + notaryNode(0) { + p2pPort(10009) + rpcPort(10010) + } + notaryNode(1) { + p2pPort(10013) + rpcPort(10014) + } + notaryNode(2) { + p2pPort(10017) + rpcPort(10018) + } + notaryNode(3) { + p2pPort(10021) + rpcPort(10022) + } + } + + override fun setup(context: CordformContext) { + ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedService.type.id, clusterName, minCorrectReplicas(clusterSize)) + } +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt similarity index 59% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt index e9b3b2f35d..b06bea78c9 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -3,7 +3,7 @@ package net.corda.notarydemo import net.corda.demorun.clean fun main(args: Array) { - listOf(SingleNotaryCordform, RaftNotaryCordform).forEach { + listOf(SingleNotaryCordform, RaftNotaryCordform, BFTNotaryCordform).forEach { it.clean() } } diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt similarity index 59% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index ccbdc5b6bf..8767dcb10a 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -2,10 +2,12 @@ package net.corda.notarydemo import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.notUsed import net.corda.core.crypto.toStringShort import net.corda.core.getOrThrow +import net.corda.core.map import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction @@ -14,11 +16,10 @@ import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient fun main(args: Array) { - val host = HostAndPort.fromString("localhost:10003") - println("Connecting to the recipient node ($host)") - CordaRPCClient(host).start("demo", "demo").use { - val api = NotaryDemoClientApi(it.proxy) - api.startNotarisation() + val address = HostAndPort.fromParts("localhost", 10003) + println("Connecting to the recipient node ($address)") + CordaRPCClient(address).start(notaryDemoUser.username, notaryDemoUser.password).use { + NotaryDemoClientApi(it.proxy).notarise(10) } } @@ -27,34 +28,23 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private val notary by lazy { val (parties, partyUpdates) = rpc.networkMapUpdates() partyUpdates.notUsed() - parties.first { it.advertisedServices.any { it.info.type.isNotary() } }.notaryIdentity + parties.filter { it.advertisedServices.any { it.info.type.isNotary() } }.map { it.notaryIdentity }.distinct().single() } private val counterpartyNode by lazy { val (parties, partyUpdates) = rpc.networkMapUpdates() partyUpdates.notUsed() - parties.first { it.legalIdentity.name == BOB.name } - } - - private companion object { - private val TRANSACTION_COUNT = 10 + parties.single { it.legalIdentity.name == BOB.name } } /** Makes calls to the node rpc to start transaction notarisation. */ - fun startNotarisation() { - notarise(TRANSACTION_COUNT) - } - fun notarise(count: Int) { + println("Notary: \"${notary.name}\", with composite key: ${notary.owningKey.toStringShort()}") val transactions = buildTransactions(count) - val signers = notariseTransactions(transactions) - val transactionSigners = transactions.zip(signers).map { - val (tx, signer) = it - "Tx [${tx.tx.id.prefixChars()}..] signed by $signer" - }.joinToString("\n") - - println("Notary: \"${notary.name}\", with composite key: ${notary.owningKey.toStringShort()}\n" + - "Notarised ${transactions.size} transactions:\n" + transactionSigners) + println("Notarised ${transactions.size} transactions:") + transactions.zip(notariseTransactions(transactions)).forEach { (tx, signersFuture) -> + println("Tx [${tx.tx.id.prefixChars()}..] signed by ${signersFuture.getOrThrow().joinToString()}") + } } /** @@ -63,10 +53,9 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { * as it consumes the original asset and creates a copy with the new owner as its output. */ private fun buildTransactions(count: Int): List { - val moveTransactions = (1..count).map { + return Futures.allAsList((1..count).map { rpc.startFlow(::DummyIssueAndMove, notary, counterpartyNode.legalIdentity).returnValue - } - return Futures.allAsList(moveTransactions).getOrThrow() + }).getOrThrow() } /** @@ -75,10 +64,9 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { * * @return a list of encoded signer public keys - one for every transaction */ - private fun notariseTransactions(transactions: List): List { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") - val signatureFutures = transactions.map { rpc.startFlow(::RPCStartableNotaryFlowClient, it).returnValue } - return Futures.allAsList(signatureFutures).getOrThrow().map { it.map { it.by.toStringShort() }.joinToString() } + private fun notariseTransactions(transactions: List): List>> { + return transactions.map { + rpc.startFlow(::RPCStartableNotaryFlowClient, it).returnValue.map { it.map { it.by.toStringShort() } } + } } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt new file mode 100644 index 0000000000..7c2dd027bf --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -0,0 +1,70 @@ +package net.corda.notarydemo + +import com.google.common.net.HostAndPort +import net.corda.core.crypto.appendToCommonName +import net.corda.core.div +import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.demorun.util.* +import net.corda.node.services.transactions.RaftValidatingNotaryService +import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformNode +import net.corda.demorun.runNodes +import net.corda.demorun.util.node +import org.bouncycastle.asn1.x500.X500Name + +fun main(args: Array) = RaftNotaryCordform.runNodes() + +internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } + +private val notaryNames = createNotaryNames(3) + +object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { + private val clusterName = X500Name("CN=Raft,O=R3,OU=corda,L=Zurich,C=CH") + private val advertisedService = ServiceInfo(RaftValidatingNotaryService.type, clusterName) + + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + fun notaryNode(index: Int, configure: CordformNode.() -> Unit) = node { + name(notaryNames[index]) + advertisedServices(advertisedService) + configure() + } + notaryNode(0) { + notaryNodePort(10008) + p2pPort(10009) + rpcPort(10010) + } + val clusterAddress = HostAndPort.fromParts("localhost", 10008) // Otherwise each notary forms its own cluster. + notaryNode(1) { + notaryNodePort(10012) + p2pPort(10013) + rpcPort(10014) + notaryClusterAddresses(clusterAddress) + } + notaryNode(2) { + notaryNodePort(10016) + p2pPort(10017) + rpcPort(10018) + notaryClusterAddresses(clusterAddress) + } + } + + override fun setup(context: CordformContext) { + ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedService.type.id, clusterName) + } +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt similarity index 66% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index b02d4f04c9..fcd2165253 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -5,7 +5,6 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.demorun.node import net.corda.demorun.runNodes import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService @@ -14,31 +13,30 @@ import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformContext +import net.corda.demorun.util.* fun main(args: Array) = SingleNotaryCordform.runNodes() +val notaryDemoUser = User("demou", "demop", setOf(startFlowPermission(), startFlowPermission())) + object SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", DUMMY_NOTARY.name) { init { node { - name(ALICE.name.toString()) - nearestCity("London") + name(ALICE.name) p2pPort(10002) rpcPort(10003) - rpcUsers = listOf(User("demo", "demo", setOf(startFlowPermission(), startFlowPermission())).toMap()) + rpcUsers(notaryDemoUser) } node { - name(BOB.name.toString()) - nearestCity("New York") + name(BOB.name) p2pPort(10005) rpcPort(10006) } node { - name(DUMMY_NOTARY.name.toString()) - nearestCity("London") - advertisedServices = listOf(ServiceInfo(ValidatingNotaryService.type).toString()) + name(DUMMY_NOTARY.name) p2pPort(10009) rpcPort(10010) - notaryNodePort(10008) + advertisedServices(ServiceInfo(ValidatingNotaryService.type)) } } diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt similarity index 100% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt similarity index 100% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt deleted file mode 100644 index 379f7dc2e1..0000000000 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.corda.notarydemo - -import net.corda.core.crypto.appendToCommonName -import net.corda.core.div -import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.demorun.node -import net.corda.demorun.runNodes -import net.corda.node.services.startFlowPermission -import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.User -import net.corda.notarydemo.flows.DummyIssueAndMove -import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformContext -import org.bouncycastle.asn1.x500.X500Name - -fun main(args: Array) = RaftNotaryCordform.runNodes() - -private val notaryNames = (1..3).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } - -object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { - private val advertisedNotary = ServiceInfo(RaftValidatingNotaryService.type, X500Name("CN=Raft,O=R3,OU=corda,L=Zurich,C=CH")) - - init { - node { - name(ALICE.name.toString()) - nearestCity("London") - p2pPort(10002) - rpcPort(10003) - rpcUsers = listOf(User("demo", "demo", setOf(startFlowPermission(), startFlowPermission())).toMap()) - } - node { - name(BOB.name.toString()) - nearestCity("New York") - p2pPort(10005) - rpcPort(10006) - } - node { - name(notaryNames[0].toString()) - nearestCity("London") - advertisedServices = listOf(advertisedNotary.toString()) - p2pPort(10009) - rpcPort(10010) - notaryNodePort(10008) - } - node { - name(notaryNames[1].toString()) - nearestCity("London") - advertisedServices = listOf(advertisedNotary.toString()) - p2pPort(10013) - rpcPort(10014) - notaryNodePort(10012) - notaryClusterAddresses = listOf("localhost:10008") - } - node { - name(notaryNames[2].toString()) - nearestCity("London") - advertisedServices = listOf(advertisedNotary.toString()) - p2pPort(10017) - rpcPort(10018) - notaryNodePort(10016) - notaryClusterAddresses = listOf("localhost:10008") - } - } - - override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedNotary.type.id, advertisedNotary.name!!) - } -} diff --git a/settings.gradle b/settings.gradle index 90b1e5797d..4fca9add5c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,7 +30,7 @@ include 'samples:trader-demo' include 'samples:irs-demo' include 'samples:network-visualiser' include 'samples:simm-valuation-demo' -include 'samples:raft-notary-demo' +include 'samples:notary-demo' include 'samples:bank-of-corda-demo' include 'cordform-common' include 'doorman' diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 13afa5e73a..35ae8093b2 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -189,7 +189,7 @@ fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int): rpcAddress = HostAndPort.fromParts("localhost", basePort + 1), messagingServerAddress = null, extraAdvertisedServiceIds = emptyList(), - notaryNodeId = null, + bftReplicaId = null, notaryNodeAddress = null, notaryClusterAddresses = emptyList(), certificateChainCheckPolicies = emptyList(), @@ -217,7 +217,6 @@ fun getTestX509Name(commonName: String): X500Name { val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) nameBuilder.addRDN(BCStyle.CN, commonName) nameBuilder.addRDN(BCStyle.O, "R3") - nameBuilder.addRDN(BCStyle.OU, "Corda QA Department") nameBuilder.addRDN(BCStyle.L, "New York") nameBuilder.addRDN(BCStyle.C, "US") return nameBuilder.build() diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index a4755f6a0a..247db93a88 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -1,7 +1,6 @@ package net.corda.testing.messaging import com.google.common.net.HostAndPort -import net.corda.core.crypto.X509Utilities import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection @@ -37,6 +36,10 @@ class SimpleMQClient(val target: HostAndPort, fun createMessage(): ClientMessage = session.createMessage(false) fun stop() { - sessionFactory.close() + try { + sessionFactory.close() + } catch (e: Exception) { + // sessionFactory might not have initialised. + } } } \ No newline at end of file diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 0d58b85366..7b44b39910 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -4,10 +4,12 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.DUMMY_MAP +import net.corda.core.utilities.WHITESPACE import net.corda.node.driver.addressMustNotBeBound import net.corda.node.internal.Node import net.corda.node.services.config.ConfigHelper @@ -107,7 +109,7 @@ abstract class NodeBasedTest { clusterSize: Int, serviceType: ServiceType = RaftValidatingNotaryService.type): ListenableFuture> { ServiceIdentityGenerator.generateToDisk( - (0 until clusterSize).map { tempFolder.root.toPath() / "${notaryName.commonName}-$it" }, + (0 until clusterSize).map { baseDirectory(notaryName.appendToCommonName("-$it")) }, serviceType.id, notaryName) @@ -133,12 +135,14 @@ abstract class NodeBasedTest { } } + protected fun baseDirectory(legalName: X500Name) = tempFolder.root.toPath() / legalName.commonName.replace(WHITESPACE, "") + private fun startNodeInternal(legalName: X500Name, platformVersion: Int, advertisedServices: Set, rpcUsers: List, configOverrides: Map): Node { - val baseDirectory = (tempFolder.root.toPath() / legalName.commonName).createDirectories() + val baseDirectory = baseDirectory(legalName).createDirectories() val localPort = getFreeLocalPorts("localhost", 2) val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory, diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index 822c506289..333dc8a878 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -5,6 +5,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.core.ErrorOr +import net.corda.core.internal.addShutdownHook import net.corda.core.div import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor @@ -51,11 +52,11 @@ class Verifier { val session = sessionFactory.createSession( VerifierApi.VERIFIER_USERNAME, VerifierApi.VERIFIER_USERNAME, false, true, true, locator.isPreAcknowledge, locator.ackBatchSize ) - Runtime.getRuntime().addShutdownHook(Thread { + addShutdownHook { log.info("Shutting down") session.close() sessionFactory.close() - }) + } val consumer = session.createConsumer(VERIFICATION_REQUESTS_QUEUE_NAME) val replyProducer = session.createProducer() consumer.setMessageHandler { diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index a0ce5b7d6b..958465bdc5 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -79,7 +79,7 @@ class NodeWebServer(val config: WebServerConfig) { httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.addCustomizer(SecureRequestCustomizer()) val sslContextFactory = SslContextFactory() - sslContextFactory.keyStorePath = config.keyStoreFile.toString() + sslContextFactory.keyStorePath = config.sslKeystore.toString() sslContextFactory.setKeyStorePassword(config.keyStorePassword) sslContextFactory.setKeyManagerPassword(config.keyStorePassword) sslContextFactory.setTrustStorePath(config.trustStoreFile.toString())