From 71cb0f90ac5d7e898499a8769e7e2336ed290f6f Mon Sep 17 00:00:00 2001 From: James Brown <33660060+jamesbr3@users.noreply.github.com> Date: Mon, 8 Apr 2019 17:04:43 +0100 Subject: [PATCH] CORDA-2833 nodeinfo signing tool (#4987) * CORDA-2833 nodeinfo signing tool * CORDA-2833 PR fixes * CORDA-2833 remove unused imports * CORDA-2833 documentation for example usage --- experimental/nodeinfo/build.gradle | 32 ++++ .../kotlin/net.corda.nodeinfo/NodeInfo.kt | 178 ++++++++++++++++++ settings.gradle | 1 + 3 files changed, 211 insertions(+) create mode 100644 experimental/nodeinfo/build.gradle create mode 100644 experimental/nodeinfo/src/main/kotlin/net.corda.nodeinfo/NodeInfo.kt diff --git a/experimental/nodeinfo/build.gradle b/experimental/nodeinfo/build.gradle new file mode 100644 index 0000000000..fe4628c119 --- /dev/null +++ b/experimental/nodeinfo/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'java' +apply plugin: 'kotlin' + +description 'NodeInfo signing tool' + +dependencies { + compile project(':tools:cliutils') + compile "org.slf4j:jul-to-slf4j:$slf4j_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + compile project(':core') + compile project(':node-api') +} + +jar { + from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + baseName = "nodeinfo" + manifest { + attributes( + 'Main-Class': 'net.corda.nodeinfo.NodeInfoKt' + ) + } +} + +processResources { + from file("$rootDir/config/dev/log4j2.xml") + from file("$rootDir/node-api/src/main/resources/certificates/cordadevcakeys.jks") +} diff --git a/experimental/nodeinfo/src/main/kotlin/net.corda.nodeinfo/NodeInfo.kt b/experimental/nodeinfo/src/main/kotlin/net.corda.nodeinfo/NodeInfo.kt new file mode 100644 index 0000000000..6cc55db2a1 --- /dev/null +++ b/experimental/nodeinfo/src/main/kotlin/net.corda.nodeinfo/NodeInfo.kt @@ -0,0 +1,178 @@ +package net.corda.nodeinfo + +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.start +import net.corda.core.crypto.* +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.* +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.SerializationEnvironment +import net.corda.core.serialization.internal.nodeSerializationEnv +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.serialization.internal.* +import net.corda.serialization.internal.amqp.* +import picocli.CommandLine.* +import java.io.File +import java.nio.file.Path +import java.security.cert.CertificateFactory + +/** + * NodeInfo signing tool for Corda + * + * This utility can be used to generate nodeInfo files without having to run a corda node + * + * The java keystore containing the signing key for the nodeInfo must specified at the commandline + * using the --keyStore parameter. HSM are not currently supported. + * + * The resulting filename of the nodeinfo will be displayed to stdout + * + * Example usage: + * + * # generate a nodeinfo + * java -jar nodeinfo.jar --address host:port --outdir /nodedir --keyStore /nodedir/certificates/nodekeystore.jks + * + * nodeinfo will prompt you for the keystore password, and the password of the node identity private key: + * Store password (nodekeystore.jks): ******* + * Key password (identity-private-key): ******* + * + * # display information about an existing nodeinfo (name, address etc) + * java -jar nodeinfo.jar --display nodeinfo-12345678 + * + */ +fun main(args: Array) { + NodeInfoSigner().start(args) +} + +class NetworkHostAndPortConverter : ITypeConverter { + override fun convert(value: String?): NetworkHostAndPort { + return NetworkHostAndPort.parse(value!!) + } +} + +class NodeInfoSigner : CordaCliWrapper("nodeinfo-signer", "Display and generate nodeinfos") { + + @Option(names = ["--display"], paramLabel = "nodeinfo-file", description = ["Path to NodeInfo"]) + private var displayPath: Path? = null + + @Option(names = ["--address"], paramLabel = "host:port", description = ["Public address of node"], converter = [NetworkHostAndPortConverter::class]) + private var addressList: MutableList = mutableListOf() + + @Option(names = ["--platformVersion"], paramLabel = "int", description = ["Platform version that this node supports"]) + private var platformVersion: Int = 4 + + @Option(names = ["--serial"], paramLabel = "long", description = [""]) + private var serial: Long = 0 + + @Option(names = ["--outdir"], paramLabel = "directory", description = ["Output directory"]) + private var outputDirectory: Path? = null + + @Option(names = ["--keyStore"], description = ["Keystore containing identity certificate for signing"]) + private var keyStorePath: Path? = null + + @Option(names = ["--keyStorePass"], description = ["Keystore password (will prompt if not specified)"]) + private var keyStorePass: String? = null + + @Option(names = ["--keyAlias"], description = ["Alias of signing key - default is identity-private-key"]) + private var keyAlias: String? = "identity-private-key" + + @Option(names = ["--keyPass"], description = ["Password of signing key (will prompt if not specified)"]) + private var keyPass: String? = null + + private fun getInput(prompt: String): String { + print(prompt) + System.out.flush() + val console = System.console() + if(console != null) + return console.readPassword().toString() + else + return readLine()!! + } + + private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic + } + + override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + } + + private fun initialiseSerialization() { + nodeSerializationEnv = SerializationEnvironment.with( + SerializationFactoryImpl().apply { + registerScheme(AMQPInspectorSerializationScheme) + }, + AMQP_P2P_CONTEXT) + } + + class NodeInfoAndSignedNodeInfo(val nodeInfo : NodeInfo, val signedInfoNode: SignedNodeInfo) + + fun generateNodeInfo() : NodeInfoAndSignedNodeInfo { + val keyStore = X509KeyStore.fromFile(keyStorePath!!, keyStorePass!!) + val signingKey = keyStore.getCertificateAndKeyPair(keyAlias!!, keyPass!!) + val x509Chain = keyStore.getCertificateChain(keyAlias!!) + + val cf = CertificateFactory.getInstance("X.509") + val certPath = cf.generateCertPath(x509Chain) + val identityList = listOf(PartyAndCertificate(certPath)) + + val nodeInfo = NodeInfo(addressList, identityList, platformVersion, serial) + val serializedNodeInfo = nodeInfo.serialize() + + val sig = signingKey.keyPair.sign(serializedNodeInfo.bytes).withoutKey() // pure DigitalSignature + val sni = SignedNodeInfo(serializedNodeInfo, listOf(sig)) + + return NodeInfoAndSignedNodeInfo(nodeInfo, sni) + } + + override fun runProgram(): Int { + + initialiseSerialization() + + if(displayPath != null) { + val nodeInfo = nodeInfoFromFile(displayPath!!.toFile()) + + println("identities: " + nodeInfo.legalIdentities[0].name) + println("address: " + nodeInfo.addresses[0]) + println("platformVersion: " + nodeInfo.platformVersion) + println("serial " + nodeInfo.serial) + return 0; + } + else { + require(addressList.size > 0){ "At least one --address must be specified" } + require(outputDirectory != null) { "The --outdir parameter must be specified" } + require(keyStorePath != null && keyAlias != null) { "The --keyStorePath and --keyAlias parameters must be specified" } + } + + if(keyStorePass == null) + keyStorePass = getInput("Store password (${keyStorePath?.fileName}): ") + + if(keyPass == null) + keyPass = getInput("Key password (${keyAlias}): ") + + + val nodeInfoSigned = generateNodeInfo() + val fileNameHash = nodeInfoSigned.nodeInfo.legalIdentities[0].name.serialize().hash + + val outputFile = outputDirectory!!.toString() / "nodeinfo-${fileNameHash.toString()}" + + println(outputFile) + + outputFile!!.toFile().writeBytes(nodeInfoSigned.signedInfoNode.serialize().bytes) + return 0 + } + + fun nodeInfoFromFile(nodeInfoPath: File) : NodeInfo { + var serializedNodeInfo = SerializedBytes(nodeInfoPath.toPath().readAll()) + var signedNodeInfo = serializedNodeInfo.deserialize() + return signedNodeInfo.verified() + } + +} + diff --git a/settings.gradle b/settings.gradle index a1392c809f..7e51eb92b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include 'experimental:avalanche' include 'experimental:behave' include 'experimental:quasar-hook' include 'experimental:corda-utils' +include 'experimental:nodeinfo' include 'jdk8u-deterministic' include 'test-common' include 'test-cli'