From fdf29f3c5804e52733c9728ea8128fbb41d6ba1b Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 16 Oct 2017 15:46:29 +0100 Subject: [PATCH 01/12] Update docker container and JDK make file * Add missing build dependencies to `docker-minimal` * Build JDK without ALSA, CUPS, FreeType, Pulse and X11 --- sgx-jvm/Makefile | 5 ++++- sgx-jvm/dependencies/README.md | 2 +- sgx-jvm/dependencies/docker-minimal/Dockerfile | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sgx-jvm/Makefile b/sgx-jvm/Makefile index a11c60182f..436a11994e 100644 --- a/sgx-jvm/Makefile +++ b/sgx-jvm/Makefile @@ -29,7 +29,10 @@ jdk8u: git clone -b deterministic-jvm8 --single-branch https://github.com/corda/openjdk $@ $(JDK_IMAGE): jdk8u - cd jdk8u && $(SHELL) ./configure && $(MAKE) JOBS=2 images docs + cd jdk8u && \ + ALSA_NOT_NEEDED=yes CUPS_NOT_NEEDED=yes FREETYPE_NOT_NEEDED=yes \ + PULSE_NOT_NEEDED=yes X11_NOT_NEEDED=yes $(SHELL) ./configure && \ + $(MAKE) JOBS=2 images docs linux-sgx/external/ippcp_internal/inc: cd linux-sgx && $(SHELL) ./download_prebuilt.sh diff --git a/sgx-jvm/dependencies/README.md b/sgx-jvm/dependencies/README.md index 1c42406d6a..e16398cab2 100644 --- a/sgx-jvm/dependencies/README.md +++ b/sgx-jvm/dependencies/README.md @@ -26,4 +26,4 @@ Some dependencies are still required to be installed, these are specified in `do ```bash $ docker build -t minimal docker-minimal # builds a Docker image using docker-minimal/ $ bash build_in_image.sh minimal # Runs the build inside the `minimal` image -``` \ No newline at end of file +``` diff --git a/sgx-jvm/dependencies/docker-minimal/Dockerfile b/sgx-jvm/dependencies/docker-minimal/Dockerfile index 27de27d65d..4de99cea0b 100644 --- a/sgx-jvm/dependencies/docker-minimal/Dockerfile +++ b/sgx-jvm/dependencies/docker-minimal/Dockerfile @@ -1,6 +1,8 @@ FROM ubuntu:xenial RUN apt-get update -y -RUN apt-get install -y make gcc autoconf cmake g++ openjdk-8-jdk libtool ocaml python2.7 -RUN apt-get install -y mercurial wget +RUN apt-get install -y \ + autoconf ccache cmake cpio g++ gcc git libtool make \ + ocaml openjdk-8-jdk python2.7 unzip wget zip + ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 From 20e0e63eeda498ed7230d6655d63fd83a3a23d00 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Mon, 16 Oct 2017 13:18:21 +0100 Subject: [PATCH 02/12] Change string of PublicKey to hash as database primary key. --- .../services/network/PersistentNetworkMapService.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 6d3600d398..62269a1ba3 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,6 +1,6 @@ package net.corda.node.services.network -import net.corda.core.utilities.toBase58String +import net.corda.core.crypto.SecureHash import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox import net.corda.core.messaging.SingleMessageRecipient @@ -31,8 +31,8 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne @Entity @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") class NetworkNode( - @Id @Column(name = "node_party_key") - var publicKey: String = "", + @Id @Column(name = "node_party_key_hash") + var publicKeyHash: String = "", @Column var nodeParty: NodeParty = NodeParty(), @@ -58,14 +58,14 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne fun createNetworkNodesMap(): PersistentMap { return PersistentMap( - toPersistentEntityKey = { it.owningKey.toBase58String() }, + toPersistentEntityKey = { SecureHash.sha256(it.owningKey.encoded).toString() }, fromPersistentEntity = { Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))), it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> NetworkNode( - publicKey = key.owningKey.toBase58String(), + publicKeyHash = SecureHash.sha256(key.owningKey.encoded).toString(), nodeParty = NodeParty( key.name.toString(), key.certificate.encoded, From 0e5346caa1bec5d1c832dba2f66f4578963559e8 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 16 Oct 2017 17:23:12 +0100 Subject: [PATCH 03/12] Updated DB schemas from using string'ified key to use hash of key. --- .../net/corda/core/schemas/NodeInfoSchema.kt | 10 +++++----- .../keys/PersistentKeyManagementService.kt | 20 +++++++++---------- .../network/PersistentNetworkMapCache.kt | 6 +++--- .../network/PersistentNetworkMapService.kt | 5 +++-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index a0e6c80f49..8c014cb9a5 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -1,11 +1,11 @@ package net.corda.core.schemas +import net.corda.core.crypto.SecureHash import net.corda.core.identity.PartyAndCertificate 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.core.utilities.toBase58String import java.io.Serializable import javax.persistence.* @@ -85,8 +85,8 @@ object NodeInfoSchemaV1 : MappedSchema( @Table(name = "node_info_party_cert") data class DBPartyAndCertificate( @Id - @Column(name = "owning_key", length = 65535, nullable = false) - val owningKey: String, + @Column(name = "owning_key_hash") + val owningKeyHash: String, //@Id // TODO Do we assume that names are unique? Note: We can't have it as Id, because our toString on X500 is inconsistent. @Column(name = "party_name", nullable = false) @@ -102,10 +102,10 @@ object NodeInfoSchemaV1 : MappedSchema( private val persistentNodeInfos: Set = emptySet() ) { constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false) - : this(partyAndCert.party.owningKey.toBase58String(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) + : this(SecureHash.sha256(partyAndCert.owningKey.encoded).toString(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) fun toLegalIdentityAndCert(): PartyAndCertificate { - return partyCertBinary.deserialize() + return partyCertBinary.deserialize() } } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 4645c1d8e0..c1957696ab 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -8,6 +8,7 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.hexToBase58 import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.utilities.toBase58String import net.corda.node.utilities.AppendOnlyPersistentMap @@ -34,29 +35,28 @@ class PersistentKeyManagementService(val identityService: IdentityService, @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( - @Id - @Column(length = 6000, name = "public_key") - var publicKey: String = "", + @Column(name = "public_key_hash") + var publicKeyHash: String, @Lob @Column(name = "private_key") var privateKey: ByteArray = ByteArray(0) - ) + ) { + constructor(publicKey: PublicKey, privateKey: ByteArray) + : this(SecureHash.sha256(publicKey.encoded).toString(), privateKey) + } private companion object { fun createKeyMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( toPersistentEntityKey = { it.toBase58String() }, fromPersistentEntity = { - Pair(parsePublicKeyBase58(it.publicKey), - it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + Pair(parsePublicKeyBase58(it.publicKeyHash.hexToBase58()), + it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> - PersistentKey().apply { - publicKey = key.toBase58String() - privateKey = value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes - } + PersistentKey(key, value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes) }, persistentEntityClass = PersistentKey::class.java ) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index eb45e7a0df..a6dffc54c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -1,6 +1,7 @@ package net.corda.node.services.network import net.corda.core.concurrent.CordaFuture +import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -21,7 +22,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.toBase58String import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.ServiceHubInternal @@ -310,9 +310,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKey = :owningKey", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("owningKey", identityKey.toBase58String()) + query.setParameter("owningKeyHash", SecureHash.sha256(identityKey.encoded).toString()) return query.resultList } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 62269a1ba3..8dcce158ac 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -31,8 +31,9 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne @Entity @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") class NetworkNode( - @Id @Column(name = "node_party_key_hash") - var publicKeyHash: String = "", + @Id + @Column(name = "node_party_key_hash") + var publicKeyHash: String, @Column var nodeParty: NodeParty = NodeParty(), From 744f2c658a828aa8ffcec4ea4a62f9ca75665e20 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 16 Oct 2017 17:35:55 +0100 Subject: [PATCH 04/12] Explicitly set length of key hash to 64. --- core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt | 2 +- .../corda/node/services/keys/PersistentKeyManagementService.kt | 2 +- .../corda/node/services/network/PersistentNetworkMapService.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index 8c014cb9a5..c0e2d57c11 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -85,7 +85,7 @@ object NodeInfoSchemaV1 : MappedSchema( @Table(name = "node_info_party_cert") data class DBPartyAndCertificate( @Id - @Column(name = "owning_key_hash") + @Column(name = "owning_key_hash", length = 64) val owningKeyHash: String, //@Id // TODO Do we assume that names are unique? Note: We can't have it as Id, because our toString on X500 is inconsistent. diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index c1957696ab..9e41ddff52 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -36,7 +36,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( @Id - @Column(name = "public_key_hash") + @Column(name = "public_key_hash", length = 64) var publicKeyHash: String, @Lob diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 8dcce158ac..073b1ea0ac 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -32,7 +32,7 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") class NetworkNode( @Id - @Column(name = "node_party_key_hash") + @Column(name = "node_party_key_hash", length = 64) var publicKeyHash: String, @Column From 72ad9a51fe71d597a9214813bf2cd22fe0aa9592 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 16 Oct 2017 17:40:40 +0100 Subject: [PATCH 05/12] Use SecureHash extension method. --- .../main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt | 4 ++-- .../node/services/keys/PersistentKeyManagementService.kt | 2 +- .../node/services/network/PersistentNetworkMapService.kt | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index c0e2d57c11..1f4d375dc9 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -1,6 +1,6 @@ package net.corda.core.schemas -import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize @@ -102,7 +102,7 @@ object NodeInfoSchemaV1 : MappedSchema( private val persistentNodeInfos: Set = emptySet() ) { constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false) - : this(SecureHash.sha256(partyAndCert.owningKey.encoded).toString(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) + : this(partyAndCert.owningKey.encoded.sha256().toString(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) fun toLegalIdentityAndCert(): PartyAndCertificate { return partyCertBinary.deserialize() diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 9e41ddff52..8eaf8fc0c9 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -44,7 +44,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, var privateKey: ByteArray = ByteArray(0) ) { constructor(publicKey: PublicKey, privateKey: ByteArray) - : this(SecureHash.sha256(publicKey.encoded).toString(), privateKey) + : this(publicKey.encoded.sha256().toString(), privateKey) } private companion object { diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 073b1ea0ac..f89f66c84e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,6 +1,7 @@ package net.corda.node.services.network import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox import net.corda.core.messaging.SingleMessageRecipient @@ -9,12 +10,13 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.messaging.MessagingService -import net.corda.node.utilities.* +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.PersistentMap import net.corda.nodeapi.ArtemisMessagingComponent import java.io.ByteArrayInputStream import java.security.cert.CertificateFactory -import javax.persistence.* import java.util.* +import javax.persistence.* /** * A network map service backed by a database to survive restarts of the node hosting it. @@ -66,7 +68,7 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne }, toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> NetworkNode( - publicKeyHash = SecureHash.sha256(key.owningKey.encoded).toString(), + publicKeyHash = key.owningKey.encoded.sha256().toString(), nodeParty = NodeParty( key.name.toString(), key.certificate.encoded, From cda3ecbc0a8434bec39000114e39b9b8476008e2 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 17 Oct 2017 11:43:29 +0100 Subject: [PATCH 06/12] Revert DBPartyAndCertificate change: PK should be party name (not owningKeyHash). --- .../kotlin/net/corda/core/schemas/NodeInfoSchema.kt | 11 +++++------ .../services/network/PersistentNetworkMapCache.kt | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index 1f4d375dc9..61247d4f80 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -1,11 +1,11 @@ package net.corda.core.schemas -import net.corda.core.crypto.sha256 import net.corda.core.identity.PartyAndCertificate 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.core.utilities.toBase58String import java.io.Serializable import javax.persistence.* @@ -85,13 +85,12 @@ object NodeInfoSchemaV1 : MappedSchema( @Table(name = "node_info_party_cert") data class DBPartyAndCertificate( @Id - @Column(name = "owning_key_hash", length = 64) - val owningKeyHash: String, - - //@Id // TODO Do we assume that names are unique? Note: We can't have it as Id, because our toString on X500 is inconsistent. @Column(name = "party_name", nullable = false) val name: String, + @Column(name = "owning_key", length = 65535, nullable = false) + val owningKey: String, + @Column(name = "party_cert_binary") @Lob val partyCertBinary: ByteArray, @@ -102,7 +101,7 @@ object NodeInfoSchemaV1 : MappedSchema( private val persistentNodeInfos: Set = emptySet() ) { constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false) - : this(partyAndCert.owningKey.encoded.sha256().toString(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) + : this(partyAndCert.party.name.toString(), partyAndCert.party.owningKey.toBase58String(), partyAndCert.serialize().bytes, isMain) fun toLegalIdentityAndCert(): PartyAndCertificate { return partyCertBinary.deserialize() diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index a6dffc54c1..6f17e86b39 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -310,9 +310,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKey = :owningKey", NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("owningKeyHash", SecureHash.sha256(identityKey.encoded).toString()) + query.setParameter("owningKey", identityKey.toBase58String()) return query.resultList } From b861f4b7cb424fac112c95c1eb7afbdcc5212a7a Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 17 Oct 2017 14:00:58 +0100 Subject: [PATCH 07/12] Standardise Public Key hash size as per discussions with KC. --- .../identity/PersistentIdentityService.kt | 7 ++--- .../keys/PersistentKeyManagementService.kt | 26 ++++++++++--------- .../network/PersistentNetworkMapService.kt | 10 +++---- .../corda/node/utilities/CordaPersistence.kt | 8 ++++++ 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 7d3151c63b..18d074cd68 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -12,6 +12,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.MAX_HASH_HEX_SIZE import net.corda.node.utilities.NODE_DATABASE_PREFIX import org.bouncycastle.cert.X509CertificateHolder import java.io.ByteArrayInputStream @@ -72,8 +73,8 @@ class PersistentIdentityService(identities: Iterable = empt @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") class PersistentIdentity( @Id - @Column(name = "pk_hash", length = 64) - var publicKeyHash: String = "", + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE) + var publicKeyHash: String, @Lob @Column @@ -87,7 +88,7 @@ class PersistentIdentityService(identities: Iterable = empt @Column(name = "name", length = 128) var name: String = "", - @Column(name = "pk_hash", length = 64) + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE) var publicKeyHash: String = "" ) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 8eaf8fc0c9..9983976146 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -8,10 +8,8 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.utilities.hexToBase58 -import net.corda.core.utilities.parsePublicKeyBase58 -import net.corda.core.utilities.toBase58String import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.MAX_HASH_HEX_SIZE import net.corda.node.utilities.NODE_DATABASE_PREFIX import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -36,27 +34,31 @@ class PersistentKeyManagementService(val identityService: IdentityService, @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( @Id - @Column(name = "public_key_hash", length = 64) + @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE) var publicKeyHash: String, + @Lob + @Column(name = "public_key") + var publicKey: ByteArray = ByteArray(0), + @Lob @Column(name = "private_key") var privateKey: ByteArray = ByteArray(0) ) { - constructor(publicKey: PublicKey, privateKey: ByteArray) - : this(publicKey.encoded.sha256().toString(), privateKey) + constructor(publicKey: PublicKey, privateKey: PrivateKey) + : this(publicKey.toStringShort(), + publicKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes, + privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes) } private companion object { fun createKeyMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( - toPersistentEntityKey = { it.toBase58String() }, - fromPersistentEntity = { - Pair(parsePublicKeyBase58(it.publicKeyHash.hexToBase58()), - it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) - }, + toPersistentEntityKey = { it.toStringShort() }, + fromPersistentEntity = { Pair(it.publicKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT), + it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> - PersistentKey(key, value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes) + PersistentKey(key, value) }, persistentEntityClass = PersistentKey::class.java ) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index f89f66c84e..df03e8c1e4 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,7 +1,6 @@ package net.corda.node.services.network -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 +import net.corda.core.crypto.toStringShort import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox import net.corda.core.messaging.SingleMessageRecipient @@ -10,6 +9,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.messaging.MessagingService +import net.corda.node.utilities.MAX_HASH_HEX_SIZE import net.corda.node.utilities.NODE_DATABASE_PREFIX import net.corda.node.utilities.PersistentMap import net.corda.nodeapi.ArtemisMessagingComponent @@ -34,7 +34,7 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") class NetworkNode( @Id - @Column(name = "node_party_key_hash", length = 64) + @Column(name = "node_party_key_hash", length = MAX_HASH_HEX_SIZE) var publicKeyHash: String, @Column @@ -61,14 +61,14 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne fun createNetworkNodesMap(): PersistentMap { return PersistentMap( - toPersistentEntityKey = { SecureHash.sha256(it.owningKey.encoded).toString() }, + toPersistentEntityKey = { it.owningKey.toStringShort() }, fromPersistentEntity = { Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))), it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> NetworkNode( - publicKeyHash = key.owningKey.encoded.sha256().toString(), + publicKeyHash = key.owningKey.toStringShort(), nodeParty = NodeParty( key.name.toString(), key.certificate.encoded, diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index 77055c6073..30c3402c49 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -20,6 +20,14 @@ import java.util.concurrent.CopyOnWriteArrayList */ const val NODE_DATABASE_PREFIX = "node_" +/** + * The maximum supported field-size for hash HEX-encoded outputs (e.g. database fields). + * This value is enough to support hash functions with outputs up to 512 bits (e.g. SHA3-512), in which + * case 128 HEX characters are required. + * 130 was selected instead of 128, to allow for 2 extra characters that will be used as hash-scheme identifiers. + */ +const val MAX_HASH_HEX_SIZE = 130 + //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable { From 698b91bc5e8cf03201dca6cc16ec573f59f5462d Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 17 Oct 2017 15:25:01 +0100 Subject: [PATCH 08/12] Fix after rebase. --- .../net/corda/node/services/network/PersistentNetworkMapCache.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 6f17e86b39..b3bd8df222 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -22,6 +22,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toBase58String import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.ServiceHubInternal From aa5b35a6585cae2f281894f998493040d895f092 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Tue, 17 Oct 2017 18:59:53 +0100 Subject: [PATCH 09/12] Upgrade doorman corda dependancies to v1.0 (#66) * upgrade corda dependancy to v1.0.0 * fixup after corda upgrade * fix CordaX500Name problem for root cert and intermediate cert * fixup after rebase * fix dependencies problem * add back dev repo --- doorman/build.gradle | 2 +- .../com/r3/corda/doorman/DoormanIntegrationTest.kt | 12 +++++------- .../src/main/kotlin/com/r3/corda/doorman/Main.kt | 10 +++++----- .../persistence/DBCertificateRequestStorage.kt | 3 ++- .../doorman/persistence/DoormanSchemaService.kt | 7 ++++++- .../persistence/JiraCertificateRequestStorage.kt | 0 .../com/r3/corda/doorman/signer/CsrHandler.kt | 6 +++--- .../kotlin/com/r3/corda/doorman/signer/Signer.kt | 5 +++-- .../com/r3/corda/doorman/DoormanServiceTest.kt | 13 +++++++------ .../persistence/DBCertificateRequestStorageTest.kt | 6 +++--- signing-server/build.gradle | 2 +- .../corda/signing/SigningServiceIntegrationTest.kt | 2 +- 12 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt diff --git a/doorman/build.gradle b/doorman/build.gradle index 2ddaa94d26..c117d5d615 100644 --- a/doorman/build.gradle +++ b/doorman/build.gradle @@ -1,7 +1,7 @@ ext { // We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are // align with the corresponding Corda release. - corda_dependency_version = '0.16-20170913.101300-6' + corda_dependency_version = '1.0.0' } version "$corda_dependency_version" diff --git a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt index d0485fa66b..15ab8e6169 100644 --- a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt +++ b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt @@ -9,8 +9,7 @@ import com.r3.corda.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.cert -import net.corda.core.utilities.subject +import net.corda.core.internal.cert import net.corda.node.utilities.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper @@ -31,8 +30,7 @@ class DoormanIntegrationTest { @Test fun `Network Registration With Doorman`() { 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").x500Name, rootCAKey) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", organisation = "R3 Ltd"), intermediateCAKey.public) @@ -62,19 +60,19 @@ class DoormanIntegrationTest { loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply { assert(containsAlias(X509Utilities.CORDA_CLIENT_CA)) - assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subject) + assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList()) } loadKeyStore(config.sslKeystore, config.keyStorePassword).apply { assert(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) - assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name, getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subject) + assertEquals(ALICE.name.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(2).toList()) } loadKeyStore(config.trustStoreFile, config.trustStorePassword).apply { assert(containsAlias(X509Utilities.CORDA_ROOT_CA)) - assertEquals(rootCACert.cert.subject, getX509Certificate(X509Utilities.CORDA_ROOT_CA).subject) + assertEquals(rootCACert.cert.subjectX500Principal, getX509Certificate(X509Utilities.CORDA_ROOT_CA).subjectX500Principal) } doorman.close() } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index 2db1035d8e..c2cb8b7e48 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -9,6 +9,7 @@ import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.signer.* import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds @@ -16,7 +17,6 @@ import net.corda.node.utilities.* import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import net.corda.node.utilities.X509Utilities.createCertificate -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 @@ -32,7 +32,6 @@ import java.time.Instant import kotlin.concurrent.thread import kotlin.system.exitProcess - /** * DoormanServer runs on Jetty server and provide certificate signing service via http. * The server will require keystorePath, keystore password and key password via command line input. @@ -134,7 +133,7 @@ private fun DoormanParameters.generateRootKeyPair() { } val selfSignKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Root CA, O=R3, OU=Corda, L=London, C=GB"), selfSignKey) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB", organisationUnit = "Corda", state = null), selfSignKey) rootStore.addOrReplaceKey(CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) rootStore.save(rootStorePath, rootKeystorePassword) @@ -172,7 +171,8 @@ private fun DoormanParameters.generateCAKeyPair() { } val intermediateKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCert = createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, X500Name("CN=Corda Intermediate CA, O=R3, OU=Corda, L=London, C=GB"), intermediateKey.public) + val intermediateCert = createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, + CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", organisationUnit = "Corda", locality = "London", country = "GB", state = null), intermediateKey.public) keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA, intermediateKey.private, caPrivateKeyPassword.toCharArray(), arrayOf(intermediateCert, rootKeyAndCert.certificate)) keyStore.save(keystorePath, keystorePassword) @@ -207,7 +207,7 @@ private fun DoormanParameters.startDoorman(isLocalSigning: Boolean = false) { } private fun buildLocalSigner(storage: CertificationRequestStorage, parameters: DoormanParameters): Signer { - checkNotNull(parameters.keystorePath) {"The keystorePath parameter must be specified when using local signing!"} + checkNotNull(parameters.keystorePath) { "The keystorePath parameter must be specified when using local signing!" } // Get password from console if not in config. val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt index 7ba749d562..9bd476cc9b 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt @@ -2,6 +2,7 @@ package com.r3.corda.doorman.persistence import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.x500Name import net.corda.node.utilities.CordaPersistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.io.ByteArrayInputStream @@ -79,7 +80,7 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : database.transaction { val (legalName, rejectReason) = try { // This will fail with IllegalArgumentException if subject name is malformed. - val legalName = CordaX500Name.build(certificationData.request.subject).copy(commonName = null) + val legalName = CordaX500Name.parse(certificationData.request.subject.toString()).copy(commonName = null) // Checks database for duplicate name. val query = session.criteriaBuilder.run { val criteriaQuery = createQuery(CertificateSigningRequest::class.java) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt index 4e765bbd8f..2e65d4b708 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt @@ -12,10 +12,15 @@ class DoormanSchemaService : SchemaService { object DoormanServicesV1 : MappedSchema(schemaFamily = DoormanServices.javaClass, version = 1, mappedTypes = listOf(DBCertificateRequestStorage.CertificateSigningRequest::class.java)) - override val schemaOptions: Map = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) + override var schemaOptions: Map = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) override fun selectSchemas(state: ContractState): Iterable = setOf(DoormanServicesV1) override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException() + override fun registerCustomSchemas(customSchemas: Set) { + schemaOptions = schemaOptions.plus(customSchemas.map { mappedSchema -> + Pair(mappedSchema, SchemaService.SchemaOptions()) + }) + } } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt index d083c3bc5e..2595db1813 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt @@ -8,10 +8,10 @@ import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestStorage -import net.corda.core.utilities.country -import net.corda.core.utilities.locality +import net.corda.core.internal.country +import net.corda.core.internal.locality +import net.corda.core.internal.organisation import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.organisation import net.corda.node.utilities.X509Utilities import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.openssl.jcajce.JcaPEMWriter diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt index c303f2f572..07c2b7ba96 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt @@ -4,6 +4,7 @@ import com.r3.corda.doorman.buildCertPath import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.toX509Certificate import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.x500Name import net.corda.node.utilities.CertificateAndKeyPair import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -28,12 +29,12 @@ class LocalSigner(private val storage: CertificationRequestStorage, // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. // We assume all attributes in the subject name has been checked prior approval. // TODO: add validation to subject name. - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(request.subject).copy(commonName = null).x500Name))), arrayOf()) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(request.subject.toString()).copy(commonName = null).x500Name))), arrayOf()) val ourCertificate = caCertAndKey.certificate val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, caCertAndKey.certificate, caCertAndKey.keyPair, - CordaX500Name.build(request.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), request.publicKey, nameConstraints = nameConstraints).toX509Certificate() buildCertPath(clientCertificate, ourCertificate.toX509Certificate(), rootCACert) diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt index 7ce497ceb4..7e6ab16a37 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt @@ -9,6 +9,7 @@ import com.r3.corda.doorman.signer.DefaultCsrHandler import com.r3.corda.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name import net.corda.node.utilities.CertificateAndKeyPair import net.corda.node.utilities.CertificateStream import net.corda.node.utilities.CertificateType @@ -38,7 +39,7 @@ import kotlin.test.assertEquals class DoormanServiceTest { private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - private val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 Ltd", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) private lateinit var doormanServer: DoormanServer @@ -66,7 +67,7 @@ class DoormanServiceTest { startSigningServer(storage) val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val request = X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName"), "my@mail.com", keyPair) + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair) // Post request to signing server via http. assertEquals(id, submitRequest(request)) @@ -91,7 +92,7 @@ class DoormanServiceTest { on { signCertificate(eq(id), any(), any()) }.then { @Suppress("UNCHECKED_CAST") val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) + val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "LegalName", country = "GB"), "my@mail.com", keyPair)) certificateStore[id] = certGen(request) true } @@ -115,7 +116,7 @@ class DoormanServiceTest { assertEquals(3, certificates.size) certificates.first().run { - assertThat(subjectDN.name).contains("CN=LegalName") + assertThat(subjectDN.name).contains("O=LegalName") assertThat(subjectDN.name).contains("L=London") } @@ -141,7 +142,7 @@ class DoormanServiceTest { on { signCertificate(eq(id), any(), any()) }.then { @Suppress("UNCHECKED_CAST") val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) + val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair)) certificateStore[id] = certGen(request) true } @@ -169,7 +170,7 @@ class DoormanServiceTest { val sslCert = X509Utilities.createCertificate(CertificateType.TLS, X509CertificateHolder(certificates.first().encoded), keyPair, X500Name("CN=LegalName,L=London"), sslKey.public).toX509Certificate() // TODO: This is temporary solution, remove all certificate re-shaping after identity refactoring is done. - X509Utilities.validateCertificateChain(X509CertificateHolder(certificates.last().encoded), sslCert, *certificates.toTypedArray()) + X509Utilities.validateCertificateChain(certificates.last(), sslCert, *certificates.toTypedArray()) } @Test diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index beb7944ef2..1665f60c51 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -106,7 +106,7 @@ class DBCertificateRequestStorageTest { storage.signCertificate(requestId) { JcaPKCS10CertificationRequest(csr.request).run { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() @@ -127,7 +127,7 @@ class DBCertificateRequestStorageTest { val generateCert: CertificationRequestData.() -> CertPath = { JcaPKCS10CertificationRequest(csr.request).run { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() @@ -188,7 +188,7 @@ class DBCertificateRequestStorageTest { val request = CertificationRequestData( "hostname", "0.0.0.0", - X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB").x500Name, "my@mail.com", keyPair)) + X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair)) return Pair(request, keyPair) } diff --git a/signing-server/build.gradle b/signing-server/build.gradle index 01fcc51b8f..4a9a76acd8 100644 --- a/signing-server/build.gradle +++ b/signing-server/build.gradle @@ -84,5 +84,5 @@ dependencies { // Unit testing helpers. testCompile 'junit:junit:4.12' testCompile "org.assertj:assertj-core:${assertj_version}" - testCompile project(':doorman') + integrationTestCompile project(':doorman') } \ No newline at end of file diff --git a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt index ff63522880..47d7398c98 100644 --- a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt +++ b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt @@ -67,7 +67,7 @@ class SigningServiceIntegrationTest { // Create all certificates 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").x500Name, rootCAKey) + organisation = "R3 Ltd", locality = "London", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", From c2ff4b74e4398b7a8d442ea283c2526a1deddcd4 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 18 Oct 2017 10:04:19 +0100 Subject: [PATCH 10/12] Make const MAX_HASH_HEX_SIZE internal --- .../main/kotlin/net/corda/node/utilities/CordaPersistence.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index 30c3402c49..a8c85d105d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -26,7 +26,7 @@ const val NODE_DATABASE_PREFIX = "node_" * case 128 HEX characters are required. * 130 was selected instead of 128, to allow for 2 extra characters that will be used as hash-scheme identifiers. */ -const val MAX_HASH_HEX_SIZE = 130 +internal const val MAX_HASH_HEX_SIZE = 130 //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, From 42b0db242ed15bcf9a219bb2008517baa5ce4e3a Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 18 Oct 2017 15:19:17 +0100 Subject: [PATCH 11/12] Upgrade the doorman to persist the stored network map entries + serving (#54) * Upgrade the doorman to persist the stored network map entries + serving * fixup after rebase * address PR issues * address PR issues * return 400 bad request instead of unauthorized --- .../corda/doorman/DoormanIntegrationTest.kt | 16 +- .../com/r3/corda/doorman/DoormanParameters.kt | 7 +- .../kotlin/com/r3/corda/doorman/JiraCient.kt | 74 ++++++ .../main/kotlin/com/r3/corda/doorman/Main.kt | 218 ++++++++++-------- .../kotlin/com/r3/corda/doorman/Utilities.kt | 9 +- .../ApprovingAllCertificateRequestStorage.kt | 14 -- .../CertificationRequestStorage.kt | 120 +++++++--- .../DBCertificateRequestStorage.kt | 186 ++++----------- .../persistence/DoormanSchemaService.kt | 2 +- .../doorman/persistence/NodeInfoStorage.kt | 53 +++++ .../persistence/PersistenceNodeInfoStorage.kt | 69 ++++++ .../com/r3/corda/doorman/signer/CsrHandler.kt | 123 ++++------ .../com/r3/corda/doorman/signer/Signer.kt | 56 ++--- .../doorman/webservice/NodeInfoWebService.kt | 80 +++++++ .../RegistrationWebService.kt} | 25 +- .../doorman/DefaultRequestProcessorTest.kt | 57 +++++ .../corda/doorman/NodeInfoWebServiceTest.kt | 167 ++++++++++++++ ...eTest.kt => RegistrationWebServiceTest.kt} | 84 +++---- .../DBCertificateRequestStorageTest.kt | 91 ++++---- .../PersistenceNodeInfoStorageTest.kt | 139 +++++++++++ .../org.mockito.plugins.MockMaker | 1 + .../signing/SigningServiceIntegrationTest.kt | 23 +- 22 files changed, 1064 insertions(+), 550 deletions(-) create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt delete mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt rename doorman/src/main/kotlin/com/r3/corda/doorman/{DoormanWebService.kt => webservice/RegistrationWebService.kt} (77%) create mode 100644 doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt create mode 100644 doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt rename doorman/src/test/kotlin/com/r3/corda/doorman/{DoormanServiceTest.kt => RegistrationWebServiceTest.kt} (66%) create mode 100644 doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt create mode 100644 doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt index 15ab8e6169..88618c6891 100644 --- a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt +++ b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt @@ -1,15 +1,13 @@ package com.r3.corda.doorman -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.whenever -import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage import com.r3.corda.doorman.persistence.DoormanSchemaService -import com.r3.corda.doorman.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.LocalSigner +import com.r3.corda.doorman.signer.Signer import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.utilities.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper @@ -28,7 +26,7 @@ class DoormanIntegrationTest { val tempFolder = TemporaryFolder() @Test - fun `Network Registration With Doorman`() { + fun `initial registration`() { 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) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) @@ -39,16 +37,17 @@ class DoormanIntegrationTest { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() }) + val signer = Signer(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())) + //Start doorman server - val storage = ApprovingAllCertificateRequestStorage(database) - val doorman = DoormanServer(HostAndPort.fromParts("localhost", 0), DefaultCsrHandler(storage, LocalSigner(storage, CertificateAndKeyPair(intermediateCACert, intermediateCAKey), rootCACert.toX509Certificate()))) - doorman.start() + val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, signer, null) // Start Corda network registration. val config = testNodeConfiguration( baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name).also { whenever(it.certificateSigningService).thenReturn(URL("http://localhost:${doorman.hostAndPort.port}")) + whenever(it.emailAddress).thenReturn("iTest@R3.com") } NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore() @@ -77,7 +76,6 @@ class DoormanIntegrationTest { doorman.close() } - private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt index 54d10cab84..073f23f778 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt @@ -18,12 +18,12 @@ data class DoormanParameters(val basedir: Path, val port: Int, val dataSourceProperties: Properties, val mode: Mode, + val approveAll: Boolean = false, val databaseProperties: Properties? = null, val jiraConfig: JiraConfig? = null, val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks", val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks" ) { - enum class Mode { DOORMAN, CA_KEYGEN, ROOT_KEYGEN } @@ -57,6 +57,7 @@ fun parseParameters(vararg args: String): DoormanParameters { } else { Paths.get(argConfig.getString("basedir")) / "node.conf" } - val config = argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))).resolve() - return config.parseAs() + return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))) + .resolve() + .parseAs() } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt new file mode 100644 index 0000000000..def85565bb --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt @@ -0,0 +1,74 @@ +package com.r3.corda.doorman + +import com.atlassian.jira.rest.client.api.JiraRestClient +import com.atlassian.jira.rest.client.api.domain.Field +import com.atlassian.jira.rest.client.api.domain.IssueType +import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder +import com.atlassian.jira.rest.client.api.domain.input.TransitionInput +import net.corda.core.internal.country +import net.corda.core.internal.locality +import net.corda.core.internal.organisation +import net.corda.core.utilities.loggerFor +import net.corda.node.utilities.X509Utilities +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.util.io.pem.PemObject +import java.io.StringWriter +import java.security.cert.CertPath + +class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) { + companion object { + val logger = loggerFor() + } + + // The JIRA project must have a Request ID field and the Task issue type. + private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'") + private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'") + + fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) { + // Make sure request has been accepted. + val request = StringWriter() + JcaPEMWriter(request).use { + it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded)) + } + val organisation = signingRequest.subject.organisation + val nearestCity = signingRequest.subject.locality + val country = signingRequest.subject.country + + val email = signingRequest.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString() + + val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) + .setProjectKey(projectCode) + .setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}") + .setSummary(organisation) + .setFieldValue(requestIdField.id, requestId) + // This will block until the issue is created. + restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() + } + + fun getApprovedRequests(): List> { + val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues + return issues.map { issue -> + issue.getField(requestIdField.id)?.value?.toString().let { + val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.") + val approvedBy = issue.assignee?.displayName ?: "Unknown" + Pair(requestId, approvedBy) + } + } + } + + fun updateSignedRequests(signedRequests: Map) { + // Retrieving certificates for signed CSRs to attach to the jira tasks. + signedRequests.forEach { (id, certPath) -> + val certificate = certPath.certificates.first() + // Jira only support ~ (contains) search for custom textfield. + val issue = restClient.searchClient.searchJql("'Request ID' ~ $id").claim().issues.firstOrNull() + if (issue != null) { + restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() + restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") + .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() + } + } + } +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index c2cb8b7e48..61c1f8c691 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -1,22 +1,24 @@ package com.r3.corda.doorman import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory -import com.google.common.net.HostAndPort import com.r3.corda.doorman.DoormanServer.Companion.logger -import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DoormanSchemaService -import com.r3.corda.doorman.signer.* +import com.r3.corda.doorman.persistence.PersistenceNodeInfoStorage +import com.r3.corda.doorman.signer.DefaultCsrHandler +import com.r3.corda.doorman.signer.JiraCsrHandler +import com.r3.corda.doorman.signer.Signer +import com.r3.corda.doorman.webservice.NodeInfoWebService +import com.r3.corda.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.utilities.* -import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA -import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA -import net.corda.node.utilities.X509Utilities.createCertificate +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.HandlerCollection @@ -25,36 +27,34 @@ import org.eclipse.jetty.servlet.ServletHolder import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.servlet.ServletContainer import java.io.Closeable -import java.lang.Thread.sleep import java.net.InetSocketAddress import java.net.URI +import java.nio.file.Path import java.time.Instant import kotlin.concurrent.thread import kotlin.system.exitProcess /** - * DoormanServer runs on Jetty server and provide certificate signing service via http. + * DoormanServer runs on Jetty server and provides certificate signing service via http. * The server will require keystorePath, keystore password and key password via command line input. * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities] */ -class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandler) : Closeable { - val serverStatus = DoormanServerStatus() - +// TODO: Move this class to its own file. +class DoormanServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { companion object { val logger = loggerFor() + val serverStatus = DoormanServerStatus() } - private val server: Server = Server(InetSocketAddress(webServerAddr.host, webServerAddr.port)).apply { + private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { handler = HandlerCollection().apply { addHandler(buildServletContextHandler()) } } - val hostAndPort: HostAndPort - get() = server.connectors - .map { it as? ServerConnector } - .filterNotNull() - .map { HostAndPort.fromParts(it.host, it.localPort) } + val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } .first() override fun close() { @@ -67,22 +67,6 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle logger.info("Starting Doorman Web Services...") server.start() logger.info("Doorman Web Services started on $hostAndPort") - serverStatus.serverStartTime = Instant.now() - - // Thread approving request periodically. - thread(name = "Request Approval Thread") { - while (true) { - try { - sleep(10.seconds.toMillis()) - // TODO: Handle rejected request? - serverStatus.lastRequestCheckTime = Instant.now() - csrHandler.sign() - } catch (e: Exception) { - // Log the error and carry on. - logger.error("Error encountered when approving request.", e) - } - } - } } private fun buildServletContextHandler(): ServletContextHandler { @@ -90,18 +74,15 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle contextPath = "/" val resourceConfig = ResourceConfig().apply { // Add your API provider classes (annotated for JAX-RS) here - register(DoormanWebService(csrHandler, serverStatus)) - } - val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { - initOrder = 0 // Initialise at server start + webServices.forEach { register(it) } } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start addServlet(jerseyServlet, "/api/*") } } } -data class DoormanServerStatus(var serverStartTime: Instant? = null, - var lastRequestCheckTime: Instant? = null) +data class DoormanServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null) /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */ internal fun readPassword(fmt: String): String { @@ -109,112 +90,134 @@ internal fun readPassword(fmt: String): String { String(System.console().readPassword(fmt)) } else { print(fmt) - readLine()!! + readLine() ?: "" } } -private fun DoormanParameters.generateRootKeyPair() { - if (rootStorePath == null) { - throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!") - } +// Keygen utilities. +// TODO: Move keygen methods to Utilities.kt +fun generateRootKeyPair(rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?) { println("Generating Root CA keypair and certificate.") // Get password from console if not in config. - val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") + val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ") // Ensure folder exists. rootStorePath.parent.createDirectories() val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword) - val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") + val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") - if (rootStore.containsAlias(CORDA_ROOT_CA)) { - val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey - println("Key $CORDA_ROOT_CA already exists in keystore, process will now terminate.") + if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) { + val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey + println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } val selfSignKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val selfSignCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB", organisationUnit = "Corda", state = null), selfSignKey) - rootStore.addOrReplaceKey(CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) + rootStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) rootStore.save(rootStorePath, rootKeystorePassword) println("Root CA keypair and certificate stored in $rootStorePath.") - println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey) + println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey) } -private fun DoormanParameters.generateCAKeyPair() { - if (keystorePath == null) { - throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!") - } - - if (rootStorePath == null) { - throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!") - } +fun generateCAKeyPair(keystorePath: Path, rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) { println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.") // Get password from console if not in config. - val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") - val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") + val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ") + val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword) - val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA, rootPrivateKeyPassword) + val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, rootPrivateKeyPassword) - val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") - val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + val keystorePassword = keystorePass ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = caPrivateKeyPass ?: readPassword("CA Private Key Password: ") // Ensure folder exists. keystorePath.parent.createDirectories() val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) - if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA)) { - val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey - println("Key $CORDA_INTERMEDIATE_CA already exists in keystore, process will now terminate.") + if (keyStore.containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) { + val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey + println("Key ${X509Utilities.CORDA_INTERMEDIATE_CA} already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } val intermediateKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCert = createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, + val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", organisationUnit = "Corda", locality = "London", country = "GB", state = null), intermediateKey.public) - keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA, intermediateKey.private, + keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateKey.private, caPrivateKeyPassword.toCharArray(), arrayOf(intermediateCert, rootKeyAndCert.certificate)) keyStore.save(keystorePath, keystorePassword) println("Intermediate CA keypair and certificate stored in $keystorePath.") - println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey) + println(loadKeyStore(keystorePath, keystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey) } -private fun DoormanParameters.startDoorman(isLocalSigning: Boolean = false) { +// TODO: Move this method to DoormanServer. +fun startDoorman(hostAndPort: NetworkHostAndPort, + database: CordaPersistence, + approveAll: Boolean, + signer: Signer? = null, + jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer { + logger.info("Starting Doorman server.") - // Create DB connection. - val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, createIdentityService = { - // Identity service not needed doorman, corda persistence is not very generic. - throw UnsupportedOperationException() - }) - val csrHandler = if (jiraConfig == null) { - logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") - val storage = ApprovingAllCertificateRequestStorage(database) - DefaultCsrHandler(storage, buildLocalSigner(storage, this)) + + val requestService = if (approveAll) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") + ApproveAllCertificateRequestStorage(DBCertificateRequestStorage(database)) } else { - val storage = DBCertificateRequestStorage(database) - val signer = if (isLocalSigning) { - buildLocalSigner(storage, this) - } else { - ExternalSigner() - } - val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - JiraCsrHandler(jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode, storage, signer) + DBCertificateRequestStorage(database) } - val doorman = DoormanServer(HostAndPort.fromParts(host, port), csrHandler) + + val requestProcessor = if (jiraConfig != null) { + val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) + val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer)) + } else { + DefaultCsrHandler(requestService, signer) + } + + val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(PersistenceNodeInfoStorage(database))) doorman.start() + + // Thread process approved request periodically. + thread(name = "Approved Request Process Thread") { + while (true) { + try { + Thread.sleep(10.seconds.toMillis()) + DoormanServer.serverStatus.lastRequestCheckTime = Instant.now() + requestProcessor.processApprovedRequests() + } catch (e: Exception) { + // Log the error and carry on. + DoormanServer.logger.error("Error encountered when approving request.", e) + } + } + } Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) + return doorman } -private fun buildLocalSigner(storage: CertificationRequestStorage, parameters: DoormanParameters): Signer { - checkNotNull(parameters.keystorePath) { "The keystorePath parameter must be specified when using local signing!" } - // Get password from console if not in config. - val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") - val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") - val keystore = loadOrCreateKeyStore(parameters.keystorePath!!, keystorePassword) - val rootCACert = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last() - val caCertAndKey = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword) - return LocalSigner(storage, caCertAndKey, rootCACert) +private fun buildLocalSigner(parameters: DoormanParameters): Signer? { + return parameters.keystorePath?.let { + // Get password from console if not in config. + val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + val keystore = loadOrCreateKeyStore(parameters.keystorePath, keystorePassword) + val caKeyPair = keystore.getKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword) + val caCertPath = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA) + Signer(caKeyPair, caCertPath) + } +} + +/** + * This storage automatically approves all created requests. + */ +private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(rawRequest) + approveRequest(requestId) + return requestId + } } fun main(args: Array) { @@ -222,9 +225,22 @@ fun main(args: Array) { // TODO : Remove config overrides and solely use config file after testnet is finalized. parseParameters(*args).run { when (mode) { - DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() - DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair() - DoormanParameters.Mode.DOORMAN -> startDoorman(keystorePath != null) + DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair( + rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"), + rootKeystorePassword, + rootPrivateKeyPassword) + DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair( + keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"), + rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"), + rootKeystorePassword, + rootPrivateKeyPassword, + keystorePassword, + caPrivateKeyPassword) + DoormanParameters.Mode.DOORMAN -> { + val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, { throw UnsupportedOperationException() }) + val signer = buildLocalSigner(this) + startDoorman(NetworkHostAndPort(host, port), database, approveAll, signer, jiraConfig) + } } } } catch (e: ShowHelpException) { diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt index 8b61c1f850..451996e532 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt @@ -4,8 +4,10 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.OptionParser +import net.corda.core.crypto.sha256 import org.bouncycastle.cert.X509CertificateHolder import java.io.ByteArrayInputStream +import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.Certificate import java.security.cert.CertificateFactory @@ -44,4 +46,9 @@ fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilitie fun buildCertPath(vararg certificates: Certificate): CertPath { return CertificateFactory.getInstance("X509").generateCertPath(certificates.asList()) -} \ No newline at end of file +} + +fun buildCertPath(certPathBytes: ByteArray): CertPath = CertificateFactory.getInstance("X509").generateCertPath(certPathBytes.inputStream()) + +// TODO: replace this with Crypto.hash when its available. +fun PublicKey.hash() = encoded.sha256().toString() diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt deleted file mode 100644 index afd7c75cbe..0000000000 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.r3.corda.doorman.persistence - -import net.corda.node.utilities.CordaPersistence - -/** - * This storage automatically approves all created requests. - */ -class ApprovingAllCertificateRequestStorage(database: CordaPersistence) : DBCertificateRequestStorage(database) { - override fun saveRequest(certificationData: CertificationRequestData): String { - val requestId = super.saveRequest(certificationData) - approveRequest(requestId) - return requestId - } -} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt index 9228bc42c3..f219717e97 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt @@ -2,71 +2,127 @@ package com.r3.corda.doorman.persistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.CertPath +import java.time.Instant +import javax.persistence.* /** * Provide certificate signing request storage for the certificate signing server. */ interface CertificationRequestStorage { - companion object { - val DOORMAN_SIGNATURE = listOf("Doorman") + val DOORMAN_SIGNATURE = "Doorman" } /** - * Persist [certificationData] in storage for further approval if it's a valid request. If not then it will be automatically + * Persist [PKCS10CertificationRequest] in storage for further approval if it's a valid request. If not then it will be automatically * rejected and not subject to any approval process. In both cases a randomly generated request ID is returned. */ - fun saveRequest(certificationData: CertificationRequestData): String + fun saveRequest(rawRequest: PKCS10CertificationRequest): String /** - * Retrieve certificate singing request and Host/IP information using [requestId]. + * Retrieve certificate singing request using [requestId]. */ - fun getRequest(requestId: String): CertificationRequestData? + fun getRequest(requestId: String): CertificateSigningRequest? /** - * Return the response for a previously saved request with ID [requestId]. + * Retrieve list of certificate singing request base on the [RequestStatus]. */ - fun getResponse(requestId: String): CertificateResponse + fun getRequests(requestStatus: RequestStatus): List /** * Approve the given request if it has not already been approved. Otherwise do nothing. * * @return True if the request has been approved and false otherwise. */ - fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE.first()): Boolean - - /** - * Signs the certificate signing request by assigning the given certificate. - * - * @return True if the request has been signed and false otherwise. - */ - fun signCertificate(requestId: String, signedBy: List = DOORMAN_SIGNATURE, generateCertificate: CertificationRequestData.() -> CertPath): Boolean + // TODO: Merge status changing methods. + fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE): Boolean /** * Reject the given request using the given reason. */ - fun rejectRequest(requestId: String, rejectedBy: String = DOORMAN_SIGNATURE.first(), rejectReason: String) + fun rejectRequest(requestId: String, rejectedBy: String = DOORMAN_SIGNATURE, rejectReason: String) /** - * Retrieve list of request IDs waiting for approval. + * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request statue to [RequestStatus.Signed]. + * + * @throws IllegalArgumentException if request is not found or not in Approved state. */ - fun getNewRequestIds(): List - - /** - * Retrieve list of approved request IDs. - */ - fun getApprovedRequestIds(): List - - /** - * Retrieve list of signed request IDs. - */ - fun getSignedRequestIds(): List + fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List = listOf(DOORMAN_SIGNATURE)) } -data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest) +@Entity +@Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) +// TODO: Use Hibernate Envers to audit the table instead of individual "changed_by"/"changed_at" columns. +class CertificateSigningRequest( + @Id + @Column(name = "request_id", length = 64) + var requestId: String = "", + + // TODO: Store X500Name with a proper schema. + @Column(name = "legal_name", length = 256) + var legalName: String = "", + + @Lob + @Column + var request: ByteArray = ByteArray(0), + + @Column(name = "created_at") + var createdAt: Instant = Instant.now(), + + @Column(name = "approved_at") + var approvedAt: Instant = Instant.now(), + + @Column(name = "approved_by", length = 64) + var approvedBy: String? = null, + + @Column + @Enumerated(EnumType.STRING) + var status: RequestStatus = RequestStatus.New, + + @Column(name = "signed_by", length = 512) + @ElementCollection(targetClass = String::class, fetch = FetchType.EAGER) + var signedBy: List? = null, + + @Column(name = "signed_at") + var signedAt: Instant? = Instant.now(), + + @Column(name = "rejected_by", length = 64) + var rejectedBy: String? = null, + + @Column(name = "rejected_at") + var rejectedAt: Instant? = Instant.now(), + + @Column(name = "reject_reason", length = 256, nullable = true) + var rejectReason: String? = null, + + // TODO: The certificate data can have its own table. + @Embedded + var certificateData: CertificateData? = null +) + +@Embeddable +class CertificateData( + @Column(name = "public_key_hash", length = 64, nullable = true) + var publicKeyHash: String? = null, + + @Lob + @Column(nullable = true) + var certificatePath: ByteArray? = null, + + @Column(name = "certificate_status", nullable = true) + var certificateStatus: CertificateStatus? = null +) + +enum class CertificateStatus { + VALID, SUSPENDED, REVOKED +} + +enum class RequestStatus { + New, Approved, Rejected, Signed +} sealed class CertificateResponse { object NotReady : CertificateResponse() - class Ready(val certificatePath: CertPath) : CertificateResponse() - class Unauthorised(val message: String) : CertificateResponse() + data class Ready(val certificatePath: CertPath) : CertificateResponse() + data class Unauthorised(val message: String) : CertificateResponse() } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt index 9bd476cc9b..e5e337d00d 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt @@ -1,92 +1,53 @@ package com.r3.corda.doorman.persistence +import com.r3.corda.doorman.hash +import com.r3.corda.doorman.persistence.RequestStatus.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.x500Name import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.DatabaseTransaction import org.bouncycastle.pkcs.PKCS10CertificationRequest -import java.io.ByteArrayInputStream +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import java.security.cert.CertPath -import java.security.cert.CertificateFactory import java.time.Instant -import javax.persistence.* import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.Path import javax.persistence.criteria.Predicate -// TODO Relax the uniqueness requirement to be on the entire X.500 subject rather than just the legal name -open class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { - @Entity - @Table(name = "certificate_signing_request") - class CertificateSigningRequest( - @Id - @Column(name = "request_id", length = 64) - var requestId: String = "", +class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { + override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List) { + return database.transaction { + val request = singleRequestWhere { builder, path -> + val requestIdEq = builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) + val statusEq = builder.equal(path.get(CertificateSigningRequest::status.name), Approved) + builder.and(requestIdEq, statusEq) + } + require(request != null) { "Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId" } - @Column(name = "host_name", length = 100) - var hostName: String = "", - - @Column(name = "ip_address", length = 15) - var ipAddress: String = "", - - @Column(name = "legal_name", length = 256) - var legalName: String = "", - - @Lob - @Column - var request: ByteArray = ByteArray(0), - - @Column(name = "created_at") - var createdAt: Instant = Instant.now(), - - @Column(name = "approved_at") - var approvedAt: Instant = Instant.now(), - - @Column(name = "approved_by", length = 64) - var approvedBy: String? = null, - - @Column - @Enumerated(EnumType.STRING) - var status: Status = Status.New, - - @Column(name = "signed_by", length = 512) - @ElementCollection(targetClass = String::class, fetch = FetchType.EAGER) - var signedBy: List? = null, - - @Column(name = "signed_at") - var signedAt: Instant? = Instant.now(), - - @Column(name = "rejected_by", length = 64) - var rejectedBy: String? = null, - - @Column(name = "rejected_at") - var rejectedAt: Instant? = Instant.now(), - - @Lob - @Column(nullable = true) - var certificatePath: ByteArray? = null, - - @Column(name = "reject_reason", length = 256, nullable = true) - var rejectReason: String? = null - ) - - enum class Status { - New, Approved, Rejected, Signed + val publicKeyHash = certificates.certificates.first().publicKey.hash() + request!!.certificateData = CertificateData(publicKeyHash, certificates.encoded, CertificateStatus.VALID) + request.status = Signed + request.signedBy = signedBy + request.signedAt = Instant.now() + session.save(request) + } } - override fun saveRequest(certificationData: CertificationRequestData): String { + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val request = JcaPKCS10CertificationRequest(rawRequest) val requestId = SecureHash.randomSHA256().toString() - database.transaction { + // TODO ensure public key not duplicated. val (legalName, rejectReason) = try { // This will fail with IllegalArgumentException if subject name is malformed. - val legalName = CordaX500Name.parse(certificationData.request.subject.toString()).copy(commonName = null) + val legalName = CordaX500Name.parse(request.subject.toString()).copy(commonName = null) // Checks database for duplicate name. val query = session.criteriaBuilder.run { val criteriaQuery = createQuery(CertificateSigningRequest::class.java) criteriaQuery.from(CertificateSigningRequest::class.java).run { val nameEq = equal(get(CertificateSigningRequest::legalName.name), legalName.toString()) - val statusNewOrApproved = get(CertificateSigningRequest::status.name).`in`(Status.Approved, Status.New) + val statusNewOrApproved = get(CertificateSigningRequest::status.name).`in`(Approved, New) criteriaQuery.where(and(nameEq, statusNewOrApproved)) } } @@ -97,50 +58,30 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : Pair(legalName.x500Name, null) } } catch (e: IllegalArgumentException) { - Pair(certificationData.request.subject, "Name validation failed with exception : ${e.message}") + Pair(request.subject, "Name validation failed with exception : ${e.message}") } - val request = CertificateSigningRequest( + session.save(CertificateSigningRequest( requestId = requestId, - hostName = certificationData.hostName, - ipAddress = certificationData.ipAddress, legalName = legalName.toString(), - request = certificationData.request.encoded, + request = request.encoded, rejectReason = rejectReason, - status = if (rejectReason == null) Status.New else Status.Rejected - ) - session.save(request) + status = if (rejectReason == null) New else Rejected + )) } return requestId } - override fun getResponse(requestId: String): CertificateResponse { - return database.transaction { - val response = singleRequestWhere { builder, path -> - builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) - } - if (response == null) { - CertificateResponse.NotReady - } else { - when (response.status) { - Status.New, Status.Approved -> CertificateResponse.NotReady - Status.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason") - Status.Signed -> CertificateResponse.Ready(buildCertPath(response.certificatePath)) - } - } - } - } - override fun approveRequest(requestId: String, approvedBy: String): Boolean { var approved = false database.transaction { val request = singleRequestWhere { builder, path -> builder.and(builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId), - builder.equal(path.get(CertificateSigningRequest::status.name), Status.New)) + builder.equal(path.get(CertificateSigningRequest::status.name), New)) } if (request != null) { request.approvedAt = Instant.now() request.approvedBy = approvedBy - request.status = Status.Approved + request.status = Approved session.save(request) approved = true } @@ -148,26 +89,6 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : return approved } - override fun signCertificate(requestId: String, signedBy: List, generateCertificate: CertificationRequestData.() -> CertPath): Boolean { - var signed = false - database.transaction { - val request = singleRequestWhere { builder, path -> - builder.and(builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId), - builder.equal(path.get(CertificateSigningRequest::status.name), Status.Approved)) - } - if (request != null) { - val now = Instant.now() - request.certificatePath = request.toRequestData().generateCertificate().encoded - request.status = Status.Signed - request.signedAt = now - request.signedBy = signedBy - session.save(request) - signed = true - } - } - return signed - } - override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { database.transaction { val request = singleRequestWhere { builder, path -> @@ -175,7 +96,7 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : } if (request != null) { request.rejectReason = rejectReason - request.status = Status.Rejected + request.status = Rejected request.rejectedBy = rejectedBy request.rejectedAt = Instant.now() session.save(request) @@ -183,51 +104,32 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : } } - override fun getRequest(requestId: String): CertificationRequestData? { + override fun getRequest(requestId: String): CertificateSigningRequest? { return database.transaction { singleRequestWhere { builder, path -> builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) } - }?.toRequestData() + } } - override fun getApprovedRequestIds(): List { - return getRequestIdsByStatus(Status.Approved) - } - - override fun getNewRequestIds(): List { - return getRequestIdsByStatus(Status.New) - } - - override fun getSignedRequestIds(): List { - return getRequestIdsByStatus(Status.Signed) - } - - private fun getRequestIdsByStatus(status: Status): List { + override fun getRequests(requestStatus: RequestStatus): List { return database.transaction { val builder = session.criteriaBuilder - val query = builder.createQuery(String::class.java).run { + val query = builder.createQuery(CertificateSigningRequest::class.java).run { from(CertificateSigningRequest::class.java).run { - select(get(CertificateSigningRequest::requestId.name)) - where(builder.equal(get(CertificateSigningRequest::status.name), status)) + where(builder.equal(get(CertificateSigningRequest::status.name), requestStatus)) } } session.createQuery(query).resultList } } - private fun singleRequestWhere(predicate: (CriteriaBuilder, Path) -> Predicate): CertificateSigningRequest? { - return database.transaction { - val builder = session.criteriaBuilder - val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java) - val query = criteriaQuery.from(CertificateSigningRequest::class.java).run { - criteriaQuery.where(predicate(builder, this)) - } - session.createQuery(query).uniqueResultOptional().orElse(null) + private fun DatabaseTransaction.singleRequestWhere(predicate: (CriteriaBuilder, Path) -> Predicate): CertificateSigningRequest? { + val builder = session.criteriaBuilder + val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java) + val query = criteriaQuery.from(CertificateSigningRequest::class.java).run { + criteriaQuery.where(predicate(builder, this)) } + return session.createQuery(query).uniqueResultOptional().orElse(null) } - - private fun CertificateSigningRequest.toRequestData() = CertificationRequestData(hostName, ipAddress, PKCS10CertificationRequest(request)) - - private fun buildCertPath(certPathBytes: ByteArray?) = CertificateFactory.getInstance("X509").generateCertPath(ByteArrayInputStream(certPathBytes)) } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt index 2e65d4b708..1ee7a39e28 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt @@ -10,7 +10,7 @@ class DoormanSchemaService : SchemaService { object DoormanServices object DoormanServicesV1 : MappedSchema(schemaFamily = DoormanServices.javaClass, version = 1, - mappedTypes = listOf(DBCertificateRequestStorage.CertificateSigningRequest::class.java)) + mappedTypes = listOf(CertificateSigningRequest::class.java, NodeInfoEntity::class.java, PublicKeyNodeInfoLink::class.java)) override var schemaOptions: Map = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt new file mode 100644 index 0000000000..bbdc7b0687 --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt @@ -0,0 +1,53 @@ +package com.r3.corda.doorman.persistence + +import net.corda.core.node.NodeInfo +import java.security.cert.CertPath +import javax.persistence.* + +interface NodeInfoStorage { + /** + * Retrieve certificate paths using the public key hash. + * @return [CertPath] or null if the public key is not registered with the Doorman. + */ + fun getCertificatePath(publicKeyHash: String): CertPath? + + /** + * Obtain list of registered node info hashes. + */ + //TODO: we might want to return [SecureHash] instead of String + fun getNodeInfoHashes(): List + + /** + * Retrieve node info using nodeInfo's hash + * @return [NodeInfo] or null if the node info is not registered. + */ + fun getNodeInfo(nodeInfoHash: String): NodeInfo? + + /** + * The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info. + */ + fun putNodeInfo(nodeInfo: NodeInfo) +} + +@Entity +@Table(name = "node_info") +class NodeInfoEntity( + @Id + @Column(name = "node_info_hash", length = 64) + var nodeInfoHash: String = "", + + @Lob + @Column(name = "node_info") + var nodeInfo: ByteArray = ByteArray(0) +) + +@Entity +@Table(name = "public_key_node_info_link") +class PublicKeyNodeInfoLink( + @Id + @Column(name = "public_key_hash", length = 64) + var publicKeyHash: String = "", + + @Column(name = "node_info_hash", length = 64) + var nodeInfoHash: String = "" +) \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt new file mode 100644 index 0000000000..2365f501bd --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt @@ -0,0 +1,69 @@ +package com.r3.corda.doorman.persistence + +import com.r3.corda.doorman.buildCertPath +import com.r3.corda.doorman.hash +import net.corda.core.crypto.sha256 +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.PersistentMap +import java.security.cert.CertPath + +class PersistenceNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { + companion object { + fun makeNodeInfoMap() = PersistentMap( + toPersistentEntityKey = { it }, + toPersistentEntity = { key, nodeInfo -> + val serializedNodeInfo = nodeInfo.serialize() + NodeInfoEntity(key, serializedNodeInfo.bytes) + }, + fromPersistentEntity = { + val nodeInfo = it.nodeInfo.deserialize() + it.nodeInfoHash to nodeInfo + }, + persistentEntityClass = NodeInfoEntity::class.java + ) + + fun makePublicKeyMap() = PersistentMap( + toPersistentEntityKey = { it }, + toPersistentEntity = { publicKeyHash, nodeInfoHash -> PublicKeyNodeInfoLink(publicKeyHash, nodeInfoHash) }, + fromPersistentEntity = { it.publicKeyHash to it.nodeInfoHash }, + persistentEntityClass = PublicKeyNodeInfoLink::class.java + ) + } + + private val nodeInfoMap = database.transaction { makeNodeInfoMap() } + private val publicKeyMap = database.transaction { makePublicKeyMap() } + + override fun putNodeInfo(nodeInfo: NodeInfo) { + return database.transaction { + val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hash() + val nodeInfoHash = nodeInfo.serialize().sha256().toString() + val existingNodeInfoHash = publicKeyMap[publicKeyHash] + if (nodeInfoHash != existingNodeInfoHash) { + // Remove node info if exists. + existingNodeInfoHash?.let { nodeInfoMap.remove(it) } + publicKeyMap[publicKeyHash] = nodeInfoHash + nodeInfoMap.put(nodeInfoHash, nodeInfo) + } + } + } + + override fun getNodeInfo(nodeInfoHash: String): NodeInfo? = database.transaction { nodeInfoMap[nodeInfoHash] } + + override fun getNodeInfoHashes(): List = database.transaction { nodeInfoMap.keys.toList() } + + override fun getCertificatePath(publicKeyHash: String): CertPath? { + return database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(ByteArray::class.java).run { + from(CertificateSigningRequest::class.java).run { + select(get(CertificateSigningRequest::certificateData.name).get(CertificateData::certificatePath.name)) + where(builder.equal(get(CertificateSigningRequest::certificateData.name).get(CertificateData::publicKeyHash.name), publicKeyHash)) + } + } + session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) } + } + } +} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt index 2595db1813..996effda55 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt @@ -1,100 +1,61 @@ package com.r3.corda.doorman.signer -import com.atlassian.jira.rest.client.api.JiraRestClient -import com.atlassian.jira.rest.client.api.domain.Field -import com.atlassian.jira.rest.client.api.domain.IssueType -import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder -import com.atlassian.jira.rest.client.api.domain.input.TransitionInput +import com.r3.corda.doorman.JiraClient +import com.r3.corda.doorman.buildCertPath import com.r3.corda.doorman.persistence.CertificateResponse -import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestStorage -import net.corda.core.internal.country -import net.corda.core.internal.locality -import net.corda.core.internal.organisation -import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.X509Utilities -import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.openssl.jcajce.JcaPEMWriter -import org.bouncycastle.util.io.pem.PemObject -import java.io.StringWriter +import com.r3.corda.doorman.persistence.RequestStatus +import org.bouncycastle.pkcs.PKCS10CertificationRequest -open class DefaultCsrHandler(protected val storage: CertificationRequestStorage, protected val signer: Signer) { - open fun saveRequest(certificationData: CertificationRequestData): String { - return storage.saveRequest(certificationData) +interface CsrHandler { + fun saveRequest(rawRequest: PKCS10CertificationRequest): String + fun processApprovedRequests() + fun getResponse(requestId: String): CertificateResponse +} + +class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: Signer?) : CsrHandler { + override fun processApprovedRequests() { + storage.getRequests(RequestStatus.Approved) + .forEach { processRequest(it.requestId, PKCS10CertificationRequest(it.request)) } } - open fun sign() { - for (id in storage.getApprovedRequestIds()) { - signer.sign(id) + private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { + if (signer != null) { + val certs = signer.sign(request) + storage.putCertificatePath(requestId, certs) } } - fun getResponse(requestId: String) = storage.getResponse(requestId) -} - -class JiraCsrHandler(private val jiraClient: JiraRestClient, - private val projectCode: String, - private val doneTransitionCode: Int, - storage: CertificationRequestStorage, signer: Signer) : DefaultCsrHandler(storage, signer) { - - companion object { - private val logger = loggerFor() + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + return storage.saveRequest(rawRequest) } - // The JIRA project must have a Request ID field and the Task issue type. - private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! - private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! + override fun getResponse(requestId: String): CertificateResponse { + val response = storage.getRequest(requestId) + return when (response?.status) { + RequestStatus.New, RequestStatus.Approved, null -> CertificateResponse.NotReady + RequestStatus.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason") + RequestStatus.Signed -> CertificateResponse.Ready(buildCertPath(response.certificateData?.certificatePath ?: throw IllegalArgumentException("Certificate should not be null."))) + } + } +} - override fun saveRequest(certificationData: CertificationRequestData): String { - val requestId = super.saveRequest(certificationData) +class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(rawRequest) // Make sure request has been accepted. - val response = storage.getResponse(requestId) - if (response !is CertificateResponse.Unauthorised) { - val request = StringWriter() - JcaPEMWriter(request).use { - it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) - } - val organisation = certificationData.request.subject.organisation - val nearestCity = certificationData.request.subject.locality - val country = certificationData.request.subject.country - - val email = certificationData.request.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString() - - val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) - .setProjectKey(projectCode) - .setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}") - .setSummary(organisation) - .setFieldValue(requestIdField.id, requestId) - // This will block until the issue is created. - jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() + if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { + jiraClient.createRequestTicket(requestId, rawRequest) } return requestId } - override fun sign() { - val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues - issues.map { - val requestId = it.getField(requestIdField.id)?.value?.toString() - if (requestId != null) { - var approvedBy = it.assignee?.displayName - if (approvedBy == null) { - approvedBy = "Unknown" - } - storage.approveRequest(requestId, approvedBy) - } - - } - super.sign() - // Retrieving certificates for signed CSRs to attach to the jira tasks. - storage.getSignedRequestIds().forEach { - val certificate = (storage.getResponse(it) as? CertificateResponse.Ready)?.certificatePath!!.certificates.first() - // Jira only support ~ (contains) search for custom textfield. - val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $it").claim().issues.firstOrNull() - if (issue != null) { - jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() - jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") - .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() - } - } + override fun processApprovedRequests() { + jiraClient.getApprovedRequests().forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } + delegate.processApprovedRequests() + val signedRequests = storage.getRequests(RequestStatus.Signed).mapNotNull { + it.certificateData?.certificatePath?.let { certs -> it.requestId to buildCertPath(certs) } + }.toMap() + jiraClient.updateSignedRequests(signedRequests) } -} \ No newline at end of file +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt index 07c2b7ba96..a43abbbeea 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt @@ -1,49 +1,39 @@ package com.r3.corda.doorman.signer import com.r3.corda.doorman.buildCertPath -import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.toX509Certificate import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.x500Name -import net.corda.node.utilities.CertificateAndKeyPair import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import java.security.KeyPair +import java.security.cert.CertPath import java.security.cert.Certificate -interface Signer { - fun sign(requestId: String) -} - -class LocalSigner(private val storage: CertificationRequestStorage, - private val caCertAndKey: CertificateAndKeyPair, - private val rootCACert: Certificate) : Signer { - - override fun sign(requestId: String) { - storage.signCertificate(requestId) { - val request = JcaPKCS10CertificationRequest(request) - // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, - // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. - // We assume all attributes in the subject name has been checked prior approval. - // TODO: add validation to subject name. - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(request.subject.toString()).copy(commonName = null).x500Name))), arrayOf()) - val ourCertificate = caCertAndKey.certificate - val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, - caCertAndKey.certificate, - caCertAndKey.keyPair, - CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), - request.publicKey, - nameConstraints = nameConstraints).toX509Certificate() - buildCertPath(clientCertificate, ourCertificate.toX509Certificate(), rootCACert) - } +/** + * The [Signer] class signs [PKCS10CertificationRequest] using provided CA keypair and certificate path. + * This is intended to be used in testing environment where hardware signing module is not available. + */ +class Signer(private val caKeyPair: KeyPair, private val caCertPath: Array) { + fun sign(certificationRequest: PKCS10CertificationRequest): CertPath { + // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, + // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. + // We assume all attributes in the subject name has been checked prior approval. + // TODO: add validation to subject name. + val request = JcaPKCS10CertificationRequest(certificationRequest) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(request.subject.toString()).copy(commonName = null).x500Name))), arrayOf()) + val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, + caCertPath.first().toX509CertHolder(), + caKeyPair, + CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + request.publicKey, + nameConstraints = nameConstraints).toX509Certificate() + return buildCertPath(clientCertificate, *caCertPath) } } - -class ExternalSigner : Signer { - override fun sign(requestId: String) { - // Do nothing - } -} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt new file mode 100644 index 0000000000..1521956c2b --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt @@ -0,0 +1,80 @@ +package com.r3.corda.doorman.webservice + +import com.r3.corda.doorman.persistence.NodeInfoStorage +import com.r3.corda.doorman.webservice.NodeInfoWebService.Companion.networkMapPath +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignedData +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.toSHA256Bytes +import org.codehaus.jackson.map.ObjectMapper +import java.io.InputStream +import java.security.InvalidKeyException +import java.security.SignatureException +import javax.servlet.http.HttpServletRequest +import javax.ws.rs.* +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.ok +import javax.ws.rs.core.Response.status + +@Path(networkMapPath) +class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage) { + companion object { + const val networkMapPath = "network-map" + } + @POST + @Path("register") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + fun registerNode(input: InputStream): Response { + // TODO: Use JSON instead. + val registrationData = input.readBytes().deserialize>() + + val nodeInfo = registrationData.verified() + val digitalSignature = registrationData.sig + + val certPath = nodeInfoStorage.getCertificatePath(nodeInfo.legalIdentities.first().owningKey.toSHA256Bytes().toString()) + return if (certPath != null) { + try { + require(Crypto.doVerify(certPath.certificates.first().publicKey, digitalSignature.bytes, nodeInfo.serialize().bytes)) + // Store the NodeInfo + // TODO: Does doorman need to sign the nodeInfo? + nodeInfoStorage.putNodeInfo(nodeInfo) + ok() + } catch (e: Exception) { + // Catch exceptions thrown by signature verification. + when (e) { + is IllegalArgumentException, is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message) + // Rethrow e if its not one of the expected exception, the server will return http 500 internal error. + else -> throw e + } + } + } else { + status(Response.Status.BAD_REQUEST).entity("Unknown node info, this public key is not registered or approved by Corda Doorman.") + }.build() + } + + @GET + fun getNetworkMap(): Response { + // TODO: Cache the response? + return ok(ObjectMapper().writeValueAsString(nodeInfoStorage.getNodeInfoHashes())).build() + } + + @GET + @Path("{var}") + fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response { + // TODO: Use JSON instead. + return nodeInfoStorage.getNodeInfo(nodeInfoHash)?.let { + ok(it.serialize().bytes).build() + } ?: status(Response.Status.NOT_FOUND).build() + } + + @GET + @Path("my-ip") + fun myIp(@Context request: HttpServletRequest): Response { + // TODO: Verify this returns IP correctly. + return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build() + } +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt similarity index 77% rename from doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt rename to doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt index 4c84cdb9e1..26e2a9ba0f 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt @@ -1,9 +1,8 @@ -package com.r3.corda.doorman +package com.r3.corda.doorman.webservice +import com.r3.corda.doorman.DoormanServerStatus import com.r3.corda.doorman.persistence.CertificateResponse -import com.r3.corda.doorman.persistence.CertificationRequestData -import com.r3.corda.doorman.persistence.CertificationRequestStorage -import com.r3.corda.doorman.signer.DefaultCsrHandler +import com.r3.corda.doorman.signer.CsrHandler import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA @@ -24,25 +23,19 @@ import javax.ws.rs.core.Response.Status.UNAUTHORIZED /** * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results. */ -@Path("") -class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: DoormanServerStatus) { +@Path("certificate") +class RegistrationWebService(private val csrHandler: CsrHandler, private val serverStatus: DoormanServerStatus) { @Context lateinit var request: HttpServletRequest /** - * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval. + * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval. * Server returns HTTP 200 response with random generated request Id after request has been persisted. */ @POST - @Path("certificate") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.TEXT_PLAIN) fun submitRequest(input: InputStream): Response { - val certificationRequest = input.use { - JcaPKCS10CertificationRequest(it.readBytes()) - } - // TODO: Certificate signing request verifications. - // TODO: Use jira api / slack bot to semi automate the approval process? - // TODO: Acknowledge to user we have received the request via email? - val requestId = csrHandler.saveRequest(CertificationRequestData(request.remoteHost, request.remoteAddr, certificationRequest)) + val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } + val requestId = csrHandler.saveRequest(certificationRequest) return ok(requestId).build() } @@ -51,7 +44,7 @@ class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: Doo * Returns HTTP 200 with DER encoded signed certificates if request has been approved else HTTP 204 No content */ @GET - @Path("certificate/{var}") + @Path("{var}") @Produces(MediaType.APPLICATION_OCTET_STREAM) fun retrieveCert(@PathParam("var") requestId: String): Response { val response = csrHandler.getResponse(requestId) diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt new file mode 100644 index 0000000000..e5c9a02a0f --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt @@ -0,0 +1,57 @@ +package com.r3.corda.doorman + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.r3.corda.doorman.persistence.* +import com.r3.corda.doorman.signer.DefaultCsrHandler +import com.r3.corda.doorman.signer.Signer +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.node.utilities.X509Utilities +import org.junit.Test +import kotlin.test.assertEquals + +class DefaultRequestProcessorTest { + @Test + fun `get response`() { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val cert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "Test", country = "GB"), keyPair) + + val requestStorage: CertificationRequestStorage = mock { + on { getRequest("New") }.thenReturn(CertificateSigningRequest(status = RequestStatus.New)) + on { getRequest("Signed") }.thenReturn(CertificateSigningRequest(status = RequestStatus.Signed, certificateData = CertificateData("", buildCertPath(cert.toX509Certificate()).encoded, CertificateStatus.VALID))) + on { getRequest("Rejected") }.thenReturn(CertificateSigningRequest(status = RequestStatus.Rejected, rejectReason = "Random reason")) + } + val signer: Signer = mock() + val requestProcessor = DefaultCsrHandler(requestStorage, signer) + + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New")) + assertEquals(CertificateResponse.Ready(buildCertPath(cert.toX509Certificate())), requestProcessor.getResponse("Signed")) + assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected")) + } + + @Test + fun `process request`() { + val request1 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test1", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val request2 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test2", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val request3 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test3", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + + val requestStorage: CertificationRequestStorage = mock { + on { getRequests(RequestStatus.Approved) }.thenReturn(listOf( + CertificateSigningRequest(requestId = "1", request = request1.encoded), + CertificateSigningRequest(requestId = "2", request = request2.encoded), + CertificateSigningRequest(requestId = "3", request = request3.encoded) + )) + } + val signer: Signer = mock() + val requestProcessor = DefaultCsrHandler(requestStorage, signer) + + requestProcessor.processApprovedRequests() + + verify(signer, times(3)).sign(any()) + verify(requestStorage, times(1)).getRequests(any()) + } +} diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt new file mode 100644 index 0000000000..4ab51a6e0a --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt @@ -0,0 +1,167 @@ +package com.r3.corda.doorman + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.r3.corda.doorman.persistence.NodeInfoStorage +import com.r3.corda.doorman.webservice.NodeInfoWebService +import net.corda.core.crypto.* +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.X509Utilities +import net.corda.nodeapi.internal.serialization.* +import org.bouncycastle.asn1.x500.X500Name +import org.codehaus.jackson.map.ObjectMapper +import org.junit.BeforeClass +import org.junit.Test +import java.io.FileNotFoundException +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import javax.ws.rs.core.MediaType +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class NodeInfoWebServiceTest { + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) + + companion object { + @BeforeClass + @JvmStatic + fun initSerialization() { + try { + SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { + registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPServerSerializationScheme()) + } + SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT + SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT + SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT + SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT + } catch (ignored: Exception) { + // Ignored + } + } + } + + @Test + fun `submit nodeInfo`() { + // Create node info. + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + // Create digital signature. + val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, nodeInfo.serialize().bytes)) + + val nodeInfoStorage: NodeInfoStorage = mock { + on { getCertificatePath(any()) }.thenReturn(certPath) + } + + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use { + it.start() + val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register") + val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes + // Post node info and signature to doorman + doPost(registerURL, nodeInfoAndSignature) + verify(nodeInfoStorage, times(1)).getCertificatePath(any()) + } + } + + @Test + fun `submit nodeInfo with invalid signature`() { + // Create node info. + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + // Create digital signature. + val attackerKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val digitalSignature = DigitalSignature.WithKey(attackerKeyPair.public, Crypto.doSign(attackerKeyPair.private, nodeInfo.serialize().bytes)) + + val nodeInfoStorage: NodeInfoStorage = mock { + on { getCertificatePath(any()) }.thenReturn(certPath) + } + + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use { + it.start() + val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register") + val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes + // Post node info and signature to doorman + assertFailsWith(IOException::class) { + doPost(registerURL, nodeInfoAndSignature) + } + verify(nodeInfoStorage, times(1)).getCertificatePath(any()) + } + } + + @Test + fun `get network map`() { + val networkMapList = listOf(SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString()) + val nodeInfoStorage: NodeInfoStorage = mock { + on { getNodeInfoHashes() }.thenReturn(networkMapList) + } + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use { + it.start() + val conn = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}").openConnection() as HttpURLConnection + val response = conn.inputStream.bufferedReader().use { it.readLine() } + val list = ObjectMapper().readValue(response, List::class.java) + verify(nodeInfoStorage, times(1)).getNodeInfoHashes() + assertEquals(networkMapList, list) + } + } + + @Test + fun `get node info`() { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + val nodeInfoHash = nodeInfo.serialize().sha256().toString() + + val nodeInfoStorage: NodeInfoStorage = mock { + on { getNodeInfo(nodeInfoHash) }.thenReturn(nodeInfo) + } + + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use { + it.start() + val nodeInfoURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/$nodeInfoHash") + val conn = nodeInfoURL.openConnection() + val nodeInfoResponse = conn.inputStream.readBytes().deserialize() + verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) + assertEquals(nodeInfo, nodeInfoResponse) + + assertFailsWith(FileNotFoundException::class) { + URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/${SecureHash.randomSHA256()}").openConnection().getInputStream() + } + } + } + + private fun doPost(url: URL, payload: ByteArray) { + val conn = url.openConnection() as HttpURLConnection + conn.doOutput = true + conn.requestMethod = "POST" + conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM) + conn.outputStream.write(payload) + + return try { + conn.inputStream.bufferedReader().use { it.readLine() } + } catch (e: IOException) { + throw IOException(conn.errorStream.bufferedReader().readLine(), e) + } + } +} \ No newline at end of file diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt similarity index 66% rename from doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt rename to doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt index 7e6ab16a37..76860b2ad3 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt @@ -1,16 +1,13 @@ package com.r3.corda.doorman -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.* import com.r3.corda.doorman.persistence.CertificateResponse -import com.r3.corda.doorman.persistence.CertificationRequestData -import com.r3.corda.doorman.persistence.CertificationRequestStorage -import com.r3.corda.doorman.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.LocalSigner +import com.r3.corda.doorman.signer.CsrHandler +import com.r3.corda.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name -import net.corda.node.utilities.CertificateAndKeyPair +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.utilities.CertificateStream import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -37,17 +34,15 @@ import java.util.zip.ZipInputStream import javax.ws.rs.core.MediaType import kotlin.test.assertEquals -class DoormanServiceTest { +class RegistrationWebServiceTest { private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 Ltd", country = "GB"), rootCAKey) private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) private lateinit var doormanServer: DoormanServer - private fun startSigningServer(storage: CertificationRequestStorage) { - val caCertAndKey = CertificateAndKeyPair(intermediateCACert, intermediateCAKey) - val csrManager = DefaultCsrHandler(storage, LocalSigner(storage, caCertAndKey, rootCACert.toX509Certificate())) - doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), csrManager) + private fun startSigningServer(csrHandler: CsrHandler) { + doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus())) doormanServer.start() } @@ -60,20 +55,20 @@ class DoormanServiceTest { fun `submit request`() { val id = SecureHash.randomSHA256().toString() - val storage = mock { + val requestProcessor = mock { on { saveRequest(any()) }.then { id } } - startSigningServer(storage) + startSigningServer(requestProcessor) val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair) // Post request to signing server via http. assertEquals(id, submitRequest(request)) - verify(storage, times(1)).saveRequest(any()) + verify(requestProcessor, times(1)).saveRequest(any()) submitRequest(request) - verify(storage, times(2)).saveRequest(any()) + verify(requestProcessor, times(2)).saveRequest(any()) } @Test @@ -83,36 +78,29 @@ class DoormanServiceTest { // Mock Storage behaviour. val certificateStore = mutableMapOf() - val storage = mock { + val requestProcessor = mock { on { getResponse(eq(id)) }.then { certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { signCertificate(eq(id), any(), any()) }.then { - @Suppress("UNCHECKED_CAST") - val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "LegalName", country = "GB"), "my@mail.com", keyPair)) - certificateStore[id] = certGen(request) - true + on { processApprovedRequests() }.then { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "LegalName", country = "GB"), "my@mail.com", keyPair) + certificateStore[id] = JcaPKCS10CertificationRequest(request).run { + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() + buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + } + null } - on { getNewRequestIds() }.then { listOf(id) } } - startSigningServer(storage) - + startSigningServer(requestProcessor) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.approveRequest(id) - storage.signCertificate(id) { - JcaPKCS10CertificationRequest(request).run { - val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() - buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - } - } + requestProcessor.processApprovedRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain - verify(storage, times(2)).getResponse(any()) + verify(requestProcessor, times(2)).getResponse(any()) assertEquals(3, certificates.size) certificates.first().run { @@ -133,34 +121,28 @@ class DoormanServiceTest { // Mock Storage behaviour. val certificateStore = mutableMapOf() - val storage = mock { + val storage = mock { on { getResponse(eq(id)) }.then { certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { signCertificate(eq(id), any(), any()) }.then { - @Suppress("UNCHECKED_CAST") - val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair)) - certificateStore[id] = certGen(request) + on { processApprovedRequests() }.then { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair) + certificateStore[id] = JcaPKCS10CertificationRequest(request).run { + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf()) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate() + buildCertPath(clientCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + } true } - on { getNewRequestIds() }.then { listOf(id) } } startSigningServer(storage) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.approveRequest(id) - storage.signCertificate(id) { - JcaPKCS10CertificationRequest(request).run { - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf()) - val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate() - buildCertPath(clientCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - } - } + storage.processApprovedRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain verify(storage, times(2)).getResponse(any()) @@ -177,13 +159,11 @@ class DoormanServiceTest { fun `request not authorised`() { val id = SecureHash.randomSHA256().toString() - val storage = mock { + val requestProcessor = mock { on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") } - on { getNewRequestIds() }.then { listOf(id) } } - startSigningServer(storage) - + startSigningServer(requestProcessor) assertThat(pollForResponse(id)).isEqualTo(PollResponse.Unauthorised("Not Allowed")) } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index 1665f60c51..7cff3c854a 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -1,10 +1,9 @@ package com.r3.corda.doorman.internal.persistence import com.r3.corda.doorman.buildCertPath -import com.r3.corda.doorman.persistence.CertificateResponse -import com.r3.corda.doorman.persistence.CertificationRequestData -import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DoormanSchemaService +import com.r3.corda.doorman.persistence.DBCertificateRequestStorage +import com.r3.corda.doorman.persistence.RequestStatus import com.r3.corda.doorman.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash @@ -15,17 +14,14 @@ import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.configureDatabase import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After import org.junit.Before import org.junit.Test import java.security.KeyPair -import java.security.cert.CertPath import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* class DBCertificateRequestStorageTest { private lateinit var storage: DBCertificateRequestStorage @@ -47,11 +43,9 @@ class DBCertificateRequestStorageTest { val request = createRequest("LegalName").first val requestId = storage.saveRequest(request) assertNotNull(storage.getRequest(requestId)).apply { - assertEquals(request.hostName, hostName) - assertEquals(request.ipAddress, ipAddress) - assertEquals(request.request, this.request) + assertEquals(request, PKCS10CertificationRequest(this.request)) } - assertThat(storage.getNewRequestIds()).containsOnly(requestId) + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId) } @Test @@ -60,17 +54,17 @@ class DBCertificateRequestStorageTest { // Add request to DB. val requestId = storage.saveRequest(request) // Pending request should equals to 1. - assertEquals(1, storage.getNewRequestIds().size) + assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) + assertNull(storage.getRequest(requestId)!!.certificateData) // Store certificate to DB. val result = storage.approveRequest(requestId) // Check request request has been approved assertTrue(result) // Check request is not ready yet. - assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) + // assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) // New request should be empty. - assertTrue(storage.getNewRequestIds().isEmpty()) + assertTrue(storage.getRequests(RequestStatus.New).isEmpty()) } @Test @@ -93,28 +87,26 @@ class DBCertificateRequestStorageTest { // Add request to DB. val requestId = storage.saveRequest(csr) // New request should equals to 1. - assertEquals(1, storage.getNewRequestIds().size) + assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) + assertNull(storage.getRequest(requestId)!!.certificateData) // Store certificate to DB. storage.approveRequest(requestId) // Check request is not ready yet. - assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) + assertEquals(RequestStatus.Approved, storage.getRequest(requestId)!!.status) // New request should be empty. - assertTrue(storage.getNewRequestIds().isEmpty()) + assertTrue(storage.getRequests(RequestStatus.New).isEmpty()) // Sign certificate - storage.signCertificate(requestId) { - JcaPKCS10CertificationRequest(csr.request).run { + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - } - } + }) // Check request is ready - assertTrue(storage.getResponse(requestId) is CertificateResponse.Ready) + assertNotNull(storage.getRequest(requestId)!!.certificateData) } @Test @@ -124,44 +116,47 @@ class DBCertificateRequestStorageTest { val requestId = storage.saveRequest(csr) // Store certificate to DB. storage.approveRequest(requestId) - val generateCert: CertificationRequestData.() -> CertPath = { - JcaPKCS10CertificationRequest(csr.request).run { + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - } - } + }) // Sign certificate - storage.signCertificate(requestId, generateCertificate = generateCert) // When subsequent signature requested - val result = storage.signCertificate(requestId, generateCertificate = generateCert) - // Then check request has not been signed - assertFalse(result) + assertFailsWith(IllegalArgumentException::class){ + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) + val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) + val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() + buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + }) + } } @Test fun `reject request`() { val requestId = storage.saveRequest(createRequest("BankA").first) storage.rejectRequest(requestId, rejectReason = "Because I said so!") - assertThat(storage.getNewRequestIds()).isEmpty() - val response = storage.getResponse(requestId) as CertificateResponse.Unauthorised - assertThat(response.message).isEqualTo("Because I said so!") + assertThat(storage.getRequests(RequestStatus.New)).isEmpty() + assertThat(storage.getRequest(requestId)!!.rejectReason).isEqualTo("Because I said so!") } @Test fun `request with the same legal name as a pending request`() { val requestId1 = storage.saveRequest(createRequest("BankA").first) - assertThat(storage.getNewRequestIds()).containsOnly(requestId1) + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1) val requestId2 = storage.saveRequest(createRequest("BankA").first) - assertThat(storage.getNewRequestIds()).containsOnly(requestId1) - val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised - assertThat(response2.message).containsIgnoringCase("duplicate") + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1) + assertEquals(RequestStatus.Rejected, storage.getRequest(requestId2)!!.status) + assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate") // Make sure the first request is processed properly storage.approveRequest(requestId1) - assertThat(storage.getResponse(requestId1)).isInstanceOf(CertificateResponse.NotReady::class.java) + assertThat(storage.getRequest(requestId1)!!.status).isEqualTo(RequestStatus.Approved) } @Test @@ -169,8 +164,7 @@ class DBCertificateRequestStorageTest { val requestId1 = storage.saveRequest(createRequest("BankA").first) storage.approveRequest(requestId1) val requestId2 = storage.saveRequest(createRequest("BankA").first) - val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised - assertThat(response2.message).containsIgnoringCase("duplicate") + assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate") } @Test @@ -178,17 +172,14 @@ class DBCertificateRequestStorageTest { val requestId1 = storage.saveRequest(createRequest("BankA").first) storage.rejectRequest(requestId1, rejectReason = "Because I said so!") val requestId2 = storage.saveRequest(createRequest("BankA").first) - assertThat(storage.getNewRequestIds()).containsOnly(requestId2) + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId2) storage.approveRequest(requestId2) - assertThat(storage.getResponse(requestId2)).isInstanceOf(CertificateResponse.NotReady::class.java) + assertThat(storage.getRequest(requestId2)!!.status).isEqualTo(RequestStatus.Approved) } - private fun createRequest(legalName: String): Pair { + private fun createRequest(legalName: String): Pair { val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val request = CertificationRequestData( - "hostname", - "0.0.0.0", - X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair)) + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair) return Pair(request, keyPair) } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt new file mode 100644 index 0000000000..166c936866 --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt @@ -0,0 +1,139 @@ +package com.r3.corda.doorman.internal.persistence + +import com.r3.corda.doorman.buildCertPath +import com.r3.corda.doorman.hash +import com.r3.corda.doorman.persistence.* +import com.r3.corda.doorman.toX509Certificate +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.sha256 +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.X509Utilities +import net.corda.node.utilities.configureDatabase +import net.corda.nodeapi.internal.serialization.* +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class PersistenceNodeInfoStorageTest { + private lateinit var nodeInfoStorage: NodeInfoStorage + private lateinit var requestStorage: CertificationRequestStorage + private lateinit var persistence: CordaPersistence + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Corda Node Intermediate CA", locality = "London", organisation = "R3 LTD", country = "GB"), intermediateCAKey.public) + + companion object { + @BeforeClass + @JvmStatic + fun initSerialization() { + try { + SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { + registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPServerSerializationScheme()) + } + SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT + SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT + SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT + SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT + } catch (ignored: Exception) { + // Ignored + } + } + } + + @Before + fun startDb() { + persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { DoormanSchemaService() }, createIdentityService = { throw UnsupportedOperationException() }) + nodeInfoStorage = PersistenceNodeInfoStorage(persistence) + requestStorage = DBCertificateRequestStorage(persistence) + } + + @After + fun closeDb() { + persistence.close() + } + + @Test + fun `test get CertificatePath`() { + // Create node info. + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + val request = X509Utilities.createCertificateSigningRequest(nodeInfo.legalIdentities.first().name, "my@mail.com", keyPair) + + val requestId = requestStorage.saveRequest(request) + requestStorage.approveRequest(requestId) + + assertNull(nodeInfoStorage.getCertificatePath(keyPair.public.hash())) + + requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())) + + val storedCertPath = nodeInfoStorage.getCertificatePath(keyPair.public.hash()) + assertNotNull(storedCertPath) + + assertEquals(clientCert.toX509Certificate(), storedCertPath!!.certificates.first()) + } + + @Test + fun `test getNodeInfoHashes`() { + // Create node info. + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val clientCert2 = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public) + val certPath2 = buildCertPath(clientCert2.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoSame = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfo2 = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath2)), 1, serial = 1L) + + nodeInfoStorage.putNodeInfo(nodeInfo) + nodeInfoStorage.putNodeInfo(nodeInfoSame) + + // getNodeInfoHashes should contain 1 hash. + assertEquals(listOf(nodeInfo.serialize().sha256().toString()), nodeInfoStorage.getNodeInfoHashes()) + + nodeInfoStorage.putNodeInfo(nodeInfo2) + // getNodeInfoHashes should contain 2 hash. + assertEquals(listOf(nodeInfo2.serialize().sha256().toString(), nodeInfo.serialize().sha256().toString()).sorted(), nodeInfoStorage.getNodeInfoHashes().sorted()) + + // Test retrieve NodeInfo. + assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString())) + assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().sha256().toString())) + } + + @Test + fun `same pub key with different node info`() { + // Create node info. + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoSamePubKey = NodeInfo(listOf(NetworkHostAndPort("my.company2.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + nodeInfoStorage.putNodeInfo(nodeInfo) + assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString())) + + // This should replace the node info. + nodeInfoStorage.putNodeInfo(nodeInfoSamePubKey) + // Old node info should be removed. + assertNull(nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString())) + assertEquals(nodeInfoSamePubKey, nodeInfoStorage.getNodeInfo(nodeInfoSamePubKey.serialize().sha256().toString())) + } +} \ No newline at end of file diff --git a/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt index 47d7398c98..210493e5ae 100644 --- a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt +++ b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt @@ -1,19 +1,16 @@ package com.r3.corda.signing -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import com.r3.corda.doorman.DoormanServer import com.r3.corda.doorman.buildCertPath -import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage import com.r3.corda.doorman.persistence.DoormanSchemaService -import com.r3.corda.doorman.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.ExternalSigner +import com.r3.corda.doorman.startDoorman import com.r3.corda.doorman.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -36,8 +33,6 @@ import java.net.URL import java.util.* import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.thread -import com.r3.corda.doorman.persistence.DBCertificateRequestStorage.CertificateSigningRequest as DoormanRequest -import com.r3.corda.signing.persistence.DBCertificateRequestStorage.CertificateSigningRequest as SigningServerRequest class SigningServiceIntegrationTest { @@ -91,12 +86,11 @@ class SigningServiceIntegrationTest { @Test fun `Signing service communicates with Doorman`() { //Start doorman server - val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { + val database = configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() - })) - val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) - doorman.start() + }) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true) // Start Corda network registration. val config = testNodeConfiguration( @@ -139,12 +133,11 @@ class SigningServiceIntegrationTest { @Ignore fun `DEMO - Create CSR and poll`() { //Start doorman server - val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { + val database = configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() - })) - val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) - doorman.start() + }) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true) thread(start = true, isDaemon = true) { val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers") From a337d1a60b9784030cd35878c0e119ba9d83ce37 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Wed, 18 Oct 2017 20:40:34 +0100 Subject: [PATCH 12/12] Fix bad Avian dir references in Makefile --- sgx-jvm/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sgx-jvm/Makefile b/sgx-jvm/Makefile index 436a11994e..2b67d641af 100644 --- a/sgx-jvm/Makefile +++ b/sgx-jvm/Makefile @@ -4,7 +4,7 @@ SHELL=/bin/bash # JAVA_HOME=$(ROOT)/usr/lib/jvm/java-8-openjdk-amd64 # JAVA_HOME=$(ROOT)/usr/lib/jvm/java-openjdk -JDK_IMAGE=$(PWD)/jdk8u/build/linux-x86_64-normal-server-release/images/j2re-image +JDK_IMAGE=$(MAKEFILE_DIR)/jdk8u/build/linux-x86_64-normal-server-release/images/j2re-image .PHONY: all all: jvm-enclave/standalone/build/standalone_sgx_verify linux-sgx-driver/isgx.ko @@ -18,7 +18,7 @@ AVIAN_EXTRA_CFLAGS="-I$(ROOT)/usr/include -I$(ROOT)/usr/include/x86_64-linux-gnu AVIAN_EXTRA_LDFLAGS="-L$(ROOT)/usr/lib -L$(ROOT)/usr/lib/x86_64-linux-gnu -ldl -lpthread -lz" .PHONY: avian avian: | $(JDK_IMAGE) - PATH=$(ROOT)/usr/bin:$(PATH) $(MAKE) -C avian build-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-build-cflags+=$(AVIAN_EXTRA_CFLAGS) extra-cflags+=$(AVIAN_EXTRA_CFLAGS) mode=debug openjdk=$(JAVA_HOME) openjdk-src=$(PWD)/jdk8u/jdk/src openjdk-image=$(JDK_IMAGE) system=sgx + PATH=$(ROOT)/usr/bin:$(PATH) $(MAKE) -C avian build-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-build-cflags+=$(AVIAN_EXTRA_CFLAGS) extra-cflags+=$(AVIAN_EXTRA_CFLAGS) mode=debug openjdk=$(JAVA_HOME) openjdk-src=$(MAKEFILE_DIR)/jdk8u/jdk/src openjdk-image=$(JDK_IMAGE) system=sgx # The SGX SDK LINUX_SGX_SOURCES=$(shell find linux-sgx \( -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.edl' \) ! \( -name '*_u.?' -o -name '*_t.?' -o -name '*.pb.h' -o -path 'linux-sgx/sdk/cpprt/linux/libunwind/*' -o -path 'linux-sgx/external/rdrand/src/config.h' \)) # Poor man's up-to-date check (they don't have one in the SDK??)