mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
Merge remote-tracking branch 'open/master' into colljos-os-merge-rc01
This commit is contained in:
@ -29,12 +29,14 @@ class ArtemisTcpTransport {
|
||||
// but we allow classical RSA certificates to work in case:
|
||||
// a) we need to use keytool certificates in some demos,
|
||||
// b) we use cloud providers or HSMs that do not support ECC.
|
||||
private val CIPHER_SUITES = listOf(
|
||||
val CIPHER_SUITES = listOf(
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
)
|
||||
|
||||
val TLS_VERSIONS = listOf("TLSv1.2")
|
||||
|
||||
fun tcpTransport(
|
||||
direction: ConnectionDirection,
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
@ -68,7 +70,7 @@ class ArtemisTcpTransport {
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","),
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
|
||||
VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames
|
||||
)
|
||||
|
@ -1,32 +0,0 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.NetworkParameters
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
|
||||
class NetworkParametersCopier(networkParameters: NetworkParameters) {
|
||||
private companion object {
|
||||
val DUMMY_MAP_KEY = entropyToKeyPair(BigInteger.valueOf(123))
|
||||
}
|
||||
|
||||
private val serializedNetworkParameters = networkParameters.let {
|
||||
val serialize = it.serialize()
|
||||
val signature = DUMMY_MAP_KEY.sign(serialize)
|
||||
SignedData(serialize, signature).serialize()
|
||||
}
|
||||
|
||||
fun install(dir: Path) {
|
||||
try {
|
||||
serializedNetworkParameters.open().copyTo(dir / "network-parameters")
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// Leave the file untouched if it already exists
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.readAll
|
||||
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.contextLogger
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_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.Path
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This class is loaded by Cordform using reflection to generate the network parameters. It is assumed that Cordform has
|
||||
* already asked each node to generate its node info file.
|
||||
*/
|
||||
@Suppress("UNUSED")
|
||||
class NetworkParametersGenerator {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
fun run(nodesDirs: List<Path>) {
|
||||
logger.info("NetworkParameters generation using node directories: $nodesDirs")
|
||||
try {
|
||||
initialiseSerialization()
|
||||
val notaryInfos = gatherNotaryIdentities(nodesDirs)
|
||||
val copier = NetworkParametersCopier(NetworkParameters(
|
||||
minimumPlatformVersion = 1,
|
||||
notaries = notaryInfos,
|
||||
modifiedTime = Instant.now(),
|
||||
eventHorizon = 10000.days,
|
||||
maxMessageSize = 40000,
|
||||
maxTransactionSize = 40000,
|
||||
epoch = 1
|
||||
))
|
||||
nodesDirs.forEach(copier::install)
|
||||
} finally {
|
||||
_contextSerializationEnv.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun gatherNotaryIdentities(nodesDirs: List<Path>): List<NotaryInfo> {
|
||||
return nodesDirs.mapNotNull { nodeDir ->
|
||||
val nodeConfig = ConfigFactory.parseFile((nodeDir / "node.conf").toFile())
|
||||
if (nodeConfig.hasPath("notary")) {
|
||||
val validating = nodeConfig.getConfig("notary").getBoolean("validating")
|
||||
val nodeInfoFile = nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
||||
processFile(nodeInfoFile)?.let { NotaryInfo(it.notaryIdentity(), validating) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
|
||||
}
|
||||
|
||||
private fun NodeInfo.notaryIdentity(): Party {
|
||||
return when (legalIdentities.size) {
|
||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||
1 -> legalIdentities[0]
|
||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||
// cluster and is shared by all the other members. This is the notary identity.
|
||||
2 -> legalIdentities[1]
|
||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun processFile(file: Path): NodeInfo? {
|
||||
return try {
|
||||
logger.info("Reading NodeInfo from file: $file")
|
||||
val signedData = file.readAll().deserialize<SignedData<NodeInfo>>()
|
||||
signedData.verified()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Exception parsing NodeInfo from file. $file", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
||||
// KryoServerSerializationScheme is not accessible from nodeapi.
|
||||
private fun initialiseSerialization() {
|
||||
val context = if (java.lang.Boolean.getBoolean("net.corda.testing.amqp.enable")) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT
|
||||
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoParametersSerializationScheme)
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
},
|
||||
context)
|
||||
)
|
||||
}
|
||||
|
||||
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.verify
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected
|
||||
* to be in the same order as the identities.
|
||||
*/
|
||||
// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key
|
||||
// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node
|
||||
// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite
|
||||
// public keys.
|
||||
@CordaSerializable
|
||||
class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<DigitalSignature>) {
|
||||
fun verified(): NodeInfo {
|
||||
val nodeInfo = raw.deserialize()
|
||||
val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey }
|
||||
|
||||
if (identities.size < signatures.size) {
|
||||
throw SignatureException("Extra signatures. Found ${signatures.size} expected ${identities.size}")
|
||||
}
|
||||
if (identities.size > signatures.size) {
|
||||
throw SignatureException("Missing signatures. Found ${signatures.size} expected ${identities.size}")
|
||||
}
|
||||
|
||||
val rawBytes = raw.bytes // To avoid cloning the byte array multiple times
|
||||
identities.zip(signatures).forEach { (identity, signature) ->
|
||||
try {
|
||||
identity.owningKey.verify(rawBytes, signature)
|
||||
} catch (e: SignatureException) {
|
||||
throw SignatureException("$identity: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return nodeInfo
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
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.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.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.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
* Class to bootstrap a local network of Corda nodes on the same filesystem.
|
||||
*/
|
||||
class NetworkBootstrapper {
|
||||
companion object {
|
||||
// TODO This will probably need to change once we start using a bundled JVM
|
||||
private val nodeInfoGenCmd = listOf(
|
||||
"java",
|
||||
"-jar",
|
||||
"corda.jar",
|
||||
"--just-generate-node-info"
|
||||
)
|
||||
|
||||
private const val LOGS_DIR_NAME = "logs"
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val arg = args.singleOrNull() ?: throw IllegalArgumentException("Expecting single argument which is the nodes' parent directory")
|
||||
NetworkBootstrapper().bootstrap(Paths.get(arg).toAbsolutePath().normalize())
|
||||
}
|
||||
}
|
||||
|
||||
fun bootstrap(directory: Path) {
|
||||
directory.createDirectories()
|
||||
println("Bootstrapping local network in $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")
|
||||
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
|
||||
println("Distributing all node info-files to all nodes")
|
||||
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
||||
println("Gathering notary identities")
|
||||
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
|
||||
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
|
||||
installNetworkParameters(notaryInfos, nodeDirs)
|
||||
println("Bootstrapping complete!")
|
||||
} finally {
|
||||
_contextSerializationEnv.set(null)
|
||||
processes.forEach { if (it.isAlive) it.destroyForcibly() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
|
||||
return nodeDirs.map { nodeDir ->
|
||||
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
||||
ProcessBuilder(nodeInfoGenCmd)
|
||||
.directory(nodeDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput((logsDir / "node-info-gen.log").toFile())
|
||||
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
"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() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
|
||||
for (nodeDir in nodeDirs) {
|
||||
val additionalNodeInfosDir = (nodeDir / CordformNode.NODE_INFO_DIRECTORY).createDirectories()
|
||||
for (nodeInfoFile in nodeInfoFiles) {
|
||||
nodeInfoFile.copyToDirectory(additionalNodeInfosDir, StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun gatherNotaryInfos(nodeInfoFiles: List<Path>): List<NotaryInfo> {
|
||||
return nodeInfoFiles.mapNotNull { nodeInfoFile ->
|
||||
// The config contains the notary type
|
||||
val nodeConfig = ConfigFactory.parseFile((nodeInfoFile.parent / "node.conf").toFile())
|
||||
if (nodeConfig.hasPath("notary")) {
|
||||
val validating = nodeConfig.getConfig("notary").getBoolean("validating")
|
||||
// And the node-info file contains the notary's identity
|
||||
val nodeInfo = nodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
|
||||
NotaryInfo(nodeInfo.notaryIdentity(), validating)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
|
||||
}
|
||||
|
||||
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>) {
|
||||
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
|
||||
val copier = NetworkParametersCopier(NetworkParameters(
|
||||
minimumPlatformVersion = 1,
|
||||
notaries = notaryInfos,
|
||||
modifiedTime = Instant.now(),
|
||||
maxMessageSize = 10485760,
|
||||
maxTransactionSize = 40000,
|
||||
epoch = 1
|
||||
), overwriteFile = true)
|
||||
|
||||
nodeDirs.forEach(copier::install)
|
||||
}
|
||||
|
||||
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
|
||||
|
||||
private fun NodeInfo.notaryIdentity(): Party {
|
||||
return when (legalIdentities.size) {
|
||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||
1 -> legalIdentities[0]
|
||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||
// cluster and is shared by all the other members. This is the notary identity.
|
||||
2 -> legalIdentities[1]
|
||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||
}
|
||||
}
|
||||
|
||||
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
||||
// KryoServerSerializationScheme is not accessible from nodeapi.
|
||||
private fun initialiseSerialization() {
|
||||
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoParametersSerializationScheme)
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
},
|
||||
AMQP_P2P_CONTEXT)
|
||||
)
|
||||
}
|
||||
|
||||
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
package net.corda.nodeapi.internal
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.verify
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import java.security.SignatureException
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
// TODO: Need more discussion on rather we should move this class out of internal.
|
||||
const val NETWORK_PARAMS_FILE_NAME = "network-parameters"
|
||||
|
||||
/**
|
||||
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
|
||||
*/
|
||||
@ -21,21 +23,21 @@ import java.time.Instant
|
||||
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
|
||||
|
||||
/**
|
||||
* @property minimumPlatformVersion
|
||||
* @property notaries
|
||||
* @property eventHorizon
|
||||
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
|
||||
* @property notaries List of well known and trusted notary identities with information on validation type.
|
||||
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
|
||||
* @property maxTransactionSize Maximum permitted transaction size in bytes.
|
||||
* @property modifiedTime
|
||||
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
||||
* of parameters.
|
||||
*/
|
||||
// TODO Wire up the parameters
|
||||
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
|
||||
// It needs separate design.
|
||||
// TODO Currently maxTransactionSize is not wired.
|
||||
@CordaSerializable
|
||||
data class NetworkParameters(
|
||||
val minimumPlatformVersion: Int,
|
||||
val notaries: List<NotaryInfo>,
|
||||
val eventHorizon: Duration,
|
||||
val maxMessageSize: Int,
|
||||
val maxTransactionSize: Int,
|
||||
val modifiedTime: Instant,
|
||||
@ -45,6 +47,8 @@ data class NetworkParameters(
|
||||
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
||||
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
||||
require(epoch > 0) { "epoch must be at least 1" }
|
||||
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,20 +60,23 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
* contained within.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class SignedNetworkMap(val raw: SerializedBytes<NetworkMap>, val sig: DigitalSignatureWithCert) {
|
||||
class SignedNetworkMap(val raw: SerializedBytes<NetworkMap>, val signature: DigitalSignatureWithCert) {
|
||||
/**
|
||||
* Return the deserialized NetworkMap if the signature and certificate can be verified.
|
||||
*
|
||||
* @throws CertPathValidatorException if the certificate path is invalid.
|
||||
* @throws SignatureException if the signature is invalid.
|
||||
*/
|
||||
@Throws(SignatureException::class)
|
||||
fun verified(): NetworkMap {
|
||||
sig.by.publicKey.verify(raw.bytes, sig)
|
||||
@Throws(SignatureException::class, CertPathValidatorException::class)
|
||||
fun verified(trustedRoot: X509Certificate): NetworkMap {
|
||||
signature.by.publicKey.verify(raw.bytes, signature)
|
||||
// Assume network map cert is under the default trust root.
|
||||
X509Utilities.validateCertificateChain(trustedRoot, signature.by, trustedRoot)
|
||||
return raw.deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This class should reside in the [DigitalSignature] class.
|
||||
// TODO: Removing the val from signatureBytes causes serialisation issues
|
||||
/** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */
|
||||
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
|
||||
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
|
@ -0,0 +1,35 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.security.KeyPair
|
||||
|
||||
class NetworkParametersCopier(
|
||||
networkParameters: NetworkParameters,
|
||||
signingKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
|
||||
overwriteFile: Boolean = false
|
||||
) {
|
||||
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
||||
private val serializedNetworkParameters = networkParameters.let {
|
||||
val serialize = it.serialize()
|
||||
val signature = signingKeyPair.sign(serialize)
|
||||
SignedData(serialize, signature).serialize()
|
||||
}
|
||||
|
||||
fun install(nodeDir: Path) {
|
||||
try {
|
||||
serializedNetworkParameters.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
||||
// ignore this exception as we're happy with the existing file.
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.nodeapi.internal
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.internal.ThreadBox
|
||||
@ -65,7 +65,6 @@ class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()) : AutoCloseabl
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nodeConfig the configuration to be removed.
|
||||
* Remove the configuration of a node which is about to be stopped or already stopped.
|
||||
* No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this
|
||||
* one.
|
25
node-api/src/test/kotlin/net/corda/nodeapi/Eventually.kt
Normal file
25
node-api/src/test/kotlin/net/corda/nodeapi/Eventually.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package net.corda.nodeapi
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Ideas borrowed from "io.kotlintest" with some improvements made
|
||||
* This is meant for use from Kotlin code use only mainly due to it's inline/reified nature
|
||||
*/
|
||||
inline fun <reified E : Throwable, R> eventually(duration: Duration, f: () -> R): R {
|
||||
val end = System.nanoTime() + duration.toNanos()
|
||||
var times = 0
|
||||
while (System.nanoTime() < end) {
|
||||
try {
|
||||
return f()
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is E -> {
|
||||
}// ignore and continue
|
||||
else -> throw e // unexpected exception type - rethrow
|
||||
}
|
||||
}
|
||||
times++
|
||||
}
|
||||
throw AssertionError("Test failed after $duration; attempted $times times")
|
||||
}
|
@ -14,6 +14,7 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
|
@ -19,6 +19,8 @@ import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
|
||||
import net.corda.nodeapi.internal.serialization.withTokenContext
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Assert.*
|
||||
|
@ -0,0 +1,80 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.TestNodeInfoBuilder
|
||||
import net.corda.testing.internal.signWith
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
|
||||
class SignedNodeInfoTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||
|
||||
@Test
|
||||
fun `verifying single identity`() {
|
||||
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
|
||||
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifying multiple identities`() {
|
||||
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
|
||||
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifying missing signature`() {
|
||||
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||
val nodeInfo = nodeInfoBuilder.build()
|
||||
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey))
|
||||
assertThatThrownBy { signedNodeInfo.verified() }
|
||||
.isInstanceOf(SignatureException::class.java)
|
||||
.hasMessageContaining("Missing signatures")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifying extra signature`() {
|
||||
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
val nodeInfo = nodeInfoBuilder.build()
|
||||
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private))
|
||||
assertThatThrownBy { signedNodeInfo.verified() }
|
||||
.isInstanceOf(SignatureException::class.java)
|
||||
.hasMessageContaining("Extra signatures")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifying incorrect signature`() {
|
||||
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
val nodeInfo = nodeInfoBuilder.build()
|
||||
val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private))
|
||||
assertThatThrownBy { signedNodeInfo.verified() }
|
||||
.isInstanceOf(SignatureException::class.java)
|
||||
.hasMessageContaining(ALICE_NAME.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifying with signatures in wrong order`() {
|
||||
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||
val (_, bobKey) = nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||
val nodeInfo = nodeInfoBuilder.build()
|
||||
val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey))
|
||||
assertThatThrownBy { signedNodeInfo.verified() }
|
||||
.isInstanceOf(SignatureException::class.java)
|
||||
.hasMessageContaining(ALICE_NAME.toString())
|
||||
}
|
||||
|
||||
private fun generateKeyPair() = Crypto.generateKeyPair()
|
||||
}
|
@ -51,7 +51,7 @@ class X509UtilitiesTest {
|
||||
val bob = TestIdentity(BOB_NAME, 80)
|
||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
val BOB get() = bob.party
|
||||
val BOB_PUBKEY get() = bob.pubkey
|
||||
val BOB_PUBKEY get() = bob.publicKey
|
||||
}
|
||||
|
||||
@Rule
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.nodeapi
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.nodeapi.internal.NodeInfoFilesCopier
|
||||
import net.corda.testing.eventually
|
||||
import net.corda.nodeapi.eventually
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -15,14 +14,7 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* tests for [NodeInfoFilesCopier]
|
||||
*/
|
||||
class NodeInfoFilesCopierTest {
|
||||
|
||||
@Rule @JvmField var folder = TemporaryFolder()
|
||||
private val rootPath get() = folder.root.toPath()
|
||||
private val scheduler = TestScheduler()
|
||||
companion object {
|
||||
private const val ORGANIZATION = "Organization"
|
||||
private const val NODE_1_PATH = "node1"
|
||||
@ -34,6 +26,13 @@ class NodeInfoFilesCopierTest {
|
||||
private val BAD_NODE_INFO_NAME = "something"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val folder = TemporaryFolder()
|
||||
|
||||
private val rootPath get() = folder.root.toPath()
|
||||
private val scheduler = TestScheduler()
|
||||
|
||||
private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase())
|
||||
|
||||
private val node1RootPath by lazy { nodeDir(NODE_1_PATH) }
|
||||
@ -41,7 +40,7 @@ class NodeInfoFilesCopierTest {
|
||||
private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
||||
private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
||||
|
||||
lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
|
||||
private lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
@ -5,6 +5,7 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
@ -23,7 +24,7 @@ class ContractAttachmentSerializerTest {
|
||||
private lateinit var factory: SerializationFactory
|
||||
private lateinit var context: SerializationContext
|
||||
private lateinit var contextWithToken: SerializationContext
|
||||
private val mockServices = MockServices(rigorousMock(), CordaX500Name("MegaCorp", "London", "GB"))
|
||||
private val mockServices = MockServices(emptyList(), rigorousMock(), CordaX500Name("MegaCorp", "London", "GB"))
|
||||
@Before
|
||||
fun setup() {
|
||||
factory = testSerialization.env.serializationFactory
|
||||
|
@ -13,7 +13,7 @@ import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
|
||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import net.corda.testing.rigorousMock
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
@ -33,7 +33,7 @@ import kotlin.test.assertTrue
|
||||
|
||||
class KryoTests {
|
||||
companion object {
|
||||
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey
|
||||
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
|
||||
}
|
||||
|
||||
@Rule
|
||||
|
@ -8,8 +8,8 @@ import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.Envelope
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.amqpSpecific
|
||||
import net.corda.testing.kryoSpecific
|
||||
import net.corda.testing.internal.amqpSpecific
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.amqpSpecific
|
||||
import net.corda.testing.kryoSpecific
|
||||
import net.corda.testing.internal.amqpSpecific
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Rule
|
||||
|
@ -8,7 +8,7 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.rigorousMock
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
|
@ -6,8 +6,8 @@ import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.kryoSpecific
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
|
@ -29,6 +29,7 @@ import org.apache.qpid.proton.codec.EncoderImpl
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
@ -49,11 +50,15 @@ class SerializationOutputTests {
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
val MEGA_CORP get() = megaCorp.party
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
data class Foo(val bar: String, val pub: Int)
|
||||
|
||||
data class testFloat(val f: Float)
|
||||
@ -473,9 +478,9 @@ class SerializationOutputTests {
|
||||
assertSerializedThrowableEquivalent(t, desThrowable)
|
||||
}
|
||||
|
||||
private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable = withTestSerialization {
|
||||
private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable {
|
||||
val newContext = SerializationFactory.defaultFactory.defaultContext.withProperty(CommonPropertyNames.IncludeInternalInfo, true)
|
||||
SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
|
||||
return SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Reference in New Issue
Block a user