From 8d48083ddfd04c647100ab0fd6aac0ebb60c5958 Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Wed, 20 Dec 2017 10:02:03 +0000 Subject: [PATCH 1/6] Fix startErrorFlowSimulation (#222) --- .../src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index edc0a0543e..7c8ef402dc 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -185,6 +185,7 @@ class ExplorerSimulation(private val options: OptionSet) { private fun startErrorFlowsSimulation() { println("Running flows with errors simulation mode ...") setUpRPC() + notary = aliceNode.rpc.notaryIdentities().first() val eventGenerator = ErrorFlowsEventGenerator( parties = parties.map { it.first }, notary = notary, From 246142173d84893fcbd9ff490e63fe4f30e91aa7 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 20 Dec 2017 10:32:42 +0000 Subject: [PATCH 2/6] Shams os merge 191217 (#223) * CORDA-876 MockNetwork no longer leaks serialization env if init fails (#2272) * Removed all remaining special treatment of the X500 common name. * Move unspecifiedCountry to internal. (#2274) * Merge fixes, which includes fixing the doorman tests and updating the doorman to not set a CN in the CSR responses --- .ci/api-current.txt | 2 +- .../net/corda/core/identity/CordaX500Name.kt | 8 +- .../net/corda/core/internal/InternalUtils.kt | 8 ++ .../corda/core/node/services/NotaryService.kt | 14 +- .../corda/core/crypto/CompositeKeyTests.kt | 11 +- .../AttachmentSerializationTest.kt | 2 +- docs/source/generating-a-node.rst | 2 - docs/source/hello-world-running.rst | 2 +- .../doorman/DoormanIntegrationTest.kt | 19 +-- .../persistence/PersistentNodeInfoStorage.kt | 75 +++++----- .../doorman/signer/LocalSigner.kt | 11 +- .../networkmanage/hsm/utils/X509Utils.kt | 3 +- .../persistence/DBNetworkMapStorageTest.kt | 68 ++++----- .../PersitenceNodeInfoStorageTest.kt | 129 +++++++----------- .../doorman/NodeInfoWebServiceTest.kt | 46 +++---- ...ntityGenerator.kt => IdentityGenerator.kt} | 44 +++--- .../nodeapi/internal/crypto/X509Utilities.kt | 12 +- .../node/services/BFTNotaryServiceTests.kt | 8 +- .../node/services/MySQLNotaryServiceTests.kt | 8 +- .../node/services/RaftNotaryServiceTests.kt | 7 +- .../services/messaging/P2PMessagingTest.kt | 5 +- .../net/corda/node/internal/AbstractNode.kt | 29 ++-- .../node/services/config/ConfigUtilities.kt | 9 +- .../identity/InMemoryIdentityService.kt | 1 + .../identity/PersistentIdentityService.kt | 1 + .../BFTNonValidatingNotaryService.kt | 11 +- .../transactions/MySQLNotaryService.kt | 6 - .../RaftNonValidatingNotaryService.kt | 13 +- .../RaftValidatingNotaryService.kt | 13 +- .../transactions/ValidatingNotaryService.kt | 4 +- .../registration/NetworkRegistrationHelper.kt | 5 +- .../statemachine/FlowFrameworkTests.kt | 2 +- .../NetworkRegistrationHelperTest.kt | 111 +++++++-------- .../net/corda/notarydemo/BFTNotaryCordform.kt | 12 +- .../corda/notarydemo/RaftNotaryCordform.kt | 11 +- testing/node-driver/build.gradle | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 88 ++++++------ .../testing/node/internal/DriverDSLImpl.kt | 42 ++---- .../corda/testing/node/MockNetworkTests.kt | 18 +++ .../kotlin/net/corda/testing/CoreTestUtils.kt | 5 +- .../corda/demobench/model/NodeController.kt | 7 +- 41 files changed, 401 insertions(+), 473 deletions(-) rename node-api/src/main/kotlin/net/corda/nodeapi/internal/{ServiceIdentityGenerator.kt => IdentityGenerator.kt} (53%) create mode 100644 testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 5fd0e2262b..09b766e46d 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1862,7 +1862,7 @@ public @interface net.corda.core.node.services.CordaService @org.jetbrains.annotations.NotNull public static final String ID_PREFIX = "corda.notary." ## public static final class net.corda.core.node.services.NotaryService$Companion extends java.lang.Object - @org.jetbrains.annotations.NotNull public final String constructId(boolean, boolean, boolean, boolean) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public final String constructId(boolean, boolean, boolean, boolean) ## public abstract class net.corda.core.node.services.PartyInfo extends java.lang.Object @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getParty() diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index 0b6a3ddeb7..ad315c065e 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -2,7 +2,7 @@ package net.corda.core.identity import com.google.common.collect.ImmutableSet import net.corda.core.internal.LegalNameValidator -import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.unspecifiedCountry import net.corda.core.internal.x500Name import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.ASN1Encodable @@ -36,7 +36,9 @@ data class CordaX500Name(val commonName: String?, val locality: String, val state: String?, val country: String) { - constructor(commonName: String, organisation: String, locality: String, country: String) : this(commonName = commonName, organisationUnit = null, organisation = organisation, locality = locality, state = null, country = country) + constructor(commonName: String, organisation: String, locality: String, country: String) : + this(commonName = commonName, organisationUnit = null, organisation = organisation, locality = locality, state = null, country = country) + /** * @param organisation name of the organisation. * @param locality locality of the organisation, typically nearest major city. @@ -79,8 +81,6 @@ data class CordaX500Name(val commonName: String?, const val MAX_LENGTH_ORGANISATION_UNIT = 64 const val MAX_LENGTH_COMMON_NAME = 64 private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU) - @VisibleForTesting - val unspecifiedCountry = "ZZ" private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries() + unspecifiedCountry) @JvmStatic fun build(principal: X500Principal): CordaX500Name { diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 36022463ff..42145a2f26 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -5,6 +5,7 @@ package net.corda.core.internal import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.SerializationContext import net.corda.core.transactions.TransactionBuilder @@ -118,6 +119,8 @@ fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(th inline val Path.size: Long get() = Files.size(this) inline fun Path.list(block: (Stream) -> R): R = Files.list(this).use(block) fun Path.deleteIfExists(): Boolean = Files.deleteIfExists(this) +fun Path.reader(charset: Charset = UTF_8): BufferedReader = Files.newBufferedReader(this, charset) +fun Path.writer(charset: Charset = UTF_8, vararg options: OpenOption): BufferedWriter = Files.newBufferedWriter(this, charset, *options) fun Path.readAll(): ByteArray = Files.readAllBytes(this) inline fun Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = Files.newInputStream(this, *options).use(block) inline fun Path.write(createDirs: Boolean = false, vararg options: OpenOption = emptyArray(), block: (OutputStream) -> Unit) { @@ -316,3 +319,8 @@ fun ExecutorService.join() { // Try forever. Do not give up, tests use this method to assert the executor has no more tasks. } } + +@Suppress("unused") +@VisibleForTesting +val CordaX500Name.Companion.unspecifiedCountry + get() = "ZZ" diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 0c928a7b59..8c6a160618 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -15,22 +15,16 @@ import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { companion object { + @Deprecated("No longer used") const val ID_PREFIX = "corda.notary." - @JvmOverloads - fun constructId( - validating: Boolean, - raft: Boolean = false, - bft: Boolean = false, - custom: Boolean = false, - mysql: Boolean = false - ): String { - require(Booleans.countTrue(raft, bft, custom, mysql) <= 1) { "At most one of raft, bft, mysql or custom may be true" } + @Deprecated("No longer used") + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { + require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } return StringBuffer(ID_PREFIX).apply { append(if (validating) "validating" else "simple") if (raft) append(".raft") if (bft) append(".bft") if (custom) append(".custom") - if (mysql) append(".mysql") }.toString() } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 734505498d..751037b1ee 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import net.corda.nodeapi.internal.crypto.* -import net.corda.testing.internal.kryoSpecific import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.kryoSpecific import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -24,6 +24,7 @@ class CompositeKeyTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() + @Rule @JvmField val tempFolder: TemporaryFolder = TemporaryFolder() @@ -40,9 +41,9 @@ class CompositeKeyTests { private val secureHash = message.sha256() // By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise). - val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) } - val bobSignature by lazy { bobKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(bobPublicKey).schemeNumberID))) } - val charlieSignature by lazy { charlieKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(charliePublicKey).schemeNumberID))) } + private val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) } + private val bobSignature by lazy { bobKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(bobPublicKey).schemeNumberID))) } + private val charlieSignature by lazy { charlieKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(charliePublicKey).schemeNumberID))) } @Test fun `(Alice) fulfilled by Alice signature`() { @@ -337,7 +338,7 @@ class CompositeKeyTests { val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair) // Sign the composite key with the self sign CA. - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, ca, caKeyPair, caName, compositeKey) // Store certificate to keystore. val keystorePath = tempFolder.root.toPath() / "keystore.jks" diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 940d959860..d72d7068d4 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -159,7 +159,7 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.dispose() - client = mockNet.createNode(MockNodeParameters(client.internals.id), { args -> + client = mockNet.createNode(MockNodeParameters(client.internals.id, client.internals.configuration.myLegalName), { args -> object : MockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index 4a721e304d..eee15b3d5d 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -92,7 +92,6 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates } node { name "O=PartyA,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -103,7 +102,6 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates } node { name "O=PartyB,L=New York,C=US" - advertisedServices = [] p2pPort 10009 rpcPort 10010 webPort 10011 diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 592cfee44c..4ac394d26c 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -22,7 +22,7 @@ service. directory "./build/nodes" node { name "O=Controller,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:corda-finance:$corda_release_version"] diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt index fe919f6114..5993cef210 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt @@ -3,7 +3,6 @@ package com.r3.corda.networkmanage.doorman import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import com.r3.corda.networkmanage.common.persistence.configureDatabase -import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto @@ -30,7 +29,6 @@ import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.rigorousMock import org.bouncycastle.cert.X509CertificateHolder -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -78,7 +76,7 @@ class DoormanIntegrationTest { loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply { assert(containsAlias(X509Utilities.CORDA_CLIENT_CA)) - assertEquals(ALICE_NAME.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal) + assertEquals(ALICE_NAME.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList()) } @@ -120,13 +118,18 @@ class DoormanIntegrationTest { // Publish NodeInfo val networkMapClient = NetworkMapClient(config.compatibilityZoneURL!!, rootCertAndKey.certificate.cert) - val certs = loadKeyStore(config.nodeKeystore, config.keyStorePassword).getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - val keyPair = loadKeyStore(config.nodeKeystore, config.keyStorePassword).getKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(buildCertPath(*certs))), 1, serial = 1L) + + val keyStore = loadKeyStore(config.nodeKeystore, config.keyStorePassword) + val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword) + val identityKeyPair = Crypto.generateKeyPair() + val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, ALICE_NAME, identityKeyPair.public) + val certPath = X509CertificateFactory().generateCertPath(identityCert.cert, *clientCertPath) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) val nodeInfoBytes = nodeInfo.serialize() // When - val signedNodeInfo = SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes))) + val signedNodeInfo = SignedNodeInfo(nodeInfoBytes, listOf(identityKeyPair.private.sign(nodeInfoBytes.bytes))) networkMapClient.publish(signedNodeInfo) // Then @@ -137,7 +140,7 @@ class DoormanIntegrationTest { doorman.close() } - fun createConfig(): NodeConfiguration { + private fun createConfig(): NodeConfiguration { return rigorousMock().also { doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt index 7f1d331877..1d4af91246 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt @@ -1,65 +1,60 @@ package com.r3.corda.networkmanage.common.persistence -import com.r3.corda.networkmanage.common.persistence.entity.* +import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity +import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity +import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.utils.buildCertPath import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.identity.CordaX500Name -import net.corda.core.serialization.SerializedBytes +import net.corda.core.internal.CertRole import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import java.security.cert.CertPath -import java.security.cert.X509Certificate /** * Database implementation of the [NetworkMapStorage] interface */ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { - override fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash = database.transaction(TransactionIsolationLevel.SERIALIZABLE) { - val nodeInfo = signedNodeInfo.verified() - val orgName = nodeInfo.legalIdentities.first().name.organisation - // TODO: use cert extension to identify NodeCA cert when Ross's work is in. - val nodeCACert = nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.map { it as X509Certificate } - .find { CordaX500Name.build(it.issuerX500Principal).organisation != orgName && CordaX500Name.build(it.subjectX500Principal).organisation == orgName } + override fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash { + return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { + val nodeInfo = signedNodeInfo.verified() + val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA } - val request = nodeCACert?.let { - singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> - val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) - val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) - builder.and(certPublicKeyHashEq, certStatusValid) + val request = nodeCaCert?.let { + singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> + val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) + val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) + builder.and(certPublicKeyHashEq, certStatusValid) + } } - } - request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") - /* - * Delete any previous [NodeInfoEntity] instance for this CSR - * Possibly it should be moved at the network signing process at the network signing process - * as for a while the network map will have invalid entries (i.e. hashes for node info which have been - * removed). Either way, there will be a period of time when the network map data will be invalid - * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. - */ - deleteRequest(NodeInfoEntity::class.java) { builder, path -> - builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) - } - val hash = signedNodeInfo.raw.hash + request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") - val hashedNodeInfo = NodeInfoEntity( - nodeInfoHash = hash.toString(), - certificateSigningRequest = request.certificateSigningRequest, - signedNodeInfoBytes = signedNodeInfo.serialize().bytes) - session.save(hashedNodeInfo) - hash + /* + * Delete any previous [NodeInfoEntity] instance for this CSR + * Possibly it should be moved at the network signing process at the network signing process + * as for a while the network map will have invalid entries (i.e. hashes for node info which have been + * removed). Either way, there will be a period of time when the network map data will be invalid + * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. + */ + deleteRequest(NodeInfoEntity::class.java) { builder, path -> + builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) + } + val hash = signedNodeInfo.raw.hash + + val hashedNodeInfo = NodeInfoEntity( + nodeInfoHash = hash.toString(), + certificateSigningRequest = request.certificateSigningRequest, + signedNodeInfoBytes = signedNodeInfo.serialize().bytes) + session.save(hashedNodeInfo) + hash + } } override fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo? { return database.transaction { - val nodeInfoEntity = session.find(NodeInfoEntity::class.java, nodeInfoHash.toString()) - if (nodeInfoEntity == null) { - null - } else { - nodeInfoEntity.signedNodeInfo() - } + session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.signedNodeInfo() } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt index cb6e253536..50a7b7f971 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt @@ -2,10 +2,10 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.signer.Signer import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -33,13 +33,14 @@ class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array< val nameConstraints = NameConstraints( arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), arrayOf()) - val clientCertificate = X509Utilities.createCertificate(CertificateType.NODE_CA, + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, caCertPath.first().toX509CertHolder(), caKeyPair, - CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + CordaX500Name.parse(request.subject.toString()), request.publicKey, - nameConstraints = nameConstraints).toX509Certificate() - return buildCertPath(clientCertificate, *caCertPath) + nameConstraints = nameConstraints) + return buildCertPath(nodeCaCert.cert, *caCertPath) } override fun sign(data: ByteArray): DigitalSignatureWithCert { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt index 671841746d..22e94ec1b5 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt @@ -7,7 +7,6 @@ import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.x500Name import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.getX509Certificate import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Sequence @@ -184,7 +183,7 @@ object X509Utilities { val certificateType = CertificateType.NODE_CA val validityWindow = getCertificateValidityWindow(0, validDays, issuerCertificate.notBefore, issuerCertificate.notAfter) val serial = BigInteger.valueOf(random63BitValue(provider)) - val subject = CordaX500Name.parse(jcaRequest.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name + val subject = CordaX500Name.parse(jcaRequest.subject.toString()).x500Name val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(jcaRequest.publicKey.encoded)) val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) val builder = JcaX509v3CertificateBuilder(issuerCertificate.subject, serial, validityWindow.first, validityWindow.second, subject, jcaRequest.publicKey) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt index 062d4c5c5a..b7d332a5fe 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt @@ -1,24 +1,21 @@ package com.r3.corda.networkmanage.common.persistence import com.r3.corda.networkmanage.TestBase -import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import net.corda.core.crypto.Crypto import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert -import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.SignedNetworkMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -31,6 +28,7 @@ class DBNetworkMapStorageTest : TestBase() { private lateinit var requestStorage: CertificationRequestStorage private lateinit var nodeInfoStorage: NodeInfoStorage private lateinit var persistence: CordaPersistence + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -53,18 +51,8 @@ class DBNetworkMapStorageTest : TestBase() { fun `signNetworkMap creates current network map`() { // given // Create node info. - val organisation = "Test" - val requestId = requestStorage.saveRequest(createRequest(organisation).first) - requestStorage.markRequestTicketCreated(requestId) - requestStorage.approveRequest(requestId, "TestUser") - val keyPair = Crypto.generateKeyPair() - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestId, certPath, emptyList()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - // Put signed node info data - val nodeInfoBytes = nodeInfo.serialize() - val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes)))) + val signedNodeInfo = createValidSignedNodeInfo("Test") + val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) // Create network parameters val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList())) @@ -103,13 +91,11 @@ class DBNetworkMapStorageTest : TestBase() { // Create network parameters val networkMapParametersHash = networkMapStorage.saveNetworkParameters(createNetworkParameters(1)) // Create empty network map - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Corda", locality = "London", country = "GB"), keyPair.public) // Sign network map making it current network map val networkMap = NetworkMap(emptyList(), networkMapParametersHash) val serializedNetworkMap = networkMap.serialize() - val signatureData = keyPair.sign(serializedNetworkMap).withCert(intermediateCert.cert) + val signatureData = intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert) val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData) networkMapStorage.saveNetworkMap(signedNetworkMap) @@ -126,36 +112,19 @@ class DBNetworkMapStorageTest : TestBase() { @Test fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() { // given - // Create node info. - val organisationA = "TestA" - val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) - requestStorage.markRequestTicketCreated(requestIdA) - requestStorage.approveRequest(requestIdA, "TestUser") - val keyPairA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCertA = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPairA.public) - val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) - val organisationB = "TestB" - val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) - requestStorage.markRequestTicketCreated(requestIdB) - requestStorage.approveRequest(requestIdB, "TestUser") - val keyPairB = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCertB = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), keyPairB.public) - val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) - val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.companyA.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) - val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.companyB.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) + // Create node infos. + val signedNodeInfoA = createValidSignedNodeInfo("TestA") + val signedNodeInfoB = createValidSignedNodeInfo("TestB") + // Put signed node info data - val nodeInfoABytes = nodeInfoA.serialize() - val nodeInfoBBytes = nodeInfoB.serialize() - val nodeInfoHashA = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoABytes, listOf(keyPairA.sign(nodeInfoABytes)))) - val nodeInfoHashB = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBBytes, listOf(keyPairB.sign(nodeInfoBBytes)))) + val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA) + val nodeInfoHashB = nodeInfoStorage.putNodeInfo(signedNodeInfoB) // Create network parameters val networkParametersHash = networkMapStorage.saveNetworkParameters(createNetworkParameters()) val networkMap = NetworkMap(listOf(nodeInfoHashA), networkParametersHash) val serializedNetworkMap = networkMap.serialize() - val signatureData = keyPairA.sign(serializedNetworkMap).withCert(clientCertA.cert) + val signatureData = intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert) val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData) // Sign network map @@ -167,4 +136,15 @@ class DBNetworkMapStorageTest : TestBase() { // then assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB) } + + private fun createValidSignedNodeInfo(organisation: String): SignedNodeInfo { + val nodeInfoBuilder = TestNodeInfoBuilder() + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.markRequestTicketCreated(requestId) + requestStorage.approveRequest(requestId, "TestUser") + val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) + val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) + requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) + return nodeInfoBuilder.buildWithSigned().second + } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt index 1014cde074..e5b2e37699 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt @@ -3,31 +3,34 @@ package com.r3.corda.networkmanage.common.persistence import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.hashString -import com.r3.corda.networkmanage.common.utils.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.cert import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize -import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.internal.TestNodeInfoBuilder +import net.corda.testing.internal.signWith import net.corda.testing.node.MockServices +import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test +import java.security.PrivateKey import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull class PersitenceNodeInfoStorageTest : TestBase() { private lateinit var requestStorage: CertificationRequestStorage - private lateinit var nodeInfoStorage: NodeInfoStorage + private lateinit var nodeInfoStorage: PersistentNodeInfoStorage private lateinit var persistence: CordaPersistence + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -46,14 +49,13 @@ class PersitenceNodeInfoStorageTest : TestBase() { } @Test - fun `test get CertificatePath`() { + fun `test getCertificatePath`() { // Create node info. val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val name = CordaX500Name(organisation = "Test", locality = "London", country = "GB") + val nodeCaCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, name, keyPair.public) - val request = X509Utilities.createCertificateSigningRequest(nodeInfo.legalIdentities.first().name, "my@mail.com", keyPair) + val request = X509Utilities.createCertificateSigningRequest(name, "my@mail.com", keyPair) val requestId = requestStorage.saveRequest(request) requestStorage.markRequestTicketCreated(requestId) @@ -61,104 +63,73 @@ class PersitenceNodeInfoStorageTest : TestBase() { assertNull(nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString()))) - requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()), listOf(CertificationRequestStorage.DOORMAN_SIGNATURE)) + requestStorage.putCertificatePath(requestId, buildCertPath(nodeCaCert.cert, intermediateCACert.cert, rootCACert.cert), listOf(CertificationRequestStorage.DOORMAN_SIGNATURE)) val storedCertPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString())) assertNotNull(storedCertPath) - assertEquals(clientCert.toX509Certificate(), storedCertPath!!.certificates.first()) + assertEquals(nodeCaCert.cert, storedCertPath!!.certificates.first()) } @Test - fun `test getNodeInfoHash returns correct data`() { + fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() { // given - val organisationA = "TestA" - val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) - requestStorage.markRequestTicketCreated(requestIdA) - requestStorage.approveRequest(requestIdA, "TestUser") - val keyPairA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCertA = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPairA.public) - val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) - val organisationB = "TestB" - val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) - requestStorage.markRequestTicketCreated(requestIdB) - requestStorage.approveRequest(requestIdB, "TestUser") - val keyPairB = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCertB = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), keyPairB.public) - val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) - val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) - val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) + val (nodeInfoA, signedNodeInfoA) = createValidSignedNodeInfo("TestA") + val (nodeInfoB, signedNodeInfoB) = createValidSignedNodeInfo("TestB") // Put signed node info data - val nodeInfoABytes = nodeInfoA.serialize() - val nodeInfoBBytes = nodeInfoB.serialize() - nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoABytes, listOf(keyPairA.sign(nodeInfoABytes)))) - nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBBytes, listOf(keyPairB.sign(nodeInfoBBytes)))) + nodeInfoStorage.putNodeInfo(signedNodeInfoA) + nodeInfoStorage.putNodeInfo(signedNodeInfoB) // when - val persistedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeInfoABytes.hash) - val persistedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeInfoBBytes.hash) + val persistedSignedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeInfoA.serialize().hash) + val persistedSignedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeInfoB.serialize().hash) // then - assertNotNull(persistedNodeInfoA) - assertNotNull(persistedNodeInfoB) - assertEquals(persistedNodeInfoA!!.verified(), nodeInfoA) - assertEquals(persistedNodeInfoB!!.verified(), nodeInfoB) + assertEquals(persistedSignedNodeInfoA?.verified(), nodeInfoA) + assertEquals(persistedSignedNodeInfoB?.verified(), nodeInfoB) } @Test - fun `same pub key with different node info`() { + fun `same public key with different node info`() { // Create node info. - val organisation = "Test" - val requestId = requestStorage.saveRequest(createRequest(organisation).first) - requestStorage.markRequestTicketCreated(requestId) - requestStorage.approveRequest(requestId, "TestUser") - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestId, certPath, emptyList()) + val (nodeInfo1, signedNodeInfo1, key) = createValidSignedNodeInfo("Test", serial = 1) + val nodeInfo2 = nodeInfo1.copy(serial = 2) + val signedNodeInfo2 = nodeInfo2.signWith(listOf(key)) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - val nodeInfoSamePubKey = NodeInfo(listOf(NetworkHostAndPort("my.company2.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - val nodeInfoBytes = nodeInfo.serialize() - val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes)))) - assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfoHash)?.verified()) + val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(signedNodeInfo1) + assertEquals(nodeInfo1, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified()) - val nodeInfoSamePubKeyBytes = nodeInfoSamePubKey.serialize() // This should replace the node info. - nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoSamePubKeyBytes, listOf(keyPair.sign(nodeInfoSamePubKeyBytes)))) + nodeInfoStorage.putNodeInfo(signedNodeInfo2) // Old node info should be removed. - assertNull(nodeInfoStorage.getNodeInfo(nodeInfoHash)) - assertEquals(nodeInfoSamePubKey, nodeInfoStorage.getNodeInfo(nodeInfoSamePubKeyBytes.hash)?.verified()) + assertNull(nodeInfoStorage.getNodeInfo(nodeInfo1Hash)) + assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().hash)?.verified()) } @Test - fun `putNodeInfo persists node info data with its signature`() { + fun `putNodeInfo persists SignedNodeInfo with its signature`() { // given - // Create node info. - val organisation = "Test" + val (_, signedNodeInfo) = createValidSignedNodeInfo("Test") + + // when + val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) + + // then + val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) + assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(signedNodeInfo.signatures) + } + + private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Triple { + val nodeInfoBuilder = TestNodeInfoBuilder() val requestId = requestStorage.saveRequest(createRequest(organisation).first) requestStorage.markRequestTicketCreated(requestId) requestStorage.approveRequest(requestId, "TestUser") - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - requestStorage.putCertificatePath(requestId, certPath, emptyList()) - - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - val nodeInfoBytes = nodeInfo.serialize() - val signature = keyPair.sign(nodeInfoBytes) - - // when - val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(signature))) - - // then - val persistedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) - assertNotNull(persistedNodeInfo) - assertEquals(nodeInfo, persistedNodeInfo!!.verified()) - assertEquals(signature, persistedNodeInfo.signatures.firstOrNull()) + val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) + val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) + requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) + val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial) + return Triple(nodeInfo, signedNodeInfo, key) } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt index 096a05534f..26452d45a9 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt @@ -1,20 +1,18 @@ package com.r3.corda.networkmanage.doorman -import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage -import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService -import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert -import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort @@ -25,6 +23,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.SignedNetworkMap import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.internal.createNodeInfoAndSigned import org.bouncycastle.asn1.x500.X500Name import org.junit.Rule import org.junit.Test @@ -41,31 +40,17 @@ class NodeInfoWebServiceTest { @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey) - private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) - private val testNetwotkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis()) + @Test fun `submit nodeInfo`() { // Create node info. - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) - // Create digital signature. - val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, nodeInfo.serialize().bytes)) - - val nodeInfoStorage: NodeInfoStorage = mock { - on { getCertificatePath(any()) }.thenReturn(certPath) - } - - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), mock(), testNetwotkMapConfig)).use { it.start() val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish") - val nodeInfoAndSignature = SignedNodeInfo(nodeInfo.serialize(), listOf(digitalSignature)).serialize().bytes + val nodeInfoAndSignature = signedNodeInfo.serialize().bytes // Post node info and signature to doorman, this should pass without any exception. doPost(registerURL, nodeInfoAndSignature) } @@ -73,6 +58,11 @@ class NodeInfoWebServiceTest { @Test fun `get network map`() { + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey) + val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) + val networkMap = NetworkMap(listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256()), SecureHash.randomSHA256()) val serializedNetworkMap = networkMap.serialize() val networkMapStorage: NetworkMapStorage = mock { @@ -89,16 +79,12 @@ class NodeInfoWebServiceTest { @Test fun `get node info`() { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) - val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) val nodeInfoHash = nodeInfo.serialize().sha256() val nodeInfoStorage: NodeInfoStorage = mock { - val serializedNodeInfo = nodeInfo.serialize() - on { getNodeInfo(nodeInfoHash) }.thenReturn(SignedNodeInfo(serializedNodeInfo, listOf(keyPair.sign(serializedNodeInfo)))) + on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) } NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt similarity index 53% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt index 65b60433c7..97927e54db 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt @@ -13,42 +13,54 @@ import org.slf4j.LoggerFactory import java.nio.file.Path import java.security.cert.X509Certificate -object ServiceIdentityGenerator { +object IdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) + const val NODE_IDENTITY_ALIAS_PREFIX = "identity" + const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" + + fun generateNodeIdentity(dir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party { + return generateToDisk(listOf(dir), legalName, NODE_IDENTITY_ALIAS_PREFIX, threshold = 1, customRootCert = customRootCert) + } + + fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party { + return generateToDisk(dirs, notaryName, DISTRIBUTED_NOTARY_ALIAS_PREFIX, threshold, customRootCert) + } + /** * Generates signing key pairs and a common distributed service identity for a set of nodes. * The key pairs and the group identity get serialized to disk in the corresponding node directories. * This method should be called *before* any of the nodes are started. * * @param dirs List of node directories to place the generated identity and key pairs in. - * @param serviceName The legal name of the distributed service. + * @param name The name of the identity. * @param threshold The threshold for the generated group [CompositeKey]. - * @param customRootCert the certificate to use a Corda root CA. If not specified the one in - * certificates/cordadevcakeys.jks is used. + * @param customRootCert the certificate to use as the Corda root CA. If not specified the one in + * internal/certificates/cordadevcakeys.jks is used. */ - fun generateToDisk(dirs: List, - serviceName: CordaX500Name, - serviceId: String, - threshold: Int = 1, - customRootCert: X509Certificate? = null): Party { - log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } + private fun generateToDisk(dirs: List, + name: CordaX500Name, + aliasPrefix: String, + threshold: Int, + customRootCert: X509Certificate?): Party { + log.trace { "Generating identity \"$name\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } - val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) + val key = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) keyPairs.zip(dirs) { keyPair, dir -> - val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, serviceName, keyPair.public) - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, serviceName, notaryKey) + val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, keyPair.public) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, key) val certPath = (dir / "certificates").createDirectories() / "distributedService.jks" val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass") - keystore.setCertificateEntry("$serviceId-composite-key", compositeKeyCert.cert) - keystore.setKeyEntry("$serviceId-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) + keystore.setCertificateEntry("$aliasPrefix-composite-key", compositeKeyCert.cert) + keystore.setKeyEntry("$aliasPrefix-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) keystore.save(certPath, "cordacadevpass") } - return Party(serviceName, notaryKey) + + return Party(name, key) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 70617daf76..563ddaa9f0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -7,7 +7,8 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.internal.CertRole import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert -import net.corda.core.internal.read +import net.corda.core.internal.reader +import net.corda.core.internal.writer import net.corda.core.internal.x500Name import net.corda.core.utilities.days import net.corda.core.utilities.millis @@ -48,8 +49,6 @@ object X509Utilities { const val CORDA_CLIENT_TLS = "cordaclienttls" const val CORDA_CLIENT_CA = "cordaclientca" - const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate" - private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days) /** @@ -162,7 +161,7 @@ object X509Utilities { */ @JvmStatic fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, file: Path) { - JcaPEMWriter(file.toFile().writer()).use { + JcaPEMWriter(file.writer()).use { it.writeObject(x509Certificate) } } @@ -174,9 +173,8 @@ object X509Utilities { */ @JvmStatic fun loadCertificateFromPEMFile(file: Path): X509Certificate { - return file.read { - val reader = PemReader(it.reader()) - val pemObject = reader.readPemObject() + return file.reader().use { + val pemObject = PemReader(it).readPemObject() val certHolder = X509CertificateHolder(pemObject.content) certHolder.isValidOn(Date()) certHolder.cert 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 aaf2cb5e04..28a04fcbd7 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 @@ -13,7 +13,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div -import net.corda.core.node.services.NotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort @@ -24,7 +23,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.IntegrationTest @@ -68,10 +67,9 @@ class BFTNotaryServiceTests : IntegrationTest() { (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - notary = ServiceIdentityGenerator.generateToDisk( + notary = IdentityGenerator.generateDistributedNotaryIdentity( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, - CordaX500Name("BFT", "Zurich", "CH"), - NotaryService.constructId(validating = false, bft = true)) + CordaX500Name("BFT", "Zurich", "CH")) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notary, false)))) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt index f1f467d24e..e36f7b35d0 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NotaryConfig -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.* @@ -49,11 +49,7 @@ class MySQLNotaryServiceTests : IntegrationTest() { @Before fun before() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) - notaryParty = ServiceIdentityGenerator.generateToDisk( - listOf(mockNet.baseDirectory(mockNet.nextNodeId)), - notaryName, - "identity" - ) + notaryParty = IdentityGenerator.generateNodeIdentity(mockNet.baseDirectory(mockNet.nextNodeId), notaryName) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryParty, false)))) val notaryNodeUnstarted = createNotaryNode() val nodeUnstarted = mockNet.createUnstartedNode() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 00f6124301..8110118362 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.concurrent.map import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.driver.NodeHandle @@ -31,15 +30,15 @@ class RaftNotaryServiceTests : IntegrationTest() { val databaseSchemas = IntegrationTestSchemas( "RAFTNotaryService_0", "RAFTNotaryService_1", "RAFTNotaryService_2", DUMMY_BANK_A_NAME.toDatabaseSchemaName()) } - private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") + private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB") @Test fun `detect double spend`() { driver( startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), - notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))) - { + notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3))) + ) { val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as NodeHandle.InProcess).node }.getOrThrow() val inputState = issueState(bankA, defaultNotaryIdentity) 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 5dfa2963b1..189506d02b 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 @@ -19,6 +19,9 @@ import net.corda.node.services.messaging.ReceivedMessage import net.corda.node.services.messaging.send import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* +import net.corda.node.services.messaging.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.chooseIdentity import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver @@ -38,7 +41,7 @@ class P2PMessagingTest : IntegrationTest() { @ClassRule @JvmField val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), "DistributedService_0", "DistributedService_1") - val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.id, "DistributedService", "London", "GB") + val DISTRIBUTED_SERVICE_NAME = CordaX500Name("DistributedService", "London", "GB") } @Test 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 b25bb1df9d..2a1b1292f1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -60,8 +60,12 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.crypto.KeyStoreWrapper +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -137,25 +141,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val services: ServiceHubInternal get() = _services private lateinit var _services: ServiceHubInternalImpl protected var myNotaryIdentity: PartyAndCertificate? = null - protected lateinit var checkpointStorage: CheckpointStorage + private lateinit var checkpointStorage: CheckpointStorage private lateinit var tokenizableServices: List protected lateinit var attachments: NodeAttachmentService protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() - protected val _nodeReadyFuture = openFuture() + private val _nodeReadyFuture = openFuture() protected var networkMapClient: NetworkMapClient? = null lateinit var securityManager: RPCSecurityManager get /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ - val nodeReadyFuture: CordaFuture - get() = _nodeReadyFuture - /** A [CordaX500Name] with null common name. */ - protected val myLegalName: CordaX500Name by lazy { - val cert = loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_CA) - CordaX500Name.build(cert.subjectX500Principal).copy(commonName = null) - } + val nodeReadyFuture: CordaFuture get() = _nodeReadyFuture open val serializationWhitelists: List by lazy { cordappLoader.cordapps.flatMap { it.serializationWhitelists } @@ -330,7 +328,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ) // Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with // a different timestamp. - networkMapCache.getNodesByLegalName(myLegalName).firstOrNull()?.let { + networkMapCache.getNodesByLegalName(configuration.myLegalName).firstOrNull()?.let { if (info.copy(serial = it.serial) == it) { info = it } @@ -756,13 +754,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { // Node's main identity or if it's a single node notary - Pair("identity", myLegalName) + Pair(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) } else { - val notaryId = notaryConfig.run { - NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom, mysql != null) - } // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(notaryId, null) + Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" @@ -770,7 +765,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, if (!keyStore.containsAlias(privateKeyAlias)) { singleName ?: throw IllegalArgumentException( "Unable to find in the key store the identity of the distributed notary ($id) the node is part of") - // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. + // TODO: Remove use of [IdentityGenerator.generateToDisk]. log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair()) } 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 7471d5d5ec..ed6028f13c 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 @@ -69,7 +69,7 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) - // Move distributed service composite key (generated by ServiceIdentityGenerator.generateToDisk) to keystore if exists. + // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" if (distributedServiceKeystore.exists()) { val serviceKeystore = loadKeyStore(distributedServiceKeystore, "cordacadevpass") @@ -111,18 +111,17 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path, val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword) val clientKey = Crypto.generateKeyPair(signatureScheme) - val clientName = legalName.copy(commonName = null) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, clientName.x500Name))), arrayOf()) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKeyPair, - clientName.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + legalName, clientKey.public, nameConstraints = nameConstraints) val tlsKey = Crypto.generateKeyPair(signatureScheme) - val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, clientName, tlsKey.public) + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) val keyPass = keyPassword.toCharArray() diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index bac9ef6902..4654473ef1 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -24,6 +24,7 @@ import javax.annotation.concurrent.ThreadSafe * * @param identities initial set of identities for the service, typically only used for unit tests. */ +// TODO There is duplicated logic between this and PersistentIdentityService @ThreadSafe class InMemoryIdentityService(identities: Array, trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal { diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 6dd98d0c63..83e7b0b267 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -26,6 +26,7 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Lob +// TODO There is duplicated logic between this and InMemoryIdentityService @ThreadSafe class PersistentIdentityService(override val trustRoot: X509Certificate, vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityServiceInternal { 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 780c4815c6..9f6ffe1d08 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 @@ -34,12 +34,13 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ -class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, - override val notaryIdentityKey: PublicKey, - private val bftSMaRtConfig: BFTSMaRtConfiguration, - cluster: BFTSMaRt.Cluster) : NotaryService() { +class BFTNonValidatingNotaryService( + override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + private val bftSMaRtConfig: BFTSMaRtConfiguration, + cluster: BFTSMaRt.Cluster +) : NotaryService() { companion object { - val id = constructId(validating = false, bft = true) private val log = contextLogger() } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt index 0bf810b2ab..adfe3e9744 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt @@ -35,9 +35,6 @@ class MySQLNonValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, dataSourceProperties: Properties, devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { - companion object { - val id = constructId(validating = false, mysql = true) - } override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this) } @@ -45,8 +42,5 @@ class MySQLValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, dataSourceProperties: Properties, devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { - companion object { - val id = constructId(validating = true, mysql = true) - } override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ValidatingNotaryFlow(otherPartySession, this) } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 1433e71f85..e672380398 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -8,14 +8,13 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider) : TrustedAuthorityNotaryService() { - companion object { - val id = constructId(validating = false, raft = true) - } - +class RaftNonValidatingNotaryService( + override val services: ServiceHub, + override val notaryIdentityKey: PublicKey, + override val uniquenessProvider: RaftUniquenessProvider +) : TrustedAuthorityNotaryService() { override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { return NonValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 8fd3448512..3e4899ae0a 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -8,14 +8,13 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider) : TrustedAuthorityNotaryService() { - companion object { - val id = constructId(validating = true, raft = true) - } - +class RaftValidatingNotaryService( + override val services: ServiceHub, + override val notaryIdentityKey: PublicKey, + override val uniquenessProvider: RaftUniquenessProvider +) : TrustedAuthorityNotaryService() { override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { return ValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 6c7e36046b..5e687c3b6d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -9,10 +9,8 @@ import java.security.PublicKey /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - companion object { - val id = constructId(validating = true) - } override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this) 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 f24cf56f11..698420ca0a 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 @@ -103,10 +103,9 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v println("Generating SSL certificate for node messaging service.") val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA).toX509CertHolder() - val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, CordaX500Name.build(caCert.cert.subjectX500Principal).copy(commonName = null), sslKey.public) + val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, CordaX500Name.build(caCert.cert.subjectX500Principal), sslKey.public) val sslKeyStore = loadOrCreateKeyStore(config.sslKeystore, keystorePassword) - sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), - arrayOf(sslCert.cert, *certificates)) + sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), arrayOf(sslCert.cert, *certificates)) sslKeyStore.save(config.sslKeystore, config.keyStorePassword) println("SSL private key and certificate stored in ${config.sslKeystore}.") // All done, clean up temp files. diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 1fe97568a8..2006c397bc 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -656,7 +656,7 @@ class FlowFrameworkTests { private inline fun > StartedNode.restartAndGetRestoredFlow() = internals.run { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(MockNodeParameters(id)) + val newNode = mockNet.createNode(MockNodeParameters(id, configuration.myLegalName)) newNode.internals.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 50922e9fde..209153fdb8 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -1,5 +1,7 @@ package net.corda.node.utilities.registration +import com.google.common.jimfs.Configuration.unix +import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.eq @@ -7,68 +9,61 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.* +import net.corda.core.internal.cert +import net.corda.core.internal.createDirectories import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.* -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.getX509Certificate -import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.ALICE_NAME import net.corda.testing.internal.rigorousMock +import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TemporaryFolder import java.security.cert.Certificate -import kotlin.test.assertEquals +import java.security.cert.X509Certificate import kotlin.test.assertFalse import kotlin.test.assertTrue class NetworkRegistrationHelperTest { - @Rule - @JvmField - val tempFolder = TemporaryFolder() - + private val fs = Jimfs.newFileSystem(unix()) private val requestId = SecureHash.randomSHA256().toString() + private val nodeLegalName = ALICE_NAME + private val intermediateCaName = CordaX500Name("CORDA_INTERMEDIATE_CA", "R3 Ltd", "London", "GB") + private val rootCaName = CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB") + private val nodeCaCert = createCaCert(nodeLegalName) + private val intermediateCaCert = createCaCert(intermediateCaName) + private val rootCaCert = createCaCert(rootCaName) + private lateinit var config: NodeConfiguration - private val identities = listOf("CORDA_CLIENT_CA", - "CORDA_INTERMEDIATE_CA", - "CORDA_ROOT_CA") - .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } - private val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } - .map { it.cert }.toTypedArray() - - private val certService = mockRegistrationResponse(*certs) - @Before fun init() { + val baseDirectory = fs.getPath("/baseDir").createDirectories() abstract class AbstractNodeConfiguration : NodeConfiguration config = rigorousMock().also { - doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn(nodeLegalName).whenever(it).myLegalName doReturn("").whenever(it).emailAddress } } + @After + fun cleanUp() { + fs.close() + } + @Test fun `successful registration`() { - assertFalse(config.nodeKeystore.exists()) - assertFalse(config.sslKeystore.exists()) - config.trustStoreFile.parent.createDirectories() - loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { - it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certs.last()) - it.save(config.trustStoreFile, config.trustStorePassword) - } + assertThat(config.nodeKeystore).doesNotExist() + assertThat(config.sslKeystore).doesNotExist() + assertThat(config.trustStoreFile).doesNotExist() - NetworkRegistrationHelper(config, certService).buildKeystore() + saveTrustStoreWithRootCa(rootCaCert) - assertTrue(config.nodeKeystore.exists()) - assertTrue(config.sslKeystore.exists()) - assertTrue(config.trustStoreFile.exists()) + createRegistrationHelper().buildKeystore() val nodeKeystore = loadKeyStore(config.nodeKeystore, config.keyStorePassword) val sslKeystore = loadKeyStore(config.sslKeystore, config.keyStorePassword) @@ -79,9 +74,8 @@ class NetworkRegistrationHelperTest { assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) 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 { it.toX509CertHolder().subject.commonName }) + val nodeCaCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + assertThat(nodeCaCertChain).containsExactly(nodeCaCert, intermediateCaCert, rootCaCert) } sslKeystore.run { @@ -89,46 +83,55 @@ class NetworkRegistrationHelperTest { 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(CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB").x500Name) + identities.map { it.x500Name }, - certificateChain.map { it.toX509CertHolder().subject }) - assertEquals(CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB").x500Principal, - getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal) + val nodeTlsCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + assertThat(nodeTlsCertChain).hasSize(4) + // The TLS cert has the same subject as the node CA cert + assertThat(CordaX500Name.build((nodeTlsCertChain[0] as X509Certificate).subjectX500Principal)).isEqualTo(nodeLegalName) + assertThat(nodeTlsCertChain.drop(1)).containsExactly(nodeCaCert, intermediateCaCert, rootCaCert) } trustStore.run { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA)) + val trustStoreRootCaCert = getCertificate(X509Utilities.CORDA_ROOT_CA) + assertThat(trustStoreRootCaCert).isEqualTo(rootCaCert) } } @Test fun `missing truststore`() { assertThatThrownBy { - NetworkRegistrationHelper(config, certService).buildKeystore() + createRegistrationHelper() }.hasMessageContaining("This file must contain the root CA cert of your compatibility zone. Please contact your CZ operator.") - .isInstanceOf(IllegalArgumentException::class.java) } @Test fun `wrong root cert in truststore`() { - val someCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert - config.trustStoreFile.parent.createDirectories() - loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { - it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, someCert) - it.save(config.trustStoreFile, config.trustStorePassword) - } + saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB"))) + val registrationHelper = createRegistrationHelper() assertThatThrownBy { - NetworkRegistrationHelper(config, certService).buildKeystore() + registrationHelper.buildKeystore() }.isInstanceOf(WrongRootCertException::class.java) } - private fun mockRegistrationResponse(vararg response: Certificate): NetworkRegistrationService { - return rigorousMock().also { + private fun createRegistrationHelper(): NetworkRegistrationHelper { + val certService = rigorousMock().also { doReturn(requestId).whenever(it).submitRequest(any()) - doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) + doReturn(arrayOf(nodeCaCert, intermediateCaCert, rootCaCert)).whenever(it).retrieveCertificates(eq(requestId)) + } + return NetworkRegistrationHelper(config, certService) + } + + private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) { + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa) + it.save(config.trustStoreFile, config.trustStorePassword) } } + + private fun createCaCert(name: CordaX500Name): X509Certificate { + return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert + } } 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 index eb11a9b443..311a5c51b2 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -4,13 +4,11 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name -import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -24,7 +22,7 @@ private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. class BFTNotaryCordform : CordformDefinition() { - private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") + private val clusterName = CordaX500Name("BFT", "Zurich", "CH") init { nodesDirectory = Paths.get("build", "nodes", "nodesBFT") @@ -64,10 +62,10 @@ class BFTNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk( + IdentityGenerator.generateDistributedNotaryIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName, - NotaryService.constructId(validating = false, bft = true), - minCorrectReplicas(clusterSize)) + minCorrectReplicas(clusterSize) + ) } } 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 index cfc21fa060..59384a412f 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -8,8 +8,7 @@ import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.RaftConfig -import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -24,7 +23,7 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. class RaftNotaryCordform : CordformDefinition() { - private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") + private val clusterName = CordaX500Name("Raft", "Zurich", "CH") init { nodesDirectory = Paths.get("build", "nodes", "nodesRaft") @@ -60,9 +59,9 @@ class RaftNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk( + IdentityGenerator.generateDistributedNotaryIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, - clusterName, - NotaryService.constructId(validating = true, raft = true)) + clusterName + ) } } diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index c50a027047..a13ec48018 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -29,7 +29,7 @@ dependencies { compile "net.corda.plugins:cordform-common:$gradle_plugins_version" // Integration test helpers - integrationTestCompile "org.assertj:assertj-core:${assertj_version}" + testCompile "org.assertj:assertj-core:$assertj_version" integrationTestCompile "junit:junit:$junit_version" // Jetty dependencies for NetworkMapClient test. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 34cd93d367..84fe07acc5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectory import net.corda.core.internal.uncheckedCast @@ -37,7 +38,7 @@ import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo @@ -125,14 +126,14 @@ data class MockNodeArgs( * By default a single notary node is automatically started, which forms part of the network parameters for all the nodes. * This node is available by calling [defaultNotaryNode]. */ -class MockNetwork(private val cordappPackages: List, - defaultParameters: MockNetworkParameters = MockNetworkParameters(), - private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, - private val threadPerNode: Boolean = defaultParameters.threadPerNode, - servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - private val defaultFactory: (MockNodeArgs) -> MockNode = defaultParameters.defaultFactory, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - private val notarySpecs: List = defaultParameters.notarySpecs) { +open class MockNetwork(private val cordappPackages: List, + defaultParameters: MockNetworkParameters = MockNetworkParameters(), + private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, + private val threadPerNode: Boolean = defaultParameters.threadPerNode, + servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, + private val defaultFactory: (MockNodeArgs) -> MockNode = defaultParameters.defaultFactory, + initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, + private val notarySpecs: List = defaultParameters.notarySpecs) { /** Helper constructor for creating a [MockNetwork] with custom parameters from Java. */ @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) @@ -141,7 +142,7 @@ class MockNetwork(private val cordappPackages: List, // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // This SFTP support loads BouncyCastle, which we want to avoid. // Please see https://issues.apache.org/jira/browse/SSHD-736 - it's easier then to create our own fork of SSHD - SecurityUtils.setAPrioriDisabledProvider("BC", true) + SecurityUtils.setAPrioriDisabledProvider("BC", true) // XXX: Why isn't this static? } var nextNodeId = 0 @@ -159,6 +160,7 @@ class MockNetwork(private val cordappPackages: List, throw IllegalStateException("Using more than one MockNetwork simultaneously is not supported.", e) } private val sharedUserCount = AtomicInteger(0) + /** A read only view of the current set of nodes. */ val nodes: List get() = _nodes @@ -172,32 +174,29 @@ class MockNetwork(private val cordappPackages: List, * Returns the single notary node on the network. Throws if there are none or more than one. * @see notaryNodes */ - val defaultNotaryNode: StartedNode - get() { - return when (notaryNodes.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryNodes[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") - } + val defaultNotaryNode: StartedNode get() { + return when (notaryNodes.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryNodes[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") } + } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentity: Party - get() { - return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentity: Party get() { + return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentityAndCert: PartyAndCertificate - get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentityAndCert: PartyAndCertificate get() { + return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Because this executor is shared, we need to be careful about nodes shutting it down. @@ -222,27 +221,31 @@ class MockNetwork(private val cordappPackages: List, } init { - filesystem.getPath("/nodes").createDirectory() - val notaryInfos = generateNotaryIdentities() - // The network parameters must be serialised before starting any of the nodes - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) - notaryNodes = createNotaries() + try { + filesystem.getPath("/nodes").createDirectory() + val notaryInfos = generateNotaryIdentities() + // The network parameters must be serialised before starting any of the nodes + networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + @Suppress("LeakingThis") + notaryNodes = createNotaries() + } catch (t: Throwable) { + stopNodes() + throw t + } } private fun generateNotaryIdentities(): List { return notarySpecs.mapIndexed { index, (name, validating) -> - val identity = ServiceIdentityGenerator.generateToDisk( - dirs = listOf(baseDirectory(nextNodeId + index)), - serviceName = name, - serviceId = "identity") + val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name) NotaryInfo(identity, validating) } } - private fun createNotaries(): List> { - return notarySpecs.map { spec -> - createNode(MockNodeParameters(legalName = spec.name, configOverrides = { - doReturn(NotaryConfig(spec.validating)).whenever(it).notary + @VisibleForTesting + internal open fun createNotaries(): List> { + return notarySpecs.map { (name, validating) -> + createNode(MockNodeParameters(legalName = name, configOverrides = { + doReturn(NotaryConfig(validating)).whenever(it).notary })) } } @@ -299,7 +302,7 @@ class MockNetwork(private val cordappPackages: List, id, serverThread, myNotaryIdentity, - myLegalName, + configuration.myLegalName, database).also { runOnStop += it::stop } } @@ -457,8 +460,11 @@ class MockNetwork(private val cordappPackages: List, } fun stopNodes() { - nodes.forEach { it.started?.dispose() } - serializationEnv.unset() + try { + nodes.forEach { it.started?.dispose() } + } finally { + serializationEnv.unset() // Must execute even if other parts of this method fail. + } messagingNetwork.stop() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 308ac4fe5a..5833840bb4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -19,7 +19,6 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.NotaryService import net.corda.core.toFuture import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger @@ -30,12 +29,9 @@ import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions import net.corda.node.services.config.* -import net.corda.node.services.transactions.BFTNonValidatingNotaryService -import net.corda.node.services.transactions.RaftNonValidatingNotaryService -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -245,9 +241,9 @@ class DriverDSLImpl( } private enum class ClusterType(val validating: Boolean, val clusterName: CordaX500Name) { - VALIDATING_RAFT(true, CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH")), - NON_VALIDATING_RAFT(false, CordaX500Name(RaftNonValidatingNotaryService.id, "Raft", "Zurich", "CH")), - NON_VALIDATING_BFT(false, CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH")) + VALIDATING_RAFT(true, CordaX500Name("Raft", "Zurich", "CH")), + NON_VALIDATING_RAFT(false, CordaX500Name("Raft", "Zurich", "CH")), + NON_VALIDATING_BFT(false, CordaX500Name("BFT", "Zurich", "CH")) } internal fun startCordformNodes(cordforms: List): CordaFuture<*> { @@ -270,24 +266,15 @@ class DriverDSLImpl( clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name) } else { // We have all we need here to generate the identity for single node notaries - val identity = ServiceIdentityGenerator.generateToDisk( - dirs = listOf(baseDirectory(name)), - serviceName = name, - serviceId = "identity" - ) + val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(name), legalName = name) notaryInfos += NotaryInfo(identity, notaryConfig.validating) } } clusterNodes.asMap().forEach { type, nodeNames -> - val identity = ServiceIdentityGenerator.generateToDisk( + val identity = IdentityGenerator.generateDistributedNotaryIdentity( dirs = nodeNames.map { baseDirectory(it) }, - serviceName = type.clusterName, - serviceId = NotaryService.constructId( - validating = type.validating, - raft = type in setOf(VALIDATING_RAFT, NON_VALIDATING_RAFT), - bft = type == ClusterType.NON_VALIDATING_BFT - ) + notaryName = type.clusterName ) notaryInfos += NotaryInfo(identity, type.validating) } @@ -369,20 +356,11 @@ class DriverDSLImpl( private fun generateNotaryIdentities(): List { return notarySpecs.map { spec -> val identity = if (spec.cluster == null) { - ServiceIdentityGenerator.generateToDisk( - dirs = listOf(baseDirectory(spec.name)), - serviceName = spec.name, - serviceId = "identity", - customRootCert = compatibilityZone?.rootCert - ) + IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) } else { - ServiceIdentityGenerator.generateToDisk( + IdentityGenerator.generateDistributedNotaryIdentity( dirs = generateNodeNames(spec).map { baseDirectory(it) }, - serviceName = spec.name, - serviceId = NotaryService.constructId( - validating = spec.validating, - raft = spec.cluster is ClusterSpec.Raft - ), + notaryName = spec.name, customRootCert = compatibilityZone?.rootCert ) } diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt new file mode 100644 index 0000000000..10d9358243 --- /dev/null +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt @@ -0,0 +1,18 @@ +package net.corda.testing.node + +import net.corda.core.serialization.internal.effectiveSerializationEnv +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + +class MockNetworkTests { + @Test + fun `does not leak serialization env if init fails`() { + val e = Exception("didn't work") + assertThatThrownBy { + object : MockNetwork(emptyList(), initialiseSerialization = true) { + override fun createNotaries() = throw e + } + }.isSameAs(e) + assertThatThrownBy { effectiveSerializationEnv }.isInstanceOf(IllegalStateException::class.java) + } +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 11e572e2d7..860af415ac 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert +import net.corda.core.internal.unspecifiedCountry import net.corda.core.internal.x500Name import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort @@ -91,13 +92,13 @@ fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT val intermediate: CertificateAndKeyPair = DEV_CA - val nodeCaName = party.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN) + val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val nodeCaCert = X509Utilities.createCertificate( CertificateType.NODE_CA, intermediate.certificate, intermediate.keyPair, - nodeCaName, + party.name, nodeCaKeyPair.public, nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf())) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 37e019b367..ee6b38fdb5 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -13,7 +13,7 @@ import net.corda.demobench.pty.R3Pty import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo -import net.corda.nodeapi.internal.ServiceIdentityGenerator +import net.corda.nodeapi.internal.IdentityGenerator import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory @@ -153,10 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Generate notary identity and save it into node's directory. This identity will be used in network parameters. private fun getNotaryIdentity(config: NodeConfigWrapper): Party { - return ServiceIdentityGenerator.generateToDisk( - dirs = listOf(config.nodeDir), - serviceName = config.nodeConfig.myLegalName, - serviceId = "identity") + return IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) } fun reset() { From c0e997c1dd9159d797d5f9d7edeb8bf067cb7184 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 20 Dec 2017 12:01:32 +0000 Subject: [PATCH 3/6] Wave 3 of Business Network changes. (#193) * R3NET-546: Re-arrange independent flows into separate packages. Functionally this is a NOP change. * R3NET-546: Start BNO as a separate Corda node and improve GUI experience for IOU. * R3NET-546: Move all the membership checks to the Business Network Owner node side, creating "InitiatedBy" flows as necessary. * R3NET-546: Make MembershipViolationException AMQP serializable. * R3NET-546: Improve GUI error reporting in case of membership violation. * R3NET-546: Code changes following review by: @shamsasari * R3NET-546: Code changes following review by: @shamsasari * R3NET-546: Added a dedicated InvalidMembershipListNameException. --- .../{Contract.kt => iou/IOUContract.kt} | 2 +- .../{Flow.kt => iou/IOUFlow.kt} | 12 +++--- .../IOUFlowResponder.kt} | 10 ++--- .../{State.kt => iou/IOUState.kt} | 2 +- .../membership/CheckMembershipFlow.kt | 38 ----------------- .../membership/MembershipAware.kt | 26 ------------ .../ObtainMembershipListContentFlow.kt | 16 ------- .../membership/flow/CheckMembershipFlow.kt | 42 +++++++++++++++++++ .../membership/flow/MembershipAware.kt | 32 ++++++++++++++ .../membership/{ => flow}/MembershipList.kt | 2 +- .../flow/ObtainMembershipListContentFlow.kt | 34 +++++++++++++++ .../membership/internal/CsvMembershipList.kt | 2 +- .../internal/MembershipListProvider.kt | 4 +- .../net/corda/explorer/ExplorerSimulation.kt | 18 ++++++-- .../explorer/model/MembershipListModel.kt | 4 +- .../net/corda/explorer/views/Network.kt | 5 ++- .../corda/explorer/views/TransactionViewer.kt | 2 +- .../explorer/views/cordapps/iou/IOUViewer.kt | 30 +++++++++++-- .../views/cordapps/iou/NewTransaction.kt | 12 +++++- 19 files changed, 181 insertions(+), 112 deletions(-) rename samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/{Contract.kt => iou/IOUContract.kt} (96%) rename samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/{Flow.kt => iou/IOUFlow.kt} (88%) rename samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/{FlowResponder.kt => iou/IOUFlowResponder.kt} (72%) rename samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/{State.kt => iou/IOUState.kt} (85%) delete mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt delete mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt delete mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt create mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/CheckMembershipFlow.kt create mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipAware.kt rename samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/{ => flow}/MembershipList.kt (90%) create mode 100644 samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/ObtainMembershipListContentFlow.kt diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Contract.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUContract.kt similarity index 96% rename from samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Contract.kt rename to samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUContract.kt index 3bafcf3a03..b4f880d36f 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Contract.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUContract.kt @@ -1,4 +1,4 @@ -package net.corda.sample.businessnetwork +package net.corda.sample.businessnetwork.iou import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Flow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlow.kt similarity index 88% rename from samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Flow.kt rename to samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlow.kt index 068a8f9161..8c8765b24e 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/Flow.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlow.kt @@ -1,4 +1,4 @@ -package net.corda.sample.businessnetwork +package net.corda.sample.businessnetwork.iou import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command @@ -9,19 +9,19 @@ import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker -import net.corda.sample.businessnetwork.membership.MembershipAware -import net.corda.sample.businessnetwork.membership.CheckMembershipFlow -import net.corda.sample.businessnetwork.membership.CheckMembershipResult +import net.corda.sample.businessnetwork.membership.flow.CheckMembershipFlow +import net.corda.sample.businessnetwork.membership.flow.CheckMembershipResult import kotlin.reflect.jvm.jvmName @InitiatingFlow @StartableByRPC class IOUFlow(val iouValue: Int, - val otherParty: Party) : FlowLogic(), MembershipAware { + val otherParty: Party) : FlowLogic() { companion object { + // TODO: Derive membership name from CorDapp config. val allowedMembershipName = - CordaX500Name("AliceBobMembershipList", "AliceBob", "Washington", "US") + CordaX500Name("AliceBobMembershipList", "Oslo", "NO") } /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/FlowResponder.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlowResponder.kt similarity index 72% rename from samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/FlowResponder.kt rename to samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlowResponder.kt index b9e6a2ac06..8c59fcbecc 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/FlowResponder.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUFlowResponder.kt @@ -1,4 +1,4 @@ -package net.corda.sample.businessnetwork +package net.corda.sample.businessnetwork.iou import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.requireThat @@ -7,14 +7,14 @@ import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy import net.corda.core.flows.SignTransactionFlow import net.corda.core.transactions.SignedTransaction -import net.corda.sample.businessnetwork.membership.MembershipAware +import net.corda.sample.businessnetwork.membership.flow.CheckMembershipFlow +import net.corda.sample.businessnetwork.membership.flow.CheckMembershipResult @InitiatedBy(IOUFlow::class) -class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic(), MembershipAware { +class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - - otherPartySession.counterparty.checkMembership(IOUFlow.allowedMembershipName, this) + check(subFlow(CheckMembershipFlow(otherPartySession.counterparty, IOUFlow.allowedMembershipName)) == CheckMembershipResult.PASS) subFlow(object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/State.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUState.kt similarity index 85% rename from samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/State.kt rename to samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUState.kt index 9cd8b888e4..b5add03b77 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/State.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/iou/IOUState.kt @@ -1,4 +1,4 @@ -package net.corda.sample.businessnetwork +package net.corda.sample.businessnetwork.iou import net.corda.core.contracts.ContractState import net.corda.core.identity.Party diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt deleted file mode 100644 index 0516a54628..0000000000 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.corda.sample.businessnetwork.membership - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.unwrap - -@CordaSerializable -enum class CheckMembershipResult { - PASS, - FAIL -} - -@InitiatingFlow -class CheckMembershipFlow(private val otherParty: Party, private val membershipName: CordaX500Name) : FlowLogic(), MembershipAware { - @Suspendable - override fun call(): CheckMembershipResult { - otherParty.checkMembership(membershipName, this) - // This will trigger CounterpartyCheckMembershipFlow - val untrustworthyData = initiateFlow(otherParty).sendAndReceive(membershipName) - return untrustworthyData.unwrap { it } - } -} - -@InitiatedBy(CheckMembershipFlow::class) -class CounterpartyCheckMembershipFlow(private val otherPartySession: FlowSession) : FlowLogic(), MembershipAware { - @Suspendable - override fun call() { - val membershipName = otherPartySession.receive().unwrap { it } - otherPartySession.counterparty.checkMembership(membershipName, this) - otherPartySession.send(CheckMembershipResult.PASS) - } -} \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt deleted file mode 100644 index dd436b6d68..0000000000 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.sample.businessnetwork.membership - -import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.node.ServiceHub -import net.corda.sample.businessnetwork.membership.internal.MembershipListProvider - -interface MembershipAware { - /** - * Checks that party has at least one common membership list with current node. - * TODO: This functionality ought to be moved into a dedicated CordaService. - */ - fun AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic) { - val membershipList = getMembershipList(membershipName, initiatorFlow.serviceHub) - if (this !in membershipList) { - val msg = "'$this' doesn't belong to membership list: ${membershipName.commonName}" - throw MembershipViolationException(msg) - } - } - - fun getMembershipList(listName: CordaX500Name, serviceHub: ServiceHub): MembershipList = MembershipListProvider.obtainMembershipList(listName, serviceHub.networkMapCache) -} - -class MembershipViolationException(msg: String) : FlowException(msg) \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt deleted file mode 100644 index d44e3865fc..0000000000 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.sample.businessnetwork.membership - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name - -/** - * Flow to obtain content of the membership lists this node belongs to. - */ -@StartableByRPC -class ObtainMembershipListContentFlow(private val membershipListName: CordaX500Name) : FlowLogic>(), MembershipAware { - @Suspendable - override fun call(): Set = getMembershipList(membershipListName, serviceHub).content() -} \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/CheckMembershipFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/CheckMembershipFlow.kt new file mode 100644 index 0000000000..6f21a054d6 --- /dev/null +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/CheckMembershipFlow.kt @@ -0,0 +1,42 @@ +package net.corda.sample.businessnetwork.membership.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.unwrap + +@CordaSerializable +enum class CheckMembershipResult { + PASS, + FAIL +} + +@InitiatingFlow +class CheckMembershipFlow(private val otherParty: Party, private val membershipName: CordaX500Name) : FlowLogic() { + @Suspendable + override fun call(): CheckMembershipResult { + val bnoParty = serviceHub.networkMapCache.getPeerByLegalName(membershipName) + return if (bnoParty != null) { + // This will trigger CounterpartyCheckMembershipFlow + val untrustworthyData = initiateFlow(bnoParty).sendAndReceive(otherParty) + untrustworthyData.unwrap { it } + } else { + throw InvalidMembershipListNameException(membershipName) + } + } +} + +@InitiatedBy(CheckMembershipFlow::class) +class OwnerSideCheckMembershipFlow(private val initiatingPartySession: FlowSession) : FlowLogic(), MembershipAware { + @Suspendable + override fun call() { + val partyToCheck = initiatingPartySession.receive().unwrap { it } + partyToCheck.checkMembership(ourIdentity.name, this) + initiatingPartySession.send(CheckMembershipResult.PASS) + } +} \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipAware.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipAware.kt new file mode 100644 index 0000000000..b55b4b475b --- /dev/null +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipAware.kt @@ -0,0 +1,32 @@ +package net.corda.sample.businessnetwork.membership.flow + +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.ServiceHub +import net.corda.sample.businessnetwork.membership.internal.MembershipListProvider +import org.slf4j.LoggerFactory + +interface MembershipAware { + /** + * Checks that party is included into the specified membership list. + */ + fun AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic) { + LoggerFactory.getLogger(javaClass).debug("Checking membership of party '${this.nameOrNull()}' in membership list '$membershipName'") + val membershipList = getMembershipList(membershipName, initiatorFlow.serviceHub) + if (this !in membershipList) { + val msg = "'$this' doesn't belong to membership list: ${membershipName.organisation}" + throw MembershipViolationException(msg) + } + } + + fun getMembershipList(listName: CordaX500Name, serviceHub: ServiceHub): MembershipList { + LoggerFactory.getLogger(javaClass).debug("Obtaining membership list for name '$listName'") + return MembershipListProvider.obtainMembershipList(listName, serviceHub.networkMapCache) + } +} + +class MembershipViolationException(val msg: String) : FlowException(msg) + +class InvalidMembershipListNameException(val membershipListName: CordaX500Name) : FlowException("Business Network owner node not found for: $membershipListName") \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipList.kt similarity index 90% rename from samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt rename to samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipList.kt index a8cc101346..fa6253c8e4 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/MembershipList.kt @@ -1,4 +1,4 @@ -package net.corda.sample.businessnetwork.membership +package net.corda.sample.businessnetwork.membership.flow import net.corda.core.identity.AbstractParty diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/ObtainMembershipListContentFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/ObtainMembershipListContentFlow.kt new file mode 100644 index 0000000000..fba113c237 --- /dev/null +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/flow/ObtainMembershipListContentFlow.kt @@ -0,0 +1,34 @@ +package net.corda.sample.businessnetwork.membership.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.unwrap + +/** + * Flow to obtain content of the membership lists this node belongs to. + */ +@StartableByRPC +@InitiatingFlow +class ObtainMembershipListContentFlow(private val membershipListName: CordaX500Name) : FlowLogic>() { + @Suspendable + override fun call(): Set { + val bnoParty = serviceHub.networkMapCache.getPeerByLegalName(membershipListName) ?: + throw InvalidMembershipListNameException(membershipListName) + val untrustworthyData = initiateFlow(bnoParty).receive>() + return untrustworthyData.unwrap { it } + } +} + +@InitiatedBy(ObtainMembershipListContentFlow::class) +class OwnerSideObtainMembershipListContentFlow(private val initiatingPartySession: FlowSession) : FlowLogic(), MembershipAware { + @Suspendable + override fun call() { + // Checking whether the calling party is a member. If not it is not even in position to enquire about membership list content. + initiatingPartySession.counterparty.checkMembership(ourIdentity.name, this) + + val membershipListContent: Set = getMembershipList(ourIdentity.name, serviceHub).content() + initiatingPartySession.send(membershipListContent) + } +} \ No newline at end of file diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt index 5a98412c97..1e1186e552 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt @@ -4,7 +4,7 @@ import com.opencsv.CSVReaderBuilder import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.NetworkMapCache -import net.corda.sample.businessnetwork.membership.MembershipList +import net.corda.sample.businessnetwork.membership.flow.MembershipList import java.io.InputStream /** diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt index b9ad6e1f92..98e215bcd4 100644 --- a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt +++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt @@ -2,9 +2,9 @@ package net.corda.sample.businessnetwork.membership.internal import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.NetworkMapCache -import net.corda.sample.businessnetwork.membership.MembershipList +import net.corda.sample.businessnetwork.membership.flow.MembershipList object MembershipListProvider { fun obtainMembershipList(listName: CordaX500Name, networkMapCache: NetworkMapCache): MembershipList = - CsvMembershipList(MembershipListProvider::class.java.getResourceAsStream("${listName.commonName}.csv"), networkMapCache) + CsvMembershipList(MembershipListProvider::class.java.getResourceAsStream("${listName.organisation}.csv"), networkMapCache) } \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 7c8ef402dc..9bc750a772 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -14,8 +14,8 @@ import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.sample.businessnetwork.IOUFlow -import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow +import net.corda.sample.businessnetwork.iou.IOUFlow +import net.corda.sample.businessnetwork.membership.flow.ObtainMembershipListContentFlow import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash @@ -32,8 +32,14 @@ import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import java.time.Instant import java.util.* +import kotlin.reflect.KClass class ExplorerSimulation(private val options: OptionSet) { + + private companion object { + fun packagesOfClasses(vararg classes: KClass<*>): List = classes.map { it.java.`package`.name } + } + private val user = User("user1", "test", permissions = setOf( startFlow(), startFlow(), @@ -52,6 +58,7 @@ class ExplorerSimulation(private val options: OptionSet) { private lateinit var bobNode: NodeHandle private lateinit var issuerNodeGBP: NodeHandle private lateinit var issuerNodeUSD: NodeHandle + private lateinit var bnoNode: NodeHandle private lateinit var notary: Party private val RPCConnections = ArrayList() @@ -65,7 +72,8 @@ class ExplorerSimulation(private val options: OptionSet) { fun startDemoNodes() { val portAllocation = PortAllocation.Incremental(20000) - driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance", IOUFlow::class.java.`package`.name), + driver(portAllocation = portAllocation, + extraCordappPackagesToScan = packagesOfClasses(CashPaymentFlow::class, IOUFlow::class, ObtainMembershipListContentFlow::class), isDebug = true, waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true)) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)) @@ -76,14 +84,16 @@ class ExplorerSimulation(private val options: OptionSet) { customOverrides = mapOf("issuableCurrencies" to listOf("GBP"))) val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), customOverrides = mapOf("issuableCurrencies" to listOf("USD"))) + val bno = startNode(providedName = IOUFlow.allowedMembershipName, rpcUsers = listOf(user)) notaryNode = defaultNotaryNode.get() aliceNode = alice.get() bobNode = bob.get() issuerNodeGBP = issuerGBP.get() issuerNodeUSD = issuerUSD.get() + bnoNode = bno.get() - arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach { + arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD, bnoNode).forEach { println("${it.nodeInfo.legalIdentities.first()} started on ${it.configuration.rpcAddress}") } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt index 0c39bfe950..df0712a615 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt @@ -9,8 +9,8 @@ import net.corda.client.jfx.utils.map import net.corda.core.identity.AbstractParty import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.sample.businessnetwork.IOUFlow -import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow +import net.corda.sample.businessnetwork.iou.IOUFlow +import net.corda.sample.businessnetwork.membership.flow.ObtainMembershipListContentFlow class MembershipListModel { private val proxy by observableValue(NodeMonitorModel::proxyObservable) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index 8970e7b1e1..9658b93a61 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -83,9 +83,10 @@ class Network : CordaView() { .map { it.stateAndRef.state.data }.getParties() val outputParties = it.transaction.tx.outputStates.observable().getParties() val signingParties = it.transaction.sigs.map { it.by.toKnownParty() } - // Input parties fire a bullets to all output parties, and to the signing parties. !! This is a rough guess of how the message moves in the network. + // Input parties fire a bullets to all output parties, then to the signing parties and then signing parties to output parties. + // !! This is a rough guess of how the message moves in the network. // TODO : Expose artemis queue to get real message information. - inputParties.cross(outputParties) + inputParties.cross(signingParties) + inputParties.cross(outputParties) + inputParties.cross(signingParties) + signingParties.cross(outputParties) } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index 8d75aec165..b4e2ef1b21 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -28,7 +28,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.utilities.toBase58String -import net.corda.sample.businessnetwork.IOUState +import net.corda.sample.businessnetwork.iou.IOUState import net.corda.explorer.AmountDiff import net.corda.explorer.formatters.AmountFormatter import net.corda.explorer.formatters.Formatter diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt index 3dd6afa82b..51ab1c2c58 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt @@ -2,10 +2,17 @@ package net.corda.explorer.views.cordapps.iou import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView +import javafx.beans.binding.Bindings +import javafx.geometry.Pos import javafx.scene.input.MouseButton import javafx.scene.layout.BorderPane +import net.corda.client.jfx.model.TransactionDataModel +import net.corda.client.jfx.model.observableList +import net.corda.client.jfx.utils.map import net.corda.core.utilities.Try import net.corda.explorer.model.CordaView +import net.corda.explorer.model.CordaWidget +import net.corda.sample.businessnetwork.iou.IOUState import net.corda.explorer.model.MembershipListModel import tornadofx.* @@ -13,6 +20,7 @@ class IOUViewer : CordaView("IOU") { // Inject UI elements. override val root: BorderPane by fxml() override val icon: FontAwesomeIcon = FontAwesomeIcon.CHEVRON_CIRCLE_RIGHT + override val widgets = listOf(CordaWidget(title, IOUWidget(), icon)).observable() // Wire up UI init { @@ -28,8 +36,22 @@ class IOUViewer : CordaView("IOU") { } fun isEnabledForNode(): Boolean = Try.on { - // Assuming if the model can be initialized - the CorDapp is installed - val allParties = MembershipListModel().allParties - allParties[0] - }.isSuccess + // Assuming if the model can be initialized - the CorDapp is installed + val allParties = MembershipListModel().allParties + allParties[0] + }.isSuccess + + private class IOUWidget : BorderPane() { + private val partiallyResolvedTransactions by observableList(TransactionDataModel::partiallyResolvedTransactions) + private val iouTransactions = partiallyResolvedTransactions.filtered { t -> t.transaction.tx.outputs.any({ ts -> ts.data is IOUState }) } + + init { + right { + label { + textProperty().bind(Bindings.size(iouTransactions).map(Number::toString)) + BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT) + } + } + } + } } \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt index b675b8d2a5..281eaa0c5c 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt @@ -1,6 +1,7 @@ package net.corda.explorer.views.cordapps.iou import com.google.common.base.Splitter +import com.sun.javafx.collections.ImmutableObservableList import javafx.beans.binding.Bindings import javafx.beans.binding.BooleanBinding import javafx.collections.FXCollections @@ -15,13 +16,15 @@ import net.corda.client.jfx.model.* import net.corda.client.jfx.utils.isNotNull import net.corda.client.jfx.utils.map import net.corda.core.flows.FlowException +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow -import net.corda.sample.businessnetwork.IOUFlow +import net.corda.core.utilities.loggerFor +import net.corda.sample.businessnetwork.iou.IOUFlow import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.MembershipListModel import net.corda.explorer.views.bigDecimalFormatter @@ -46,7 +49,12 @@ class NewTransaction : Fragment() { fun show(window: Window) { // Every time re-query from the server side - val elementsFromServer = MembershipListModel().allParties + val elementsFromServer = try { + MembershipListModel().allParties + } catch (ex: Exception) { + loggerFor().error("Unexpected error fetching membership list content", ex) + ImmutableObservableList() + } partyBChoiceBox.apply { items = FXCollections.observableList(parties.map { it.chooseIdentityAndCert() }).filtered { elementsFromServer.contains(it.party) }.sorted() From c2b10a18826c05c7752aa2ccf4fb4372db87ac2e Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 20 Dec 2017 12:07:42 +0000 Subject: [PATCH 4/6] Business Network design document (#204) * Design doc (unfinished). * Design doc, requirements completed. * Design doc, more content and the diagram. * Design doc, more content. * Design doc, minor changes. * Changes following review from @davidleeuk * Changes following review from @gendal * Changes following review from @shamsasari --- .../businessNetwork/businessNetwork.png | Bin 0 -> 110806 bytes docs/source/design/businessNetwork/design.md | 235 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 docs/source/design/businessNetwork/businessNetwork.png create mode 100644 docs/source/design/businessNetwork/design.md diff --git a/docs/source/design/businessNetwork/businessNetwork.png b/docs/source/design/businessNetwork/businessNetwork.png new file mode 100644 index 0000000000000000000000000000000000000000..3da68e37708e3b9ebe3e1f2ab4cecde278685ec8 GIT binary patch literal 110806 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 b?^~o Date: Wed, 20 Dec 2017 12:59:01 +0000 Subject: [PATCH 5/6] Switch from KRYO serialization to AMQP (#229) --- .../main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index ce34342627..1779dfcbab 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -23,7 +23,7 @@ import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -301,7 +301,7 @@ fun main(args: Array) { } private fun initialiseSerialization() { - val context = KRYO_P2P_CONTEXT + val context = AMQP_P2P_CONTEXT nodeSerializationEnv = SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme()) From d48bc41a7ceaa303cbdf5ab3e0b6a5a4e6408a11 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 20 Dec 2017 13:01:19 +0000 Subject: [PATCH 6/6] Revert "Shams os merge 191217 (#223)" This reverts commit 2461421 --- .ci/api-current.txt | 2 +- .../net/corda/core/identity/CordaX500Name.kt | 8 +- .../net/corda/core/internal/InternalUtils.kt | 8 - .../corda/core/node/services/NotaryService.kt | 14 +- .../corda/core/crypto/CompositeKeyTests.kt | 11 +- .../AttachmentSerializationTest.kt | 2 +- docs/source/generating-a-node.rst | 2 + docs/source/hello-world-running.rst | 2 +- .../doorman/DoormanIntegrationTest.kt | 19 +-- .../persistence/PersistentNodeInfoStorage.kt | 75 ++++----- .../doorman/signer/LocalSigner.kt | 11 +- .../networkmanage/hsm/utils/X509Utils.kt | 3 +- .../persistence/DBNetworkMapStorageTest.kt | 68 +++++--- .../PersitenceNodeInfoStorageTest.kt | 147 +++++++++++------- .../doorman/NodeInfoWebServiceTest.kt | 46 ++++-- ...nerator.kt => ServiceIdentityGenerator.kt} | 44 ++---- .../nodeapi/internal/crypto/X509Utilities.kt | 12 +- .../node/services/BFTNotaryServiceTests.kt | 8 +- .../node/services/MySQLNotaryServiceTests.kt | 8 +- .../node/services/RaftNotaryServiceTests.kt | 7 +- .../services/messaging/P2PMessagingTest.kt | 5 +- .../net/corda/node/internal/AbstractNode.kt | 29 ++-- .../node/services/config/ConfigUtilities.kt | 9 +- .../identity/InMemoryIdentityService.kt | 1 - .../identity/PersistentIdentityService.kt | 1 - .../BFTNonValidatingNotaryService.kt | 11 +- .../transactions/MySQLNotaryService.kt | 6 + .../RaftNonValidatingNotaryService.kt | 13 +- .../RaftValidatingNotaryService.kt | 13 +- .../transactions/ValidatingNotaryService.kt | 4 +- .../registration/NetworkRegistrationHelper.kt | 5 +- .../statemachine/FlowFrameworkTests.kt | 2 +- .../NetworkRegistrationHelperTest.kt | 111 +++++++------ .../net/corda/notarydemo/BFTNotaryCordform.kt | 12 +- .../corda/notarydemo/RaftNotaryCordform.kt | 11 +- testing/node-driver/build.gradle | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 88 +++++------ .../testing/node/internal/DriverDSLImpl.kt | 42 +++-- .../corda/testing/node/MockNetworkTests.kt | 18 --- .../kotlin/net/corda/testing/CoreTestUtils.kt | 5 +- .../corda/demobench/model/NodeController.kt | 7 +- 41 files changed, 482 insertions(+), 410 deletions(-) rename node-api/src/main/kotlin/net/corda/nodeapi/internal/{IdentityGenerator.kt => ServiceIdentityGenerator.kt} (53%) delete mode 100644 testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 09b766e46d..5fd0e2262b 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1862,7 +1862,7 @@ public @interface net.corda.core.node.services.CordaService @org.jetbrains.annotations.NotNull public static final String ID_PREFIX = "corda.notary." ## public static final class net.corda.core.node.services.NotaryService$Companion extends java.lang.Object - @kotlin.Deprecated @org.jetbrains.annotations.NotNull public final String constructId(boolean, boolean, boolean, boolean) + @org.jetbrains.annotations.NotNull public final String constructId(boolean, boolean, boolean, boolean) ## public abstract class net.corda.core.node.services.PartyInfo extends java.lang.Object @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getParty() diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index ad315c065e..0b6a3ddeb7 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -2,7 +2,7 @@ package net.corda.core.identity import com.google.common.collect.ImmutableSet import net.corda.core.internal.LegalNameValidator -import net.corda.core.internal.unspecifiedCountry +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.x500Name import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.ASN1Encodable @@ -36,9 +36,7 @@ data class CordaX500Name(val commonName: String?, val locality: String, val state: String?, val country: String) { - constructor(commonName: String, organisation: String, locality: String, country: String) : - this(commonName = commonName, organisationUnit = null, organisation = organisation, locality = locality, state = null, country = country) - + constructor(commonName: String, organisation: String, locality: String, country: String) : this(commonName = commonName, organisationUnit = null, organisation = organisation, locality = locality, state = null, country = country) /** * @param organisation name of the organisation. * @param locality locality of the organisation, typically nearest major city. @@ -81,6 +79,8 @@ data class CordaX500Name(val commonName: String?, const val MAX_LENGTH_ORGANISATION_UNIT = 64 const val MAX_LENGTH_COMMON_NAME = 64 private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU) + @VisibleForTesting + val unspecifiedCountry = "ZZ" private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries() + unspecifiedCountry) @JvmStatic fun build(principal: X500Principal): CordaX500Name { diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 42145a2f26..36022463ff 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -5,7 +5,6 @@ package net.corda.core.internal import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.SerializationContext import net.corda.core.transactions.TransactionBuilder @@ -119,8 +118,6 @@ fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(th inline val Path.size: Long get() = Files.size(this) inline fun Path.list(block: (Stream) -> R): R = Files.list(this).use(block) fun Path.deleteIfExists(): Boolean = Files.deleteIfExists(this) -fun Path.reader(charset: Charset = UTF_8): BufferedReader = Files.newBufferedReader(this, charset) -fun Path.writer(charset: Charset = UTF_8, vararg options: OpenOption): BufferedWriter = Files.newBufferedWriter(this, charset, *options) fun Path.readAll(): ByteArray = Files.readAllBytes(this) inline fun Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = Files.newInputStream(this, *options).use(block) inline fun Path.write(createDirs: Boolean = false, vararg options: OpenOption = emptyArray(), block: (OutputStream) -> Unit) { @@ -319,8 +316,3 @@ fun ExecutorService.join() { // Try forever. Do not give up, tests use this method to assert the executor has no more tasks. } } - -@Suppress("unused") -@VisibleForTesting -val CordaX500Name.Companion.unspecifiedCountry - get() = "ZZ" diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 8c6a160618..0c928a7b59 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -15,16 +15,22 @@ import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { companion object { - @Deprecated("No longer used") const val ID_PREFIX = "corda.notary." - @Deprecated("No longer used") - fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { - require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } + @JvmOverloads + fun constructId( + validating: Boolean, + raft: Boolean = false, + bft: Boolean = false, + custom: Boolean = false, + mysql: Boolean = false + ): String { + require(Booleans.countTrue(raft, bft, custom, mysql) <= 1) { "At most one of raft, bft, mysql or custom may be true" } return StringBuffer(ID_PREFIX).apply { append(if (validating) "validating" else "simple") if (raft) append(".raft") if (bft) append(".bft") if (custom) append(".custom") + if (mysql) append(".mysql") }.toString() } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 751037b1ee..734505498d 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import net.corda.nodeapi.internal.crypto.* -import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.internal.kryoSpecific +import net.corda.testing.SerializationEnvironmentRule import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -24,7 +24,6 @@ class CompositeKeyTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - @Rule @JvmField val tempFolder: TemporaryFolder = TemporaryFolder() @@ -41,9 +40,9 @@ class CompositeKeyTests { private val secureHash = message.sha256() // By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise). - private val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) } - private val bobSignature by lazy { bobKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(bobPublicKey).schemeNumberID))) } - private val charlieSignature by lazy { charlieKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(charliePublicKey).schemeNumberID))) } + val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) } + val bobSignature by lazy { bobKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(bobPublicKey).schemeNumberID))) } + val charlieSignature by lazy { charlieKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(charliePublicKey).schemeNumberID))) } @Test fun `(Alice) fulfilled by Alice signature`() { @@ -338,7 +337,7 @@ class CompositeKeyTests { val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair) // Sign the composite key with the self sign CA. - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, ca, caKeyPair, caName, compositeKey) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey) // Store certificate to keystore. val keystorePath = tempFolder.root.toPath() / "keystore.jks" diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index d72d7068d4..940d959860 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -159,7 +159,7 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.dispose() - client = mockNet.createNode(MockNodeParameters(client.internals.id, client.internals.configuration.myLegalName), { args -> + client = mockNet.createNode(MockNodeParameters(client.internals.id), { args -> object : MockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index eee15b3d5d..4a721e304d 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -92,6 +92,7 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates } node { name "O=PartyA,L=London,C=GB" + advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -102,6 +103,7 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates } node { name "O=PartyB,L=New York,C=US" + advertisedServices = [] p2pPort 10009 rpcPort 10010 webPort 10011 diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 4ac394d26c..592cfee44c 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -22,7 +22,7 @@ service. directory "./build/nodes" node { name "O=Controller,L=London,C=GB" - notary = [validating : true] + advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:corda-finance:$corda_release_version"] diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt index 5993cef210..fe919f6114 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt @@ -3,6 +3,7 @@ package com.r3.corda.networkmanage.doorman import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto @@ -29,6 +30,7 @@ import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.rigorousMock import org.bouncycastle.cert.X509CertificateHolder +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -76,7 +78,7 @@ class DoormanIntegrationTest { loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply { assert(containsAlias(X509Utilities.CORDA_CLIENT_CA)) - assertEquals(ALICE_NAME.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal) + assertEquals(ALICE_NAME.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList()) } @@ -118,18 +120,13 @@ class DoormanIntegrationTest { // Publish NodeInfo val networkMapClient = NetworkMapClient(config.compatibilityZoneURL!!, rootCertAndKey.certificate.cert) - - val keyStore = loadKeyStore(config.nodeKeystore, config.keyStorePassword) - val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword) - val identityKeyPair = Crypto.generateKeyPair() - val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, ALICE_NAME, identityKeyPair.public) - val certPath = X509CertificateFactory().generateCertPath(identityCert.cert, *clientCertPath) - val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val certs = loadKeyStore(config.nodeKeystore, config.keyStorePassword).getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val keyPair = loadKeyStore(config.nodeKeystore, config.keyStorePassword).getKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(buildCertPath(*certs))), 1, serial = 1L) val nodeInfoBytes = nodeInfo.serialize() // When - val signedNodeInfo = SignedNodeInfo(nodeInfoBytes, listOf(identityKeyPair.private.sign(nodeInfoBytes.bytes))) + val signedNodeInfo = SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes))) networkMapClient.publish(signedNodeInfo) // Then @@ -140,7 +137,7 @@ class DoormanIntegrationTest { doorman.close() } - private fun createConfig(): NodeConfiguration { + fun createConfig(): NodeConfiguration { return rigorousMock().also { doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt index 1d4af91246..7f1d331877 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt @@ -1,60 +1,65 @@ package com.r3.corda.networkmanage.common.persistence -import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity -import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity -import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity +import com.r3.corda.networkmanage.common.persistence.entity.* import com.r3.corda.networkmanage.common.utils.buildCertPath import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.internal.CertRole +import net.corda.core.identity.CordaX500Name +import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import java.security.cert.CertPath +import java.security.cert.X509Certificate /** * Database implementation of the [NetworkMapStorage] interface */ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { - override fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash { - return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { - val nodeInfo = signedNodeInfo.verified() - val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA } + override fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash = database.transaction(TransactionIsolationLevel.SERIALIZABLE) { + val nodeInfo = signedNodeInfo.verified() + val orgName = nodeInfo.legalIdentities.first().name.organisation + // TODO: use cert extension to identify NodeCA cert when Ross's work is in. + val nodeCACert = nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.map { it as X509Certificate } + .find { CordaX500Name.build(it.issuerX500Principal).organisation != orgName && CordaX500Name.build(it.subjectX500Principal).organisation == orgName } - val request = nodeCaCert?.let { - singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> - val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) - val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) - builder.and(certPublicKeyHashEq, certStatusValid) - } + val request = nodeCACert?.let { + singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> + val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) + val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) + builder.and(certPublicKeyHashEq, certStatusValid) } - request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") - - /* - * Delete any previous [NodeInfoEntity] instance for this CSR - * Possibly it should be moved at the network signing process at the network signing process - * as for a while the network map will have invalid entries (i.e. hashes for node info which have been - * removed). Either way, there will be a period of time when the network map data will be invalid - * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. - */ - deleteRequest(NodeInfoEntity::class.java) { builder, path -> - builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) - } - val hash = signedNodeInfo.raw.hash - - val hashedNodeInfo = NodeInfoEntity( - nodeInfoHash = hash.toString(), - certificateSigningRequest = request.certificateSigningRequest, - signedNodeInfoBytes = signedNodeInfo.serialize().bytes) - session.save(hashedNodeInfo) - hash } + request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") + /* + * Delete any previous [NodeInfoEntity] instance for this CSR + * Possibly it should be moved at the network signing process at the network signing process + * as for a while the network map will have invalid entries (i.e. hashes for node info which have been + * removed). Either way, there will be a period of time when the network map data will be invalid + * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. + */ + deleteRequest(NodeInfoEntity::class.java) { builder, path -> + builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) + } + val hash = signedNodeInfo.raw.hash + + val hashedNodeInfo = NodeInfoEntity( + nodeInfoHash = hash.toString(), + certificateSigningRequest = request.certificateSigningRequest, + signedNodeInfoBytes = signedNodeInfo.serialize().bytes) + session.save(hashedNodeInfo) + hash } override fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo? { return database.transaction { - session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.signedNodeInfo() + val nodeInfoEntity = session.find(NodeInfoEntity::class.java, nodeInfoHash.toString()) + if (nodeInfoEntity == null) { + null + } else { + nodeInfoEntity.signedNodeInfo() + } } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt index 50a7b7f971..cb6e253536 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt @@ -2,10 +2,10 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.signer.Signer import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities @@ -33,14 +33,13 @@ class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array< val nameConstraints = NameConstraints( arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), arrayOf()) - val nodeCaCert = X509Utilities.createCertificate( - CertificateType.NODE_CA, + val clientCertificate = X509Utilities.createCertificate(CertificateType.NODE_CA, caCertPath.first().toX509CertHolder(), caKeyPair, - CordaX500Name.parse(request.subject.toString()), + CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), request.publicKey, - nameConstraints = nameConstraints) - return buildCertPath(nodeCaCert.cert, *caCertPath) + nameConstraints = nameConstraints).toX509Certificate() + return buildCertPath(clientCertificate, *caCertPath) } override fun sign(data: ByteArray): DigitalSignatureWithCert { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt index 22e94ec1b5..671841746d 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt @@ -7,6 +7,7 @@ import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.x500Name import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.getX509Certificate import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Sequence @@ -183,7 +184,7 @@ object X509Utilities { val certificateType = CertificateType.NODE_CA val validityWindow = getCertificateValidityWindow(0, validDays, issuerCertificate.notBefore, issuerCertificate.notAfter) val serial = BigInteger.valueOf(random63BitValue(provider)) - val subject = CordaX500Name.parse(jcaRequest.subject.toString()).x500Name + val subject = CordaX500Name.parse(jcaRequest.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(jcaRequest.publicKey.encoded)) val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) val builder = JcaX509v3CertificateBuilder(issuerCertificate.subject, serial, validityWindow.first, validityWindow.second, subject, jcaRequest.publicKey) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt index b7d332a5fe..062d4c5c5a 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt @@ -1,21 +1,24 @@ package com.r3.corda.networkmanage.common.persistence import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import net.corda.core.crypto.Crypto import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert +import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.SignedNetworkMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -28,7 +31,6 @@ class DBNetworkMapStorageTest : TestBase() { private lateinit var requestStorage: CertificationRequestStorage private lateinit var nodeInfoStorage: NodeInfoStorage private lateinit var persistence: CordaPersistence - private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -51,8 +53,18 @@ class DBNetworkMapStorageTest : TestBase() { fun `signNetworkMap creates current network map`() { // given // Create node info. - val signedNodeInfo = createValidSignedNodeInfo("Test") - val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.markRequestTicketCreated(requestId) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair() + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + // Put signed node info data + val nodeInfoBytes = nodeInfo.serialize() + val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes)))) // Create network parameters val networkParametersHash = networkMapStorage.saveNetworkParameters(testNetworkParameters(emptyList())) @@ -91,11 +103,13 @@ class DBNetworkMapStorageTest : TestBase() { // Create network parameters val networkMapParametersHash = networkMapStorage.saveNetworkParameters(createNetworkParameters(1)) // Create empty network map + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Corda", locality = "London", country = "GB"), keyPair.public) // Sign network map making it current network map val networkMap = NetworkMap(emptyList(), networkMapParametersHash) val serializedNetworkMap = networkMap.serialize() - val signatureData = intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert) + val signatureData = keyPair.sign(serializedNetworkMap).withCert(intermediateCert.cert) val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData) networkMapStorage.saveNetworkMap(signedNetworkMap) @@ -112,19 +126,36 @@ class DBNetworkMapStorageTest : TestBase() { @Test fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() { // given - // Create node infos. - val signedNodeInfoA = createValidSignedNodeInfo("TestA") - val signedNodeInfoB = createValidSignedNodeInfo("TestB") - + // Create node info. + val organisationA = "TestA" + val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) + requestStorage.markRequestTicketCreated(requestIdA) + requestStorage.approveRequest(requestIdA, "TestUser") + val keyPairA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertA = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPairA.public) + val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) + val organisationB = "TestB" + val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) + requestStorage.markRequestTicketCreated(requestIdB) + requestStorage.approveRequest(requestIdB, "TestUser") + val keyPairB = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertB = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), keyPairB.public) + val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) + val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.companyA.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) + val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.companyB.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) // Put signed node info data - val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA) - val nodeInfoHashB = nodeInfoStorage.putNodeInfo(signedNodeInfoB) + val nodeInfoABytes = nodeInfoA.serialize() + val nodeInfoBBytes = nodeInfoB.serialize() + val nodeInfoHashA = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoABytes, listOf(keyPairA.sign(nodeInfoABytes)))) + val nodeInfoHashB = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBBytes, listOf(keyPairB.sign(nodeInfoBBytes)))) // Create network parameters val networkParametersHash = networkMapStorage.saveNetworkParameters(createNetworkParameters()) val networkMap = NetworkMap(listOf(nodeInfoHashA), networkParametersHash) val serializedNetworkMap = networkMap.serialize() - val signatureData = intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert) + val signatureData = keyPairA.sign(serializedNetworkMap).withCert(clientCertA.cert) val signedNetworkMap = SignedNetworkMap(serializedNetworkMap, signatureData) // Sign network map @@ -136,15 +167,4 @@ class DBNetworkMapStorageTest : TestBase() { // then assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB) } - - private fun createValidSignedNodeInfo(organisation: String): SignedNodeInfo { - val nodeInfoBuilder = TestNodeInfoBuilder() - val requestId = requestStorage.saveRequest(createRequest(organisation).first) - requestStorage.markRequestTicketCreated(requestId) - requestStorage.approveRequest(requestId, "TestUser") - val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) - val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) - requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) - return nodeInfoBuilder.buildWithSigned().second - } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt index e5b2e37699..1014cde074 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt @@ -3,34 +3,31 @@ package com.r3.corda.networkmanage.common.persistence import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.hashString +import com.r3.corda.networkmanage.common.utils.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.cert +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.testing.internal.TestNodeInfoBuilder -import net.corda.testing.internal.signWith import net.corda.testing.node.MockServices -import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test -import java.security.PrivateKey import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull class PersitenceNodeInfoStorageTest : TestBase() { private lateinit var requestStorage: CertificationRequestStorage - private lateinit var nodeInfoStorage: PersistentNodeInfoStorage + private lateinit var nodeInfoStorage: NodeInfoStorage private lateinit var persistence: CordaPersistence - private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -49,13 +46,14 @@ class PersitenceNodeInfoStorageTest : TestBase() { } @Test - fun `test getCertificatePath`() { + fun `test get CertificatePath`() { // Create node info. val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val name = CordaX500Name(organisation = "Test", locality = "London", country = "GB") - val nodeCaCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, name, keyPair.public) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - val request = X509Utilities.createCertificateSigningRequest(name, "my@mail.com", keyPair) + val request = X509Utilities.createCertificateSigningRequest(nodeInfo.legalIdentities.first().name, "my@mail.com", keyPair) val requestId = requestStorage.saveRequest(request) requestStorage.markRequestTicketCreated(requestId) @@ -63,73 +61,104 @@ class PersitenceNodeInfoStorageTest : TestBase() { assertNull(nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString()))) - requestStorage.putCertificatePath(requestId, buildCertPath(nodeCaCert.cert, intermediateCACert.cert, rootCACert.cert), listOf(CertificationRequestStorage.DOORMAN_SIGNATURE)) + requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()), listOf(CertificationRequestStorage.DOORMAN_SIGNATURE)) val storedCertPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString())) assertNotNull(storedCertPath) - assertEquals(nodeCaCert.cert, storedCertPath!!.certificates.first()) + assertEquals(clientCert.toX509Certificate(), storedCertPath!!.certificates.first()) } @Test - fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() { + fun `test getNodeInfoHash returns correct data`() { // given - val (nodeInfoA, signedNodeInfoA) = createValidSignedNodeInfo("TestA") - val (nodeInfoB, signedNodeInfoB) = createValidSignedNodeInfo("TestB") + val organisationA = "TestA" + val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) + requestStorage.markRequestTicketCreated(requestIdA) + requestStorage.approveRequest(requestIdA, "TestUser") + val keyPairA = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertA = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPairA.public) + val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) + val organisationB = "TestB" + val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) + requestStorage.markRequestTicketCreated(requestIdB) + requestStorage.approveRequest(requestIdB, "TestUser") + val keyPairB = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertB = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), keyPairB.public) + val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) + val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) + val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) // Put signed node info data - nodeInfoStorage.putNodeInfo(signedNodeInfoA) - nodeInfoStorage.putNodeInfo(signedNodeInfoB) + val nodeInfoABytes = nodeInfoA.serialize() + val nodeInfoBBytes = nodeInfoB.serialize() + nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoABytes, listOf(keyPairA.sign(nodeInfoABytes)))) + nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBBytes, listOf(keyPairB.sign(nodeInfoBBytes)))) // when - val persistedSignedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeInfoA.serialize().hash) - val persistedSignedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeInfoB.serialize().hash) + val persistedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeInfoABytes.hash) + val persistedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeInfoBBytes.hash) // then - assertEquals(persistedSignedNodeInfoA?.verified(), nodeInfoA) - assertEquals(persistedSignedNodeInfoB?.verified(), nodeInfoB) + assertNotNull(persistedNodeInfoA) + assertNotNull(persistedNodeInfoB) + assertEquals(persistedNodeInfoA!!.verified(), nodeInfoA) + assertEquals(persistedNodeInfoB!!.verified(), nodeInfoB) } @Test - fun `same public key with different node info`() { + fun `same pub key with different node info`() { // Create node info. - val (nodeInfo1, signedNodeInfo1, key) = createValidSignedNodeInfo("Test", serial = 1) - val nodeInfo2 = nodeInfo1.copy(serial = 2) - val signedNodeInfo2 = nodeInfo2.signWith(listOf(key)) - - val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(signedNodeInfo1) - assertEquals(nodeInfo1, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified()) - - // This should replace the node info. - nodeInfoStorage.putNodeInfo(signedNodeInfo2) - - // Old node info should be removed. - assertNull(nodeInfoStorage.getNodeInfo(nodeInfo1Hash)) - assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().hash)?.verified()) - } - - @Test - fun `putNodeInfo persists SignedNodeInfo with its signature`() { - // given - val (_, signedNodeInfo) = createValidSignedNodeInfo("Test") - - // when - val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) - - // then - val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) - assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(signedNodeInfo.signatures) - } - - private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Triple { - val nodeInfoBuilder = TestNodeInfoBuilder() + val organisation = "Test" val requestId = requestStorage.saveRequest(createRequest(organisation).first) requestStorage.markRequestTicketCreated(requestId) requestStorage.approveRequest(requestId, "TestUser") - val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) - val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) - requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) - val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial) - return Triple(nodeInfo, signedNodeInfo, key) + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoSamePubKey = NodeInfo(listOf(NetworkHostAndPort("my.company2.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoBytes = nodeInfo.serialize() + val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(keyPair.sign(nodeInfoBytes)))) + assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfoHash)?.verified()) + + val nodeInfoSamePubKeyBytes = nodeInfoSamePubKey.serialize() + // This should replace the node info. + nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoSamePubKeyBytes, listOf(keyPair.sign(nodeInfoSamePubKeyBytes)))) + + // Old node info should be removed. + assertNull(nodeInfoStorage.getNodeInfo(nodeInfoHash)) + assertEquals(nodeInfoSamePubKey, nodeInfoStorage.getNodeInfo(nodeInfoSamePubKeyBytes.hash)?.verified()) + } + + @Test + fun `putNodeInfo persists node info data with its signature`() { + // given + // Create node info. + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.markRequestTicketCreated(requestId) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoBytes = nodeInfo.serialize() + val signature = keyPair.sign(nodeInfoBytes) + + // when + val nodeInfoHash = nodeInfoStorage.putNodeInfo(SignedNodeInfo(nodeInfoBytes, listOf(signature))) + + // then + val persistedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) + assertNotNull(persistedNodeInfo) + assertEquals(nodeInfo, persistedNodeInfo!!.verified()) + assertEquals(signature, persistedNodeInfo.signatures.firstOrNull()) } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt index 26452d45a9..096a05534f 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt @@ -1,18 +1,20 @@ package com.r3.corda.networkmanage.doorman +import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.common.utils.withCert import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 -import net.corda.core.crypto.sign +import net.corda.core.crypto.* import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert +import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort @@ -23,7 +25,6 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.SignedNetworkMap import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.internal.createNodeInfoAndSigned import org.bouncycastle.asn1.x500.X500Name import org.junit.Rule import org.junit.Test @@ -40,17 +41,31 @@ class NodeInfoWebServiceTest { @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val testNetwotkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis()) + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) + private val testNetwotkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis()) @Test fun `submit nodeInfo`() { // Create node info. - val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), mock(), testNetwotkMapConfig)).use { + // Create digital signature. + val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, nodeInfo.serialize().bytes)) + + val nodeInfoStorage: NodeInfoStorage = mock { + on { getCertificatePath(any()) }.thenReturn(certPath) + } + + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use { it.start() val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish") - val nodeInfoAndSignature = signedNodeInfo.serialize().bytes + val nodeInfoAndSignature = SignedNodeInfo(nodeInfo.serialize(), listOf(digitalSignature)).serialize().bytes // Post node info and signature to doorman, this should pass without any exception. doPost(registerURL, nodeInfoAndSignature) } @@ -58,11 +73,6 @@ class NodeInfoWebServiceTest { @Test fun `get network map`() { - val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey) - val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) - val networkMap = NetworkMap(listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256()), SecureHash.randomSHA256()) val serializedNetworkMap = networkMap.serialize() val networkMapStorage: NetworkMapStorage = mock { @@ -79,12 +89,16 @@ class NodeInfoWebServiceTest { @Test fun `get node info`() { - val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) val nodeInfoHash = nodeInfo.serialize().sha256() val nodeInfoStorage: NodeInfoStorage = mock { - on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) + val serializedNodeInfo = nodeInfo.serialize() + on { getNodeInfo(nodeInfoHash) }.thenReturn(SignedNodeInfo(serializedNodeInfo, listOf(keyPair.sign(serializedNodeInfo)))) } NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt similarity index 53% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt index 97927e54db..65b60433c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/IdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt @@ -13,54 +13,42 @@ import org.slf4j.LoggerFactory import java.nio.file.Path import java.security.cert.X509Certificate -object IdentityGenerator { +object ServiceIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) - const val NODE_IDENTITY_ALIAS_PREFIX = "identity" - const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" - - fun generateNodeIdentity(dir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party { - return generateToDisk(listOf(dir), legalName, NODE_IDENTITY_ALIAS_PREFIX, threshold = 1, customRootCert = customRootCert) - } - - fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party { - return generateToDisk(dirs, notaryName, DISTRIBUTED_NOTARY_ALIAS_PREFIX, threshold, customRootCert) - } - /** * Generates signing key pairs and a common distributed service identity for a set of nodes. * The key pairs and the group identity get serialized to disk in the corresponding node directories. * This method should be called *before* any of the nodes are started. * * @param dirs List of node directories to place the generated identity and key pairs in. - * @param name The name of the identity. + * @param serviceName The legal name of the distributed service. * @param threshold The threshold for the generated group [CompositeKey]. - * @param customRootCert the certificate to use as the Corda root CA. If not specified the one in - * internal/certificates/cordadevcakeys.jks is used. + * @param customRootCert the certificate to use a Corda root CA. If not specified the one in + * certificates/cordadevcakeys.jks is used. */ - private fun generateToDisk(dirs: List, - name: CordaX500Name, - aliasPrefix: String, - threshold: Int, - customRootCert: X509Certificate?): Party { - log.trace { "Generating identity \"$name\" for nodes: ${dirs.joinToString()}" } + fun generateToDisk(dirs: List, + serviceName: CordaX500Name, + serviceId: String, + threshold: Int = 1, + customRootCert: X509Certificate? = null): Party { + log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } - val key = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) + val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) keyPairs.zip(dirs) { keyPair, dir -> - val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, keyPair.public) - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, key) + val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, serviceName, keyPair.public) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, serviceName, notaryKey) val certPath = (dir / "certificates").createDirectories() / "distributedService.jks" val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass") - keystore.setCertificateEntry("$aliasPrefix-composite-key", compositeKeyCert.cert) - keystore.setKeyEntry("$aliasPrefix-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) + keystore.setCertificateEntry("$serviceId-composite-key", compositeKeyCert.cert) + keystore.setKeyEntry("$serviceId-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert)) keystore.save(certPath, "cordacadevpass") } - - return Party(name, key) + return Party(serviceName, notaryKey) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 563ddaa9f0..70617daf76 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -7,8 +7,7 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.internal.CertRole import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert -import net.corda.core.internal.reader -import net.corda.core.internal.writer +import net.corda.core.internal.read import net.corda.core.internal.x500Name import net.corda.core.utilities.days import net.corda.core.utilities.millis @@ -49,6 +48,8 @@ object X509Utilities { const val CORDA_CLIENT_TLS = "cordaclienttls" const val CORDA_CLIENT_CA = "cordaclientca" + const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate" + private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days) /** @@ -161,7 +162,7 @@ object X509Utilities { */ @JvmStatic fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, file: Path) { - JcaPEMWriter(file.writer()).use { + JcaPEMWriter(file.toFile().writer()).use { it.writeObject(x509Certificate) } } @@ -173,8 +174,9 @@ object X509Utilities { */ @JvmStatic fun loadCertificateFromPEMFile(file: Path): X509Certificate { - return file.reader().use { - val pemObject = PemReader(it).readPemObject() + return file.read { + val reader = PemReader(it.reader()) + val pemObject = reader.readPemObject() val certHolder = X509CertificateHolder(pemObject.content) certHolder.isValidOn(Date()) certHolder.cert 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 28a04fcbd7..aaf2cb5e04 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 @@ -13,6 +13,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div +import net.corda.core.node.services.NotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort @@ -23,7 +24,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.IntegrationTest @@ -67,9 +68,10 @@ class BFTNotaryServiceTests : IntegrationTest() { (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - notary = IdentityGenerator.generateDistributedNotaryIdentity( + notary = ServiceIdentityGenerator.generateToDisk( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, - CordaX500Name("BFT", "Zurich", "CH")) + CordaX500Name("BFT", "Zurich", "CH"), + NotaryService.constructId(validating = false, bft = true)) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notary, false)))) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt index e36f7b35d0..f1f467d24e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NotaryConfig -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo import net.corda.testing.* @@ -49,7 +49,11 @@ class MySQLNotaryServiceTests : IntegrationTest() { @Before fun before() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) - notaryParty = IdentityGenerator.generateNodeIdentity(mockNet.baseDirectory(mockNet.nextNodeId), notaryName) + notaryParty = ServiceIdentityGenerator.generateToDisk( + listOf(mockNet.baseDirectory(mockNet.nextNodeId)), + notaryName, + "identity" + ) val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryParty, false)))) val notaryNodeUnstarted = createNotaryNode() val nodeUnstarted = mockNet.createUnstartedNode() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 8110118362..00f6124301 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.concurrent.map import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode +import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.driver.NodeHandle @@ -30,15 +31,15 @@ class RaftNotaryServiceTests : IntegrationTest() { val databaseSchemas = IntegrationTestSchemas( "RAFTNotaryService_0", "RAFTNotaryService_1", "RAFTNotaryService_2", DUMMY_BANK_A_NAME.toDatabaseSchemaName()) } - private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB") + private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") @Test fun `detect double spend`() { driver( startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), - notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3))) - ) { + notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))) + { val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as NodeHandle.InProcess).node }.getOrThrow() val inputState = issueState(bankA, defaultNotaryIdentity) 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 189506d02b..5dfa2963b1 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 @@ -19,9 +19,6 @@ import net.corda.node.services.messaging.ReceivedMessage import net.corda.node.services.messaging.send import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* -import net.corda.node.services.messaging.* -import net.corda.testing.ALICE_NAME -import net.corda.testing.chooseIdentity import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver @@ -41,7 +38,7 @@ class P2PMessagingTest : IntegrationTest() { @ClassRule @JvmField val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), "DistributedService_0", "DistributedService_1") - val DISTRIBUTED_SERVICE_NAME = CordaX500Name("DistributedService", "London", "GB") + val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.id, "DistributedService", "London", "GB") } @Test 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 2a1b1292f1..b25bb1df9d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -60,12 +60,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor -import net.corda.nodeapi.internal.IdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.nodeapi.internal.crypto.KeyStoreWrapper -import net.corda.nodeapi.internal.crypto.X509CertificateFactory -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -141,19 +137,25 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val services: ServiceHubInternal get() = _services private lateinit var _services: ServiceHubInternalImpl protected var myNotaryIdentity: PartyAndCertificate? = null - private lateinit var checkpointStorage: CheckpointStorage + protected lateinit var checkpointStorage: CheckpointStorage private lateinit var tokenizableServices: List protected lateinit var attachments: NodeAttachmentService protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() - private val _nodeReadyFuture = openFuture() + protected val _nodeReadyFuture = openFuture() protected var networkMapClient: NetworkMapClient? = null lateinit var securityManager: RPCSecurityManager get /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ - val nodeReadyFuture: CordaFuture get() = _nodeReadyFuture + val nodeReadyFuture: CordaFuture + get() = _nodeReadyFuture + /** A [CordaX500Name] with null common name. */ + protected val myLegalName: CordaX500Name by lazy { + val cert = loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_CA) + CordaX500Name.build(cert.subjectX500Principal).copy(commonName = null) + } open val serializationWhitelists: List by lazy { cordappLoader.cordapps.flatMap { it.serializationWhitelists } @@ -328,7 +330,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ) // Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with // a different timestamp. - networkMapCache.getNodesByLegalName(configuration.myLegalName).firstOrNull()?.let { + networkMapCache.getNodesByLegalName(myLegalName).firstOrNull()?.let { if (info.copy(serial = it.serial) == it) { info = it } @@ -754,10 +756,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { // Node's main identity or if it's a single node notary - Pair(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) + Pair("identity", myLegalName) } else { + val notaryId = notaryConfig.run { + NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom, mysql != null) + } // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) + Pair(notaryId, null) } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" @@ -765,7 +770,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, if (!keyStore.containsAlias(privateKeyAlias)) { singleName ?: throw IllegalArgumentException( "Unable to find in the key store the identity of the distributed notary ($id) the node is part of") - // TODO: Remove use of [IdentityGenerator.generateToDisk]. + // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair()) } 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 ed6028f13c..7471d5d5ec 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 @@ -69,7 +69,7 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) - // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. + // Move distributed service composite key (generated by ServiceIdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" if (distributedServiceKeystore.exists()) { val serviceKeystore = loadKeyStore(distributedServiceKeystore, "cordacadevpass") @@ -111,17 +111,18 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path, val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword) val clientKey = Crypto.generateKeyPair(signatureScheme) + val clientName = legalName.copy(commonName = null) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, clientName.x500Name))), arrayOf()) val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKeyPair, - legalName, + clientName.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), clientKey.public, nameConstraints = nameConstraints) val tlsKey = Crypto.generateKeyPair(signatureScheme) - val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, clientName, tlsKey.public) val keyPass = keyPassword.toCharArray() diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 4654473ef1..bac9ef6902 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -24,7 +24,6 @@ import javax.annotation.concurrent.ThreadSafe * * @param identities initial set of identities for the service, typically only used for unit tests. */ -// TODO There is duplicated logic between this and PersistentIdentityService @ThreadSafe class InMemoryIdentityService(identities: Array, trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal { diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 83e7b0b267..6dd98d0c63 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -26,7 +26,6 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Lob -// TODO There is duplicated logic between this and InMemoryIdentityService @ThreadSafe class PersistentIdentityService(override val trustRoot: X509Certificate, vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityServiceInternal { 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 9f6ffe1d08..780c4815c6 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 @@ -34,13 +34,12 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ -class BFTNonValidatingNotaryService( - override val services: ServiceHubInternal, - override val notaryIdentityKey: PublicKey, - private val bftSMaRtConfig: BFTSMaRtConfiguration, - cluster: BFTSMaRt.Cluster -) : NotaryService() { +class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + private val bftSMaRtConfig: BFTSMaRtConfiguration, + cluster: BFTSMaRt.Cluster) : NotaryService() { companion object { + val id = constructId(validating = false, bft = true) private val log = contextLogger() } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt index adfe3e9744..0bf810b2ab 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt @@ -35,6 +35,9 @@ class MySQLNonValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, dataSourceProperties: Properties, devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { + companion object { + val id = constructId(validating = false, mysql = true) + } override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this) } @@ -42,5 +45,8 @@ class MySQLValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, dataSourceProperties: Properties, devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { + companion object { + val id = constructId(validating = true, mysql = true) + } override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ValidatingNotaryFlow(otherPartySession, this) } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index e672380398..1433e71f85 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -8,13 +8,14 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService( - override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider -) : TrustedAuthorityNotaryService() { +class RaftNonValidatingNotaryService(override val services: ServiceHub, + override val notaryIdentityKey: PublicKey, + override val uniquenessProvider: RaftUniquenessProvider) : TrustedAuthorityNotaryService() { + companion object { + val id = constructId(validating = false, raft = true) + } + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { return NonValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 3e4899ae0a..8fd3448512 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -8,13 +8,14 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService( - override val services: ServiceHub, - override val notaryIdentityKey: PublicKey, - override val uniquenessProvider: RaftUniquenessProvider -) : TrustedAuthorityNotaryService() { - override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) +class RaftValidatingNotaryService(override val services: ServiceHub, + override val notaryIdentityKey: PublicKey, + override val uniquenessProvider: RaftUniquenessProvider) : TrustedAuthorityNotaryService() { + companion object { + val id = constructId(validating = true, raft = true) + } + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { return ValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 5e687c3b6d..6c7e36046b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -9,8 +9,10 @@ import java.security.PublicKey /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { + companion object { + val id = constructId(validating = true) + } override val timeWindowChecker = TimeWindowChecker(services.clock) - override val uniquenessProvider = PersistentUniquenessProvider() override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this) 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 698420ca0a..f24cf56f11 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 @@ -103,9 +103,10 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v println("Generating SSL certificate for node messaging service.") val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA).toX509CertHolder() - val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, CordaX500Name.build(caCert.cert.subjectX500Principal), sslKey.public) + val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, CordaX500Name.build(caCert.cert.subjectX500Principal).copy(commonName = null), sslKey.public) val sslKeyStore = loadOrCreateKeyStore(config.sslKeystore, keystorePassword) - sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), arrayOf(sslCert.cert, *certificates)) + sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), + arrayOf(sslCert.cert, *certificates)) sslKeyStore.save(config.sslKeystore, config.keyStorePassword) println("SSL private key and certificate stored in ${config.sslKeystore}.") // All done, clean up temp files. diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 2006c397bc..1fe97568a8 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -656,7 +656,7 @@ class FlowFrameworkTests { private inline fun > StartedNode.restartAndGetRestoredFlow() = internals.run { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(MockNodeParameters(id, configuration.myLegalName)) + val newNode = mockNet.createNode(MockNodeParameters(id)) newNode.internals.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 209153fdb8..50922e9fde 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -1,7 +1,5 @@ package net.corda.node.utilities.registration -import com.google.common.jimfs.Configuration.unix -import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.eq @@ -9,61 +7,68 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.cert -import net.corda.core.internal.createDirectories +import net.corda.core.internal.* import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.getX509Certificate +import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.ALICE_NAME import net.corda.testing.internal.rigorousMock -import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import java.security.cert.Certificate -import java.security.cert.X509Certificate +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class NetworkRegistrationHelperTest { - private val fs = Jimfs.newFileSystem(unix()) - private val requestId = SecureHash.randomSHA256().toString() - private val nodeLegalName = ALICE_NAME - private val intermediateCaName = CordaX500Name("CORDA_INTERMEDIATE_CA", "R3 Ltd", "London", "GB") - private val rootCaName = CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB") - private val nodeCaCert = createCaCert(nodeLegalName) - private val intermediateCaCert = createCaCert(intermediateCaName) - private val rootCaCert = createCaCert(rootCaName) + @Rule + @JvmField + val tempFolder = TemporaryFolder() + private val requestId = SecureHash.randomSHA256().toString() private lateinit var config: NodeConfiguration + private val identities = listOf("CORDA_CLIENT_CA", + "CORDA_INTERMEDIATE_CA", + "CORDA_ROOT_CA") + .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } + private val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } + .map { it.cert }.toTypedArray() + + private val certService = mockRegistrationResponse(*certs) + @Before fun init() { - val baseDirectory = fs.getPath("/baseDir").createDirectories() abstract class AbstractNodeConfiguration : NodeConfiguration config = rigorousMock().also { - doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn(nodeLegalName).whenever(it).myLegalName + doReturn(ALICE_NAME).whenever(it).myLegalName doReturn("").whenever(it).emailAddress } } - @After - fun cleanUp() { - fs.close() - } - @Test fun `successful registration`() { - assertThat(config.nodeKeystore).doesNotExist() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertFalse(config.nodeKeystore.exists()) + assertFalse(config.sslKeystore.exists()) + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certs.last()) + it.save(config.trustStoreFile, config.trustStorePassword) + } - saveTrustStoreWithRootCa(rootCaCert) + NetworkRegistrationHelper(config, certService).buildKeystore() - createRegistrationHelper().buildKeystore() + assertTrue(config.nodeKeystore.exists()) + assertTrue(config.sslKeystore.exists()) + assertTrue(config.trustStoreFile.exists()) val nodeKeystore = loadKeyStore(config.nodeKeystore, config.keyStorePassword) val sslKeystore = loadKeyStore(config.sslKeystore, config.keyStorePassword) @@ -74,8 +79,9 @@ class NetworkRegistrationHelperTest { assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) - val nodeCaCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - assertThat(nodeCaCertChain).containsExactly(nodeCaCert, intermediateCaCert, rootCaCert) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + assertEquals(3, certificateChain.size) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { it.toX509CertHolder().subject.commonName }) } sslKeystore.run { @@ -83,55 +89,46 @@ class NetworkRegistrationHelperTest { assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA)) assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) - val nodeTlsCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) - assertThat(nodeTlsCertChain).hasSize(4) - // The TLS cert has the same subject as the node CA cert - assertThat(CordaX500Name.build((nodeTlsCertChain[0] as X509Certificate).subjectX500Principal)).isEqualTo(nodeLegalName) - assertThat(nodeTlsCertChain.drop(1)).containsExactly(nodeCaCert, intermediateCaCert, rootCaCert) + val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + assertEquals(4, certificateChain.size) + assertEquals(listOf(CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB").x500Name) + identities.map { it.x500Name }, + certificateChain.map { it.toX509CertHolder().subject }) + assertEquals(CordaX500Name(organisation = "R3 Ltd", locality = "London", country = "GB").x500Principal, + getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal) } trustStore.run { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA)) - val trustStoreRootCaCert = getCertificate(X509Utilities.CORDA_ROOT_CA) - assertThat(trustStoreRootCaCert).isEqualTo(rootCaCert) } } @Test fun `missing truststore`() { assertThatThrownBy { - createRegistrationHelper() + NetworkRegistrationHelper(config, certService).buildKeystore() }.hasMessageContaining("This file must contain the root CA cert of your compatibility zone. Please contact your CZ operator.") + .isInstanceOf(IllegalArgumentException::class.java) } @Test fun `wrong root cert in truststore`() { - saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB"))) - val registrationHelper = createRegistrationHelper() + val someCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, someCert) + it.save(config.trustStoreFile, config.trustStorePassword) + } assertThatThrownBy { - registrationHelper.buildKeystore() + NetworkRegistrationHelper(config, certService).buildKeystore() }.isInstanceOf(WrongRootCertException::class.java) } - private fun createRegistrationHelper(): NetworkRegistrationHelper { - val certService = rigorousMock().also { + private fun mockRegistrationResponse(vararg response: Certificate): NetworkRegistrationService { + return rigorousMock().also { doReturn(requestId).whenever(it).submitRequest(any()) - doReturn(arrayOf(nodeCaCert, intermediateCaCert, rootCaCert)).whenever(it).retrieveCertificates(eq(requestId)) + doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) } - return NetworkRegistrationHelper(config, certService) - } - - private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) { - config.trustStoreFile.parent.createDirectories() - loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { - it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa) - it.save(config.trustStoreFile, config.trustStorePassword) - } - } - - private fun createCaCert(name: CordaX500Name): X509Certificate { - return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert } } 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 index 311a5c51b2..eb11a9b443 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -4,11 +4,13 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name +import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -22,7 +24,7 @@ private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. class BFTNotaryCordform : CordformDefinition() { - private val clusterName = CordaX500Name("BFT", "Zurich", "CH") + private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { nodesDirectory = Paths.get("build", "nodes", "nodesBFT") @@ -62,10 +64,10 @@ class BFTNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - IdentityGenerator.generateDistributedNotaryIdentity( + ServiceIdentityGenerator.generateToDisk( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName, - minCorrectReplicas(clusterSize) - ) + NotaryService.constructId(validating = false, bft = true), + minCorrectReplicas(clusterSize)) } } 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 index 59384a412f..cfc21fa060 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -8,7 +8,8 @@ import net.corda.core.node.services.NotaryService import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.RaftConfig -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.node.services.transactions.RaftValidatingNotaryService +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.testing.node.internal.demorun.* import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME @@ -23,7 +24,7 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. class RaftNotaryCordform : CordformDefinition() { - private val clusterName = CordaX500Name("Raft", "Zurich", "CH") + private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { nodesDirectory = Paths.get("build", "nodes", "nodesRaft") @@ -59,9 +60,9 @@ class RaftNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - IdentityGenerator.generateDistributedNotaryIdentity( + ServiceIdentityGenerator.generateToDisk( notaryNames.map { context.baseDirectory(it.toString()) }, - clusterName - ) + clusterName, + NotaryService.constructId(validating = true, raft = true)) } } diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index a13ec48018..c50a027047 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -29,7 +29,7 @@ dependencies { compile "net.corda.plugins:cordform-common:$gradle_plugins_version" // Integration test helpers - testCompile "org.assertj:assertj-core:$assertj_version" + integrationTestCompile "org.assertj:assertj-core:${assertj_version}" integrationTestCompile "junit:junit:$junit_version" // Jetty dependencies for NetworkMapClient test. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 84fe07acc5..34cd93d367 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -9,7 +9,6 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectory import net.corda.core.internal.uncheckedCast @@ -38,7 +37,7 @@ import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo @@ -126,14 +125,14 @@ data class MockNodeArgs( * By default a single notary node is automatically started, which forms part of the network parameters for all the nodes. * This node is available by calling [defaultNotaryNode]. */ -open class MockNetwork(private val cordappPackages: List, - defaultParameters: MockNetworkParameters = MockNetworkParameters(), - private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, - private val threadPerNode: Boolean = defaultParameters.threadPerNode, - servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - private val defaultFactory: (MockNodeArgs) -> MockNode = defaultParameters.defaultFactory, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - private val notarySpecs: List = defaultParameters.notarySpecs) { +class MockNetwork(private val cordappPackages: List, + defaultParameters: MockNetworkParameters = MockNetworkParameters(), + private val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, + private val threadPerNode: Boolean = defaultParameters.threadPerNode, + servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, + private val defaultFactory: (MockNodeArgs) -> MockNode = defaultParameters.defaultFactory, + initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, + private val notarySpecs: List = defaultParameters.notarySpecs) { /** Helper constructor for creating a [MockNetwork] with custom parameters from Java. */ @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) @@ -142,7 +141,7 @@ open class MockNetwork(private val cordappPackages: List, // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // This SFTP support loads BouncyCastle, which we want to avoid. // Please see https://issues.apache.org/jira/browse/SSHD-736 - it's easier then to create our own fork of SSHD - SecurityUtils.setAPrioriDisabledProvider("BC", true) // XXX: Why isn't this static? + SecurityUtils.setAPrioriDisabledProvider("BC", true) } var nextNodeId = 0 @@ -160,7 +159,6 @@ open class MockNetwork(private val cordappPackages: List, throw IllegalStateException("Using more than one MockNetwork simultaneously is not supported.", e) } private val sharedUserCount = AtomicInteger(0) - /** A read only view of the current set of nodes. */ val nodes: List get() = _nodes @@ -174,29 +172,32 @@ open class MockNetwork(private val cordappPackages: List, * Returns the single notary node on the network. Throws if there are none or more than one. * @see notaryNodes */ - val defaultNotaryNode: StartedNode get() { - return when (notaryNodes.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryNodes[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") + val defaultNotaryNode: StartedNode + get() { + return when (notaryNodes.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryNodes[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } } - } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentity: Party get() { - return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentity: Party + get() { + return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Return the identity of the default notary node. * @see defaultNotaryNode */ - val defaultNotaryIdentityAndCert: PartyAndCertificate get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } + val defaultNotaryIdentityAndCert: PartyAndCertificate + get() { + return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + } /** * Because this executor is shared, we need to be careful about nodes shutting it down. @@ -221,31 +222,27 @@ open class MockNetwork(private val cordappPackages: List, } init { - try { - filesystem.getPath("/nodes").createDirectory() - val notaryInfos = generateNotaryIdentities() - // The network parameters must be serialised before starting any of the nodes - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) - @Suppress("LeakingThis") - notaryNodes = createNotaries() - } catch (t: Throwable) { - stopNodes() - throw t - } + filesystem.getPath("/nodes").createDirectory() + val notaryInfos = generateNotaryIdentities() + // The network parameters must be serialised before starting any of the nodes + networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos)) + notaryNodes = createNotaries() } private fun generateNotaryIdentities(): List { return notarySpecs.mapIndexed { index, (name, validating) -> - val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name) + val identity = ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(nextNodeId + index)), + serviceName = name, + serviceId = "identity") NotaryInfo(identity, validating) } } - @VisibleForTesting - internal open fun createNotaries(): List> { - return notarySpecs.map { (name, validating) -> - createNode(MockNodeParameters(legalName = name, configOverrides = { - doReturn(NotaryConfig(validating)).whenever(it).notary + private fun createNotaries(): List> { + return notarySpecs.map { spec -> + createNode(MockNodeParameters(legalName = spec.name, configOverrides = { + doReturn(NotaryConfig(spec.validating)).whenever(it).notary })) } } @@ -302,7 +299,7 @@ open class MockNetwork(private val cordappPackages: List, id, serverThread, myNotaryIdentity, - configuration.myLegalName, + myLegalName, database).also { runOnStop += it::stop } } @@ -460,11 +457,8 @@ open class MockNetwork(private val cordappPackages: List, } fun stopNodes() { - try { - nodes.forEach { it.started?.dispose() } - } finally { - serializationEnv.unset() // Must execute even if other parts of this method fail. - } + nodes.forEach { it.started?.dispose() } + serializationEnv.unset() messagingNetwork.stop() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 5833840bb4..308ac4fe5a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -19,6 +19,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.NotaryService import net.corda.core.toFuture import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger @@ -29,9 +30,12 @@ import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions import net.corda.node.services.config.* +import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.services.transactions.RaftNonValidatingNotaryService +import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -241,9 +245,9 @@ class DriverDSLImpl( } private enum class ClusterType(val validating: Boolean, val clusterName: CordaX500Name) { - VALIDATING_RAFT(true, CordaX500Name("Raft", "Zurich", "CH")), - NON_VALIDATING_RAFT(false, CordaX500Name("Raft", "Zurich", "CH")), - NON_VALIDATING_BFT(false, CordaX500Name("BFT", "Zurich", "CH")) + VALIDATING_RAFT(true, CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH")), + NON_VALIDATING_RAFT(false, CordaX500Name(RaftNonValidatingNotaryService.id, "Raft", "Zurich", "CH")), + NON_VALIDATING_BFT(false, CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH")) } internal fun startCordformNodes(cordforms: List): CordaFuture<*> { @@ -266,15 +270,24 @@ class DriverDSLImpl( clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name) } else { // We have all we need here to generate the identity for single node notaries - val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(name), legalName = name) + val identity = ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(name)), + serviceName = name, + serviceId = "identity" + ) notaryInfos += NotaryInfo(identity, notaryConfig.validating) } } clusterNodes.asMap().forEach { type, nodeNames -> - val identity = IdentityGenerator.generateDistributedNotaryIdentity( + val identity = ServiceIdentityGenerator.generateToDisk( dirs = nodeNames.map { baseDirectory(it) }, - notaryName = type.clusterName + serviceName = type.clusterName, + serviceId = NotaryService.constructId( + validating = type.validating, + raft = type in setOf(VALIDATING_RAFT, NON_VALIDATING_RAFT), + bft = type == ClusterType.NON_VALIDATING_BFT + ) ) notaryInfos += NotaryInfo(identity, type.validating) } @@ -356,11 +369,20 @@ class DriverDSLImpl( private fun generateNotaryIdentities(): List { return notarySpecs.map { spec -> val identity = if (spec.cluster == null) { - IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert) + ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(spec.name)), + serviceName = spec.name, + serviceId = "identity", + customRootCert = compatibilityZone?.rootCert + ) } else { - IdentityGenerator.generateDistributedNotaryIdentity( + ServiceIdentityGenerator.generateToDisk( dirs = generateNodeNames(spec).map { baseDirectory(it) }, - notaryName = spec.name, + serviceName = spec.name, + serviceId = NotaryService.constructId( + validating = spec.validating, + raft = spec.cluster is ClusterSpec.Raft + ), customRootCert = compatibilityZone?.rootCert ) } diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt deleted file mode 100644 index 10d9358243..0000000000 --- a/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.testing.node - -import net.corda.core.serialization.internal.effectiveSerializationEnv -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Test - -class MockNetworkTests { - @Test - fun `does not leak serialization env if init fails`() { - val e = Exception("didn't work") - assertThatThrownBy { - object : MockNetwork(emptyList(), initialiseSerialization = true) { - override fun createNotaries() = throw e - } - }.isSameAs(e) - assertThatThrownBy { effectiveSerializationEnv }.isInstanceOf(IllegalStateException::class.java) - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 860af415ac..11e572e2d7 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert -import net.corda.core.internal.unspecifiedCountry import net.corda.core.internal.x500Name import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort @@ -92,13 +91,13 @@ fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT val intermediate: CertificateAndKeyPair = DEV_CA - + val nodeCaName = party.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN) val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val nodeCaCert = X509Utilities.createCertificate( CertificateType.NODE_CA, intermediate.certificate, intermediate.keyPair, - party.name, + nodeCaName, nodeCaKeyPair.public, nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf())) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index ee6b38fdb5..37e019b367 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -13,7 +13,7 @@ import net.corda.demobench.pty.R3Pty import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NotaryInfo -import net.corda.nodeapi.internal.IdentityGenerator +import net.corda.nodeapi.internal.ServiceIdentityGenerator import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory @@ -153,7 +153,10 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Generate notary identity and save it into node's directory. This identity will be used in network parameters. private fun getNotaryIdentity(config: NodeConfigWrapper): Party { - return IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName) + return ServiceIdentityGenerator.generateToDisk( + dirs = listOf(config.nodeDir), + serviceName = config.nodeConfig.myLegalName, + serviceId = "identity") } fun reset() {