mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'open/master' into shams-os-merge-020118
# Conflicts: # client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java # node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt # node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt # node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt # node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt # node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt # node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt # testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
This commit is contained in:
commit
52173219c3
@ -110,7 +110,11 @@ object Crypto {
|
||||
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
||||
)
|
||||
|
||||
/** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */
|
||||
/**
|
||||
* EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing.
|
||||
* The actual algorithm is PureEdDSA Ed25519 as defined in https://tools.ietf.org/html/rfc8032
|
||||
* Not to be confused with the EdDSA variants, Ed25519ctx and Ed25519ph.
|
||||
*/
|
||||
@JvmField
|
||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||
4,
|
||||
|
@ -11,6 +11,8 @@ import net.corda.core.serialization.CordaSerializable
|
||||
@DoNotImplement
|
||||
interface FlowLogicRefFactory {
|
||||
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*>
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
|
@ -27,8 +27,8 @@ import kotlin.test.*
|
||||
*/
|
||||
class CryptoUtilsTest {
|
||||
|
||||
val testString = "Hello World"
|
||||
val testBytes = testString.toByteArray()
|
||||
private val testString = "Hello World"
|
||||
private val testBytes = testString.toByteArray()
|
||||
|
||||
// key generation test
|
||||
@Test
|
||||
|
190
core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt
Normal file
190
core/src/test/kotlin/net/corda/core/crypto/EdDSATests.kt
Normal file
@ -0,0 +1,190 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.utilities.hexToByteArray
|
||||
import net.corda.core.utilities.toHex
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import org.junit.Test
|
||||
import java.security.PrivateKey
|
||||
import java.security.Signature
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
/**
|
||||
* Testing PureEdDSA Ed25519 using test vectors from https://tools.ietf.org/html/rfc8032#section-7.1
|
||||
*/
|
||||
class EdDSATests {
|
||||
@Test
|
||||
fun `PureEdDSA Ed25519 test vectors`() {
|
||||
val edParams = Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
|
||||
|
||||
// MESSAGE (length 0 bytes).
|
||||
val testVector1 = SignatureTestVector(
|
||||
"9d61b19deffd5a60ba844af492ec2cc4" +
|
||||
"4449c5697b326919703bac031cae7f60",
|
||||
"d75a980182b10ab7d54bfed3c964073a" +
|
||||
"0ee172f3daa62325af021a68f707511a",
|
||||
"",
|
||||
"e5564300c360ac729086e2cc806e828a" +
|
||||
"84877f1eb8e5d974d873e06522490155" +
|
||||
"5fb8821590a33bacc61e39701cf9b46b" +
|
||||
"d25bf5f0595bbe24655141438e7a100b"
|
||||
)
|
||||
|
||||
// MESSAGE (length 1 byte).
|
||||
val testVector2 = SignatureTestVector(
|
||||
"4ccd089b28ff96da9db6c346ec114e0f" +
|
||||
"5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
"3d4017c3e843895a92b70aa74d1b7ebc" +
|
||||
"9c982ccf2ec4968cc0cd55f12af4660c",
|
||||
"72",
|
||||
"92a009a9f0d4cab8720e820b5f642540" +
|
||||
"a2b27b5416503f8fb3762223ebdb69da" +
|
||||
"085ac1e43e15996e458f3613d0f11d8c" +
|
||||
"387b2eaeb4302aeeb00d291612bb0c00"
|
||||
)
|
||||
|
||||
// MESSAGE (length 2 bytes).
|
||||
val testVector3 = SignatureTestVector(
|
||||
"c5aa8df43f9f837bedb7442f31dcb7b1" +
|
||||
"66d38535076f094b85ce3a2e0b4458f7",
|
||||
"fc51cd8e6218a1a38da47ed00230f058" +
|
||||
"0816ed13ba3303ac5deb911548908025",
|
||||
"af82",
|
||||
"6291d657deec24024827e69c3abe01a3" +
|
||||
"0ce548a284743a445e3680d7db5ac3ac" +
|
||||
"18ff9b538d16f290ae67f760984dc659" +
|
||||
"4a7c15e9716ed28dc027beceea1ec40a"
|
||||
)
|
||||
|
||||
// MESSAGE (length 1023 bytes).
|
||||
val testVector1024 = SignatureTestVector(
|
||||
"f5e5767cf153319517630f226876b86c" +
|
||||
"8160cc583bc013744c6bf255f5cc0ee5",
|
||||
"278117fc144c72340f67d0f2316e8386" +
|
||||
"ceffbf2b2428c9c51fef7c597f1d426e",
|
||||
"08b8b2b733424243760fe426a4b54908" +
|
||||
"632110a66c2f6591eabd3345e3e4eb98" +
|
||||
"fa6e264bf09efe12ee50f8f54e9f77b1" +
|
||||
"e355f6c50544e23fb1433ddf73be84d8" +
|
||||
"79de7c0046dc4996d9e773f4bc9efe57" +
|
||||
"38829adb26c81b37c93a1b270b20329d" +
|
||||
"658675fc6ea534e0810a4432826bf58c" +
|
||||
"941efb65d57a338bbd2e26640f89ffbc" +
|
||||
"1a858efcb8550ee3a5e1998bd177e93a" +
|
||||
"7363c344fe6b199ee5d02e82d522c4fe" +
|
||||
"ba15452f80288a821a579116ec6dad2b" +
|
||||
"3b310da903401aa62100ab5d1a36553e" +
|
||||
"06203b33890cc9b832f79ef80560ccb9" +
|
||||
"a39ce767967ed628c6ad573cb116dbef" +
|
||||
"efd75499da96bd68a8a97b928a8bbc10" +
|
||||
"3b6621fcde2beca1231d206be6cd9ec7" +
|
||||
"aff6f6c94fcd7204ed3455c68c83f4a4" +
|
||||
"1da4af2b74ef5c53f1d8ac70bdcb7ed1" +
|
||||
"85ce81bd84359d44254d95629e9855a9" +
|
||||
"4a7c1958d1f8ada5d0532ed8a5aa3fb2" +
|
||||
"d17ba70eb6248e594e1a2297acbbb39d" +
|
||||
"502f1a8c6eb6f1ce22b3de1a1f40cc24" +
|
||||
"554119a831a9aad6079cad88425de6bd" +
|
||||
"e1a9187ebb6092cf67bf2b13fd65f270" +
|
||||
"88d78b7e883c8759d2c4f5c65adb7553" +
|
||||
"878ad575f9fad878e80a0c9ba63bcbcc" +
|
||||
"2732e69485bbc9c90bfbd62481d9089b" +
|
||||
"eccf80cfe2df16a2cf65bd92dd597b07" +
|
||||
"07e0917af48bbb75fed413d238f5555a" +
|
||||
"7a569d80c3414a8d0859dc65a46128ba" +
|
||||
"b27af87a71314f318c782b23ebfe808b" +
|
||||
"82b0ce26401d2e22f04d83d1255dc51a" +
|
||||
"ddd3b75a2b1ae0784504df543af8969b" +
|
||||
"e3ea7082ff7fc9888c144da2af58429e" +
|
||||
"c96031dbcad3dad9af0dcbaaaf268cb8" +
|
||||
"fcffead94f3c7ca495e056a9b47acdb7" +
|
||||
"51fb73e666c6c655ade8297297d07ad1" +
|
||||
"ba5e43f1bca32301651339e22904cc8c" +
|
||||
"42f58c30c04aafdb038dda0847dd988d" +
|
||||
"cda6f3bfd15c4b4c4525004aa06eeff8" +
|
||||
"ca61783aacec57fb3d1f92b0fe2fd1a8" +
|
||||
"5f6724517b65e614ad6808d6f6ee34df" +
|
||||
"f7310fdc82aebfd904b01e1dc54b2927" +
|
||||
"094b2db68d6f903b68401adebf5a7e08" +
|
||||
"d78ff4ef5d63653a65040cf9bfd4aca7" +
|
||||
"984a74d37145986780fc0b16ac451649" +
|
||||
"de6188a7dbdf191f64b5fc5e2ab47b57" +
|
||||
"f7f7276cd419c17a3ca8e1b939ae49e4" +
|
||||
"88acba6b965610b5480109c8b17b80e1" +
|
||||
"b7b750dfc7598d5d5011fd2dcc5600a3" +
|
||||
"2ef5b52a1ecc820e308aa342721aac09" +
|
||||
"43bf6686b64b2579376504ccc493d97e" +
|
||||
"6aed3fb0f9cd71a43dd497f01f17c0e2" +
|
||||
"cb3797aa2a2f256656168e6c496afc5f" +
|
||||
"b93246f6b1116398a346f1a641f3b041" +
|
||||
"e989f7914f90cc2c7fff357876e506b5" +
|
||||
"0d334ba77c225bc307ba537152f3f161" +
|
||||
"0e4eafe595f6d9d90d11faa933a15ef1" +
|
||||
"369546868a7f3a45a96768d40fd9d034" +
|
||||
"12c091c6315cf4fde7cb68606937380d" +
|
||||
"b2eaaa707b4c4185c32eddcdd306705e" +
|
||||
"4dc1ffc872eeee475a64dfac86aba41c" +
|
||||
"0618983f8741c5ef68d3a101e8a3b8ca" +
|
||||
"c60c905c15fc910840b94c00a0b9d0",
|
||||
"0aab4c900501b3e24d7cdf4663326a3a" +
|
||||
"87df5e4843b2cbdb67cbf6e460fec350" +
|
||||
"aa5371b1508f9f4528ecea23c436d94b" +
|
||||
"5e8fcd4f681e30a6ac00a9704a188a03"
|
||||
)
|
||||
|
||||
// MESSAGE (length 64 bytes). TEST SHA(abc).
|
||||
val testVectorSHAabc = SignatureTestVector(
|
||||
"833fe62409237b9d62ec77587520911e" +
|
||||
"9a759cec1d19755b7da901b96dca3d42",
|
||||
"ec172b93ad5e563bf4932c70e1245034" +
|
||||
"c35467ef2efd4d64ebf819683467e2bf",
|
||||
"ddaf35a193617abacc417349ae204131" +
|
||||
"12e6fa4e89a97ea20a9eeee64b55d39a" +
|
||||
"2192992a274fc1a836ba3c23a3feebbd" +
|
||||
"454d4423643ce80e2a9ac94fa54ca49f",
|
||||
"dc2a4459e7369633a52b1bf277839a00" +
|
||||
"201009a3efbf3ecb69bea2186c26b589" +
|
||||
"09351fc9ac90b3ecfdfbc7c66431e030" +
|
||||
"3dca179c138ac17ad9bef1177331a704"
|
||||
)
|
||||
|
||||
val testVectors = listOf(testVector1, testVector2, testVector3, testVector1024, testVectorSHAabc)
|
||||
testVectors.forEach {
|
||||
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(it.privateKeyHex.hexToByteArray(), edParams))
|
||||
assertEquals(it.signatureOutputHex, doSign(privateKey, it.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
|
||||
}
|
||||
|
||||
// Test vector for the variant Ed25519ctx, expected to fail.
|
||||
val testVectorEd25519ctx = SignatureTestVector(
|
||||
"0305334e381af78f141cb666f6199f57" +
|
||||
"bc3495335a256a95bd2a55bf546663f6",
|
||||
"dfc9425e4f968f7f0c29f0259cf5f9ae" +
|
||||
"d6851c2bb4ad8bfb860cfee0ab248292",
|
||||
"f726936d19c800494e3fdaff20b276a8",
|
||||
"fc60d5872fc46b3aa69f8b5b4351d580" +
|
||||
"8f92bcc044606db097abab6dbcb1aee3" +
|
||||
"216c48e8b3b66431b5b186d1d28f8ee1" +
|
||||
"5a5ca2df6668346291c2043d4eb3e90d"
|
||||
)
|
||||
|
||||
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(testVectorEd25519ctx.privateKeyHex.hexToByteArray(), edParams))
|
||||
assertNotEquals(testVectorEd25519ctx.signatureOutputHex, doSign(privateKey, testVectorEd25519ctx.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
|
||||
}
|
||||
|
||||
/** A test vector object for digital signature schemes. */
|
||||
private data class SignatureTestVector(val privateKeyHex: String,
|
||||
val publicKeyHex: String,
|
||||
val messageToSignHex: String,
|
||||
val signatureOutputHex: String)
|
||||
|
||||
// Required to implement a custom doSign function, because Corda's Crypto.doSign does not allow empty messages (testVector1).
|
||||
private fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
val signature = Signature.getInstance(Crypto.EDDSA_ED25519_SHA512.signatureName, EdDSASecurityProvider())
|
||||
signature.initSign(privateKey)
|
||||
signature.update(clearData)
|
||||
return signature.sign()
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
@ -11,35 +11,40 @@ import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.junit.Test
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.*
|
||||
import java.util.stream.Stream
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.PKIXParameters
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class X509NameConstraintsTest {
|
||||
|
||||
private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair<KeyStore, KeyStore> {
|
||||
val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys)
|
||||
|
||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public)
|
||||
|
||||
val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, CordaX500Name(commonName = "Corda Client CA", organisation = "R3 Ltd", locality = "London", country = "GB"), clientCAKeyPair.public, nameConstraints = nameConstraints)
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val nodeCaCert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA,
|
||||
intermediateCa.certificate,
|
||||
intermediateCa.keyPair,
|
||||
CordaX500Name("Corda Client CA", "R3 Ltd", "London", "GB"),
|
||||
nodeCaKeyPair.public,
|
||||
nameConstraints = nameConstraints)
|
||||
|
||||
val keyPass = "password"
|
||||
val trustStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||
trustStore.load(null, keyPass.toCharArray())
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate.cert)
|
||||
|
||||
val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public)
|
||||
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, subjectName, tlsKeyPair.public)
|
||||
|
||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||
keyStore.load(null, keyPass.toCharArray())
|
||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(),
|
||||
Stream.of(tlsCert, clientCACert, intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
|
||||
keyStore.addOrReplaceKey(
|
||||
X509Utilities.CORDA_CLIENT_TLS,
|
||||
tlsKeyPair.private,
|
||||
keyPass.toCharArray(),
|
||||
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate))
|
||||
return Pair(keyStore, trustStore)
|
||||
}
|
||||
|
||||
|
@ -28,18 +28,16 @@ Setting your dependencies
|
||||
|
||||
Choosing your Corda version
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The following two lines of the ``build.gradle`` file define the Corda version used to build your CorDapp:
|
||||
``ext.corda_release_version`` and ``ext.corda_gradle_plugins_version`` are used in the ``build.gradle`` to define the
|
||||
versions of Corda and the Corda Gradle Plugins that are used to build your CorDapp.
|
||||
|
||||
For example, to use version 1.0 of Corda and version 1.0 of the Corda gradle plugins, you'd write:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
ext.corda_release_version = '1.0.0'
|
||||
ext.corda_gradle_plugins_version = '1.0.0'
|
||||
|
||||
In this case, our CorDapp will use:
|
||||
|
||||
* Version 1.0 of Corda
|
||||
* Version 1.0 of the Corda gradle plugins
|
||||
|
||||
You can find the latest published version of both here: https://bintray.com/r3/corda.
|
||||
|
||||
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
||||
@ -50,25 +48,32 @@ In certain cases, you may also wish to build against the unstable Master branch.
|
||||
|
||||
Corda dependencies
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
The ``cordformation`` plugin adds:
|
||||
The ``cordformation`` plugin adds two new gradle configurations:
|
||||
|
||||
* ``cordaCompile`` as a new configuration that ``compile`` extends from
|
||||
* ``cordaRuntime`` which ``runtime`` extends from
|
||||
* ``cordaCompile``, which extends ``compile``
|
||||
* ``cordaRuntime``, which extends ``runtime``
|
||||
|
||||
To build against Corda you must add the following to your ``build.gradle`` file;
|
||||
To build against Corda, you must add the following to your ``build.gradle`` file:
|
||||
|
||||
* The ``net.corda:corda:<version>`` JAR as a ``cordaRuntime`` dependency
|
||||
* Each compile dependency (eg ``corda-core``) as a ``cordaCompile`` dependency
|
||||
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
||||
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
|
||||
|
||||
To use Corda's test facilities you must add ``net.corda:corda-test-utils:<version>`` as a ``testCompile`` dependency
|
||||
(i.e. a default Java/Kotlin test compile task).
|
||||
You may also want to add:
|
||||
|
||||
* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test
|
||||
frameworks
|
||||
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
|
||||
built-in development webserver
|
||||
|
||||
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
|
||||
|
||||
Dependencies on other CorDapps
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Sometimes, a CorDapp you build will depend on states, contracts or flows defined in another CorDapp. You must include
|
||||
the CorDapp your CorDapp depends upon as a ``cordapp`` dependency in your ``build.gradle`` file.
|
||||
You CorDapp may also depend on classes defined in another CorDapp, such as states, contracts and flows. There are two
|
||||
ways to add another CorDapp as a dependency in your CorDapp's ``build.gradle`` file:
|
||||
|
||||
* ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project)
|
||||
* ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise)
|
||||
|
||||
Other dependencies
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
@ -10,7 +10,6 @@ import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.webserver.services.WebServerPluginRegistry
|
||||
import java.util.function.Function
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
|
@ -67,10 +67,21 @@ The bootstrapper tool can be built with the command:
|
||||
|
||||
The resulting jar can be found in ``tools/bootstrapper/build/libs/``.
|
||||
|
||||
To use it, run the following command, specifying the root directory which hosts all the node directories as the argument:
|
||||
To use it, create a directory containing a ``node.conf`` file for each node you want to create. Then run the following command:
|
||||
|
||||
``java -jar network-bootstrapper.jar <nodes-root-dir>``
|
||||
|
||||
For example running the command on a directory containing these files :
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
.
|
||||
├── notary.conf // The notary's node.conf file
|
||||
├── partya.conf // Party A's node.conf file
|
||||
└── partyb.conf // Party B's node.conf file
|
||||
|
||||
Would generate directories containing three nodes: notary, partya and partyb.
|
||||
|
||||
Starting the nodes
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -7,25 +7,32 @@
|
||||
Shell
|
||||
=====
|
||||
|
||||
The Corda shell is an embedded command line that allows an administrator to control and monitor the node.
|
||||
Some of its features include:
|
||||
.. contents::
|
||||
|
||||
* Invoking any of the RPCs the node exposes to applications.
|
||||
* Starting flows.
|
||||
* View a dashboard of threads, heap usage, VM properties.
|
||||
* Uploading and downloading zips from the attachment store.
|
||||
* Issue SQL queries to the underlying database.
|
||||
* View JMX metrics and monitoring exports.
|
||||
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data.
|
||||
The Corda shell is an embedded command line that allows an administrator to control and monitor a node. It is based on
|
||||
the `CRaSH`_ shell and supports many of the same features. These features include:
|
||||
|
||||
It is based on the popular `CRaSH`_ shell used in various other projects and supports many of the same features.
|
||||
* Invoking any of the node's RPC methods
|
||||
* Viewing a dashboard of threads, heap usage, VM properties
|
||||
* Uploading and downloading attachments
|
||||
* Issuing SQL queries to the underlying database
|
||||
* Viewing JMX metrics and monitoring exports
|
||||
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data
|
||||
|
||||
Local terminal shell runs only in development mode. It may be disabled by passing the ``--no-local-shell`` flag to the node.
|
||||
The shell via the local terminal
|
||||
--------------------------------
|
||||
|
||||
SSH server
|
||||
----------
|
||||
In development mode, the shell will display in the node's terminal window. It may be disabled by passing the
|
||||
``--no-local-shell`` flag when running the node.
|
||||
|
||||
Shell can also be accessible via SSH. By default SSH server is *disabled*. To enable it port must be configured - in ``node.conf`` file
|
||||
The shell via SSH
|
||||
-----------------
|
||||
The shell is also accessible via SSH.
|
||||
|
||||
Enabling SSH access
|
||||
*******************
|
||||
|
||||
By default, the SSH server is *disabled*. To enable it, a port must be configured in the node's ``node.conf`` file:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@ -33,80 +40,159 @@ Shell can also be accessible via SSH. By default SSH server is *disabled*. To en
|
||||
port = 2222
|
||||
}
|
||||
|
||||
Authentication and authorization
|
||||
--------------------------------
|
||||
SSH requires users to login first - using the same users as RPC system. In fact, the shell serves as a proxy to RPC and communicates
|
||||
with the node using RPC calls. This also means that RPC permissions are enforced. No permissions are required to allow the connection
|
||||
and log in.
|
||||
Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` while starting flows requires
|
||||
``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows`` in addition to a permission for a particular flow.
|
||||
Authentication
|
||||
**************
|
||||
Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates
|
||||
with the node using RPC calls. No RPC permissions are required to allow the connection and log in.
|
||||
|
||||
Host key
|
||||
--------
|
||||
The host key is loaded from the ``<node root directory>/sshkey/hostkey.pem`` file. If this file does not exist, it is
|
||||
generated automatically. In development mode, the seed may be specified to give the same results on the same computer
|
||||
in order to avoid host-checking errors.
|
||||
|
||||
The host key is loaded from ``sshkey/hostkey.pem`` file. If the file does not exist, it will be generated randomly, however
|
||||
in the development mode seed may be tuned to give the same results on the same computer - in order to avoid host checking
|
||||
errors.
|
||||
Connecting to the shell
|
||||
***********************
|
||||
|
||||
Connecting
|
||||
----------
|
||||
Linux and MacOS
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Linux and MacOS computers usually come with SSH client preinstalled. On Windows it usually requires extra download.
|
||||
Usual connection syntax is ``ssh user@host -p 2222`` - where ``user`` is a RPC username, and ``-p`` specifies a port parameters -
|
||||
it's the same as setup in ``node.conf`` file. ``host`` should point to a node hostname, usually ``localhost`` if connecting and
|
||||
running node on the same computer. Password will be asked after establishing connection.
|
||||
Run the following command from the terminal:
|
||||
|
||||
:note: While developing, checking multiple samples or simply restarting a node frequently host key may be regenerated. SSH usually
|
||||
saved once trusted hosts and will refuse to connect in case of a change. Then check may be disabled with extra options
|
||||
``ssh -o StrictHostKeyChecking=no user@host -p2222``. This option should never be used in production environment!
|
||||
.. code:: bash
|
||||
|
||||
Getting help
|
||||
------------
|
||||
ssh -p [portNumber] [host] -l [user]
|
||||
|
||||
You can run ``help`` to list the available commands.
|
||||
Where:
|
||||
|
||||
The shell has a ``man`` command that can be used to get interactive help on many commands. You can also use the
|
||||
``--help`` or ``-h`` flags to a command to get info about what switches it supports.
|
||||
* ``[portNumber]`` is the port number specified in the ``node.conf`` file
|
||||
* ``[host]`` is the node's host (e.g. ``localhost`` if running the node locally)
|
||||
* ``[user]`` is the RPC username
|
||||
|
||||
Commands may have subcommands, in the same style as ``git``. In that case running the command by itself will
|
||||
list the supported subcommands.
|
||||
The RPC password will be requested after a connection is established.
|
||||
|
||||
Starting flows and performing remote method calls
|
||||
-------------------------------------------------
|
||||
:note: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves
|
||||
trusted hosts and will refuse to connect in case of a change. This check can be disabled using the
|
||||
``-o StrictHostKeyChecking=no`` flag. This option should never be used in production environment!
|
||||
|
||||
**Flows** are the way the ledger is changed. If you aren't familiar with them, please review ":doc:`flow-state-machines`"
|
||||
first. The ``flow list`` command can be used to list the flows understood by the node, ``flow watch`` shows all the flows
|
||||
currently running on the node with the result (or error) information in a user friendly way, ``flow start`` can be
|
||||
used to start flows. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and
|
||||
then the data to be passed to the flow constructor. The unambiguous substring feature is helpful for reducing
|
||||
the needed typing. If the match is ambiguous the possible matches will be printed out. If a flow has multiple
|
||||
constructors then the names and types of the arguments will be used to try and determine which to use automatically.
|
||||
If the match against available constructors is unclear, the reasons each available constructor failed to match
|
||||
will be printed out. In the case of an ambiguous match, the first applicable will be used.
|
||||
Windows
|
||||
^^^^^^^
|
||||
|
||||
**RPCs** (remote procedure calls) are commands that can be sent to the node to query it, control it and manage it.
|
||||
RPCs don't typically do anything that changes the global ledger, but they may change node-specific data in the
|
||||
database. Each RPC is one method on the ``CordaRPCOps`` interface, and may return a stream of events that will
|
||||
be shown on screen until you press Ctrl-C. You perform an RPC by using ``run`` followed by the name.
|
||||
Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used.
|
||||
|
||||
.. raw:: html
|
||||
Permissions
|
||||
***********
|
||||
|
||||
<center><b><a href="api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html">Documentation of available RPCs</a></b><p></center>
|
||||
When accessing the shell via SSH, some additional RPC permissions are required:
|
||||
|
||||
Whichever form of change is used, there is a need to provide *parameters* to either the RPC or the flow
|
||||
constructor. Because parameters can be any arbitrary Java object graph, we need a convenient syntax to express
|
||||
this sort of data. The shell uses a syntax called `Yaml`_ to do this.
|
||||
* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``
|
||||
* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows``, as well as a
|
||||
permission for the flow being started
|
||||
|
||||
Data syntax
|
||||
-----------
|
||||
Interacting with the node via the shell
|
||||
---------------------------------------
|
||||
|
||||
Yaml (yet another markup language) is a simple JSON-like way to describe object graphs. It has several features
|
||||
that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can
|
||||
often skip the quotes around strings. Here is an example of how this syntax is used:
|
||||
The shell interacts with the node by issuing RPCs (remote procedure calls). You make an RPC from the shell by typing
|
||||
``run`` followed by the name of the desired RPC method. For example, you'd see a list of the registered flows on your
|
||||
node by running:
|
||||
|
||||
``run registeredFlows``
|
||||
|
||||
Some RPCs return a stream of events that will be shown on screen until you press Ctrl-C.
|
||||
|
||||
You can find a list of the available RPC methods
|
||||
`here <https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html>`_.
|
||||
|
||||
Flow commands
|
||||
*************
|
||||
|
||||
The shell also has special commands for working with flows:
|
||||
|
||||
* ``flow list`` lists the flows available on the node
|
||||
* ``flow watch`` shows all the flows currently running on the node with result (or error) information
|
||||
* ``flow start`` starts a flow. The ``flow start`` command takes the name of a flow class, or
|
||||
*any unambiguous substring* thereof, as well as the data to be passed to the flow constructor. If there are several
|
||||
matches for a given substring, the possible matches will be printed out. If a flow has multiple constructors then the
|
||||
names and types of the arguments will be used to try and automatically determine which one to use. If the match
|
||||
against available constructors is unclear, the reasons each available constructor failed to match will be printed
|
||||
out. In the case of an ambiguous match, the first applicable constructor will be used
|
||||
|
||||
Parameter syntax
|
||||
****************
|
||||
|
||||
Parameters are passed to RPC or flow commands using a syntax called `Yaml`_ (yet another markup language), a
|
||||
simple JSON-like language. The key features of Yaml are:
|
||||
|
||||
* Parameters are separated by commas
|
||||
* Each parameter is specified as a ``key: value`` pair
|
||||
|
||||
* There **MUST** to be a space after the colon, otherwise you'll get a syntax error
|
||||
|
||||
* Strings do not need to be surrounded by quotes unless they contain commas, colons or embedded quotes
|
||||
* Class names must be fully-qualified (e.g. ``java.lang.String``)
|
||||
|
||||
.. note:: If your CorDapp is written in Java, named arguments won't work unless you compiled the node using the
|
||||
``-parameters`` argument to javac. See :doc:`generating-a-node` for how to specify it via Gradle.
|
||||
|
||||
Creating an instance of a class
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Class instances are created using curly-bracket syntax. For example, if we have a ``Campaign`` class with the following
|
||||
constructor:
|
||||
|
||||
``data class Campaign(val name: String, val target: Int)``
|
||||
|
||||
Then we could create an instance of this class to pass as a parameter as follows:
|
||||
|
||||
``newCampaign: { name: Roger, target: 1000 }``
|
||||
|
||||
Where ``newCampaign`` is a parameter of type ``Campaign``.
|
||||
|
||||
Mappings from strings to types
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Several parameter types can automatically be mapped from strings. See the `defined parsers`_ for more information. We
|
||||
cover the most common types here.
|
||||
|
||||
Amount
|
||||
~~~~~~
|
||||
A parameter of type ``Amount<Currency>`` can be written as either:
|
||||
|
||||
* A dollar ($), pound (£) or euro (€) symbol followed by the amount as a decimal
|
||||
* The amount as a decimal followed by the ISO currency code (e.g. "100.12 CHF")
|
||||
|
||||
OpaqueBytes
|
||||
~~~~~~~~~~~
|
||||
A parameter of type ``OpaqueBytes`` can be provided as a string, which will be automatically converted to
|
||||
``OpaqueBytes``.
|
||||
|
||||
Party
|
||||
~~~~~
|
||||
A parameter of type ``Party`` can be written in several ways:
|
||||
|
||||
* By using the node's full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"``
|
||||
* By specifying the organisation name only: ``"Monogram Bank"``
|
||||
* By specifying any other non-ambiguous part of the name: ``"Sao Paulo"`` (if only one network node is located in Sao
|
||||
Paulo)
|
||||
|
||||
Instant
|
||||
~~~~~~~
|
||||
A parameter of type ``Instant`` can be written as follows: ``"2017-12-22T00:00:00Z"``.
|
||||
|
||||
Examples
|
||||
^^^^^^^^
|
||||
|
||||
Starting a flow
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
We would start the ``CashIssue`` flow as follows:
|
||||
|
||||
``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"``
|
||||
|
||||
This invokes a constructor of a flow with the following prototype in the code:
|
||||
This breaks down as follows:
|
||||
|
||||
* ``flow start`` is a shell command for starting a flow
|
||||
* ``CashIssue`` is the flow we want to start
|
||||
* Each ``name: value`` pair after that is a flow constructor argument
|
||||
|
||||
This command invokes the following ``CashIssue`` constructor:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -117,50 +203,44 @@ This invokes a constructor of a flow with the following prototype in the code:
|
||||
val recipient: Party,
|
||||
val notary: Party) : AbstractCashFlow(progressTracker)
|
||||
|
||||
Here, everything after ``CashIssue`` is specifying the arguments to the constructor of a flow. In Yaml, an object
|
||||
is specified as a set of ``key: value`` pairs and in our form, we separate them by commas. There are a few things
|
||||
to note about this syntax:
|
||||
Querying the vault
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* When a parameter is of type ``Amount<Currency>`` you can write it as either one of the dollar symbol ($),
|
||||
pound (£), euro (€) followed by the amount as a decimal, or as the value followed by the ISO currency code
|
||||
e.g. "100.12 CHF"
|
||||
* ``OpaqueBytes`` is filled with the contents of whatever is provided as a string.
|
||||
* ``Party`` objects are looked up by name.
|
||||
* Strings do not need to be surrounded by quotes unless they contain a comma or embedded quotes. This makes it
|
||||
a lot more convenient to type such strings.
|
||||
We would query the vault for ``IOUState`` states as follows:
|
||||
|
||||
Other types also have sensible mappings from strings. See `the defined parsers`_ for more information.
|
||||
``run vaultQuery contractStateType: com.template.IOUState``
|
||||
|
||||
Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This is helpful when no particular
|
||||
parser is defined for the type you need, for instance, if an API requires a ``Pair<String, Int>``
|
||||
which could be represented as ``{ first: foo, second: 123 }``.
|
||||
This breaks down as follows:
|
||||
|
||||
.. note:: If your CorDapp is written in Java,
|
||||
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
|
||||
See :doc:`generating-a-node` for how to specify it via Gradle.
|
||||
|
||||
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:
|
||||
|
||||
``run registeredFlows``
|
||||
* ``run`` is a shell command for making an RPC call
|
||||
* ``vaultQuery`` is the RPC call we want to make
|
||||
* ``contractStateType: com.template.IOUState`` is the fully-qualified name of the state type we are querying for
|
||||
|
||||
Attachments
|
||||
-----------
|
||||
***********
|
||||
|
||||
The shell can be used to upload and download attachments from the node interactively. To learn more, see
|
||||
the tutorial ":doc:`tutorial-attachments`".
|
||||
The shell can be used to upload and download attachments from the node. To learn more, see the tutorial
|
||||
":doc:`tutorial-attachments`".
|
||||
|
||||
Getting help
|
||||
************
|
||||
|
||||
You can type ``help`` in the shell to list the available commands, and ``man`` to get interactive help on many
|
||||
commands. You can also pass the ``--help`` or ``-h`` flags to a command to get info about what switches it supports.
|
||||
|
||||
Commands may have subcommands, in the same style as ``git``. In that case, running the command by itself will
|
||||
list the supported subcommands.
|
||||
|
||||
Extending the shell
|
||||
-------------------
|
||||
|
||||
The shell can be extended using commands written in either Java or `Groovy`_ (Groovy is a scripting language that
|
||||
is Java compatible). Such commands have full access to the node internal APIs and thus can be used to achieve
|
||||
almost anything.
|
||||
The shell can be extended using commands written in either Java or `Groovy`_ (a Java-compatible scripting language).
|
||||
These commands have full access to the node's internal APIs and thus can be used to achieve almost anything.
|
||||
|
||||
A full tutorial on how to write such commands is out of scope for this documentation, to learn more please
|
||||
refer to the `CRaSH`_ documentation. New commands can be placed in the ``shell-commands`` subdirectory in the
|
||||
node directory. Edits to existing commands will be used automatically, but at this time commands added after the
|
||||
node has started won't be automatically detected. Commands should be named in all lower case with either a
|
||||
``.java`` or ``.groovy`` extension.
|
||||
A full tutorial on how to write such commands is out of scope for this documentation. To learn more, please refer to
|
||||
the `CRaSH`_ documentation. New commands are placed in the ``shell-commands`` subdirectory in the node directory. Edits
|
||||
to existing commands will be used automatically, but currently commands added after the node has started won't be
|
||||
automatically detected. Commands must have names all in lower-case with either a ``.java`` or ``.groovy`` extension.
|
||||
|
||||
.. warning:: Commands written in Groovy ignore Java security checks, so have unrestricted access to node and JVM
|
||||
internals regardless of any sandboxing that may be in place. Don't allow untrusted users to edit files in the
|
||||
@ -171,14 +251,13 @@ Limitations
|
||||
|
||||
The shell will be enhanced over time. The currently known limitations include:
|
||||
|
||||
* SSH access is currently not available.
|
||||
* There is no command completion for flows or RPCs.
|
||||
* Command history is not preserved across restarts.
|
||||
* The ``jdbc`` command requires you to explicitly log into the database first.
|
||||
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted.
|
||||
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using.
|
||||
* There is no command completion for flows or RPCs
|
||||
* Command history is not preserved across restarts
|
||||
* The ``jdbc`` command requires you to explicitly log into the database first
|
||||
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted
|
||||
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using
|
||||
|
||||
.. _Yaml: http://www.yaml.org/spec/1.2/spec.html
|
||||
.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
|
||||
.. _defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
|
||||
.. _Groovy: http://groovy-lang.org/
|
||||
.. _CRaSH: http://www.crashub.org/
|
||||
|
@ -35,8 +35,14 @@ the transaction to the regulator. There are two important aspects to note here:
|
||||
If the states define a relational mapping (see :doc:`api-persistence`) then the regulator will be able to query the
|
||||
reports from their database and observe new transactions coming in via RPC.
|
||||
|
||||
.. warning:: Nodes which act as both observers and which directly take part in the ledger are not supported at this
|
||||
time. In particular, coin selection may return states which you do not have the private keys to be able to sign
|
||||
for. Future versions of Corda may address this issue, but for now, if you wish to both participate in the ledger
|
||||
and also observe transactions that you can't sign for you will need to run two nodes and have two separate
|
||||
identities.
|
||||
Caveats
|
||||
-------
|
||||
|
||||
* Nodes which act as both observers and direct participants in the ledger are not supported at this time. In
|
||||
particular, coin selection may return states which you do not have the private keys to be able to sign for. Future
|
||||
versions of Corda may address this issue, but for now, if you wish to both participate in the ledger and also observe
|
||||
transactions that you can't sign for you will need to run two nodes and have two separate identities
|
||||
|
||||
* Nodes only record each transaction once. If a node has already recorded a transaction in non-observer mode, it cannot
|
||||
later re-record the same transaction as an observer. This issue is tracked here:
|
||||
https://r3-cev.atlassian.net/browse/CORDA-883
|
@ -26,7 +26,7 @@ additional classes are used when the jar is invoked directly. To do this we'll u
|
||||
./gradlew experimental:quasar-hook:jar
|
||||
./gradlew samples:irs-demo:deployNodes
|
||||
cd samples/irs-demo/build/nodes/NotaryService
|
||||
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix -jar corda.jar
|
||||
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix,org.jolokia -jar corda.jar
|
||||
```
|
||||
|
||||
Once the node is started just exit the node.
|
||||
|
@ -90,8 +90,12 @@ private abstract class JavaCommand(
|
||||
add(getJavaPath())
|
||||
addAll(jvmArgs)
|
||||
add("-Dname=$nodeName")
|
||||
null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
||||
null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
|
||||
val jvmArgs: MutableList<String> = mutableListOf()
|
||||
null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
||||
null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
|
||||
if (jvmArgs.isNotEmpty()) {
|
||||
add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}")
|
||||
}
|
||||
add("-jar")
|
||||
add(jarName)
|
||||
init()
|
||||
|
@ -0,0 +1,80 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Contains utility methods for generating identities for a node.
|
||||
*
|
||||
* WARNING: This is not application for production use.
|
||||
*/
|
||||
object DevIdentityGenerator {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
// TODO These don't need to be prefixes but can be the full aliases
|
||||
// TODO Move these constants out of here as the node needs access to them
|
||||
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
|
||||
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
|
||||
|
||||
/** Install a node key store for the given node directory using the given legal name. */
|
||||
fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party {
|
||||
val nodeSslConfig = object : NodeSSLConfiguration {
|
||||
override val baseDirectory = nodeDir
|
||||
override val keyStorePassword: String = "cordacadevpass"
|
||||
override val trustStorePassword get() = throw NotImplementedError("Not expected to be called")
|
||||
}
|
||||
|
||||
// TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location
|
||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||
|
||||
nodeSslConfig.certificatesDirectory.createDirectories()
|
||||
nodeSslConfig.createDevKeyStores(rootCert.toX509CertHolder(), intermediateCa, legalName)
|
||||
|
||||
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
|
||||
val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair())
|
||||
return identity.party
|
||||
}
|
||||
|
||||
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||
require(dirs.isNotEmpty())
|
||||
|
||||
log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||
val compositeKey = 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 = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||
|
||||
keyPairs.zip(dirs) { keyPair, nodeDir ->
|
||||
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
|
||||
X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, notaryName, publicKey)
|
||||
}
|
||||
val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks"
|
||||
val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass")
|
||||
keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert.cert)
|
||||
keystore.setKeyEntry(
|
||||
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
|
||||
keyPair.private,
|
||||
"cordacadevkeypass".toCharArray(),
|
||||
arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert))
|
||||
keystore.save(distServKeyStoreFile, "cordacadevpass")
|
||||
}
|
||||
|
||||
return Party(notaryName, compositeKey)
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
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<Path>, 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 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.
|
||||
*/
|
||||
private fun generateToDisk(dirs: List<Path>,
|
||||
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 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, 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("$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(name, key)
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.x500Name
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
|
||||
/**
|
||||
* Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using
|
||||
* the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA.
|
||||
*/
|
||||
fun SSLConfiguration.createDevKeyStores(rootCert: X509CertificateHolder, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) {
|
||||
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
|
||||
|
||||
loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply {
|
||||
addOrReplaceKey(
|
||||
X509Utilities.CORDA_CLIENT_CA,
|
||||
nodeCaKeyPair.private,
|
||||
keyStorePassword.toCharArray(),
|
||||
arrayOf(nodeCaCert, intermediateCa.certificate, rootCert))
|
||||
save(nodeKeystore, keyStorePassword)
|
||||
}
|
||||
|
||||
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName, tlsKeyPair.public)
|
||||
|
||||
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
|
||||
addOrReplaceKey(
|
||||
X509Utilities.CORDA_CLIENT_TLS,
|
||||
tlsKeyPair.private,
|
||||
keyStorePassword.toCharArray(),
|
||||
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert))
|
||||
save(sslKeystore, keyStorePassword)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
|
||||
* [CordaX500Name] as the cert subject.
|
||||
*/
|
||||
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||
val cert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA,
|
||||
intermediateCa.certificate,
|
||||
intermediateCa.keyPair,
|
||||
legalName,
|
||||
keyPair.public,
|
||||
nameConstraints = nameConstraints)
|
||||
return CertificateAndKeyPair(cert, keyPair)
|
||||
}
|
@ -8,6 +8,7 @@ interface SSLConfiguration {
|
||||
val trustStorePassword: String
|
||||
val certificatesDirectory: Path
|
||||
val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks"
|
||||
// TODO This looks like it should be in NodeSSLConfiguration
|
||||
val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks"
|
||||
val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks"
|
||||
}
|
||||
|
@ -1,41 +1,26 @@
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.read
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
|
||||
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
||||
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
|
||||
|
||||
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
|
||||
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
||||
// TODO This method seems misplaced in this class.
|
||||
fun storeLegalIdentity(legalName: CordaX500Name, alias: String, keyPair: KeyPair): PartyAndCertificate {
|
||||
val nodeCaCertChain = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
||||
val nodeCa = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCa.certificate, nodeCa.keyPair, legalName, keyPair.public)
|
||||
val identityCertPath = X509CertificateFactory().generateCertPath(identityCert.cert, *nodeCaCertChain)
|
||||
// Assume key password = store password.
|
||||
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
// Create new keys and store in keystore.
|
||||
val cert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
|
||||
val certPath = X509CertificateFactory().generateCertPath(cert.cert, *clientCertPath)
|
||||
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
|
||||
// TODO: X509Utilities.validateCertificateChain()
|
||||
return certPath
|
||||
}
|
||||
|
||||
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
|
||||
val certPath = createCertificate(serviceName, keyPair.public)
|
||||
// Assume key password = store password.
|
||||
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
|
||||
keyStore.save(storePath, storePassword)
|
||||
}
|
||||
|
||||
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
|
||||
val certPath = createCertificate(serviceName, pubKey)
|
||||
// Assume key password = store password.
|
||||
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
|
||||
keyStore.addOrReplaceKey(alias, keyPair.private, storePassword.toCharArray(), identityCertPath.certificates.toTypedArray())
|
||||
keyStore.save(storePath, storePassword)
|
||||
return PartyAndCertificate(identityCertPath)
|
||||
}
|
||||
|
||||
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
|
||||
@ -47,5 +32,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St
|
||||
|
||||
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
|
||||
|
||||
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
|
||||
fun getCertificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
|
||||
}
|
@ -4,12 +4,8 @@ import net.corda.core.CordaOID
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
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.x500Name
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
import org.bouncycastle.asn1.*
|
||||
@ -43,6 +39,7 @@ object X509Utilities {
|
||||
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
|
||||
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
||||
|
||||
// TODO This class is more of a general purpose utility class and as such these constants belong elsewhere
|
||||
// Aliases for private keys and certificates.
|
||||
const val CORDA_ROOT_CA = "cordarootca"
|
||||
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
|
||||
|
@ -4,23 +4,28 @@ import com.typesafe.config.ConfigFactory
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
@ -48,13 +53,14 @@ class NetworkBootstrapper {
|
||||
fun bootstrap(directory: Path) {
|
||||
directory.createDirectories()
|
||||
println("Bootstrapping local network in $directory")
|
||||
generateDirectoriesIfNeeded(directory)
|
||||
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
|
||||
require(nodeDirs.isNotEmpty()) { "No nodes found" }
|
||||
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
|
||||
val processes = startNodeInfoGeneration(nodeDirs)
|
||||
initialiseSerialization()
|
||||
try {
|
||||
println("Waiting for all nodes to generate their node-info files")
|
||||
println("Waiting for all nodes to generate their node-info files...")
|
||||
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
|
||||
println("Distributing all node info-files to all nodes")
|
||||
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
||||
@ -69,6 +75,27 @@ class NetworkBootstrapper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDirectoriesIfNeeded(directory: Path) {
|
||||
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() }
|
||||
if (confFiles.isEmpty()) return
|
||||
println("Node config files found in the root directory - generating node directories")
|
||||
val cordaJar = extractCordaJarTo(directory)
|
||||
for (confFile in confFiles) {
|
||||
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
|
||||
println("Generating directory for $nodeName")
|
||||
val nodeDir = (directory / nodeName).createDirectory()
|
||||
confFile.moveTo(nodeDir / "node.conf")
|
||||
Files.copy(cordaJar, (nodeDir / "corda.jar"))
|
||||
}
|
||||
Files.delete(cordaJar)
|
||||
}
|
||||
|
||||
private fun extractCordaJarTo(directory: Path): Path {
|
||||
val cordaJarPath = (directory / "corda.jar")
|
||||
Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath)
|
||||
return cordaJarPath
|
||||
}
|
||||
|
||||
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
|
||||
return nodeDirs.map { nodeDir ->
|
||||
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
||||
@ -82,18 +109,25 @@ class NetworkBootstrapper {
|
||||
}
|
||||
|
||||
private fun gatherNodeInfoFiles(processes: List<Process>, nodeDirs: List<Path>): List<Path> {
|
||||
val timeOutInSeconds = 60L
|
||||
return processes.zip(nodeDirs).map { (process, nodeDir) ->
|
||||
check(process.waitFor(timeOutInSeconds, SECONDS)) {
|
||||
"Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
||||
}
|
||||
check(process.exitValue() == 0) {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
val future = executor.fork {
|
||||
processes.zip(nodeDirs).map { (process, nodeDir) ->
|
||||
check(process.waitFor() == 0) {
|
||||
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
||||
}
|
||||
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
future.getOrThrow(60.seconds)
|
||||
} catch (e: TimeoutException) {
|
||||
println("...still waiting. If this is taking longer than usual, check the node logs.")
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
|
||||
for (nodeDir in nodeDirs) {
|
||||
val additionalNodeInfosDir = (nodeDir / CordformNode.NODE_INFO_DIRECTORY).createDirectories()
|
||||
@ -161,6 +195,7 @@ class NetworkBootstrapper {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
@ -12,12 +12,17 @@ import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.config.createKeystoreForCordaNode
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.createDevKeyStores
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.TestIdentity
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
@ -33,11 +38,8 @@ import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.stream.Stream
|
||||
@ -52,6 +54,11 @@ class X509UtilitiesTest {
|
||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
val BOB get() = bob.party
|
||||
val BOB_PUBKEY get() = bob.publicKey
|
||||
val CIPHER_SUITES = arrayOf(
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@ -156,106 +163,61 @@ class X509UtilitiesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create full CA keystore`() {
|
||||
val tmpKeyStore = tempFile("keystore.jks")
|
||||
val tmpTrustStore = tempFile("truststore.jks")
|
||||
|
||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
||||
createCAKeyStoreAndTrustStore(tmpKeyStore, "keystorepass", "keypass", tmpTrustStore, "trustpass")
|
||||
|
||||
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
|
||||
val keyStore = loadKeyStore(tmpKeyStore, "keystorepass")
|
||||
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
|
||||
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
||||
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey
|
||||
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
||||
assertEquals(rootCaCert, rootCaFromTrustStore)
|
||||
rootCaCert.checkValidity(Date())
|
||||
rootCaCert.verify(rootCaCert.publicKey)
|
||||
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val testData = "12345".toByteArray()
|
||||
val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
|
||||
|
||||
// Load back generated intermediate CA Cert and private key
|
||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate
|
||||
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey
|
||||
intermediateCaCert.checkValidity(Date())
|
||||
intermediateCaCert.verify(rootCaCert.publicKey)
|
||||
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
|
||||
fun `create server certificate in keystore for SSL`() {
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val certificatesDirectory = tempFolder.root.toPath()
|
||||
override val keyStorePassword = "serverstorepass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create server certificate in keystore for SSL`() {
|
||||
val tmpCAKeyStore = tempFile("keystore.jks")
|
||||
val tmpTrustStore = tempFile("truststore.jks")
|
||||
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
|
||||
val tmpServerKeyStore = tempFile("serverkeystore.jks")
|
||||
|
||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
||||
createCAKeyStoreAndTrustStore(tmpCAKeyStore,
|
||||
"cakeystorepass",
|
||||
"cakeypass",
|
||||
tmpTrustStore,
|
||||
"trustpass")
|
||||
|
||||
// Load signing intermediate CA cert
|
||||
val caKeyStore = loadKeyStore(tmpCAKeyStore, "cakeystorepass")
|
||||
val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass")
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
|
||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
|
||||
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
||||
|
||||
// Load back server certificate
|
||||
val serverKeyStore = loadKeyStore(tmpServerKeyStore, "serverstorepass")
|
||||
val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass")
|
||||
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
|
||||
val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword)
|
||||
|
||||
serverCertAndKey.certificate.isValidOn(Date())
|
||||
serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo))
|
||||
serverCert.cert.checkValidity()
|
||||
serverCert.cert.verify(intermediateCa.certificate.cert.publicKey)
|
||||
assertThat(CordaX500Name.parse(serverCert.subject.toString())).isEqualTo(MEGA_CORP.name)
|
||||
|
||||
assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
|
||||
// Load back SSL certificate
|
||||
val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||
val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword)
|
||||
|
||||
// Load back server certificate
|
||||
val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
|
||||
val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass")
|
||||
sslCert.cert.checkValidity()
|
||||
sslCert.cert.verify(serverCert.cert.publicKey)
|
||||
assertThat(CordaX500Name.parse(sslCert.subject.toString())).isEqualTo(MEGA_CORP.name)
|
||||
|
||||
sslCertAndKey.certificate.isValidOn(Date())
|
||||
sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo))
|
||||
|
||||
assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val testData = "123456".toByteArray()
|
||||
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
|
||||
val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo)
|
||||
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverKeyPair.private, testData)
|
||||
val publicKey = Crypto.toSupportedPublicKey(serverCert.subjectPublicKeyInfo)
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create server cert and use in SSL socket`() {
|
||||
val tmpCAKeyStore = tempFile("keystore.jks")
|
||||
val tmpTrustStore = tempFile("truststore.jks")
|
||||
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
|
||||
val tmpServerKeyStore = tempFile("serverkeystore.jks")
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val certificatesDirectory = tempFolder.root.toPath()
|
||||
override val keyStorePassword = "serverstorepass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
}
|
||||
|
||||
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
|
||||
val caKeyStore = createCAKeyStoreAndTrustStore(tmpCAKeyStore,
|
||||
"cakeystorepass",
|
||||
"cakeypass",
|
||||
tmpTrustStore,
|
||||
"trustpass")
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
|
||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name)
|
||||
val keyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
|
||||
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
|
||||
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
|
||||
sslConfig.createTrustStore(rootCa.certificate.cert)
|
||||
|
||||
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
|
||||
val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
|
||||
|
||||
val context = SSLContext.getInstance("TLS")
|
||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore, "serverstorepass".toCharArray())
|
||||
keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray())
|
||||
val keyManagers = keyManagerFactory.keyManagers
|
||||
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustMgrFactory.init(trustStore)
|
||||
@ -266,13 +228,7 @@ class X509UtilitiesTest {
|
||||
val clientSocketFactory = context.socketFactory
|
||||
|
||||
val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket
|
||||
val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
|
||||
arrayOf("TLSv1.2"))
|
||||
val serverParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
|
||||
serverParams.wantClientAuth = true
|
||||
serverParams.needClientAuth = true
|
||||
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||
@ -280,13 +236,7 @@ class X509UtilitiesTest {
|
||||
serverSocket.useClientMode = false
|
||||
|
||||
val clientSocket = clientSocketFactory.createSocket() as SSLSocket
|
||||
val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
|
||||
arrayOf("TLSv1.2"))
|
||||
val clientParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
|
||||
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||
clientSocket.sslParameters = clientParams
|
||||
clientSocket.useClientMode = true
|
||||
@ -344,60 +294,14 @@ class X509UtilitiesTest {
|
||||
|
||||
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
|
||||
|
||||
/**
|
||||
* All in one wrapper to manufacture a root CA cert and an Intermediate CA cert.
|
||||
* Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs
|
||||
* @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into.
|
||||
* @param storePassword The storage password to protect access to the generated KeyStore and public certificates
|
||||
* @param keyPassword The password that protects the CA private keys.
|
||||
* Unlike the SSL libraries that tend to assume the password is the same as the keystore password.
|
||||
* These CA private keys should be protected more effectively with a distinct password.
|
||||
* @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore
|
||||
* @param trustStorePassword The password to protect the truststore
|
||||
* @return The KeyStore object that was saved to file
|
||||
*/
|
||||
private fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path,
|
||||
storePassword: String,
|
||||
keyPassword: String,
|
||||
trustStoreFilePath: Path,
|
||||
trustStorePassword: String
|
||||
): KeyStore {
|
||||
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
|
||||
|
||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCACert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
rootCACert,
|
||||
rootCAKey,
|
||||
baseName.copy(commonName = "Corda Node Intermediate CA"),
|
||||
intermediateCAKeyPair.public)
|
||||
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||
|
||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf<Certificate>(rootCACert.cert))
|
||||
|
||||
keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA,
|
||||
intermediateCAKeyPair.private,
|
||||
keyPass,
|
||||
Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
|
||||
|
||||
keyStore.save(keyStoreFilePath, storePassword)
|
||||
|
||||
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
|
||||
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert)
|
||||
|
||||
trustStore.save(trustStoreFilePath, trustStorePassword)
|
||||
|
||||
return keyStore
|
||||
private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) {
|
||||
val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword)
|
||||
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
trustStore.save(trustStoreFile, trustStorePassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Get correct private key type from Keystore`() {
|
||||
fun `get correct private key type from Keystore`() {
|
||||
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
|
||||
|
@ -44,7 +44,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
|
||||
applicationVersion = corda_release_version
|
||||
appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||
// See experimental/quasar-hook/README.md for how to generate.
|
||||
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
|
||||
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
||||
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
|
||||
minJavaVersion = '1.8.0'
|
||||
|
@ -14,11 +14,9 @@ import net.corda.testing.IntegrationTestSchemas
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.toDatabaseSchemaName
|
||||
import org.junit.ClassRule
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NodeKeystoreCheckTest : IntegrationTest() {
|
||||
companion object {
|
||||
@ -27,15 +25,17 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
||||
fun `starting node in non-dev mode with no key store`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
// This will fail because there are no keystore configured.
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
assertThatThrownBy {
|
||||
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
}.apply {
|
||||
assertTrue(message?.startsWith("Identity certificate not found. ") ?: false)
|
||||
}.hasMessageContaining("Identity certificate not found")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
// Create keystores
|
||||
val keystorePassword = "password"
|
||||
val config = object : SSLConfiguration {
|
||||
@ -46,9 +46,12 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
||||
config.configureDevKeyAndTrustStores(ALICE_NAME)
|
||||
|
||||
// This should pass with correct keystore.
|
||||
val node = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false,
|
||||
val node = startNode(
|
||||
providedName = ALICE_NAME,
|
||||
customOverrides = mapOf("devMode" to false,
|
||||
"keyStorePassword" to keystorePassword,
|
||||
"trustStorePassword" to keystorePassword)).get()
|
||||
"trustStorePassword" to keystorePassword)
|
||||
).getOrThrow()
|
||||
node.stop()
|
||||
|
||||
// Fiddle with node keystore.
|
||||
@ -62,11 +65,9 @@ class NodeKeystoreCheckTest : IntegrationTest() {
|
||||
keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert))
|
||||
keystore.save(config.nodeKeystore, config.keyStorePassword)
|
||||
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
assertThatThrownBy {
|
||||
startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
}.apply {
|
||||
assertEquals("Client CA certificate must chain to the trusted root.", message)
|
||||
}
|
||||
}.hasMessage("Client CA certificate must chain to the trusted root.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,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.IdentityGenerator
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||
import net.corda.testing.IntegrationTest
|
||||
@ -69,7 +69,7 @@ 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 = DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
|
||||
CordaX500Name("BFT", "Zurich", "CH"))
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.readAll
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -10,6 +15,10 @@ import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||
@ -18,6 +27,8 @@ import net.corda.testing.node.internal.network.NetworkMapServer
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.*
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class NetworkMapTest : IntegrationTest() {
|
||||
@ -29,6 +40,7 @@ class NetworkMapTest : IntegrationTest() {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val cacheTimeout = 1.seconds
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
|
||||
@ -39,7 +51,9 @@ class NetworkMapTest : IntegrationTest() {
|
||||
fun start() {
|
||||
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
|
||||
val address = networkMapServer.start()
|
||||
compatibilityZone = CompatibilityZoneParams(URL("http://$address"))
|
||||
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), publishNotaries = {
|
||||
networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
|
||||
})
|
||||
}
|
||||
|
||||
@After
|
||||
@ -49,25 +63,35 @@ class NetworkMapTest : IntegrationTest() {
|
||||
|
||||
@Test
|
||||
fun `node correctly downloads and saves network parameters file on startup`() {
|
||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false,
|
||||
notarySpecs = emptyList()
|
||||
) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val networkParameters = alice.configuration.baseDirectory
|
||||
.list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() }
|
||||
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
||||
.readAll()
|
||||
.deserialize<SignedData<NetworkParameters>>()
|
||||
.verified()
|
||||
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters)
|
||||
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
|
||||
// from the server
|
||||
assertEquals(networkMapServer.networkParameters, networkParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes can see each other using the http network map`() {
|
||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
||||
val alice = startNode(providedName = ALICE_NAME)
|
||||
val bob = startNode(providedName = BOB_NAME)
|
||||
val notaryNode = defaultNotaryNode.get()
|
||||
val aliceNode = alice.get()
|
||||
val bobNode = bob.get()
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false
|
||||
) {
|
||||
val (aliceNode, bobNode, notaryNode) = listOf(
|
||||
startNode(providedName = ALICE_NAME),
|
||||
startNode(providedName = BOB_NAME),
|
||||
defaultNotaryNode
|
||||
).transpose().getOrThrow()
|
||||
|
||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
@ -77,15 +101,20 @@ class NetworkMapTest : IntegrationTest() {
|
||||
|
||||
@Test
|
||||
fun `nodes process network map add updates correctly when adding new node to network map`() {
|
||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
||||
val alice = startNode(providedName = ALICE_NAME)
|
||||
val notaryNode = defaultNotaryNode.get()
|
||||
val aliceNode = alice.get()
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false
|
||||
) {
|
||||
val (aliceNode, notaryNode) = listOf(
|
||||
startNode(providedName = ALICE_NAME),
|
||||
defaultNotaryNode
|
||||
).transpose().getOrThrow()
|
||||
|
||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
|
||||
val bob = startNode(providedName = BOB_NAME)
|
||||
val bobNode = bob.get()
|
||||
|
||||
val bobNode = startNode(providedName = BOB_NAME).getOrThrow()
|
||||
|
||||
// Wait for network map client to poll for the next update.
|
||||
Thread.sleep(cacheTimeout.toMillis() * 2)
|
||||
@ -98,12 +127,16 @@ class NetworkMapTest : IntegrationTest() {
|
||||
|
||||
@Test
|
||||
fun `nodes process network map remove updates correctly`() {
|
||||
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
|
||||
val alice = startNode(providedName = ALICE_NAME)
|
||||
val bob = startNode(providedName = BOB_NAME)
|
||||
val notaryNode = defaultNotaryNode.get()
|
||||
val aliceNode = alice.get()
|
||||
val bobNode = bob.get()
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false
|
||||
) {
|
||||
val (aliceNode, bobNode, notaryNode) = listOf(
|
||||
startNode(providedName = ALICE_NAME),
|
||||
startNode(providedName = BOB_NAME),
|
||||
defaultNotaryNode
|
||||
).transpose().getOrThrow()
|
||||
|
||||
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
|
||||
@ -119,5 +152,12 @@ class NetworkMapTest : IntegrationTest() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) = assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) {
|
||||
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
|
||||
if (nodeInfosDir.exists()) {
|
||||
assertThat(nodeInfosDir.list { it.toList() }).isEmpty()
|
||||
}
|
||||
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,15 @@ package net.corda.node.utilities.registration
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
@ -16,11 +21,16 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.testing.IntegrationTest
|
||||
import net.corda.testing.IntegrationTestSchemas
|
||||
import net.corda.testing.ROOT_CA
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||
import net.corda.testing.singleIdentity
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
@ -31,6 +41,7 @@ import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.Certificate
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
@ -42,20 +53,25 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
companion object {
|
||||
@ClassRule @JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas("Alice")
|
||||
|
||||
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
|
||||
private val aliceName = CordaX500Name("Alice", "London", "GB")
|
||||
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(13000)
|
||||
private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
|
||||
private val registrationHandler = RegistrationHandler(rootCertAndKeyPair)
|
||||
private val registrationHandler = RegistrationHandler(ROOT_CA)
|
||||
|
||||
private lateinit var server: NetworkMapServer
|
||||
private lateinit var serverHostAndPort: NetworkHostAndPort
|
||||
|
||||
@Before
|
||||
fun startServer() {
|
||||
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler)
|
||||
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), ROOT_CA, "localhost", registrationHandler)
|
||||
serverHostAndPort = server.start()
|
||||
}
|
||||
|
||||
@ -64,50 +80,65 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
server.close()
|
||||
}
|
||||
|
||||
// TODO Ideally this test should be checking that two nodes that register are able to transact with each other. However
|
||||
// starting a second node hangs so that needs to be fixed.
|
||||
@Test
|
||||
fun `node registration correct root cert`() {
|
||||
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert)
|
||||
val compatibilityZone = CompatibilityZoneParams(
|
||||
URL("http://$serverHostAndPort"),
|
||||
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||
rootCert = ROOT_CA.certificate.cert)
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
notarySpecs = emptyList(),
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false
|
||||
initialiseSerialization = false,
|
||||
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance")
|
||||
) {
|
||||
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
|
||||
assertThat(registrationHandler.idsPolled).contains("Alice")
|
||||
val nodes = listOf(
|
||||
startNode(providedName = aliceName),
|
||||
startNode(providedName = genevieveName),
|
||||
defaultNotaryNode
|
||||
).transpose().getOrThrow()
|
||||
val (alice, genevieve) = nodes
|
||||
|
||||
assertThat(registrationHandler.idsPolled).containsOnly(
|
||||
aliceName.organisation,
|
||||
genevieveName.organisation,
|
||||
notaryName.organisation)
|
||||
|
||||
// Check the nodes can communicate among themselves (and the notary).
|
||||
val anonymous = false
|
||||
genevieve.rpc.startFlow(
|
||||
::CashIssueAndPaymentFlow,
|
||||
1000.DOLLARS,
|
||||
OpaqueBytes.of(12),
|
||||
alice.nodeInfo.singleIdentity(),
|
||||
anonymous,
|
||||
defaultNotaryIdentity
|
||||
).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node registration wrong root cert`() {
|
||||
val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert
|
||||
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert)
|
||||
val someRootCert = X509Utilities.createSelfSignedCACertificate(
|
||||
CordaX500Name("Integration Test Corda Node Root CA", "R3 Ltd", "London", "GB"),
|
||||
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
|
||||
val compatibilityZone = CompatibilityZoneParams(
|
||||
URL("http://$serverHostAndPort"),
|
||||
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||
rootCert = someRootCert.cert)
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
notarySpecs = emptyList(),
|
||||
compatibilityZone = compatibilityZone,
|
||||
// Changing the content of the truststore makes the node fail in a number of ways if started out process.
|
||||
startNodesInProcess = true
|
||||
initialiseSerialization = false,
|
||||
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||
startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception
|
||||
) {
|
||||
assertThatThrownBy {
|
||||
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
|
||||
}.isInstanceOf(WrongRootCertException::class.java)
|
||||
defaultNotaryNode.getOrThrow()
|
||||
}.isInstanceOf(CertPathValidatorException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair {
|
||||
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(
|
||||
CordaX500Name(
|
||||
commonName = "Integration Test Corda Node Root CA",
|
||||
organisation = "R3 Ltd",
|
||||
locality = "London",
|
||||
country = "GB"),
|
||||
rootCAKey)
|
||||
return CertificateAndKeyPair(rootCACert, rootCAKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Path("certificate")
|
||||
@ -124,6 +155,7 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
|
||||
certificationRequest,
|
||||
rootCertAndKeyPair.keyPair,
|
||||
arrayOf(rootCertAndKeyPair.certificate.cert))
|
||||
require(!name.organisation.contains("\\s".toRegex())) { "Whitespace in the organisation name not supported" }
|
||||
certPaths[name.organisation] = certPath
|
||||
return Response.ok(name.organisation).build()
|
||||
}
|
||||
@ -154,13 +186,14 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
|
||||
caCertPath: Array<Certificate>): Pair<CertPath, CordaX500Name> {
|
||||
val request = JcaPKCS10CertificationRequest(certificationRequest)
|
||||
val name = CordaX500Name.parse(request.subject.toString())
|
||||
val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.NODE_CA,
|
||||
val nodeCaCert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA,
|
||||
caCertPath.first().toX509CertHolder(),
|
||||
caKeyPair,
|
||||
name,
|
||||
request.publicKey,
|
||||
nameConstraints = null)
|
||||
val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath)
|
||||
val certPath = X509CertificateFactory().generateCertPath(nodeCaCert.cert, *caCertPath)
|
||||
return Pair(certPath, name)
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.internal.configureTestSSL
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
|
||||
|
@ -6,7 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.testing.configureTestSSL
|
||||
import net.corda.testing.internal.configureTestSSL
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
|
||||
/**
|
||||
|
@ -60,7 +60,7 @@ 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.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.crypto.KeyStoreWrapper
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
@ -108,7 +108,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
|
||||
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
|
||||
abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
val platformClock: Clock,
|
||||
val platformClock: CordaClock,
|
||||
protected val versionInfo: VersionInfo,
|
||||
protected val cordappLoader: CordappLoader,
|
||||
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||
@ -184,13 +184,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
return SignedNodeInfo(serialised, listOf(signature))
|
||||
}
|
||||
|
||||
open fun generateNodeInfo() {
|
||||
open fun generateAndSaveNodeInfo(): NodeInfo {
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Generating nodeInfo ...")
|
||||
initCertificate()
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
|
||||
return initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
|
||||
// TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like
|
||||
// a code smell.
|
||||
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
|
||||
@ -200,6 +200,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
privateKey.sign(serialised.bytes)
|
||||
}
|
||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,14 +240,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
val notaryService = makeNotaryService(nodeServices, database)
|
||||
val smm = makeStateMachineManager(database)
|
||||
val flowStarter = FlowStarterImpl(serverThread, smm)
|
||||
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
|
||||
val flowStarter = FlowStarterImpl(serverThread, smm, flowLogicRefFactory)
|
||||
val schedulerService = NodeSchedulerService(
|
||||
platformClock,
|
||||
database,
|
||||
flowStarter,
|
||||
transactionStorage,
|
||||
unfinishedSchedules = busyNodeLatch,
|
||||
serverThread = serverThread)
|
||||
serverThread = serverThread,
|
||||
flowLogicRefFactory = flowLogicRefFactory)
|
||||
if (serverThread is ExecutorService) {
|
||||
runOnStop += {
|
||||
// We wait here, even though any in-flight messages should have been drained away because the
|
||||
@ -255,7 +258,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS)
|
||||
}
|
||||
}
|
||||
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService)
|
||||
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory)
|
||||
val rpcOps = makeRPCOps(flowStarter, database, smm)
|
||||
startMessagingService(rpcOps)
|
||||
installCoreFlows()
|
||||
@ -263,7 +266,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
tokenizableServices = nodeServices + cordaServices + schedulerService
|
||||
registerCordappFlows(smm)
|
||||
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
|
||||
FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader
|
||||
startShell(rpcOps)
|
||||
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
|
||||
}
|
||||
@ -581,10 +583,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage()
|
||||
|
||||
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService) {
|
||||
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService, flowLogicRefFactory: FlowLogicRefFactory) {
|
||||
VaultSoftLockManager.install(services.vaultService, smm)
|
||||
ScheduledActivityObserver.install(services.vaultService, schedulerService)
|
||||
ScheduledActivityObserver.install(services.vaultService, schedulerService, flowLogicRefFactory)
|
||||
HibernateObserver.install(services.vaultService.rawUpdates, hibernateConfig, schemaService)
|
||||
}
|
||||
|
||||
@ -724,7 +725,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
|
||||
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
|
||||
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
||||
val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
val clientCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
val caCertificates = arrayOf(identityCert, clientCa.certificate.cert)
|
||||
return PersistentIdentityService(trustRoot, *caCertificates)
|
||||
}
|
||||
@ -754,10 +755,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(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||
Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||
} else {
|
||||
// The node is part of a distributed notary whose identity must already be generated beforehand.
|
||||
Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||
Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||
}
|
||||
// TODO: Integrate with Key management service?
|
||||
val privateKeyAlias = "$id-private-key"
|
||||
@ -767,10 +768,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
"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].
|
||||
log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!")
|
||||
keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair())
|
||||
keyStore.storeLegalIdentity(singleName, privateKeyAlias, generateKeyPair())
|
||||
}
|
||||
|
||||
val (x509Cert, keyPair) = keyStore.certificateAndKeyPair(privateKeyAlias)
|
||||
val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias)
|
||||
|
||||
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
|
||||
val compositeKeyAlias = "$id-composite-key"
|
||||
@ -848,10 +849,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager) : FlowStarter {
|
||||
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager, private val flowLogicRefFactory: FlowLogicRefFactory) : FlowStarter {
|
||||
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>> {
|
||||
return serverThread.fetchFrom { smm.startFlow(logic, context) }
|
||||
}
|
||||
|
||||
override fun <T> invokeFlowAsync(
|
||||
logicType: Class<out FlowLogic<T>>,
|
||||
context: InvocationContext,
|
||||
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
|
||||
val logicRef = flowLogicRefFactory.createForRPC(logicType, *args)
|
||||
val logic: FlowLogic<T> = uncheckedCast(flowLogicRefFactory.toFlowLogic(logicRef))
|
||||
return startFlow(logic, context)
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationException(message: String) : CordaException(message)
|
||||
|
@ -22,10 +22,15 @@ abstract class CordaClock : Clock(), SerializeAsToken {
|
||||
override fun getZone(): ZoneId = delegateClock.zone
|
||||
@Deprecated("Do not use this. Instead seek to use ZonedDateTime methods.", level = DeprecationLevel.ERROR)
|
||||
override fun withZone(zone: ZoneId) = throw UnsupportedOperationException("Tokenized clock does not support withZone()")
|
||||
|
||||
/** This is an observer on the mutation count of this [Clock], which reflects the occurrence of mutations. */
|
||||
abstract val mutations: Observable<Long>
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
class SimpleClock(override val delegateClock: Clock) : CordaClock()
|
||||
class SimpleClock(override val delegateClock: Clock) : CordaClock() {
|
||||
override val mutations: Observable<Long> = Observable.never()
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract class with helper methods for a type of Clock that might have it's concept of "now" adjusted externally.
|
||||
@ -38,8 +43,7 @@ abstract class MutableClock(private var _delegateClock: Clock) : CordaClock() {
|
||||
_delegateClock = clock
|
||||
}
|
||||
private val _version = AtomicLong(0L)
|
||||
/** This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. */
|
||||
val mutations: Observable<Long> by lazy {
|
||||
override val mutations: Observable<Long> by lazy {
|
||||
Observable.create { subscriber: Subscriber<in Long> ->
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
mutationObservers.add(subscriber)
|
||||
|
@ -2,7 +2,6 @@ package net.corda.node.internal
|
||||
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
@ -20,7 +19,9 @@ import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.SecurityConfiguration
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
@ -33,6 +34,7 @@ import net.corda.nodeapi.internal.serialization.*
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Scheduler
|
||||
import rx.schedulers.Schedulers
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -67,7 +69,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
private fun createClock(configuration: NodeConfiguration): Clock {
|
||||
private fun createClock(configuration: NodeConfiguration): CordaClock {
|
||||
return (if (configuration.useTestClock) ::DemoClock else ::SimpleClock)(Clock.systemUTC())
|
||||
}
|
||||
|
||||
@ -269,15 +271,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
private val _startupComplete = openFuture<Unit>()
|
||||
val startupComplete: CordaFuture<Unit> get() = _startupComplete
|
||||
|
||||
override fun generateNodeInfo() {
|
||||
override fun generateAndSaveNodeInfo(): NodeInfo {
|
||||
initialiseSerialization()
|
||||
super.generateNodeInfo()
|
||||
return super.generateAndSaveNodeInfo()
|
||||
}
|
||||
|
||||
override fun start(): StartedNode<Node> {
|
||||
if (initialiseSerialization) {
|
||||
initialiseSerialization()
|
||||
}
|
||||
val started: StartedNode<Node> = uncheckedCast(super.start())
|
||||
nodeReadyFuture.thenMatch({
|
||||
serverThread.execute {
|
||||
@ -310,8 +310,10 @@ open class Node(configuration: NodeConfiguration,
|
||||
return started
|
||||
}
|
||||
|
||||
override fun getRxIoScheduler() = Schedulers.io()!!
|
||||
override fun getRxIoScheduler(): Scheduler = Schedulers.io()
|
||||
|
||||
private fun initialiseSerialization() {
|
||||
if (!initialiseSerialization) return
|
||||
val classloader = cordappLoader.appClassLoader
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import com.typesafe.config.ConfigException
|
||||
import joptsimple.OptionException
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
@ -126,7 +125,7 @@ open class NodeStartup(val args: Array<String>) {
|
||||
val node = createNode(conf, versionInfo)
|
||||
if (cmdlineOptions.justGenerateNodeInfo) {
|
||||
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
||||
node.generateNodeInfo()
|
||||
node.generateAndSaveNodeInfo()
|
||||
return
|
||||
}
|
||||
if (cmdlineOptions.justRunDbMigration) {
|
||||
|
@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -21,7 +20,6 @@ import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.MessagingService
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
|
||||
@ -137,11 +135,7 @@ interface FlowStarter {
|
||||
fun <T> invokeFlowAsync(
|
||||
logicType: Class<out FlowLogic<T>>,
|
||||
context: InvocationContext,
|
||||
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
|
||||
val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args)
|
||||
val logic: FlowLogic<T> = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef))
|
||||
return startFlow(logic, context)
|
||||
}
|
||||
vararg args: Any?): CordaFuture<FlowStateMachine<T>>
|
||||
}
|
||||
|
||||
interface StartedNodeServices : ServiceHubInternal, FlowStarter
|
||||
|
@ -3,17 +3,21 @@ package net.corda.node.services.config
|
||||
import com.typesafe.config.*
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.toProperties
|
||||
import net.corda.nodeapi.internal.createDevKeyStores
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
|
||||
fun configOf(vararg pairs: Pair<String, Any?>): Config = ConfigFactory.parseMap(mapOf(*pairs))
|
||||
operator fun Config.plus(overrides: Map<String, Any?>): Config = ConfigFactory.parseMap(overrides).withFallback(this)
|
||||
@ -58,8 +62,10 @@ object ConfigHelper {
|
||||
* Strictly for dev only automatically construct a server certificate/private key signed from
|
||||
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
|
||||
*/
|
||||
// TODO Move this to KeyStoreConfigHelpers
|
||||
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
|
||||
|
||||
// TODO Move this to KeyStoreConfigHelpers
|
||||
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
certificatesDirectory.createDirectories()
|
||||
if (!trustStoreFile.exists()) {
|
||||
@ -67,7 +73,9 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
}
|
||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||
createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
||||
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
||||
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
|
||||
createDevKeyStores(rootCert, intermediateCa, myLegalName)
|
||||
|
||||
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
||||
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
||||
@ -86,58 +94,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine.
|
||||
* @param sslKeyStorePath KeyStore path to save ssl key and cert to.
|
||||
* @param clientCAKeystorePath KeyStore path to save client CA key and cert to.
|
||||
* @param storePassword access password for KeyStore.
|
||||
* @param keyPassword PrivateKey access password for the generated keys.
|
||||
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
|
||||
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore.
|
||||
* @param caKeyPassword password to unlock private keys in the CA KeyStore.
|
||||
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications.
|
||||
*/
|
||||
fun createKeystoreForCordaNode(sslKeyStorePath: Path,
|
||||
clientCAKeystorePath: Path,
|
||||
storePassword: String,
|
||||
keyPassword: String,
|
||||
caKeyStore: KeyStore,
|
||||
caKeyPassword: String,
|
||||
legalName: CordaX500Name,
|
||||
signatureScheme: SignatureScheme = X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) {
|
||||
|
||||
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
|
||||
val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword)
|
||||
|
||||
val clientKey = Crypto.generateKeyPair(signatureScheme)
|
||||
|
||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||
val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA,
|
||||
intermediateCACert,
|
||||
intermediateCAKeyPair,
|
||||
legalName,
|
||||
clientKey.public,
|
||||
nameConstraints = nameConstraints)
|
||||
|
||||
val tlsKey = Crypto.generateKeyPair(signatureScheme)
|
||||
val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public)
|
||||
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
|
||||
val clientCAKeystore = loadOrCreateKeyStore(clientCAKeystorePath, storePassword)
|
||||
clientCAKeystore.addOrReplaceKey(
|
||||
X509Utilities.CORDA_CLIENT_CA,
|
||||
clientKey.private,
|
||||
keyPass,
|
||||
arrayOf(clientCACert, intermediateCACert, rootCACert))
|
||||
clientCAKeystore.save(clientCAKeystorePath, storePassword)
|
||||
|
||||
val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword)
|
||||
tlsKeystore.addOrReplaceKey(
|
||||
X509Utilities.CORDA_CLIENT_TLS,
|
||||
tlsKey.private,
|
||||
keyPass,
|
||||
arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
|
||||
tlsKeystore.save(sslKeyStorePath, storePassword)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.contracts.ScheduledStateRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.concurrent.flatMap
|
||||
@ -19,10 +20,10 @@ import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.internal.CordaClock
|
||||
import net.corda.node.internal.MutableClock
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.SchedulerService
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.PersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -55,13 +56,14 @@ import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture
|
||||
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeSchedulerService(private val clock: Clock,
|
||||
class NodeSchedulerService(private val clock: CordaClock,
|
||||
private val database: CordaPersistence,
|
||||
private val flowStarter: FlowStarter,
|
||||
private val stateLoader: StateLoader,
|
||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
||||
private val unfinishedSchedules: ReusableLatch = ReusableLatch(),
|
||||
private val serverThread: AffinityExecutor)
|
||||
private val serverThread: AffinityExecutor,
|
||||
private val flowLogicRefFactory: FlowLogicRefFactory)
|
||||
: SchedulerService, SingletonSerializeAsToken() {
|
||||
|
||||
companion object {
|
||||
@ -78,17 +80,13 @@ class NodeSchedulerService(private val clock: Clock,
|
||||
@Suspendable
|
||||
@VisibleForTesting
|
||||
// We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name
|
||||
fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
|
||||
fun awaitWithDeadline(clock: CordaClock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
|
||||
var nanos: Long
|
||||
do {
|
||||
val originalFutureCompleted = makeStrandFriendlySettableFuture(future)
|
||||
val subscription = if (clock is MutableClock) {
|
||||
clock.mutations.first().subscribe {
|
||||
val subscription = clock.mutations.first().subscribe {
|
||||
originalFutureCompleted.set(false)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
nanos = (clock.instant() until deadline).toNanos()
|
||||
if (nanos > 0) {
|
||||
try {
|
||||
@ -102,7 +100,7 @@ class NodeSchedulerService(private val clock: Clock,
|
||||
// No need to take action as will fall out of the loop due to future.isDone
|
||||
}
|
||||
}
|
||||
subscription?.unsubscribe()
|
||||
subscription.unsubscribe()
|
||||
originalFutureCompleted.cancel(false)
|
||||
} while (nanos > 0 && !future.isDone)
|
||||
return future.isDone
|
||||
@ -279,7 +277,7 @@ class NodeSchedulerService(private val clock: Clock,
|
||||
scheduledStatesQueue.remove(scheduledState)
|
||||
scheduledStatesQueue.add(newState)
|
||||
} else {
|
||||
val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef)
|
||||
val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef)
|
||||
log.trace { "Scheduler starting FlowLogic $flowLogic" }
|
||||
scheduledFlow = flowLogic
|
||||
scheduledStates.remove(scheduledState.ref)
|
||||
@ -297,7 +295,7 @@ class NodeSchedulerService(private val clock: Clock,
|
||||
val state = txState.data as SchedulableState
|
||||
return try {
|
||||
// This can throw as running contract code.
|
||||
state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl)
|
||||
state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory)
|
||||
} catch (e: Exception) {
|
||||
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
|
||||
null
|
||||
|
@ -4,19 +4,19 @@ import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.SchedulableState
|
||||
import net.corda.core.contracts.ScheduledStateRef
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.node.services.api.SchedulerService
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
|
||||
/**
|
||||
* This observes the vault and schedules and unschedules activities appropriately based on state production and
|
||||
* consumption.
|
||||
*/
|
||||
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService) {
|
||||
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService, private val FlowLogicRefFactory: FlowLogicRefFactory) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun install(vaultService: VaultService, schedulerService: SchedulerService) {
|
||||
val observer = ScheduledActivityObserver(schedulerService)
|
||||
fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) {
|
||||
val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory)
|
||||
vaultService.rawUpdates.subscribe { (consumed, produced) ->
|
||||
consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) }
|
||||
produced.forEach { observer.scheduleStateActivity(it) }
|
||||
@ -32,7 +32,7 @@ class ScheduledActivityObserver private constructor(private val schedulerService
|
||||
private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
|
||||
val producedState = produced.state.data
|
||||
if (producedState is SchedulableState) {
|
||||
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return
|
||||
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactory)?.scheduledAt } ?: return
|
||||
schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt))
|
||||
}
|
||||
}
|
||||
|
@ -31,10 +31,8 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String,
|
||||
* measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital"
|
||||
* in response to a potential malicious use or buggy update to an app etc.
|
||||
*/
|
||||
object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
||||
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
|
||||
var classloader: ClassLoader = javaClass.classLoader
|
||||
|
||||
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
|
||||
class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
||||
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
|
||||
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
|
||||
@ -42,7 +40,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
|
||||
return createForRPC(flowClass, *args)
|
||||
}
|
||||
|
||||
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
override fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
|
||||
// to avoid requiring only a single constructor.
|
||||
val argTypes = args.map { it?.javaClass }
|
||||
@ -81,7 +79,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
|
||||
return FlowLogicRefImpl(type.name, args)
|
||||
}
|
||||
|
||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||
override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
|
||||
val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java)
|
||||
return createConstructor(klass, ref.args)()
|
||||
|
@ -12,10 +12,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import java.io.StringWriter
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Helper for managing the node registration process, which checks for any existing certificates and requests them if
|
||||
@ -32,7 +32,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
||||
// TODO: Use different password for private key.
|
||||
private val privateKeyPassword = config.keyStorePassword
|
||||
private val trustStore: KeyStore
|
||||
private val rootCert: Certificate
|
||||
private val rootCert: X509Certificate
|
||||
|
||||
init {
|
||||
require(config.trustStoreFile.exists()) {
|
||||
@ -46,7 +46,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
||||
"This file must contain the root CA cert of your compatibility zone. " +
|
||||
"Please contact your CZ operator."
|
||||
}
|
||||
this.rootCert = rootCert
|
||||
this.rootCert = rootCert as X509Certificate
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,11 +94,8 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
||||
caKeyStore.save(config.nodeKeystore, keystorePassword)
|
||||
println("Node private key and certificate stored in ${config.nodeKeystore}.")
|
||||
|
||||
// Check that the root of the signed certificate matches the expected certificate in the truststore.
|
||||
if (rootCert != certificates.last()) {
|
||||
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||
throw WrongRootCertException(rootCert, certificates.last(), config.trustStoreFile)
|
||||
}
|
||||
println("Checking root of the certificate path is what we expect.")
|
||||
X509Utilities.validateCertificateChain(rootCert, *certificates)
|
||||
|
||||
println("Generating SSL certificate for node messaging service.")
|
||||
val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
@ -168,17 +165,3 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate.
|
||||
* This usually means that there has been a Man-in-the-middle attack when contacting the doorman.
|
||||
*/
|
||||
class WrongRootCertException(expected: Certificate,
|
||||
actual: Certificate,
|
||||
expectedFilePath: Path):
|
||||
Exception("""
|
||||
The Root CA returned back from the registration process does not match the expected Root CA
|
||||
expected: $expected
|
||||
actual: $actual
|
||||
the expected certificate is stored in: $expectedFilePath with alias $CORDA_ROOT_CA
|
||||
""".trimMargin())
|
||||
|
@ -48,13 +48,15 @@ public class FlowLogicRefFromJavaTest {
|
||||
}
|
||||
}
|
||||
|
||||
private final FlowLogicRefFactoryImpl flowLogicRefFactory = new FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl.class.getClassLoader());
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
||||
flowLogicRefFactory.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoArg() {
|
||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class);
|
||||
flowLogicRefFactory.createForRPC(JavaNoArgFlowLogic.class);
|
||||
}
|
||||
}
|
||||
|
@ -34,47 +34,48 @@ class FlowLogicRefTest {
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
|
||||
@Test
|
||||
fun `create kotlin no arg`() {
|
||||
FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java)
|
||||
flowLogicRefFactory.createForRPC(KotlinNoArgFlowLogic::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create kotlin`() {
|
||||
val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack")))
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create primary`() {
|
||||
FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||
flowLogicRefFactory.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create kotlin void`() {
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
||||
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create kotlin non primary`() {
|
||||
val args = mapOf(Pair("C", ParamType2("Hello Jack")))
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create java primitive no registration required`() {
|
||||
val args = mapOf(Pair("primitive", "A string"))
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create kotlin primitive no registration required`() {
|
||||
val args = mapOf(Pair("kotlinType", 3))
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalFlowLogicException::class)
|
||||
fun `create for non-schedulable flow logic`() {
|
||||
FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java)
|
||||
flowLogicRefFactory.create(NonSchedulableFlow::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
|
||||
private val realClock: Clock = Clock.systemUTC()
|
||||
private val stoppedClock: Clock = Clock.fixed(realClock.instant(), realClock.zone)
|
||||
private val testClock = TestClock(stoppedClock)
|
||||
@ -122,7 +123,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
}
|
||||
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
||||
mockSMM = StateMachineManagerImpl(services, DBCheckpointStorage(), smmExecutor, database, newSecureRandom())
|
||||
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor)
|
||||
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM, flowLogicRefFactory), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor, flowLogicRefFactory = flowLogicRefFactory)
|
||||
mockSMM.changes.subscribe { change ->
|
||||
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
|
||||
smmHasRemovedAllFlows.countDown()
|
||||
@ -305,7 +306,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
database.transaction {
|
||||
apply {
|
||||
val freshKey = kms.freshKey()
|
||||
val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
|
||||
val state = TestState(flowLogicRefFactory.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
|
||||
val builder = TransactionBuilder(null).apply {
|
||||
addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY)
|
||||
addCommand(Command(), freshKey)
|
||||
|
@ -70,7 +70,7 @@ class NetworkMapClientTest {
|
||||
// The test server returns same network parameter for any hash.
|
||||
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
|
||||
assertNotNull(networkParameter)
|
||||
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter)
|
||||
assertEquals(server.networkParameters, networkParameter)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -8,6 +8,8 @@ import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.hours
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.node.internal.CordaClock
|
||||
import net.corda.node.internal.SimpleClock
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.testing.node.TestClock
|
||||
import org.junit.After
|
||||
@ -25,13 +27,13 @@ import kotlin.test.fail
|
||||
class ClockUtilsTest {
|
||||
|
||||
lateinit var realClock: Clock
|
||||
lateinit var stoppedClock: Clock
|
||||
lateinit var stoppedClock: CordaClock
|
||||
lateinit var executor: ExecutorService
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
realClock = Clock.systemUTC()
|
||||
stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
|
||||
stoppedClock = SimpleClock(Clock.fixed(realClock.instant(), realClock.zone))
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,14 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.internal.createDevNodeCaCertPath
|
||||
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.Test
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.test.assertFalse
|
||||
@ -29,16 +31,19 @@ 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)
|
||||
|
||||
private lateinit var rootCaCert: X509Certificate
|
||||
private lateinit var intermediateCaCert: X509Certificate
|
||||
private lateinit var nodeCaCert: X509Certificate
|
||||
private lateinit var config: NodeConfiguration
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(nodeLegalName)
|
||||
this.rootCaCert = rootCa.certificate.cert
|
||||
this.intermediateCaCert = intermediateCa.certificate.cert
|
||||
this.nodeCaCert = nodeCa.certificate.cert
|
||||
|
||||
val baseDirectory = fs.getPath("/baseDir").createDirectories()
|
||||
abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||
config = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
@ -108,11 +113,13 @@ class NetworkRegistrationHelperTest {
|
||||
|
||||
@Test
|
||||
fun `wrong root cert in truststore`() {
|
||||
saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB")))
|
||||
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), rootKeyPair)
|
||||
saveTrustStoreWithRootCa(rootCert.cert)
|
||||
val registrationHelper = createRegistrationHelper()
|
||||
assertThatThrownBy {
|
||||
registrationHelper.buildKeystore()
|
||||
}.isInstanceOf(WrongRootCertException::class.java)
|
||||
}.isInstanceOf(CertPathValidatorException::class.java)
|
||||
}
|
||||
|
||||
private fun createRegistrationHelper(): NetworkRegistrationHelper {
|
||||
@ -123,15 +130,11 @@ class NetworkRegistrationHelperTest {
|
||||
return NetworkRegistrationHelper(config, certService)
|
||||
}
|
||||
|
||||
private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) {
|
||||
config.trustStoreFile.parent.createDirectories()
|
||||
private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) {
|
||||
config.certificatesDirectory.createDirectories()
|
||||
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa)
|
||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
it.save(config.trustStoreFile, config.trustStorePassword)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCaCert(name: CordaX500Name): X509Certificate {
|
||||
return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ define([
|
||||
$scope.formError = resp.data;
|
||||
}, handleHttpFail);
|
||||
};
|
||||
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
|
||||
$('input.percent').mask("9.999999", {placeholder: "", autoclear: false});
|
||||
$('#swapirscolumns').click(() => {
|
||||
let first = $('#irscolumns .irscolumn:eq( 0 )');
|
||||
let last = $('#irscolumns .irscolumn:eq( 1 )');
|
||||
|
@ -8,7 +8,7 @@ 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.minCorrectReplicas
|
||||
import net.corda.nodeapi.internal.IdentityGenerator
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.testing.node.internal.demorun.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() {
|
||||
}
|
||||
|
||||
override fun setup(context: CordformContext) {
|
||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
||||
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
notaryNames.map { context.baseDirectory(it.toString()) },
|
||||
clusterName,
|
||||
minCorrectReplicas(clusterSize)
|
||||
|
@ -4,11 +4,10 @@ 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.NotaryConfig
|
||||
import net.corda.node.services.config.RaftConfig
|
||||
import net.corda.nodeapi.internal.IdentityGenerator
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.testing.node.internal.demorun.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
@ -59,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
|
||||
}
|
||||
|
||||
override fun setup(context: CordformContext) {
|
||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
||||
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
notaryNames.map { context.baseDirectory(it.toString()) },
|
||||
clusterName
|
||||
)
|
||||
|
@ -72,6 +72,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
extraConfig = [
|
||||
jvmArgs : [ "-Xmx1g"]
|
||||
]
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
@ -80,6 +83,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
rpcPort 10006
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
extraConfig = [
|
||||
jvmArgs : [ "-Xmx1g"]
|
||||
]
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
@ -88,6 +94,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
rpcPort 10009
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
extraConfig = [
|
||||
jvmArgs : [ "-Xmx1g"]
|
||||
]
|
||||
}
|
||||
node {
|
||||
name "O=Bank C,L=Tokyo,C=JP"
|
||||
@ -96,6 +105,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
rpcPort 10012
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
extraConfig = [
|
||||
jvmArgs : [ "-Xmx1g"]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,8 @@ class SellerFlow(private val otherParty: Party,
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first()
|
||||
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java)
|
||||
.states.firstOrNull() ?: throw IllegalStateException("No commercial paper found. Please check if you issued the papers first, follow the README for instructions.")
|
||||
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.testing.driver
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.readLines
|
||||
@ -47,12 +48,28 @@ class DriverTests : IntegrationTest() {
|
||||
@Test
|
||||
fun `simple node startup and shutdown`() {
|
||||
val handle = driver {
|
||||
val regulator = startNode(providedName = DUMMY_REGULATOR_NAME)
|
||||
nodeMustBeUp(regulator)
|
||||
val node = startNode(providedName = DUMMY_REGULATOR_NAME)
|
||||
nodeMustBeUp(node)
|
||||
}
|
||||
nodeMustBeDown(handle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `starting with default notary`() {
|
||||
driver {
|
||||
// Make sure the default is a single-node notary
|
||||
val notary = defaultNotaryNode.getOrThrow()
|
||||
val notaryIdentities = notary.nodeInfo.legalIdentitiesAndCerts
|
||||
// Make sure the notary node has only one identity
|
||||
assertThat(notaryIdentities).hasSize(1)
|
||||
val identity = notaryIdentities[0]
|
||||
// Make sure this identity is a legal identity, like it is for normal nodes.
|
||||
assertThat(CertRole.extract(identity.certificate)).isEqualTo(CertRole.LEGAL_IDENTITY)
|
||||
// And make sure this identity is published as the notary identity (via the network parameters)
|
||||
assertThat(notary.rpc.notaryIdentities()).containsOnly(identity.party)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random free port allocation`() {
|
||||
val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) {
|
||||
|
@ -14,11 +14,11 @@ import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.DriverDSLImpl
|
||||
import net.corda.testing.node.internal.genericDriver
|
||||
import net.corda.testing.node.internal.getTimestampAsDirectoryName
|
||||
import net.corda.testing.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.ServerSocket
|
||||
import java.nio.file.Path
|
||||
|
@ -38,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.IdentityGenerator
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||
@ -236,7 +236,7 @@ open class MockNetwork(private val cordappPackages: List<String>,
|
||||
|
||||
private fun generateNotaryIdentities(): List<NotaryInfo> {
|
||||
return notarySpecs.mapIndexed { index, (name, validating) ->
|
||||
val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name)
|
||||
val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name)
|
||||
NotaryInfo(identity, validating)
|
||||
}
|
||||
}
|
||||
|
@ -10,15 +10,12 @@ import net.corda.cordform.CordformContext
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.concurrent.firstOf
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.*
|
||||
import net.corda.core.internal.copyTo
|
||||
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.serialization.deserialize
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
@ -31,7 +28,8 @@ import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.config.*
|
||||
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.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
@ -68,7 +66,7 @@ import java.nio.file.StandardCopyOption
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZoneOffset.UTC
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
@ -96,15 +94,16 @@ class DriverDSLImpl(
|
||||
private var _shutdownManager: ShutdownManager? = null
|
||||
override val shutdownManager get() = _shutdownManager!!
|
||||
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
|
||||
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
|
||||
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
|
||||
// Investigate whether we can avoid that.
|
||||
private var nodeInfoFilesCopier: NodeInfoFilesCopier? = null
|
||||
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
|
||||
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
|
||||
private lateinit var _notaries: List<NotaryHandle>
|
||||
override val notaryHandles: List<NotaryHandle> get() = _notaries
|
||||
private var networkParameters: NetworkParametersCopier? = null
|
||||
/**
|
||||
* Future which completes when the network map is available, whether a local one or one from the CZ. This future acts
|
||||
* as a gate to prevent nodes from starting too early. The value of the future is a [LocalNetworkMap] object, which
|
||||
* is null if the network map is being provided by the CZ.
|
||||
*/
|
||||
private lateinit var networkMapAvailability: CordaFuture<LocalNetworkMap?>
|
||||
private lateinit var _notaries: CordaFuture<List<NotaryHandle>>
|
||||
override val notaryHandles: List<NotaryHandle> get() = _notaries.getOrThrow()
|
||||
|
||||
class State {
|
||||
val processes = ArrayList<Process>()
|
||||
@ -145,12 +144,12 @@ class DriverDSLImpl(
|
||||
_executorService?.shutdownNow()
|
||||
}
|
||||
|
||||
private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
|
||||
val rpcAddress = config.rpcAddress!!
|
||||
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
|
||||
val rpcAddress = config.corda.rpcAddress!!
|
||||
val client = CordaRPCClient(rpcAddress)
|
||||
val connectionFuture = poll(executorService, "RPC connection") {
|
||||
try {
|
||||
client.start(config.rpcUsers[0].username, config.rpcUsers[0].password)
|
||||
config.corda.rpcUsers[0].run { client.start(username, password) }
|
||||
} catch (e: Exception) {
|
||||
if (processDeathFuture.isDone) throw e
|
||||
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
|
||||
@ -178,19 +177,38 @@ class DriverDSLImpl(
|
||||
): CordaFuture<NodeHandle> {
|
||||
val p2pAddress = portAllocation.nextHostAndPort()
|
||||
// TODO: Derive name from the full picked name, don't just wrap the common name
|
||||
val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB")
|
||||
|
||||
val name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB")
|
||||
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
||||
nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
|
||||
// We don't need the network map to be available to be able to register the node
|
||||
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
|
||||
} else {
|
||||
doneFuture(Unit)
|
||||
}
|
||||
|
||||
return registrationFuture.flatMap {
|
||||
networkMapAvailability.flatMap {
|
||||
// But starting the node proper does require the network map
|
||||
startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRegisteredNode(name: CordaX500Name,
|
||||
localNetworkMap: LocalNetworkMap?,
|
||||
rpcUsers: List<User>,
|
||||
verifierType: VerifierType,
|
||||
customOverrides: Map<String, Any?>,
|
||||
startInSameProcess: Boolean? = null,
|
||||
maximumHeapSize: String = "200m",
|
||||
p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort()): CordaFuture<NodeHandle> {
|
||||
val rpcAddress = portAllocation.nextHostAndPort()
|
||||
val webAddress = portAllocation.nextHostAndPort()
|
||||
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
|
||||
val configMap = configOf(
|
||||
val czUrlConfig = if (compatibilityZone != null) mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) else emptyMap()
|
||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory(name),
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOf(
|
||||
"myLegalName" to name.toString(),
|
||||
"p2pAddress" to p2pAddress.toString(),
|
||||
"rpcAddress" to rpcAddress.toString(),
|
||||
@ -198,45 +216,35 @@ class DriverDSLImpl(
|
||||
"useTestClock" to useTestClock,
|
||||
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
|
||||
"verifierType" to verifierType.name
|
||||
) + customOverrides
|
||||
val config = ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory(name),
|
||||
allowMissingConfig = true,
|
||||
configOverrides = if (compatibilityZone != null) {
|
||||
configMap + mapOf("compatibilityZoneURL" to compatibilityZone.url.toString())
|
||||
} else {
|
||||
configMap
|
||||
}
|
||||
)
|
||||
startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize)
|
||||
}
|
||||
) + czUrlConfig + customOverrides
|
||||
))
|
||||
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap)
|
||||
}
|
||||
|
||||
private fun nodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<Unit> {
|
||||
private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NodeConfig> {
|
||||
val baseDirectory = baseDirectory(providedName).createDirectories()
|
||||
val config = ConfigHelper.loadConfig(
|
||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOf(
|
||||
"p2pAddress" to "localhost:1222", // required argument, not really used
|
||||
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
|
||||
"myLegalName" to providedName.toString())
|
||||
)
|
||||
val configuration = config.parseAsNodeConfiguration()
|
||||
))
|
||||
|
||||
configuration.trustStoreFile.parent.createDirectories()
|
||||
loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also {
|
||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
it.save(configuration.trustStoreFile, configuration.trustStorePassword)
|
||||
config.corda.certificatesDirectory.createDirectories()
|
||||
loadOrCreateKeyStore(config.corda.trustStoreFile, config.corda.trustStorePassword).apply {
|
||||
addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
save(config.corda.trustStoreFile, config.corda.trustStorePassword)
|
||||
}
|
||||
|
||||
return if (startNodesInProcess) {
|
||||
// This is a bit cheating, we're not starting a full node, we're just calling the code nodes call
|
||||
// when registering.
|
||||
NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
|
||||
doneFuture(Unit)
|
||||
executorService.fork {
|
||||
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
|
||||
config
|
||||
}
|
||||
} else {
|
||||
startOutOfProcessNodeRegistration(config, configuration)
|
||||
startOutOfProcessMiniNode(config, "--initial-registration").map { config }
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,7 +255,9 @@ class DriverDSLImpl(
|
||||
}
|
||||
|
||||
internal fun startCordformNodes(cordforms: List<CordformNode>): CordaFuture<*> {
|
||||
check(compatibilityZone == null) { "Cordform nodes should be run without compatibilityZone configuration" }
|
||||
check(notarySpecs.isEmpty()) { "Specify notaries in the CordformDefinition" }
|
||||
check(compatibilityZone == null) { "Cordform nodes cannot be run with compatibilityZoneURL" }
|
||||
|
||||
val clusterNodes = HashMultimap.create<ClusterType, CordaX500Name>()
|
||||
val notaryInfos = ArrayList<NotaryInfo>()
|
||||
|
||||
@ -266,23 +276,23 @@ 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 = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name)
|
||||
notaryInfos += NotaryInfo(identity, notaryConfig.validating)
|
||||
}
|
||||
}
|
||||
|
||||
clusterNodes.asMap().forEach { type, nodeNames ->
|
||||
val identity = IdentityGenerator.generateDistributedNotaryIdentity(
|
||||
val identity = DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
dirs = nodeNames.map { baseDirectory(it) },
|
||||
notaryName = type.clusterName
|
||||
)
|
||||
notaryInfos += NotaryInfo(identity, type.validating)
|
||||
}
|
||||
|
||||
networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
||||
val localNetworkMap = LocalNetworkMap(notaryInfos)
|
||||
|
||||
return cordforms.map {
|
||||
val startedNode = startCordformNode(it)
|
||||
val startedNode = startCordformNode(it, localNetworkMap)
|
||||
if (it.webAddress != null) {
|
||||
// Start a webserver if an address for it was specified
|
||||
startedNode.flatMap { startWebserver(it) }
|
||||
@ -292,21 +302,21 @@ class DriverDSLImpl(
|
||||
}.transpose()
|
||||
}
|
||||
|
||||
private fun startCordformNode(cordform: CordformNode): CordaFuture<NodeHandle> {
|
||||
private fun startCordformNode(cordform: CordformNode, localNetworkMap: LocalNetworkMap): CordaFuture<NodeHandle> {
|
||||
val name = CordaX500Name.parse(cordform.name)
|
||||
// TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal
|
||||
val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap()
|
||||
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
|
||||
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
|
||||
val rpcUsers = cordform.rpcUsers
|
||||
val config = ConfigHelper.loadConfig(
|
||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory(name),
|
||||
allowMissingConfig = true,
|
||||
configOverrides = cordform.config + rpcAddress + notary + mapOf(
|
||||
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
|
||||
)
|
||||
)
|
||||
return startNodeInternal(config, webAddress, null, "200m")
|
||||
))
|
||||
return startNodeInternal(config, webAddress, null, "200m", localNetworkMap)
|
||||
}
|
||||
|
||||
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
|
||||
@ -340,43 +350,92 @@ class DriverDSLImpl(
|
||||
}
|
||||
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
|
||||
_shutdownManager = ShutdownManager(executorService)
|
||||
if (compatibilityZone == null) {
|
||||
// Without a compatibility zone URL we have to copy the node info files ourselves to make sure the nodes see each other
|
||||
nodeInfoFilesCopier = NodeInfoFilesCopier().also {
|
||||
shutdownManager.registerShutdown(it::close)
|
||||
val notaryInfosFuture = if (compatibilityZone == null) {
|
||||
// If no CZ is specified then the driver does the generation of the network parameters and the copying of the
|
||||
// node info files.
|
||||
startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos)) }
|
||||
} else {
|
||||
// Otherwise it's the CZ's job to distribute thse via the HTTP network map, as that is what the nodes will be expecting.
|
||||
val notaryInfosFuture = if (compatibilityZone.rootCert == null) {
|
||||
// No root cert specified so we use the dev root cert to generate the notary identities.
|
||||
startNotaryIdentityGeneration()
|
||||
} else {
|
||||
// With a root cert specified we delegate generation of the notary identities to the CZ.
|
||||
startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.url)
|
||||
}
|
||||
notaryInfosFuture.map { notaryInfos ->
|
||||
compatibilityZone.publishNotaries(notaryInfos)
|
||||
Pair(notaryInfos, null)
|
||||
}
|
||||
val notaryInfos = generateNotaryIdentities()
|
||||
// The network parameters must be serialised before starting any of the nodes
|
||||
if (compatibilityZone == null) networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
||||
val nodeHandles = startNotaries()
|
||||
_notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) }
|
||||
}
|
||||
|
||||
private fun generateNotaryIdentities(): List<NotaryInfo> {
|
||||
return notarySpecs.map { spec ->
|
||||
networkMapAvailability = notaryInfosFuture.map { it.second }
|
||||
|
||||
_notaries = notaryInfosFuture.map { (notaryInfos, localNetworkMap) ->
|
||||
val listOfFutureNodeHandles = startNotaries(localNetworkMap)
|
||||
notaryInfos.zip(listOfFutureNodeHandles) { (identity, validating), nodeHandlesFuture ->
|
||||
NotaryHandle(identity, validating, nodeHandlesFuture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNotaryIdentityGeneration(): CordaFuture<List<NotaryInfo>> {
|
||||
return executorService.fork {
|
||||
notarySpecs.map { spec ->
|
||||
val identity = if (spec.cluster == null) {
|
||||
IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert)
|
||||
DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name)
|
||||
} else {
|
||||
IdentityGenerator.generateDistributedNotaryIdentity(
|
||||
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
dirs = generateNodeNames(spec).map { baseDirectory(it) },
|
||||
notaryName = spec.name,
|
||||
customRootCert = compatibilityZone?.rootCert
|
||||
notaryName = spec.name
|
||||
)
|
||||
}
|
||||
NotaryInfo(identity, spec.validating)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<List<NotaryInfo>> {
|
||||
// Start the registration process for all the notaries together then wait for their responses.
|
||||
return notarySpecs.map { spec ->
|
||||
require(spec.cluster == null) { "Registering distributed notaries not supported" }
|
||||
startNotaryRegistration(spec, rootCert, compatibilityZoneURL)
|
||||
}.transpose()
|
||||
}
|
||||
|
||||
private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NotaryInfo> {
|
||||
return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config ->
|
||||
// Node registration only gives us the node CA cert, not the identity cert. That is only created on first
|
||||
// startup or when the node is told to just generate its node info file. We do that here.
|
||||
if (startNodesInProcess) {
|
||||
executorService.fork {
|
||||
val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo()
|
||||
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
|
||||
}
|
||||
} else {
|
||||
// TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper
|
||||
// This causes two node info files to be generated.
|
||||
startOutOfProcessMiniNode(config, "--just-generate-node-info").map {
|
||||
// Once done we have to read the signed node info file that's been generated
|
||||
val nodeInfoFile = config.corda.baseDirectory.list { paths ->
|
||||
paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
|
||||
}
|
||||
val nodeInfo = nodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
|
||||
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateNodeNames(spec: NotarySpec): List<CordaX500Name> {
|
||||
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") }
|
||||
}
|
||||
|
||||
private fun startNotaries(): List<CordaFuture<List<NodeHandle>>> {
|
||||
private fun startNotaries(localNetworkMap: LocalNetworkMap?): List<CordaFuture<List<NodeHandle>>> {
|
||||
return notarySpecs.map {
|
||||
when {
|
||||
it.cluster == null -> startSingleNotary(it)
|
||||
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it)
|
||||
it.cluster == null -> startSingleNotary(it, localNetworkMap)
|
||||
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap)
|
||||
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
|
||||
}
|
||||
}
|
||||
@ -386,16 +445,17 @@ class DriverDSLImpl(
|
||||
// generating the configs for the nodes, probably making use of Any.toConfig()
|
||||
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
|
||||
|
||||
private fun startSingleNotary(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
|
||||
return startNode(
|
||||
providedName = spec.name,
|
||||
rpcUsers = spec.rpcUsers,
|
||||
verifierType = spec.verifierType,
|
||||
private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
|
||||
return startRegisteredNode(
|
||||
spec.name,
|
||||
localNetworkMap,
|
||||
spec.rpcUsers,
|
||||
spec.verifierType,
|
||||
customOverrides = NotaryConfig(spec.validating).toConfigMap()
|
||||
).map { listOf(it) }
|
||||
}
|
||||
|
||||
private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
|
||||
private fun startRaftNotaryCluster(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
|
||||
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
|
||||
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
|
||||
val config = NotaryConfig(
|
||||
@ -408,20 +468,22 @@ class DriverDSLImpl(
|
||||
val clusterAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
// Start the first node that will bootstrap the cluster
|
||||
val firstNodeFuture = startNode(
|
||||
providedName = nodeNames[0],
|
||||
rpcUsers = spec.rpcUsers,
|
||||
verifierType = spec.verifierType,
|
||||
val firstNodeFuture = startRegisteredNode(
|
||||
nodeNames[0],
|
||||
localNetworkMap,
|
||||
spec.rpcUsers,
|
||||
spec.verifierType,
|
||||
customOverrides = notaryConfig(clusterAddress)
|
||||
)
|
||||
|
||||
// All other nodes will join the cluster
|
||||
val restNodeFutures = nodeNames.drop(1).map {
|
||||
val nodeAddress = portAllocation.nextHostAndPort()
|
||||
startNode(
|
||||
providedName = it,
|
||||
rpcUsers = spec.rpcUsers,
|
||||
verifierType = spec.verifierType,
|
||||
startRegisteredNode(
|
||||
it,
|
||||
localNetworkMap,
|
||||
spec.rpcUsers,
|
||||
spec.verifierType,
|
||||
customOverrides = notaryConfig(nodeAddress, clusterAddress)
|
||||
)
|
||||
}
|
||||
@ -436,8 +498,6 @@ class DriverDSLImpl(
|
||||
return driverDirectory / nodeDirectoryName
|
||||
}
|
||||
|
||||
override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName))
|
||||
|
||||
/**
|
||||
* @param initial number of nodes currently in the network map of a running node.
|
||||
* @param networkMapCacheChangeObservable an observable returning the updates to the node network map.
|
||||
@ -447,7 +507,7 @@ class DriverDSLImpl(
|
||||
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
|
||||
ConnectableObservable<Int> {
|
||||
val count = AtomicInteger(initial)
|
||||
return networkMapCacheChangeObservable.map { it ->
|
||||
return networkMapCacheChangeObservable.map {
|
||||
when (it) {
|
||||
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
|
||||
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
|
||||
@ -482,31 +542,44 @@ class DriverDSLImpl(
|
||||
return future
|
||||
}
|
||||
|
||||
private fun startOutOfProcessNodeRegistration(config: Config, configuration: NodeConfiguration): CordaFuture<Unit> {
|
||||
/**
|
||||
* Start the node with the given flag which is expected to start the node for some function, which once complete will
|
||||
* terminate the node.
|
||||
*/
|
||||
private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture<Unit> {
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
|
||||
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort,
|
||||
systemProperties, cordappPackages, "200m", initialRegistration = true)
|
||||
val process = startOutOfProcessNode(
|
||||
config,
|
||||
quasarJarPath,
|
||||
debugPort,
|
||||
jolokiaJarPath,
|
||||
monitorPort,
|
||||
systemProperties,
|
||||
cordappPackages,
|
||||
"200m",
|
||||
extraCmdLineFlag
|
||||
)
|
||||
|
||||
return poll(executorService, "node registration (${configuration.myLegalName})") {
|
||||
return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") {
|
||||
if (process.isAlive) null else Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNodeInternal(config: Config,
|
||||
private fun startNodeInternal(config: NodeConfig,
|
||||
webAddress: NetworkHostAndPort,
|
||||
startInProcess: Boolean?,
|
||||
maximumHeapSize: String): CordaFuture<NodeHandle> {
|
||||
val configuration = config.parseAsNodeConfiguration()
|
||||
val baseDirectory = configuration.baseDirectory.createDirectories()
|
||||
nodeInfoFilesCopier?.addConfig(baseDirectory)
|
||||
networkParameters?.install(baseDirectory)
|
||||
maximumHeapSize: String,
|
||||
localNetworkMap: LocalNetworkMap?): CordaFuture<NodeHandle> {
|
||||
val baseDirectory = config.corda.baseDirectory.createDirectories()
|
||||
localNetworkMap?.networkParametersCopier?.install(baseDirectory)
|
||||
localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory)
|
||||
val onNodeExit: () -> Unit = {
|
||||
nodeInfoFilesCopier?.removeConfig(baseDirectory)
|
||||
countObservables.remove(configuration.myLegalName)
|
||||
localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory)
|
||||
countObservables.remove(config.corda.myLegalName)
|
||||
}
|
||||
if (startInProcess ?: startNodesInProcess) {
|
||||
val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages)
|
||||
val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages)
|
||||
shutdownManager.registerShutdown(
|
||||
nodeAndThreadFuture.map { (node, thread) ->
|
||||
{
|
||||
@ -516,16 +589,16 @@ class DriverDSLImpl(
|
||||
}
|
||||
)
|
||||
return nodeAndThreadFuture.flatMap { (node, thread) ->
|
||||
establishRpc(configuration, openFuture()).flatMap { rpc ->
|
||||
establishRpc(config, openFuture()).flatMap { rpc ->
|
||||
allNodesConnected(rpc).map {
|
||||
NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit)
|
||||
NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
|
||||
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, initialRegistration = false)
|
||||
val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null)
|
||||
if (waitForNodesToFinish) {
|
||||
state.locked {
|
||||
processes += process
|
||||
@ -533,22 +606,21 @@ class DriverDSLImpl(
|
||||
} else {
|
||||
shutdownManager.registerProcessShutdown(process)
|
||||
}
|
||||
val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process)
|
||||
val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process)
|
||||
return p2pReadyFuture.flatMap {
|
||||
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${configuration.myLegalName})") {
|
||||
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") {
|
||||
if (process.isAlive) null else process
|
||||
}
|
||||
establishRpc(configuration, processDeathFuture).flatMap { rpc ->
|
||||
establishRpc(config, processDeathFuture).flatMap { rpc ->
|
||||
// Check for all nodes to have all other nodes in background in case RPC is failing over:
|
||||
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
|
||||
firstOf(processDeathFuture, networkMapFuture) {
|
||||
if (it == processDeathFuture) {
|
||||
throw ListenProcessDeathException(configuration.p2pAddress, process)
|
||||
throw ListenProcessDeathException(config.corda.p2pAddress, process)
|
||||
}
|
||||
processDeathFuture.cancel(false)
|
||||
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
|
||||
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process,
|
||||
onNodeExit)
|
||||
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -561,6 +633,25 @@ class DriverDSLImpl(
|
||||
return pollFuture
|
||||
}
|
||||
|
||||
/**
|
||||
* The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories.
|
||||
*/
|
||||
private inner class LocalNetworkMap(notaryInfos: List<NotaryInfo>) {
|
||||
val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos))
|
||||
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
|
||||
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
|
||||
// Investigate whether we can avoid that.
|
||||
val nodeInfosCopier = NodeInfoFilesCopier().also { shutdownManager.registerShutdown(it::close) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration].
|
||||
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
|
||||
*/
|
||||
private class NodeConfig(val typesafe: Config) {
|
||||
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val log = contextLogger()
|
||||
|
||||
@ -588,28 +679,26 @@ class DriverDSLImpl(
|
||||
|
||||
private fun startInProcessNode(
|
||||
executorService: ScheduledExecutorService,
|
||||
nodeConf: NodeConfiguration,
|
||||
config: Config,
|
||||
config: NodeConfig,
|
||||
cordappPackages: List<String>
|
||||
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
|
||||
return executorService.fork {
|
||||
log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}")
|
||||
log.info("Starting in-process Node ${config.corda.myLegalName.organisation}")
|
||||
// Write node.conf
|
||||
writeConfig(nodeConf.baseDirectory, "node.conf", config)
|
||||
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
|
||||
// TODO pass the version in?
|
||||
val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start()
|
||||
val nodeThread = thread(name = nodeConf.myLegalName.organisation) {
|
||||
val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start()
|
||||
val nodeThread = thread(name = config.corda.myLegalName.organisation) {
|
||||
node.internals.run()
|
||||
}
|
||||
node to nodeThread
|
||||
}.flatMap { nodeAndThread ->
|
||||
addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread }
|
||||
addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread }
|
||||
}
|
||||
}
|
||||
|
||||
private fun startOutOfProcessNode(
|
||||
nodeConf: NodeConfiguration,
|
||||
config: Config,
|
||||
config: NodeConfig,
|
||||
quasarJarPath: String,
|
||||
debugPort: Int?,
|
||||
jolokiaJarPath: String,
|
||||
@ -617,15 +706,17 @@ class DriverDSLImpl(
|
||||
overriddenSystemProperties: Map<String, String>,
|
||||
cordappPackages: List<String>,
|
||||
maximumHeapSize: String,
|
||||
initialRegistration: Boolean
|
||||
extraCmdLineFlag: String?
|
||||
): Process {
|
||||
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled"))
|
||||
log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " +
|
||||
"debug port is " + (debugPort ?: "not enabled") + ", " +
|
||||
"jolokia monitoring port is " + (monitorPort ?: "not enabled"))
|
||||
// Write node.conf
|
||||
writeConfig(nodeConf.baseDirectory, "node.conf", config)
|
||||
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
|
||||
|
||||
val systemProperties = mutableMapOf(
|
||||
"name" to nodeConf.myLegalName,
|
||||
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
|
||||
"name" to config.corda.myLegalName,
|
||||
"visualvm.display.name" to "corda-${config.corda.myLegalName}",
|
||||
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
|
||||
"log4j2.debug" to if(debugPort != null) "true" else "false"
|
||||
)
|
||||
@ -643,18 +734,18 @@ class DriverDSLImpl(
|
||||
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
|
||||
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
|
||||
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
|
||||
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
|
||||
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;)"
|
||||
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
|
||||
"-javaagent:$quasarJarPath=$excludePattern"
|
||||
val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" }
|
||||
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
|
||||
|
||||
val arguments = mutableListOf(
|
||||
"--base-directory=${nodeConf.baseDirectory}",
|
||||
"--base-directory=${config.corda.baseDirectory}",
|
||||
"--logging-level=$loggingLevel",
|
||||
"--no-local-shell").also {
|
||||
if (initialRegistration) {
|
||||
it += "--initial-registration"
|
||||
if (extraCmdLineFlag != null) {
|
||||
it += extraCmdLineFlag
|
||||
}
|
||||
}.toList()
|
||||
|
||||
@ -663,8 +754,8 @@ class DriverDSLImpl(
|
||||
arguments = arguments,
|
||||
jdwpPort = debugPort,
|
||||
extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent),
|
||||
errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
|
||||
workingDirectory = nodeConf.baseDirectory,
|
||||
errorLogPath = config.corda.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
|
||||
workingDirectory = config.corda.baseDirectory,
|
||||
maximumHeapSize = maximumHeapSize
|
||||
)
|
||||
}
|
||||
@ -831,11 +922,16 @@ fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API to enable testing of the network map service and node registration process using the internal driver.
|
||||
* @property url The base CZ URL for registration and network map updates
|
||||
* @property rootCert If specified then the node will register itself using [url] and expect the registration response
|
||||
* to be rooted at this cert.
|
||||
* @property publishNotaries Hook for a network map server to capture the generated [NotaryInfo] objects needed for
|
||||
* creating the network parameters. This is needed as the network map server is expected to distribute it. The callback
|
||||
* will occur on a different thread to the driver-calling thread.
|
||||
* @property rootCert If specified then the nodes will register themselves with the doorman service using [url] and expect
|
||||
* the registration response to be rooted at this cert. If not specified then no registration is performed and the dev
|
||||
* root cert is used as normal.
|
||||
*/
|
||||
data class CompatibilityZoneParams(val url: URL, val rootCert: X509Certificate? = null)
|
||||
data class CompatibilityZoneParams(val url: URL, val publishNotaries: (List<NotaryInfo>) -> Unit, val rootCert: X509Certificate? = null)
|
||||
|
||||
fun <A> internalDriver(
|
||||
isDebug: Boolean = DriverParameters().isDebug,
|
||||
@ -875,8 +971,7 @@ fun <A> internalDriver(
|
||||
}
|
||||
|
||||
fun getTimestampAsDirectoryName(): String {
|
||||
// Add a random number in case 2 tests are started in the same instant.
|
||||
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + random63BitValue()
|
||||
return DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC).format(Instant.now())
|
||||
}
|
||||
|
||||
fun writeConfig(path: Path, filename: String, config: Config) {
|
||||
|
@ -1,22 +1,22 @@
|
||||
package net.corda.testing.node.internal.network
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
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.network.DigitalSignatureWithCert
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
||||
import net.corda.nodeapi.internal.*
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.ROOT_CA
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||
@ -36,26 +36,36 @@ import javax.ws.rs.core.Response.ok
|
||||
|
||||
class NetworkMapServer(cacheTimeout: Duration,
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
|
||||
rootCa: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
|
||||
private val myHostNameValue: String = "test.host.name",
|
||||
vararg additionalServices: Any) : Closeable {
|
||||
companion object {
|
||||
val stubNetworkParameter = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
||||
private val serializedParameters = stubNetworkParameter.serialize()
|
||||
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
|
||||
|
||||
private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair {
|
||||
val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val networkMapCert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
CertificateType.NETWORK_MAP,
|
||||
rootCAKeyAndCert.certificate,
|
||||
rootCAKeyAndCert.keyPair,
|
||||
X500Name("CN=Corda Network Map,L=London"),
|
||||
CordaX500Name("Corda Network Map", "R3 Ltd", "London","GB"),
|
||||
networkMapKey.public).cert
|
||||
// Check that the certificate validates. Nodes will perform this check upon receiving a network map,
|
||||
// it's better to fail here than there.
|
||||
X509Utilities.validateCertificateChain(rootCAKeyAndCert.certificate.cert, networkMapCert)
|
||||
return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey)
|
||||
}
|
||||
}
|
||||
|
||||
private val server: Server
|
||||
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca))
|
||||
var networkParameters: NetworkParameters = stubNetworkParameters
|
||||
set(networkParameters) {
|
||||
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
|
||||
field = networkParameters
|
||||
}
|
||||
private val serializedParameters get() = networkParameters.serialize()
|
||||
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa))
|
||||
|
||||
|
||||
init {
|
||||
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||
@ -95,13 +105,13 @@ class NetworkMapServer(cacheTimeout: Duration,
|
||||
}
|
||||
|
||||
@Path("network-map")
|
||||
class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) {
|
||||
inner class InMemoryNetworkMapService(private val cacheTimeout: Duration,
|
||||
private val networkMapKeyAndCert: CertificateAndKeyPair) {
|
||||
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
|
||||
private val parametersHash = serializedParameters.hash
|
||||
private val signedParameters = SignedData(
|
||||
private val parametersHash by lazy { serializedParameters.hash }
|
||||
private val signedParameters by lazy { SignedData(
|
||||
serializedParameters,
|
||||
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))
|
||||
)
|
||||
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))) }
|
||||
|
||||
@POST
|
||||
@Path("publish")
|
||||
@ -151,7 +161,7 @@ class NetworkMapServer(cacheTimeout: Duration,
|
||||
@GET
|
||||
@Path("my-hostname")
|
||||
fun getHostName(): Response {
|
||||
return Response.ok("test.host.name").build()
|
||||
return Response.ok(myHostNameValue).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,27 +5,24 @@ package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.toStringShort
|
||||
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
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -78,29 +75,11 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<NetworkHostAnd
|
||||
return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) }
|
||||
}
|
||||
|
||||
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLConfiguration {
|
||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
|
||||
init {
|
||||
configureDevKeyAndTrustStores(legalName)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTestPartyAndCertificate(party: Party): PartyAndCertificate {
|
||||
val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT
|
||||
val intermediate: CertificateAndKeyPair = DEV_CA
|
||||
|
||||
|
||||
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val nodeCaCert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA,
|
||||
intermediate.certificate,
|
||||
intermediate.keyPair,
|
||||
party.name,
|
||||
nodeCaKeyPair.public,
|
||||
nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()))
|
||||
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediate, party.name)
|
||||
|
||||
val identityCert = X509Utilities.createCertificate(
|
||||
CertificateType.LEGAL_IDENTITY,
|
||||
|
@ -1,10 +1,19 @@
|
||||
package net.corda.testing.internal
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
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.serialization.amqp.AMQP_ENABLED
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.internal.stubbing.answers.ThrowsException
|
||||
import java.lang.reflect.Modifier
|
||||
import java.nio.file.Files
|
||||
import java.util.*
|
||||
|
||||
@Suppress("unused")
|
||||
@ -42,3 +51,56 @@ fun <T> rigorousMock(clazz: Class<T>): T = Mockito.mock(clazz) {
|
||||
it.callRealMethod()
|
||||
}
|
||||
}
|
||||
|
||||
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration {
|
||||
return object : SSLConfiguration {
|
||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
|
||||
init {
|
||||
configureDevKeyAndTrustStores(legalName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultRootCaName = CordaX500Name("Corda Root CA", "R3 Ltd", "London", "GB")
|
||||
private val defaultIntermediateCaName = CordaX500Name("Corda Intermediate CA", "R3 Ltd", "London", "GB")
|
||||
|
||||
/**
|
||||
* Returns a pair of [CertificateAndKeyPair]s, the first being the root CA and the second the intermediate CA.
|
||||
* @param rootCaName The subject name for the root CA cert.
|
||||
* @param intermediateCaName The subject name for the intermediate CA cert.
|
||||
*/
|
||||
fun createDevIntermediateCaCertPath(
|
||||
rootCaName: CordaX500Name = defaultRootCaName,
|
||||
intermediateCaName: CordaX500Name = defaultIntermediateCaName
|
||||
): Pair<CertificateAndKeyPair, CertificateAndKeyPair> {
|
||||
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCert = X509Utilities.createSelfSignedCACertificate(rootCaName, rootKeyPair)
|
||||
|
||||
val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCaCert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
rootCert,
|
||||
rootKeyPair,
|
||||
intermediateCaName,
|
||||
intermediateCaKeyPair.public)
|
||||
|
||||
return Pair(CertificateAndKeyPair(rootCert, rootKeyPair), CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a triple of [CertificateAndKeyPair]s, the first being the root CA, the second the intermediate CA and the third
|
||||
* the node CA.
|
||||
* @param legalName The subject name for the node CA cert.
|
||||
*/
|
||||
fun createDevNodeCaCertPath(
|
||||
legalName: CordaX500Name,
|
||||
rootCaName: CordaX500Name = defaultRootCaName,
|
||||
intermediateCaName: CordaX500Name = defaultIntermediateCaName
|
||||
): Triple<CertificateAndKeyPair, CertificateAndKeyPair, CertificateAndKeyPair> {
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName)
|
||||
val nodeCa = createDevNodeCa(intermediateCa, legalName)
|
||||
return Triple(rootCa, intermediateCa, nodeCa)
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ configurations {
|
||||
runtimeArtifacts
|
||||
}
|
||||
|
||||
// TODO Fix SLF4J warnings that occur when running the bootstrapper
|
||||
dependencies {
|
||||
compile "org.slf4j:slf4j-nop:$slf4j_version"
|
||||
}
|
||||
|
||||
task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
|
||||
applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
|
||||
archiveName "network-bootstrapper.jar"
|
||||
@ -16,6 +19,9 @@ task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').comp
|
||||
minJavaVersion = '1.8.0'
|
||||
jvmArgs = ['-XX:+UseG1GC']
|
||||
}
|
||||
from(project(':node:capsule').tasks['buildCordaJAR']) {
|
||||
rename 'corda-(.*)', 'corda.jar'
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':node-api').configurations.runtime,
|
||||
project(':node-api').jar
|
||||
|
@ -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.DevIdentityGenerator
|
||||
import tornadofx.*
|
||||
import java.io.IOException
|
||||
import java.lang.management.ManagementFactory
|
||||
@ -153,7 +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 IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
|
||||
return DevIdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
|
Loading…
Reference in New Issue
Block a user