diff --git a/network-management/build.gradle b/network-management/build.gradle index d57306563c..0a4b965c9a 100644 --- a/network-management/build.gradle +++ b/network-management/build.gradle @@ -1,7 +1,7 @@ ext { - // We use Corda release artifact dependencies instead of project dependencies to make sure each doorman release is + // We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are // aligned with the corresponding Corda release. - corda_dependency_version = '2.0-20171104.000037-23' + corda_dependency_version = '2.0-20171108.000038-27' } version "$corda_dependency_version" @@ -10,6 +10,7 @@ description 'Network management module encapsulating components such as Doorman, apply plugin: 'us.kirchmeier.capsule' apply plugin: 'kotlin' +apply plugin: 'kotlin-jpa' repositories { mavenLocal() @@ -92,7 +93,6 @@ dependencies { compile "net.corda:corda-node-api:$corda_dependency_version" testCompile "net.corda:corda-test-utils:$corda_dependency_version" testCompile "net.corda:corda-node-driver:$corda_dependency_version" - testCompile "net.corda:corda-test-common:$corda_dependency_version" // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" @@ -122,7 +122,9 @@ dependencies { testCompile 'junit:junit:4.12' testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "com.nhaarman:mockito-kotlin:0.6.1" + testRuntime "net.corda:corda-rpc:$corda_dependency_version" testCompile "com.spotify:docker-client:8.9.1" + integrationTestRuntime "net.corda:corda-rpc:$corda_dependency_version" compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') { // The jira client includes jersey-core 1.5 which breaks everything. diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt index 40b87a90cc..0ab970ff15 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/DoormanIntegrationTest.kt @@ -3,7 +3,7 @@ package com.r3.corda.networkmanage.doorman import com.nhaarman.mockito_kotlin.whenever import com.r3.corda.networkmanage.common.persistence.SchemaService import com.r3.corda.networkmanage.common.utils.toX509Certificate -import com.r3.corda.networkmanage.doorman.signer.Signer +import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name @@ -39,10 +39,10 @@ class DoormanIntegrationTest { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() }, SchemaService()) - val signer = Signer(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())) + val signer = LocalSigner(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())) //Start doorman server - val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, testNetworkParameters(emptyList()), signer, null) + val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, testNetworkParameters(emptyList()), signer, 2, 10,null) // Start Corda network registration. val config = testNodeConfiguration( diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt index 9c94d60d85..ef694f6752 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt @@ -8,12 +8,13 @@ import com.r3.corda.networkmanage.common.persistence.SchemaService import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.doorman.startDoorman -import com.r3.corda.networkmanage.hsm.persistence.CertificateRequestData +import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStorage import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage -import com.r3.corda.networkmanage.hsm.signer.HsmSigner +import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.utilities.CertificateType @@ -32,6 +33,7 @@ import org.junit.* import org.junit.rules.TemporaryFolder import java.net.URL import java.util.* +import javax.persistence.PersistenceException import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.thread @@ -59,7 +61,7 @@ class SigningServiceIntegrationTest { timer.cancel() } - private fun givenSignerSigningAllRequests(storage: SignedCertificateRequestStorage): HsmSigner { + private fun givenSignerSigningAllRequests(storage: SignedCertificateRequestStorage): HsmCsrSigner { // Create all certificates val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", @@ -71,8 +73,7 @@ class SigningServiceIntegrationTest { // Mock signing logic but keep certificate persistence return mock { on { sign(any()) }.then { - @Suppress("UNCHECKED_CAST") - val toSign = it.arguments[0] as List + val toSign: List = uncheckedCast(it.arguments[0]) toSign.forEach { JcaPKCS10CertificationRequest(it.request).run { val certificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() @@ -91,8 +92,7 @@ class SigningServiceIntegrationTest { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() }, SchemaService()) - val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, - initialNetworkMapParameters = testNetworkParameters(emptyList())) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 10, initialNetworkMapParameters = testNetworkParameters(emptyList())) // Start Corda network registration. val config = testNodeConfiguration( @@ -108,14 +108,22 @@ class SigningServiceIntegrationTest { val hsmSigner = givenSignerSigningAllRequests(signingServiceStorage) // Poll the database for approved requests - timer.scheduleAtFixedRate(2.seconds.toMillis(), 1.seconds.toMillis()) { + timer.scheduleAtFixedRate(0, 1.seconds.toMillis()) { // The purpose of this tests is to validate the communication between this service and Doorman // by the means of data in the shared database. // Therefore the HSM interaction logic is mocked here. - val approved = signingServiceStorage.getApprovedRequests() - if (approved.isNotEmpty()) { - hsmSigner.sign(approved) - timer.cancel() + try { + val approved = signingServiceStorage.getApprovedRequests() + if (approved.isNotEmpty()) { + hsmSigner.sign(approved) + timer.cancel() + } + } catch (exception: PersistenceException) { + // It may happen that Doorman DB is not created at the moment when the signing service polls it. + // This is due to the fact that schema is initialized at the time first hibernate session is established. + // Since Doorman does this at the time the first CSR arrives, which in turn happens after signing service + // startup, the very first iteration of the signing service polling fails with + // [org.hibernate.tool.schema.spi.SchemaManagementException] being thrown as the schema is missing. } } NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore() @@ -139,8 +147,7 @@ class SigningServiceIntegrationTest { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() }, SchemaService()) - val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, - initialNetworkMapParameters = testNetworkParameters(emptyList())) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 10, initialNetworkMapParameters = testNetworkParameters(emptyList())) thread(start = true, isDaemon = true) { val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt index 500fe35062..7834dae5e6 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt @@ -3,6 +3,16 @@ package com.r3.corda.networkmanage.common.persistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.CertPath +data class CertificateData(val publicKeyHash: String, val certStatus: CertificateStatus, val certPath: CertPath) + +data class CertificateSigningRequest(val requestId: String, + val legalName: String, + val status: RequestStatus, + val request: PKCS10CertificationRequest, + val remark: String?, + val modifiedBy: List, + val certData: CertificateData?) + /** * Provide certificate signing request storage for the certificate signing server. */ @@ -12,20 +22,21 @@ interface CertificationRequestStorage { } /** - * 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. - * @param certificationData certificate request data to be persisted. - * @param createdBy authority (its identifier) creating this request. + * 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. + * @param request request to be stored */ - fun saveRequest(rawRequest: PKCS10CertificationRequest): String + fun saveRequest(request: PKCS10CertificationRequest): String /** * Retrieve certificate singing request using [requestId]. + * @return certificate signing request or null if the request does not exist */ fun getRequest(requestId: String): CertificateSigningRequest? /** - * Retrieve list of certificate singing request base on the [RequestStatus]. + * Retrieve list of certificate signing request based on the [RequestStatus]. */ fun getRequests(requestStatus: RequestStatus): List @@ -33,21 +44,20 @@ interface CertificationRequestStorage { * Approve the given request if it has not already been approved. Otherwise do nothing. * @param requestId id of the certificate signing request * @param approvedBy authority (its identifier) approving this request. - * @return True if the request has been approved and false otherwise. */ // TODO: Merge status changing methods. - fun approveRequest(requestId: String, approvedBy: String): Boolean + fun approveRequest(requestId: String, approvedBy: String) /** * Reject the given request using the given reason. * @param requestId id of the certificate signing request - * @param rejectBy authority (its identifier) rejecting this request. + * @param rejectedBy authority (its identifier) rejecting this request. * @param rejectReason brief description of the rejection reason */ fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) /** - * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request statue to [RequestStatus.Signed]. + * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.Signed]. * @param requestId id of the certificate signing request * @param signedBy authority (its identifier) signing this request. * @throws IllegalArgumentException if request is not found or not in Approved state. @@ -59,4 +69,18 @@ sealed class CertificateResponse { object NotReady : CertificateResponse() data class Ready(val certificatePath: CertPath) : CertificateResponse() data class Unauthorised(val message: String) : CertificateResponse() +} + +/** + * Describes certificate status + */ +enum class CertificateStatus { + VALID, SUSPENDED, REVOKED +} + +/** + * Describes entire certificate signing request status + */ +enum class RequestStatus { + New, Approved, Rejected, Signed } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorage.kt deleted file mode 100644 index 20525d89bd..0000000000 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorage.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.r3.corda.networkmanage.common.persistence - -import com.r3.corda.networkmanage.common.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 org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest -import java.security.cert.CertPath -import java.sql.Connection -import java.time.Instant -import javax.persistence.LockModeType -import javax.persistence.criteria.CriteriaBuilder -import javax.persistence.criteria.Path -import javax.persistence.criteria.Predicate - -class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { - override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List) { - return database.transaction(Connection.TRANSACTION_SERIALIZABLE) { - 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" } - - val publicKeyHash = certificates.certificates.first().publicKey.hash() - request!!.certificateData = CertificateData(publicKeyHash, certificates.encoded, CertificateStatus.VALID) - request.status = Signed - request.modifiedBy = signedBy - request.modifiedAt = Instant.now() - session.save(request) - } - } - - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { - val request = JcaPKCS10CertificationRequest(rawRequest) - val requestId = SecureHash.randomSHA256().toString() - database.transaction(Connection.TRANSACTION_SERIALIZABLE) { - // TODO ensure public key not duplicated. - val (legalName, rejectReason) = try { - // This will fail with IllegalArgumentException if subject name is malformed. - 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`(Approved, New) - criteriaQuery.where(and(nameEq, statusNewOrApproved)) - } - } - val duplicate = session.createQuery(query).resultList.isNotEmpty() - if (duplicate) { - Pair(legalName.x500Name, "Duplicate legal name") - } else { - Pair(legalName.x500Name, null) - } - } catch (e: IllegalArgumentException) { - Pair(request.subject, "Name validation failed with exception : ${e.message}") - } - session.save(CertificateSigningRequest( - requestId = requestId, - legalName = legalName.toString(), - request = request.encoded, - remark = rejectReason, - modifiedBy = emptyList(), - status = if (rejectReason == null) New else Rejected - )) - } - return requestId - } - - override fun approveRequest(requestId: String, approvedBy: String): Boolean { - var approved = false - database.transaction(Connection.TRANSACTION_SERIALIZABLE) { - val request = singleRequestWhere { builder, path -> - builder.and(builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId), - builder.equal(path.get(CertificateSigningRequest::status.name), New)) - } - if (request != null) { - request.modifiedAt = Instant.now() - request.modifiedBy = listOf(approvedBy) - request.status = Approved - session.save(request) - approved = true - } - } - return approved - } - - override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { - database.transaction(Connection.TRANSACTION_SERIALIZABLE) { - val request = singleRequestWhere { builder, path -> - builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) - } - if (request != null) { - request.remark = rejectReason - request.status = Rejected - request.modifiedBy = listOf(rejectedBy) - request.modifiedAt = Instant.now() - session.save(request) - } - } - } - - override fun getRequest(requestId: String): CertificateSigningRequest? { - return database.transaction { - singleRequestWhere { builder, path -> - builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) - } - } - } - - override fun getRequests(requestStatus: RequestStatus): List { - return database.transaction { - val builder = session.criteriaBuilder - val query = builder.createQuery(CertificateSigningRequest::class.java).run { - from(CertificateSigningRequest::class.java).run { - where(builder.equal(get(CertificateSigningRequest::status.name), requestStatus)) - } - } - session.createQuery(query).resultList - } - } - - 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).setLockMode(LockModeType.PESSIMISTIC_WRITE).uniqueResultOptional().orElse(null) - } -} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/Entities.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/Entities.kt deleted file mode 100644 index b6f369ac09..0000000000 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/Entities.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.r3.corda.networkmanage.common.persistence - -import org.hibernate.envers.Audited -import java.time.Instant -import javax.persistence.* - -@Entity -@Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) -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), - - @Audited - @Column(name = "status") - @Enumerated(EnumType.STRING) - var status: RequestStatus = RequestStatus.New, - - @Audited - @Column(name = "modified_by", length = 512) - @ElementCollection(targetClass = String::class, fetch = FetchType.EAGER) - var modifiedBy: List = emptyList(), - - @Audited - @Column(name = "modified_at") - var modifiedAt: Instant? = Instant.now(), - - @Audited - @Column(name = "remark", length = 256, nullable = true) - var remark: 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 -} - -@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/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt new file mode 100644 index 0000000000..967cea7f47 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt @@ -0,0 +1,64 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.common.signer.SignedNetworkMap +import net.corda.core.crypto.SecureHash +import net.corda.core.node.NetworkParameters + +/** + * Data access object interface for NetworkMap persistence layer + */ +interface NetworkMapStorage { + /** + * Retrieves current network map. Current in this context means the one that has been most recently signed. + * @return current network map + */ + fun getCurrentNetworkMap(): SignedNetworkMap + + /** + * Retrieves current map node info hashes only. Hashes are further filtered by the [certificateStatuses] parameter + * that restricts considered node info to only those which [CertificateStatus] value corresponds to one in the passed + * collection. If null or empty list is passed then filtering has no effect and all node info hashes from the current + * network map are returned. + * @param certificateStatuses certificate statuses to be used in the node info filtering. Node info hash is returned + * in the result collection only if it is in the current network map and its certificate status belongs to the + * [certificateStatuses] collection or if [certificateStatuses] collection is null or empty. + * @return list of current network map node info hashes satisfying the filtering criteria given by [certificateStatuses]. + */ + fun getCurrentNetworkMapNodeInfoHashes(certificateStatuses: List): List + + /** + * Persists a new instance of the signed network map. + * @param signedNetworkMap encapsulates all the information needed for persisting current network map state. + */ + fun saveNetworkMap(signedNetworkMap: SignedNetworkMap) + + /** + * Retrieve all node info hashes for all signed node info with valid certificates, + * that are not associated with any network map yet. + */ + fun getDetachedSignedAndValidNodeInfoHashes(): List + + /** + * Retrieve network parameters by their hash. + * @return network parameters corresponding to the given hash or null if it does not exist + */ + fun getNetworkParameters(parameterHash: SecureHash): NetworkParameters + + /** + * Retrieve network map parameters that are used in the current network map. + * @return current network map parameters + */ + fun getCurrentNetworkParameters(): NetworkParameters + + /** + * Persists given network parameters. + * @return hash corresponding to newly create network parameters entry + */ + fun putNetworkParameters(networkParameters: NetworkParameters): SecureHash + + /** + * Retrieves the latest (i.e. most recently inserted) network parameters + * @return latest network parameters + */ + fun getLatestNetworkParameters(): NetworkParameters +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt index 0272dc989e..d768f4c780 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt @@ -1,29 +1,56 @@ package com.r3.corda.networkmanage.common.persistence +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData import net.corda.core.node.NodeInfo import java.security.cert.CertPath +/** + * Data access object interface for NetworkMap/NodeInfo persistence layer + */ interface NodeInfoStorage { /** - * Retrieve certificate paths using the public key hash. + * Retrieve node certificate path using the node public key hash. * @return [CertPath] or null if the public key is not registered with the Doorman. */ - fun getCertificatePath(publicKeyHash: String): CertPath? + fun getCertificatePath(publicKeyHash: SecureHash): CertPath? /** - * Obtain list of registered node info hashes. + * Obtain list of registered node info hashes that haven't been signed yet and have valid certificates. */ - //TODO: we might want to return [SecureHash] instead of String - fun getNodeInfoHashes(): List + fun getUnsignedNodeInfoHashes(): List + + /** + * Similar to [getUnsignedNodeInfoHashes] but instead of hashes, map of node info bytes is returned. + * @return map of node info hashes to their corresponding node info bytes + */ + fun getUnsignedNodeInfoBytes(): Map /** * Retrieve node info using nodeInfo's hash * @return [NodeInfo] or null if the node info is not registered. */ - fun getNodeInfo(nodeInfoHash: String): NodeInfo? + fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? + + /** + * Retrieve node info together with its signature using nodeInfo's hash + * @return [NodeInfo] or null if the node info is not registered. + */ + fun getSignedNodeInfo(nodeInfoHash: SecureHash): SignedData? /** * The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info. + * @param nodeInfo node info to be stored + * @param signature (optional) signature associated with the node info + * @return hash for the newly created node info entry */ - fun putNodeInfo(nodeInfo: NodeInfo) + fun putNodeInfo(nodeInfo: NodeInfo, signature: DigitalSignature? = null): SecureHash + + /** + * Stores the signature for the given node info hash. + * @param nodeInfoHash node info hash which signature corresponds to + * @param signature signature for the node info + */ + fun signNodeInfo(nodeInfoHash: SecureHash, signature: DigitalSignature.WithKey) } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorage.kt deleted file mode 100644 index 85d2c284bd..0000000000 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorage.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.r3.corda.networkmanage.common.persistence - -import com.r3.corda.networkmanage.common.utils.buildCertPath -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/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt index ef58a89c81..6a709ed2ee 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt @@ -1,7 +1,26 @@ package com.r3.corda.networkmanage.common.persistence -import net.corda.core.crypto.sha256 -import java.security.PublicKey +import net.corda.node.utilities.DatabaseTransaction +import javax.persistence.LockModeType +import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.Path +import javax.persistence.criteria.Predicate + +fun DatabaseTransaction.singleRequestWhere(clazz: Class, predicate: (CriteriaBuilder, Path) -> Predicate): T? { + val builder = session.criteriaBuilder + val criteriaQuery = builder.createQuery(clazz) + val query = criteriaQuery.from(clazz).run { + criteriaQuery.where(predicate(builder, this)) + } + return session.createQuery(query).setLockMode(LockModeType.PESSIMISTIC_WRITE).resultList.firstOrNull() +} + +fun DatabaseTransaction.deleteRequest(clazz: Class, predicate: (CriteriaBuilder, Path) -> Predicate): Int { + val builder = session.criteriaBuilder + val criteriaDelete = builder.createCriteriaDelete(clazz) + val delete = criteriaDelete.from(clazz).run { + criteriaDelete.where(predicate(builder, this)) + } + return session.createQuery(delete).executeUpdate() +} -// TODO: replace this with Crypto.hash when its available. -fun PublicKey.hash() = encoded.sha256().toString() diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt new file mode 100644 index 0000000000..9b1e7b01fc --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt @@ -0,0 +1,132 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity +import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity +import com.r3.corda.networkmanage.common.utils.hashString +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.asn1.x500.X500Name +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.hibernate.Session +import java.security.cert.CertPath +import java.sql.Connection +import java.time.Instant + +/** + * Database implementation of the [CertificationRequestStorage] interface. + */ +class PersistentCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { + override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List) { + return database.transaction(Connection.TRANSACTION_SERIALIZABLE) { + val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> + val requestIdEq = builder.equal(path.get(CertificateSigningRequestEntity::requestId.name), requestId) + val statusEq = builder.equal(path.get(CertificateSigningRequestEntity::status.name), RequestStatus.Approved) + builder.and(requestIdEq, statusEq) + } + request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId") + val publicKeyHash = certificates.certificates.first().publicKey.hashString() + val certificateSigningRequest = request.copy( + modifiedBy = signedBy, + modifiedAt = Instant.now(), + status = RequestStatus.Signed) + session.merge(certificateSigningRequest) + val certificateDataEntity = CertificateDataEntity( + publicKeyHash = publicKeyHash, + certificateStatus = CertificateStatus.VALID, + certificatePathBytes = certificates.encoded, + certificateSigningRequest = certificateSigningRequest) + session.persist(certificateDataEntity) + } + } + + override fun saveRequest(request: PKCS10CertificationRequest): String { + val requestId = SecureHash.randomSHA256().toString() + database.transaction(Connection.TRANSACTION_SERIALIZABLE) { + val (legalName, rejectReason) = parseAndValidateLegalName(request, session) + session.save(CertificateSigningRequestEntity( + requestId = requestId, + legalName = legalName.toString(), + requestBytes = request.encoded, + remark = rejectReason, + modifiedBy = emptyList(), + status = if (rejectReason == null) RequestStatus.New else RequestStatus.Rejected + )) + } + return requestId + } + + override fun approveRequest(requestId: String, approvedBy: String) { + return database.transaction(Connection.TRANSACTION_SERIALIZABLE) { + val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> + builder.and(builder.equal(path.get(CertificateSigningRequestEntity::requestId.name), requestId), + builder.equal(path.get(CertificateSigningRequestEntity::status.name), RequestStatus.New)) + } + request ?: throw IllegalArgumentException("Error when approving request with id: $requestId. Request does not exist or its status is not NEW.") + val update = request.copy( + modifiedBy = listOf(approvedBy), + modifiedAt = Instant.now(), + status = RequestStatus.Approved) + session.merge(update) + } + } + + override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { + database.transaction(Connection.TRANSACTION_SERIALIZABLE) { + val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> + builder.equal(path.get(CertificateSigningRequestEntity::requestId.name), requestId) + } + request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.") + val update = request.copy( + modifiedBy = listOf(rejectedBy), + modifiedAt = Instant.now(), + status = RequestStatus.Rejected, + remark = rejectReason + ) + session.merge(update) + } + } + + override fun getRequest(requestId: String): CertificateSigningRequest? { + return database.transaction { + singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> + builder.equal(path.get(CertificateSigningRequestEntity::requestId.name), requestId) + }?.toCertificateSigningRequest() + } + } + + override fun getRequests(requestStatus: RequestStatus): List { + return database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(CertificateSigningRequestEntity::class.java).run { + from(CertificateSigningRequestEntity::class.java).run { + where(builder.equal(get(CertificateSigningRequestEntity::status.name), requestStatus)) + } + } + session.createQuery(query).resultList.map { it.toCertificateSigningRequest() } + } + } + + private fun parseAndValidateLegalName(request: PKCS10CertificationRequest, session: Session): Pair { + val legalName = try { + CordaX500Name.parse(request.subject.toString()) + } catch (e: IllegalArgumentException) { + return Pair(request.subject, "Name validation failed with exception : ${e.message}") + } + val query = session.criteriaBuilder.run { + val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) + criteriaQuery.from(CertificateSigningRequestEntity::class.java).run { + criteriaQuery.where(equal(get(CertificateSigningRequestEntity::legalName.name), legalName.toString())) + } + } + val duplicates = session.createQuery(query).resultList.filter { + it.status == RequestStatus.New || it.status == RequestStatus.Approved || it.certificateData?.certificateStatus == CertificateStatus.VALID + } + return if (duplicates.isEmpty()) { + Pair(legalName.x500Name, null) + } else { + Pair(legalName.x500Name, "Duplicate legal name") + } + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt new file mode 100644 index 0000000000..18ecc617e6 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt @@ -0,0 +1,160 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity +import com.r3.corda.networkmanage.common.persistence.entity.NetworkParametersEntity +import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity +import com.r3.corda.networkmanage.common.signer.NetworkMap +import com.r3.corda.networkmanage.common.signer.SignedNetworkMap +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.serialize +import net.corda.node.utilities.CordaPersistence +import org.hibernate.Session +import org.hibernate.jpa.QueryHints + +/** + * Database implementation of the [NetworkMapStorage] interface + */ +class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage { + override fun getCurrentNetworkMap(): SignedNetworkMap = database.transaction { + val networkMapEntity = getCurrentNetworkMapEntity(getNetworkMapWithNodeInfoAndParametersHint(session)) + networkMapEntity ?: throw NoSuchElementException("Current Network Map does not exist.") + val nodeInfoHashes = networkMapEntity.nodeInfoList.map { it.nodeInfoHash } + val networkParameterHash = networkMapEntity.parameters.parametersHash + val signatureAndCertPath = networkMapEntity.signatureAndCertificate() + SignedNetworkMap(NetworkMap(nodeInfoHashes, networkParameterHash), signatureAndCertPath!!) + } + + override fun getCurrentNetworkParameters(): NetworkParameters = database.transaction { + val networkMapEntity = getCurrentNetworkMapEntity(getNetworkMapWithParametersHint(session)) + if (networkMapEntity != null) { + networkMapEntity.parameters.networkParameters() + } else { + throw NoSuchElementException("Current Network Parameters do not exist.") + } + } + + override fun saveNetworkMap(signedNetworkMap: SignedNetworkMap) { + database.transaction { + val networkMap = signedNetworkMap.networkMap + val signatureAndCertPath = signedNetworkMap.signatureData + val signature = signatureAndCertPath.signature + val networkParametersEntity = getNetworkParametersEntity(networkMap.parametersHash.toString()) + networkParametersEntity ?: throw IllegalArgumentException("Error when retrieving network parameters entity for network map signing! - Entity does not exist") + val networkMapEntity = NetworkMapEntity( + parameters = networkParametersEntity, + signatureBytes = signature.bytes, + certificatePathBytes = signatureAndCertPath.certPath.serialize().bytes + ) + session.save(networkMapEntity) + networkMap.nodeInfoHashes.forEach { + val nodeInfoEntity = session.find(NodeInfoEntity::class.java, it) + session.merge(nodeInfoEntity.copy(networkMap = networkMapEntity)) + } + } + } + + override fun getNetworkParameters(parameterHash: SecureHash): NetworkParameters { + val entity = getNetworkParametersEntity(parameterHash.toString()) + if (entity != null) { + return entity.networkParameters() + } else { + throw NoSuchElementException("Network parameters with $parameterHash do not exist") + } + } + + override fun getCurrentNetworkMapNodeInfoHashes(certificateStatuses: List): List = database.transaction { + val networkMapEntity = getCurrentNetworkMapEntity(getNetworkMapWithNodeInfoAndCsrHint(session)) + if (networkMapEntity != null) { + networkMapEntity.nodeInfoList.filter({ + certificateStatuses == null || certificateStatuses.isEmpty() || certificateStatuses.contains(it.certificateSigningRequest?.certificateData?.certificateStatus) + }).map { SecureHash.parse(it.nodeInfoHash) } + } else { + emptyList() + } + } + + override fun putNetworkParameters(networkParameters: NetworkParameters): SecureHash = database.transaction { + val bytes = networkParameters.serialize().bytes + val hash = bytes.sha256() + session.save(NetworkParametersEntity( + parametersBytes = bytes, + parametersHash = hash.toString() + )) + hash + } + + override fun getLatestNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters() + + private fun getLatestNetworkParametersEntity(): NetworkParametersEntity = database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(NetworkParametersEntity::class.java).run { + from(NetworkParametersEntity::class.java).run { + orderBy(builder.desc(get(NetworkParametersEntity::version.name))) + } + } + // We just want the last signed entry + session.createQuery(query).resultList.first() + } + + override fun getDetachedSignedAndValidNodeInfoHashes(): List = database.transaction { + val builder = session.criteriaBuilder + // Get signed NodeInfoEntities + val query = builder.createQuery(NodeInfoEntity::class.java).run { + from(NodeInfoEntity::class.java).run { + where(builder.and( + builder.isNull(get(NodeInfoEntity::networkMap.name)), + builder.isNotNull(get(NodeInfoEntity::signatureBytes.name)))) + } + } + session.createQuery(query).resultList.map { SecureHash.parse(it.nodeInfoHash) } + } + + private fun getCurrentNetworkMapEntity(hint: Pair): NetworkMapEntity? = database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(NetworkMapEntity::class.java).run { + from(NetworkMapEntity::class.java).run { + where(builder.isNotNull(get(NetworkMapEntity::signatureBytes.name))) + orderBy(builder.desc(get(NetworkMapEntity::version.name))) + } + } + // We just want the last signed entry + session.createQuery(query).setHint(hint.first, hint.second).resultList.firstOrNull() + } + + private fun getNetworkParametersEntity(parameterHash: String): NetworkParametersEntity? = database.transaction { + singleRequestWhere(NetworkParametersEntity::class.java) { builder, path -> + builder.equal(path.get(NetworkParametersEntity::parametersHash.name), parameterHash) + } + } + + /** + * Creates Hibernate query hint for pulling [NetworkParametersEntity] when querying for [NetworkMapEntity] + */ + private fun getNetworkMapWithParametersHint(session: Session): Pair { + val graph = session.createEntityGraph(NetworkMapEntity::class.java) + graph.addAttributeNodes(NetworkMapEntity::parameters.name) + return QueryHints.HINT_LOADGRAPH to graph + } + + /** + * Creates Hibernate query hint for pulling [NodeInfoEntity] and [CertificateSigningRequestEntity] when querying for [NetworkMapEntity] + */ + private fun getNetworkMapWithNodeInfoAndCsrHint(session: Session): Pair { + val graph = session.createEntityGraph(NetworkMapEntity::class.java) + val subGraph = graph.addSubgraph(NetworkMapEntity::nodeInfoList.name, NodeInfoEntity::class.java) + subGraph.addAttributeNodes(NodeInfoEntity::certificateSigningRequest.name) + return QueryHints.HINT_LOADGRAPH to graph + } + + /** + * Creates Hibernate query hint for pulling [NodeInfoEntity] and [NetworkParametersEntity] when querying for [NetworkMapEntity] + */ + private fun getNetworkMapWithNodeInfoAndParametersHint(session: Session): Pair { + val graph = session.createEntityGraph(NetworkMapEntity::class.java) + graph.addAttributeNodes(NetworkMapEntity::nodeInfoList.name) + graph.addAttributeNodes(NetworkMapEntity::parameters.name) + return QueryHints.HINT_LOADGRAPH to graph + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt new file mode 100644 index 0000000000..1714dd4796 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt @@ -0,0 +1,129 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity +import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity +import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.hashString +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.crypto.sha256 +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.serialize +import net.corda.node.utilities.CordaPersistence +import org.hibernate.Session +import org.hibernate.jpa.QueryHints +import java.security.cert.CertPath +import java.sql.Connection + +/** + * Database implementation of the [NetworkMapStorage] interface + */ +class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { + override fun putNodeInfo(nodeInfo: NodeInfo, signature: DigitalSignature?): SecureHash = database.transaction(Connection.TRANSACTION_SERIALIZABLE) { + val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hashString() + val request = singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> + val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), publicKeyHash) + val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) + builder.and(certPublicKeyHashEq, certStatusValid) + } + request ?: throw IllegalArgumentException("CSR data missing for provided node info: $nodeInfo") + /* + * Delete any previous [HashedNodeInfo] instance for this CSR + * Possibly it should be moved at the network signing process at the network signing process + * as for a while the network map will have invalid entries (i.e. hashes for node info which have been + * removed). Either way, there will be a period of time when the network map data will be invalid + * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. + */ + deleteRequest(NodeInfoEntity::class.java) { builder, path -> + builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) + } + val serializedNodeInfo = nodeInfo.serialize().bytes + val hash = serializedNodeInfo.sha256() + val hashedNodeInfo = NodeInfoEntity( + nodeInfoHash = hash.toString(), + certificateSigningRequest = request.certificateSigningRequest, + nodeInfoBytes = serializedNodeInfo, + signatureBytes = signature?.bytes) + session.save(hashedNodeInfo) + hash + } + + override fun getSignedNodeInfo(nodeInfoHash: SecureHash): SignedData? = database.transaction { + val nodeInfoEntity = session.find(NodeInfoEntity::class.java, nodeInfoHash.toString()) + if (nodeInfoEntity?.signatureBytes == null) { + null + } else { + SignedData(SerializedBytes(nodeInfoEntity.nodeInfoBytes), nodeInfoEntity.signature()!!) + } + } + + override fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo? = database.transaction { + session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.nodeInfo() + } + + override fun getUnsignedNodeInfoBytes(): Map { + return getUnsignedNodeInfoEntities().associate { SecureHash.parse(it.nodeInfoHash) to it.nodeInfoBytes } + } + + override fun getUnsignedNodeInfoHashes(): List { + return getUnsignedNodeInfoEntities().map { SecureHash.parse(it.nodeInfoHash) } + } + + override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? { + return database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(ByteArray::class.java).run { + from(CertificateSigningRequestEntity::class.java).run { + select(get(CertificateSigningRequestEntity::certificateData.name) + .get(CertificateDataEntity::certificatePathBytes.name)) + where(builder.equal(get(CertificateSigningRequestEntity::certificateData.name) + .get(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString())) + } + } + session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) } + } + } + + override fun signNodeInfo(nodeInfoHash: SecureHash, signature: DigitalSignature.WithKey) { + database.transaction { + val nodeInfoEntity = session.find(NodeInfoEntity::class.java, nodeInfoHash.toString()) + if (nodeInfoEntity != null) { + session.merge(nodeInfoEntity.copy( + signatureBytes = signature.bytes, + signaturePublicKeyAlgorithm = signature.by.algorithm, + signaturePublicKeyBytes = signature.by.encoded + )) + } + } + } + + private fun getUnsignedNodeInfoEntities(): List = database.transaction { + val builder = session.criteriaBuilder + // Retrieve all unsigned NodeInfoHash + val query = builder.createQuery(NodeInfoEntity::class.java).run { + from(NodeInfoEntity::class.java).run { + where(builder.and(builder.isNull(get(NodeInfoEntity::signatureBytes.name)))) + } + } + // Retrieve them together with their CSR + val (hintKey, hintValue) = getNodeInfoWithCsrHint(session) + val unsigned = session.createQuery(query).setHint(hintKey, hintValue).resultList + // Get only those that are valid + unsigned.filter({ + val certificateStatus = it.certificateSigningRequest?.certificateData?.certificateStatus + certificateStatus == CertificateStatus.VALID + }) + } + + /** + * Creates Hibernate query hint for pulling [CertificateSigningRequestEntity] when querying for [NodeInfoEntity] + */ + private fun getNodeInfoWithCsrHint(session: Session): Pair { + val graph = session.createEntityGraph(NodeInfoEntity::class.java) + graph.addAttributeNodes(NodeInfoEntity::certificateSigningRequest.name) + return QueryHints.HINT_LOADGRAPH to graph + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt index 958520bd11..3dad8bf102 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt @@ -1,5 +1,6 @@ package com.r3.corda.networkmanage.common.persistence +import com.r3.corda.networkmanage.common.persistence.entity.* import net.corda.core.contracts.ContractState import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState @@ -9,12 +10,17 @@ class SchemaService : SchemaService { // Entities for compulsory services object SchemaServices - object DoormanServicesV1 : MappedSchema(schemaFamily = SchemaServices.javaClass, version = 1, - mappedTypes = listOf(CertificateSigningRequest::class.java, NodeInfoEntity::class.java, PublicKeyNodeInfoLink::class.java)) + object NetworkServicesV1 : MappedSchema(schemaFamily = SchemaServices.javaClass, version = 1, + mappedTypes = listOf( + CertificateSigningRequestEntity::class.java, + CertificateDataEntity::class.java, + NodeInfoEntity::class.java, + NetworkParametersEntity::class.java, + NetworkMapEntity::class.java)) - override var schemaOptions: Map = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) + override var schemaOptions: Map = mapOf(Pair(NetworkServicesV1, SchemaService.SchemaOptions())) - override fun selectSchemas(state: ContractState): Iterable = setOf(DoormanServicesV1) + override fun selectSchemas(state: ContractState): Iterable = setOf(NetworkServicesV1) override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException() diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt new file mode 100644 index 0000000000..030b7af83f --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt @@ -0,0 +1,115 @@ +package com.r3.corda.networkmanage.common.persistence.entity + +import com.r3.corda.networkmanage.common.persistence.CertificateData +import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.hibernate.envers.Audited +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.time.Instant +import javax.persistence.* + +@Entity +@Table(name = "certificate_signing_request") +class CertificateSigningRequestEntity( + @Id + @Column(name = "request_id", length = 64) + val requestId: String, + + // TODO: Store X500Name with a proper schema. + @Column(name = "legal_name", length = 256, nullable = false) + val legalName: String, + + @Audited + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + val status: RequestStatus = RequestStatus.New, + + @Audited + @Column(name = "modified_by", length = 512) + @ElementCollection(targetClass = String::class, fetch = FetchType.EAGER) + val modifiedBy: List = emptyList(), + + @Audited + @Column(name = "modified_at", nullable = false) + val modifiedAt: Instant = Instant.now(), + + @Audited + @Column(name = "remark", length = 256) + val remark: String? = null, + + @OneToOne(fetch = FetchType.EAGER, mappedBy = "certificateSigningRequest") + val certificateData: CertificateDataEntity? = null, + + @Lob + @Column(name = "request_bytes", nullable = false) + val requestBytes: ByteArray +) { + fun toCertificateSigningRequest() = CertificateSigningRequest( + requestId = requestId, + legalName = legalName, + status = status, + request = request(), + remark = remark, + modifiedBy = modifiedBy, + certData = certificateData?.toCertificateData() + ) + + fun copy(requestId: String = this.requestId, + legalName: String = this.legalName, + status: RequestStatus = this.status, + modifiedBy: List = this.modifiedBy, + modifiedAt: Instant = this.modifiedAt, + remark: String? = this.remark, + certificateData: CertificateDataEntity? = this.certificateData, + requestBytes: ByteArray = this.requestBytes + ): CertificateSigningRequestEntity { + return CertificateSigningRequestEntity( + requestId = requestId, + legalName = legalName, + status = status, + modifiedAt = modifiedAt, + modifiedBy = modifiedBy, + remark = remark, + certificateData = certificateData, + requestBytes = requestBytes + ) + } + + private fun request() = PKCS10CertificationRequest(requestBytes) +} + +@Entity +@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) +class CertificateDataEntity( + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + val id: Long? = null, + + @Column(name = "public_key_hash", length = 64) + val publicKeyHash: String, + + @Column(name = "certificate_status") + val certificateStatus: CertificateStatus, + + @Lob + @Column(name = "certificate_path_bytes") + val certificatePathBytes: ByteArray, + + @OneToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "certificate_signing_request") + val certificateSigningRequest: CertificateSigningRequestEntity +) { + fun toCertificateData(): CertificateData { + return CertificateData( + publicKeyHash = publicKeyHash, + certStatus = certificateStatus, + certPath = toCertificatePath() + ) + } + + private fun toCertificatePath(): CertPath = CertificateFactory.getInstance("X.509").generateCertPath(certificatePathBytes.inputStream()) +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt new file mode 100644 index 0000000000..19ab90f2b7 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt @@ -0,0 +1,38 @@ +package com.r3.corda.networkmanage.common.persistence.entity + +import com.r3.corda.networkmanage.common.signer.SignatureAndCertPath +import net.corda.core.crypto.DigitalSignature +import net.corda.core.serialization.deserialize +import javax.persistence.* + +@Entity +@Table(name = "network_map") +class NetworkMapEntity( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + val version: Long? = null, + + // Reverting relation ownership due to (potentially) unlimited number of node info items. + @OneToMany(mappedBy = "networkMap", fetch = FetchType.LAZY) + val nodeInfoList: List = mutableListOf(), + + @OneToOne + @JoinColumn(name = "network_parameters") + val parameters: NetworkParametersEntity, + + @Lob + @Column(name = "signature_bytes") + val signatureBytes: ByteArray, + + @Lob + @Column(name = "certificate_path_bytes") + val certificatePathBytes: ByteArray +) { + /** + * Deserializes NetworkMapEntity.signatureBytes into the [SignatureAndCertPath] instance + */ + fun signatureAndCertificate(): SignatureAndCertPath? { + return SignatureAndCertPath(DigitalSignature(signatureBytes), certificatePathBytes.deserialize()) + } + +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt new file mode 100644 index 0000000000..281c0b0121 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt @@ -0,0 +1,22 @@ +package com.r3.corda.networkmanage.common.persistence.entity + +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.deserialize +import javax.persistence.* + +@Entity +@Table(name = "network_parameters") +class NetworkParametersEntity( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + val version: Long? = null, + + @Column(name = "hash", length = 64, unique = true) + val parametersHash: String, + + @Lob + @Column(name = "bytes") + val parametersBytes: ByteArray +) { + fun networkParameters(): NetworkParameters = parametersBytes.deserialize() +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NodeInfoEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NodeInfoEntity.kt new file mode 100644 index 0000000000..3161539202 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NodeInfoEntity.kt @@ -0,0 +1,77 @@ +package com.r3.corda.networkmanage.common.persistence.entity + +import net.corda.core.crypto.DigitalSignature +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.deserialize +import java.security.KeyFactory +import java.security.spec.X509EncodedKeySpec +import javax.persistence.* + +@Entity +@Table(name = "node_info") +class NodeInfoEntity( + @Id + @Column(name = "node_info_hash", length = 64) + val nodeInfoHash: String = "", + + @OneToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "certificate_signing_request", nullable = true) + val certificateSigningRequest: CertificateSigningRequestEntity? = null, + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "network_map", nullable = true) + val networkMap: NetworkMapEntity? = null, + + @Lob + @Column(name = "node_info_bytes") + val nodeInfoBytes: ByteArray, + + @Lob + @Column(name = "signature_bytes", nullable = true) + val signatureBytes: ByteArray? = null, + + @Lob + @Column(name = "signature_public_key_bytes", nullable = true) + val signaturePublicKeyBytes: ByteArray? = null, + + @Lob + @Column(name = "signature_public_key_algorithm", nullable = true) + val signaturePublicKeyAlgorithm: String? = null +) { + /** + * Deserializes NodeInfoEntity.nodeInfoBytes into the [NodeInfo] instance + */ + fun nodeInfo() = nodeInfoBytes.deserialize() + + /** + * Deserializes NodeInfoEntity.signatureBytes into the [DigitalSignature.WithKey] instance together with its public key + */ + fun signature(): DigitalSignature.WithKey? { + return if (signatureBytes != null && signaturePublicKeyBytes != null && !signaturePublicKeyAlgorithm.isNullOrEmpty()) { + DigitalSignature.WithKey(KeyFactory.getInstance(signaturePublicKeyAlgorithm).generatePublic(X509EncodedKeySpec(signaturePublicKeyBytes)), signatureBytes) + } else { + null + } + } + + fun copy(nodeInfoHash: String = this.nodeInfoHash, + networkMap: NetworkMapEntity? = this.networkMap, + certificateSigningRequest: CertificateSigningRequestEntity? = this.certificateSigningRequest, + nodeInfoBytes: ByteArray = this.nodeInfoBytes, + signatureBytes: ByteArray? = this.signatureBytes, + signaturePublicKeyBytes: ByteArray? = this.signaturePublicKeyBytes, + signaturePublicKeyAlgorithm: String? = this.signaturePublicKeyAlgorithm + ): NodeInfoEntity { + return NodeInfoEntity( + nodeInfoHash = nodeInfoHash, + certificateSigningRequest = certificateSigningRequest, + nodeInfoBytes = nodeInfoBytes, + signatureBytes = signatureBytes, + signaturePublicKeyBytes = signaturePublicKeyBytes, + signaturePublicKeyAlgorithm = signaturePublicKeyAlgorithm, + networkMap = networkMap + ) + } +} + + diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt new file mode 100644 index 0000000000..793ebaab5a --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt @@ -0,0 +1,45 @@ +package com.r3.corda.networkmanage.common.signer + +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.serialize + +/** + * Encapsulates the network map signing procedure. + * To sign a network map following steps need to be done: + * 1) Collect all node info data that has been signed and has valid certificates + * 2) Retrieve most up-to-date network parameters + * 3) Sign hashed version of the network map + * 4) Persist network map data together with its signature + * Once the network map is signed it is considered to be the current network map. + * + * This class resides in the common package as it is intended to be used in both local and distributed deployments. + * This means that it can be executed by a remote (e.g. HSM) signing service or locally by Doorman. + */ +@CordaSerializable +data class NetworkMap(val nodeInfoHashes: List, val parametersHash: String) + +@CordaSerializable +data class SignedNetworkMap(val networkMap: NetworkMap, val signatureData: SignatureAndCertPath) + +class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, + private val signer: Signer) { + /** + * Signs the network map. + */ + fun signNetworkMap() { + val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() + val currentNetworkMapValidNodeInfo = networkMapStorage.getCurrentNetworkMapNodeInfoHashes(listOf(CertificateStatus.VALID)) + val detachedValidNodeInfo = networkMapStorage.getDetachedSignedAndValidNodeInfoHashes() + val nodeInfoHashes = currentNetworkMapValidNodeInfo + detachedValidNodeInfo + val networkParameters = networkMapStorage.getLatestNetworkParameters() + val networkMap = NetworkMap(nodeInfoHashes.map { it.toString() }, networkParameters.serialize().hash.toString()) + if (currentSignedNetworkMap == null || networkMap != currentSignedNetworkMap.networkMap) { + val digitalSignature = signer.sign(networkMap.serialize().bytes) + require(digitalSignature != null) { "Error while signing network map." } + val signedHashedNetworkMap = SignedNetworkMap(networkMap, digitalSignature!!) + networkMapStorage.saveNetworkMap(signedHashedNetworkMap) + } + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/Signer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/Signer.kt new file mode 100644 index 0000000000..ee47e8825f --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/Signer.kt @@ -0,0 +1,20 @@ +package com.r3.corda.networkmanage.common.signer + +import net.corda.core.crypto.DigitalSignature +import net.corda.core.serialization.CordaSerializable +import java.security.cert.CertPath + +@CordaSerializable +data class SignatureAndCertPath(val signature: DigitalSignature, val certPath: CertPath) + +/** + * An interface for arbitrary data signing functionality. + */ +interface Signer { + + /** + * Signs given [data]. The signing key selction strategy is left to the implementing class. + * @return [SignatureAndCertPath] that encapsulates the signature and the certificate path used in the signing process. + */ + fun sign(data: ByteArray): SignatureAndCertPath? +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt index 73defb601c..7ef2ca08b7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt @@ -4,13 +4,21 @@ 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 import java.security.cert.X509Certificate +// TODO: replace this with Crypto.hash when its available. +/** + * Returns SHA256 hash of this public key + */ +fun PublicKey.hashString() = encoded.sha256().toString() + fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config { val parser = OptionParser() val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp(); diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt index 70bd9d5464..9d61b18679 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt @@ -1,12 +1,16 @@ package com.r3.corda.networkmanage.doorman import com.r3.corda.networkmanage.common.utils.toConfigWithOptions +import com.r3.corda.networkmanage.doorman.DoormanParameters.Companion.DEFAULT_APPROVE_INTERVAL +import com.r3.corda.networkmanage.doorman.DoormanParameters.Companion.DEFAULT_SIGN_INTERVAL import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.core.internal.div +import net.corda.core.utilities.seconds import net.corda.nodeapi.config.parseAs import java.nio.file.Path import java.nio.file.Paths +import java.time.Duration import java.util.* data class DoormanParameters(val basedir: Path, @@ -23,6 +27,9 @@ data class DoormanParameters(val basedir: Path, val jiraConfig: JiraConfig? = null, val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks", val rootStorePath: Path? = null, // basedir / "certificates" / "rootCAKeystore.jks" + // TODO Change these to Duration in the future + val approveInterval: Long = DEFAULT_APPROVE_INTERVAL, + val signInterval: Long = DEFAULT_SIGN_INTERVAL, val initialNetworkParameters: Path ) { enum class Mode { @@ -36,6 +43,11 @@ data class DoormanParameters(val basedir: Path, val password: String, val doneTransitionCode: Int ) + + companion object { + val DEFAULT_APPROVE_INTERVAL = 5L // seconds + val DEFAULT_SIGN_INTERVAL = 5L // seconds + } } fun parseParameters(vararg args: String): DoormanParameters { @@ -51,6 +63,8 @@ fun parseParameters(vararg args: String): DoormanParameters { accepts("rootPrivateKeyPassword", "Root private key password.").withRequiredArg().describedAs("password") accepts("host", "Doorman web service host override").withRequiredArg().describedAs("hostname") accepts("port", "Doorman web service port override").withRequiredArg().ofType(Int::class.java).describedAs("port number") + accepts("approveInterval", "Time interval (in seconds) in which CSRs are approved (default: ${DEFAULT_APPROVE_INTERVAL})").withRequiredArg().ofType(Long::class.java).defaultsTo(DEFAULT_APPROVE_INTERVAL) + accepts("signInterval", "Time interval (in seconds) in which network map is signed (default: ${DEFAULT_SIGN_INTERVAL})").withRequiredArg().ofType(Long::class.java).defaultsTo(DEFAULT_SIGN_INTERVAL) accepts("initialNetworkParameters", "initial network parameters filepath").withRequiredArg().describedAs("The initial network map").describedAs("filepath") } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index 5ac338a81b..0918705dd9 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -1,16 +1,14 @@ package com.r3.corda.networkmanage.doorman import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.* import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE -import com.r3.corda.networkmanage.common.persistence.DBCertificateRequestStorage -import com.r3.corda.networkmanage.common.persistence.PersistenceNodeInfoStorage -import com.r3.corda.networkmanage.common.persistence.SchemaService +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.doorman.DoormanServer.Companion.logger import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler -import com.r3.corda.networkmanage.doorman.signer.Signer +import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto @@ -19,7 +17,6 @@ import net.corda.core.internal.createDirectories import net.corda.core.node.NetworkParameters import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.seconds import net.corda.node.utilities.* import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.eclipse.jetty.server.Server @@ -34,6 +31,8 @@ import java.net.InetSocketAddress import java.net.URI import java.nio.file.Path import java.time.Instant +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -161,16 +160,18 @@ fun startDoorman(hostAndPort: NetworkHostAndPort, database: CordaPersistence, approveAll: Boolean, initialNetworkMapParameters: NetworkParameters, - signer: Signer? = null, + signer: LocalSigner? = null, + approveInterval: Long, + signInterval: Long, jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer { logger.info("Starting Doorman server.") val requestService = if (approveAll) { logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") - ApproveAllCertificateRequestStorage(DBCertificateRequestStorage(database)) + ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database)) } else { - DBCertificateRequestStorage(database) + PersistentCertificateRequestStorage(database) } val requestProcessor = if (jiraConfig != null) { @@ -180,29 +181,46 @@ fun startDoorman(hostAndPort: NetworkHostAndPort, } else { DefaultCsrHandler(requestService, signer) } + val networkMapStorage = PersistentNetworkMapStorage(database) + val nodeInfoStorage = PersistentNodeInfoStorage(database) - val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), - NodeInfoWebService(PersistenceNodeInfoStorage(database), initialNetworkMapParameters)) + val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(nodeInfoStorage, networkMapStorage, signer)) doorman.start() + val networkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null + // 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) - } + val scheduledExecutor = Executors.newScheduledThreadPool(2) + val approvalThread = Runnable { + try { + 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() }) + scheduledExecutor.scheduleAtFixedRate(approvalThread, approveInterval, approveInterval, TimeUnit.SECONDS) + // Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed). + if (networkMapSigner != null) { + val signingThread = Runnable { + try { + networkMapSigner.signNetworkMap() + } catch (e: Exception) { + // Log the error and carry on. + DoormanServer.logger.error("Error encountered when processing node info changes.", e) + } + } + scheduledExecutor.scheduleAtFixedRate(signingThread, signInterval, signInterval, TimeUnit.SECONDS) + } + Runtime.getRuntime().addShutdownHook(thread(start = false) { + scheduledExecutor.shutdown() + doorman.close() + }) return doorman } -private fun buildLocalSigner(parameters: DoormanParameters): Signer? { +private fun buildLocalSigner(parameters: DoormanParameters): LocalSigner? { return parameters.keystorePath?.let { // Get password from console if not in config. val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") @@ -210,7 +228,7 @@ private fun buildLocalSigner(parameters: DoormanParameters): Signer? { 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) + LocalSigner(caKeyPair, caCertPath) } } @@ -218,8 +236,8 @@ private fun buildLocalSigner(parameters: DoormanParameters): Signer? { * 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) + override fun saveRequest(request: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(request) approveRequest(requestId, DOORMAN_SIGNATURE) return requestId } @@ -246,7 +264,7 @@ fun main(args: Array) { val signer = buildLocalSigner(this) val networkParameters = parseNetworkParametersFrom(initialNetworkParameters) - startDoorman(NetworkHostAndPort(host, port), database, approveAll, networkParameters, signer, jiraConfig) + startDoorman(NetworkHostAndPort(host, port), database, approveAll, networkParameters, signer, approveInterval, signInterval, jiraConfig) } } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt index 4c899d7752..3b2c59a06c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt @@ -4,7 +4,6 @@ import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE import com.r3.corda.networkmanage.common.persistence.RequestStatus -import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.doorman.JiraClient import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -14,15 +13,15 @@ interface CsrHandler { fun getResponse(requestId: String): CertificateResponse } -class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: Signer?) : CsrHandler { +class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler { override fun processApprovedRequests() { storage.getRequests(RequestStatus.Approved) - .forEach { processRequest(it.requestId, PKCS10CertificationRequest(it.request)) } + .forEach { processRequest(it.requestId, it.request) } } private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { if (signer != null) { - val certs = signer.sign(request) + val certs = signer.createSignedClientCertificate(request) // Since Doorman is deployed in the auto-signing mode (i.e. signer != null), // we use DOORMAN_SIGNATURE as the signer. storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE)) @@ -38,7 +37,7 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat return when (response?.status) { RequestStatus.New, RequestStatus.Approved, null -> CertificateResponse.NotReady RequestStatus.Rejected -> CertificateResponse.Unauthorised(response.remark ?: "Unknown reason") - RequestStatus.Signed -> CertificateResponse.Ready(buildCertPath(response.certificateData?.certificatePath ?: throw IllegalArgumentException("Certificate should not be null."))) + RequestStatus.Signed -> CertificateResponse.Ready(response.certData?.certPath?: throw IllegalArgumentException("Certificate should not be null.")) } } } @@ -57,7 +56,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce 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) } + it.certData?.certPath.let { certs -> it.requestId to certs!! } }.toMap() jiraClient.updateSignedRequests(signedRequests) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/Signer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt similarity index 69% rename from network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/Signer.kt rename to network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt index 8206f017bd..3e9d1b0a89 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/Signer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt @@ -1,7 +1,10 @@ package com.r3.corda.networkmanage.doorman.signer +import com.r3.corda.networkmanage.common.signer.SignatureAndCertPath +import com.r3.corda.networkmanage.common.signer.Signer import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate +import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.x500Name @@ -17,17 +20,19 @@ import java.security.cert.CertPath import java.security.cert.Certificate /** - * The [Signer] class signs [PKCS10CertificationRequest] using provided CA keypair and certificate path. + * The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair 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 { +class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array) : Signer { + fun createSignedClientCertificate(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 nameConstraints = NameConstraints( + arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), + arrayOf()) val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, caCertPath.first().toX509CertHolder(), caKeyPair, @@ -36,4 +41,8 @@ class Signer(private val caKeyPair: KeyPair, private val caCertPath: Array) -> Unit = { - val signer = HsmSigner( - storage, - certificateName, - privateKeyPass, + val csrStorage = DBSignedCertificateRequestStorage(database) + val networkMapStorage = PersistentNetworkMapStorage(database) + val nodeInfoStorage = PersistentNodeInfoStorage(database) + val hsmNetworkMapSigningThread = HsmNetworkMapSigner( + networkMapStorage, + nodeInfoStorage, + networkMapCertificateName, + networkMapPrivateKeyPass, + keyStorePass, + Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePass, signAuthThreshold)).start() + val sign: (List) -> Unit = { + val signer = HsmCsrSigner( + csrStorage, + csrCertificateName, + csrPrivateKeyPass, rootCertificateName, validDays, keyStorePass, - Authenticator(provider, authMode, autoUsername, authKeyFilePath, authKeyFilePass, signAuthThreshold)) + Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePass, signAuthThreshold)) signer.sign(it) } Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", { if (confirmedKeyGen()) { val generator = KeyCertificateGenerator( - Authenticator(provider, authMode, autoUsername, authKeyFilePath, authKeyFilePass, keyGenAuthThreshold), + Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePass, keyGenAuthThreshold), keySpecifier, keyGroup) - generator.generateAllCertificates(keyStorePass, certificateName, privateKeyPass, rootCertificateName, rootPrivateKeyPass, validDays) + generator.generateAllCertificates( + keyStorePass, + listOf(CertificateNameAndPass(csrCertificateName, csrPrivateKeyPass), CertificateNameAndPass(networkMapCertificateName, networkMapPrivateKeyPass)), + rootCertificateName, + rootPrivateKeyPass, + validDays) } }).addItem("2", "Sign all approved and unsigned CSRs", { - val approved = storage.getApprovedRequests() + val approved = csrStorage.getApprovedRequests() if (approved.isNotEmpty()) { if (confirmedSign(approved)) { sign(approved) @@ -57,7 +74,7 @@ fun run(parameters: Parameters) { println("There is no approved CSR") } }).addItem("3", "List all approved and unsigned CSRs", { - val approved = storage.getApprovedRequests() + val approved = csrStorage.getApprovedRequests() if (approved.isNotEmpty()) { println("Approved CSRs:") approved.forEachIndexed { index, item -> println("${index + 1}. ${item.request.subject}") } @@ -77,6 +94,7 @@ fun run(parameters: Parameters) { println("There is no approved and unsigned CSR") } }).showMenu() + hsmNetworkMapSigningThread.stop() } } @@ -85,7 +103,7 @@ private fun processError(exception: Exception) { println("An error occured: ${processed.message}") } -private fun confirmedSign(selectedItems: List): Boolean { +private fun confirmedSign(selectedItems: List): Boolean { println("Are you sure you want to sign the following requests:") selectedItems.forEachIndexed { index, data -> println("${index + 1} ${data.request.subject}") @@ -102,7 +120,7 @@ private fun confirmedKeyGen(): Boolean { return result } -private fun getSelection(toSelect: List): List { +private fun getSelection(toSelect: List): List { print("CSRs to be signed (comma separated list): ") val line = readLine() if (line == null) { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/configuration/Configuration.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/configuration/Configuration.kt index e1f4ac78fd..7911c89fc2 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/configuration/Configuration.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/configuration/Configuration.kt @@ -2,6 +2,16 @@ package com.r3.corda.networkmanage.hsm.configuration import com.r3.corda.networkmanage.common.utils.toConfigWithOptions import com.r3.corda.networkmanage.hsm.authentication.AuthMode +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_AUTH_MODE +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_DEVICE +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_KEY_GEN_AUTH_THRESHOLD +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_KEY_GROUP +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_KEY_SPECIFIER +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_ROOT_CERTIFICATE_NAME +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_SIGN_AUTH_THRESHOLD +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_SIGN_INTERVAL +import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_VALID_DAYS import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.core.internal.div @@ -19,11 +29,14 @@ data class Parameters(val basedir: Path = Paths.get("."), val databaseProperties: Properties? = null, val device: String = DEFAULT_DEVICE, val keyStorePass: String? = null, + // TODO this needs cleaning up after the config-file-only support is implemented val keyGroup: String = DEFAULT_KEY_GROUP, val keySpecifier: Int = DEFAULT_KEY_SPECIFIER, - val rootPrivateKeyPass: String = "", - val privateKeyPass: String = "", - val certificateName: String = DEFAULT_CERTIFICATE_NAME, + val rootPrivateKeyPass: String = DEFAULT_ROOT_PRIVATE_KEY, + val csrPrivateKeyPass: String = DEFAULT_CSR_PRIVATE_KEY, + val csrCertificateName: String = DEFAULT_CSR_CERTIFICATE_NAME, + val networkMapCertificateName: String = DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, + val networkMapPrivateKeyPass: String = DEFAULT_NETWORK_MAP_PRIVATE_KEY_PASS, val rootCertificateName: String = DEFAULT_ROOT_CERTIFICATE_NAME, val validDays: Int = DEFAULT_VALID_DAYS, val signAuthThreshold: Int = DEFAULT_SIGN_AUTH_THRESHOLD, @@ -31,13 +44,15 @@ data class Parameters(val basedir: Path = Paths.get("."), val authMode: AuthMode = DEFAULT_AUTH_MODE, val authKeyFilePath: Path? = DEFAULT_KEY_FILE_PATH, val authKeyFilePass: String? = DEFAULT_KEY_FILE_PASS, - val autoUsername: String? = DEFAULT_AUTO_USERNAME) { + val autoUsername: String? = DEFAULT_AUTO_USERNAME, + // TODO Change this to Duration in the future + val signInterval: Long = DEFAULT_SIGN_INTERVAL) { companion object { val DEFAULT_DEVICE = "3001@127.0.0.1" val DEFAULT_AUTH_MODE = AuthMode.PASSWORD val DEFAULT_SIGN_AUTH_THRESHOLD = 2 val DEFAULT_KEY_GEN_AUTH_THRESHOLD = 2 - val DEFAULT_CERTIFICATE_NAME = X509Utilities.CORDA_INTERMEDIATE_CA + val DEFAULT_CSR_CERTIFICATE_NAME = X509Utilities.CORDA_INTERMEDIATE_CA val DEFAULT_ROOT_CERTIFICATE_NAME = X509Utilities.CORDA_ROOT_CA val DEFAULT_VALID_DAYS = 3650 val DEFAULT_KEY_GROUP = "DEV.DOORMAN" @@ -45,6 +60,11 @@ data class Parameters(val basedir: Path = Paths.get("."), val DEFAULT_KEY_FILE_PATH: Path? = null //Paths.get("/Users/michalkit/WinDev1706Eval/Shared/TEST4.key") val DEFAULT_KEY_FILE_PASS: String? = null val DEFAULT_AUTO_USERNAME: String? = null + val DEFAULT_CSR_PRIVATE_KEY = "" + val DEFAULT_ROOT_PRIVATE_KEY = "" + val DEFAULT_NETWORK_MAP_CERTIFICATE_NAME = "cordaintermediateca_nm" + val DEFAULT_NETWORK_MAP_PRIVATE_KEY_PASS = "" + val DEFAULT_SIGN_INTERVAL = 600L // in seconds (10 minutes) } } @@ -57,21 +77,22 @@ fun parseParameters(vararg args: String): Parameters { val argConfig = args.toConfigWithOptions { accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().defaultsTo(".").describedAs("filepath") accepts("configFile", "Overriding configuration file. (default: <>/node.conf)").withRequiredArg().describedAs("filepath") - accepts("device", "CryptoServer device address (default: ${Parameters.DEFAULT_DEVICE})").withRequiredArg() + accepts("device", "CryptoServer device address (default: $DEFAULT_DEVICE)").withRequiredArg() accepts("keyStorePass", "Password for the key store").withRequiredArg().describedAs("password") - accepts("keyGroup", "CryptoServer key group (default: ${Parameters.DEFAULT_KEY_GROUP})").withRequiredArg().defaultsTo(Parameters.DEFAULT_KEY_GROUP) - accepts("keySpecifier", "CryptoServer key specifier (default: ${Parameters.DEFAULT_KEY_SPECIFIER})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_KEY_SPECIFIER) + accepts("keyGroup", "CryptoServer key group (default: $DEFAULT_KEY_GROUP)").withRequiredArg().defaultsTo(DEFAULT_KEY_GROUP) + accepts("keySpecifier", "CryptoServer key specifier (default: $DEFAULT_KEY_SPECIFIER)").withRequiredArg().ofType(Int::class.java).defaultsTo(DEFAULT_KEY_SPECIFIER) accepts("rootPrivateKeyPass", "Password for the root certificate private key").withRequiredArg().describedAs("password") - accepts("privateKeyPass", "Password for the certificate private key").withRequiredArg().describedAs("password") - accepts("keyGenAuthThreshold", "Authentication strength threshold for the HSM key generation (default: ${Parameters.DEFAULT_KEY_GEN_AUTH_THRESHOLD})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_KEY_GEN_AUTH_THRESHOLD) - accepts("signAuthThreshold", "Authentication strength threshold for the HSM CSR signing (default: ${Parameters.DEFAULT_SIGN_AUTH_THRESHOLD})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_SIGN_AUTH_THRESHOLD) - accepts("authMode", "Authentication mode. Allowed values: ${AuthMode.values()} (default: ${Parameters.DEFAULT_AUTH_MODE} )").withRequiredArg().defaultsTo(Parameters.DEFAULT_AUTH_MODE.name) + accepts("csrPrivateKeyPass", "Password for the CSR signing certificate private key").withRequiredArg().describedAs("password") + accepts("keyGenAuthThreshold", "Authentication strength threshold for the HSM key generation (default: $DEFAULT_KEY_GEN_AUTH_THRESHOLD)").withRequiredArg().ofType(Int::class.java).defaultsTo(DEFAULT_KEY_GEN_AUTH_THRESHOLD) + accepts("signAuthThreshold", "Authentication strength threshold for the HSM CSR signing (default: $DEFAULT_SIGN_AUTH_THRESHOLD)").withRequiredArg().ofType(Int::class.java).defaultsTo(DEFAULT_SIGN_AUTH_THRESHOLD) + accepts("authMode", "Authentication mode. Allowed values: ${AuthMode.values()} (default: $DEFAULT_AUTH_MODE)").withRequiredArg().defaultsTo(DEFAULT_AUTH_MODE.name) accepts("authKeyFilePath", "Key file path when authentication is based on a key file (i.e. authMode=${AuthMode.KEY_FILE.name})").withRequiredArg().describedAs("filepath") accepts("authKeyFilePass", "Key file password when authentication is based on a key file (i.e. authMode=${AuthMode.KEY_FILE.name})").withRequiredArg() accepts("autoUsername", "Username to be used for certificate signing (if not specified it will be prompted for input)").withRequiredArg() - accepts("certificateName", "Name of the certificate to be used by this CA (default: ${Parameters.DEFAULT_CERTIFICATE_NAME})").withRequiredArg().defaultsTo(Parameters.DEFAULT_CERTIFICATE_NAME) - accepts("rootCertificateName", "Name of the root certificate to be used by this CA (default: ${Parameters.DEFAULT_ROOT_CERTIFICATE_NAME})").withRequiredArg().defaultsTo(Parameters.DEFAULT_ROOT_CERTIFICATE_NAME) - accepts("validDays", "Validity duration in days (default: ${Parameters.DEFAULT_VALID_DAYS})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_VALID_DAYS) + accepts("csrCertificateName", "Name of the certificate to be used by this CA to sign CSR (default: $DEFAULT_CSR_CERTIFICATE_NAME)").withRequiredArg().defaultsTo(DEFAULT_CSR_CERTIFICATE_NAME) + accepts("rootCertificateName", "Name of the root certificate to be used by this CA (default: $DEFAULT_ROOT_CERTIFICATE_NAME)").withRequiredArg().defaultsTo(DEFAULT_ROOT_CERTIFICATE_NAME) + accepts("validDays", "Validity duration in days (default: $DEFAULT_VALID_DAYS)").withRequiredArg().ofType(Int::class.java).defaultsTo(DEFAULT_VALID_DAYS) + accepts("signInterval", "Time interval (in seconds) in which network map is signed (default: $DEFAULT_SIGN_INTERVAL)").withRequiredArg().ofType(Long::class.java).defaultsTo(DEFAULT_SIGN_INTERVAL) } val configFile = if (argConfig.hasPath("configFile")) { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/KeyCertificateGenerator.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/KeyCertificateGenerator.kt index b5566890c5..25dac32009 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/KeyCertificateGenerator.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/KeyCertificateGenerator.kt @@ -13,14 +13,14 @@ import java.security.KeyPair import java.security.KeyStore import java.security.PrivateKey +data class CertificateNameAndPass(val certificateName: String, val privateKeyPassword: String) + /** * Encapsulates logic for root and intermediate key/certificate generation. */ class KeyCertificateGenerator(private val authenticator: Authenticator, private val keySpecifier: Int, private val keyGroup: String) { - - /** * Generates root and intermediate key and certificates and stores them in the key store given by provider. * If the keys and certificates already exists they will be overwritten. @@ -32,15 +32,16 @@ class KeyCertificateGenerator(private val authenticator: Authenticator, * @param validDays days of certificate validity */ fun generateAllCertificates(keyStorePassword: String?, - certificateKeyName: String, - privateKeyPassword: String, + intermediateCertificatesCredentials: List, parentCertificateName: String, parentPrivateKeyPassword: String, validDays: Int) { - authenticator.connectAndAuthenticate { provider, signers -> + authenticator.connectAndAuthenticate { provider, _ -> val keyStore = getAndInitializeKeyStore(provider, keyStorePassword) generateRootCertificate(provider, keyStore, parentCertificateName, parentPrivateKeyPassword, validDays) - generateIntermediateCertificate(provider, keyStore, certificateKeyName, privateKeyPassword, parentCertificateName, parentPrivateKeyPassword, validDays) + intermediateCertificatesCredentials.forEach { + generateIntermediateCertificate(provider, keyStore, it.certificateName, it.privateKeyPassword, parentCertificateName, parentPrivateKeyPassword, validDays) + } } } @@ -71,7 +72,7 @@ class KeyCertificateGenerator(private val authenticator: Authenticator, val parentCACertKey = retrieveCertificateAndKeys(parentCertificateName, parentPrivateKeyPassword, keyStore) val keyPair = generateEcdsaKeyPair(provider, keyStore, certificateKeyName, privateKeyPassword) val intermediateCertificate = createIntermediateCert("R3 Intermediate", parentCACertKey, keyPair, validDays, provider) - keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(intermediateCertificate.certificate)) + keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(intermediateCertificate.certificate, parentCACertKey.certificate)) println("New certificate and key pair named $certificateKeyName have been generated") } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/DBSignedCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/DBSignedCertificateRequestStorage.kt index 6fde36cb5b..49382f760e 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/DBSignedCertificateRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/DBSignedCertificateRequestStorage.kt @@ -1,27 +1,27 @@ package com.r3.corda.networkmanage.hsm.persistence import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest -import com.r3.corda.networkmanage.common.persistence.DBCertificateRequestStorage +import com.r3.corda.networkmanage.common.persistence.PersistentCertificateRequestStorage import com.r3.corda.networkmanage.common.persistence.RequestStatus import net.corda.node.utilities.CordaPersistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.CertPath -data class CertificateRequestData(val requestId: String, val request: PKCS10CertificationRequest, var certPath: CertPath? = null) +data class ApprovedCertificateRequestData(val requestId: String, val request: PKCS10CertificationRequest, var certPath: CertPath? = null) class DBSignedCertificateRequestStorage(database: CordaPersistence) : SignedCertificateRequestStorage { - private val storage = DBCertificateRequestStorage(database) + private val storage = PersistentCertificateRequestStorage(database) - override fun store(requests: List, signers: List) { + override fun store(requests: List, signers: List) { for ((requestId, _, certPath) in requests) { storage.putCertificatePath(requestId, certPath!!, signers) } } - override fun getApprovedRequests(): List { + override fun getApprovedRequests(): List { return storage.getRequests(RequestStatus.Approved).map { it.toRequestData() } } - private fun CertificateSigningRequest.toRequestData() = CertificateRequestData(requestId, PKCS10CertificationRequest(request)) + private fun CertificateSigningRequest.toRequestData() = ApprovedCertificateRequestData(requestId, request) } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/SignedCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/SignedCertificateRequestStorage.kt index 9b329712c7..ba74e8865c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/SignedCertificateRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/persistence/SignedCertificateRequestStorage.kt @@ -8,7 +8,7 @@ interface SignedCertificateRequestStorage { /** * Returns all certificate signing requests that have been approved for signing. */ - fun getApprovedRequests(): List + fun getApprovedRequests(): List /** * Marks the database CSR entries as signed. Also it persists the certificate and the signature in the database. @@ -16,5 +16,5 @@ interface SignedCertificateRequestStorage { * @param requests Signed requests that are to be stored. * @param signers List of user names that signed those requests. To be specific, each request has been signed by all of those users. */ - fun store(requests: List, signers: List) + fun store(requests: List, signers: List) } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/CertificateSigningRequestSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/CertificateSigningRequestSigner.kt new file mode 100644 index 0000000000..fa96e09926 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/CertificateSigningRequestSigner.kt @@ -0,0 +1,16 @@ +package com.r3.corda.networkmanage.hsm.signer + +import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData + +/** + * Encapsulates the logic related to the certificate signing process. + */ +interface CertificateSigningRequestSigner { + + /** + * Signs the provided list of [ApprovedCertificateRequestData] with the key/certificate chosen + * by the implementing class. + */ + fun sign(toSign: List) + +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt similarity index 78% rename from network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmSigner.kt rename to network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt index a48a4cd450..becc8613d6 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt @@ -2,7 +2,7 @@ package com.r3.corda.networkmanage.hsm.signer import com.r3.corda.networkmanage.hsm.authentication.Authenticator import com.r3.corda.networkmanage.hsm.authentication.readPassword -import com.r3.corda.networkmanage.hsm.persistence.CertificateRequestData +import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage import com.r3.corda.networkmanage.hsm.utils.X509Utilities.buildCertPath import com.r3.corda.networkmanage.hsm.utils.X509Utilities.createClientCertificate @@ -12,24 +12,25 @@ import com.r3.corda.networkmanage.hsm.utils.X509Utilities.retrieveCertificateAnd /** * Encapsulates certificate signing logic */ -class HsmSigner(private val storage: SignedCertificateRequestStorage, - private val caCertificateName: String, - private val caPrivateKeyPass: String?, - private val caParentCertificateName: String, - private val validDays: Int, - private val keyStorePassword: String?, - private val authenticator: Authenticator) : Signer { +class HsmCsrSigner(private val storage: SignedCertificateRequestStorage, + private val caCertificateName: String, + private val caPrivateKeyPass: String?, + private val caParentCertificateName: String, + private val validDays: Int, + private val keyStorePassword: String?, + private val authenticator: Authenticator) : CertificateSigningRequestSigner { /** * Signs the provided list of approved certificate signing requests. By signature we mean creation of the client-level certificate - * that is accompanied with a key pair (public + private) and signed by the intermediate CA using its private key. + * that is accompanied with a key pair (public + private) and signed by the intermediate CA (stored on the HSM) + * using its private key. * That key (along with the certificate) is retrieved from the key store obtained from the provider given as a result of the * connectAndAuthenticate method of the authenticator. * The method iterates through the collection of the [ApprovedCertificateRequestData] instances passed as the method parameter * and sets the certificate field with an appropriate value. * @param toSign list of approved certificates to be signed */ - override fun sign(toSign: List) { + override fun sign(toSign: List) { authenticator.connectAndAuthenticate { provider, signers -> val keyStore = getAndInitializeKeyStore(provider, keyStorePassword) // This should be changed once we allow for more certificates in the chain. Preferably we should use diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt new file mode 100644 index 0000000000..d6dd84922a --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt @@ -0,0 +1,92 @@ +package com.r3.corda.networkmanage.hsm.signer + +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage +import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner +import com.r3.corda.networkmanage.common.signer.SignatureAndCertPath +import com.r3.corda.networkmanage.common.signer.Signer +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.hsm.authentication.Authenticator +import com.r3.corda.networkmanage.hsm.utils.X509Utilities +import com.r3.corda.networkmanage.hsm.utils.X509Utilities.signData +import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.minutes +import java.security.KeyPair +import java.security.PrivateKey +import java.time.Duration +import java.util.* +import kotlin.concurrent.fixedRateTimer + +/** + * Encapsulates logic for periodic network map signing execution. + * It uses HSM as the signing entity with keys and certificates specified at the construction time. + */ +class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage, + private val nodeInfoStorage: NodeInfoStorage, + private val caCertificateKeyName: String, + private val caPrivateKeyPass: String, + private val keyStorePassword: String?, + private val authenticator: Authenticator, + private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer { + + companion object { + val log = loggerFor() + val DEFAULT_SIGNING_PERIOD_MS = 10.minutes + } + + private val networkMapSigner = NetworkMapSigner(networkMapStorage, this) + private var fixedRateTimer: Timer? = null + + fun start(): HsmNetworkMapSigner { + stop() + fixedRateTimer = fixedRateTimer( + name = "Network Map Signing Thread", + period = signingPeriod.toMillis(), + action = { + try { + signNodeInfo() + networkMapSigner.signNetworkMap() + } catch (exception: Exception) { + log.warn("Exception thrown while signing network map", exception) + } + }) + return this + } + + fun stop() { + fixedRateTimer?.cancel() + } + + private fun signNodeInfo() { + // Retrieve data + val nodeInfoBytes = nodeInfoStorage.getUnsignedNodeInfoBytes() + // Authenticate and sign + authenticator.connectAndAuthenticate { provider, _ -> + val keyStore = X509Utilities.getAndInitializeKeyStore(provider, keyStorePassword) + val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName) + val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey + for ((nodeInfoHash, bytes) in nodeInfoBytes) { + val signature = signData(bytes, KeyPair(caCertificateChain.first().publicKey, caKey), provider) + verify(bytes, signature, caCertificateChain.first().publicKey) + nodeInfoStorage.signNodeInfo(nodeInfoHash, signature) + } + } + } + + /** + * Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM. + */ + override fun sign(data: ByteArray): SignatureAndCertPath? { + var result: SignatureAndCertPath? = null + authenticator.connectAndAuthenticate { provider, _ -> + val keyStore = X509Utilities.getAndInitializeKeyStore(provider, keyStorePassword) + val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName) + val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey + val signature = signData(data, KeyPair(caCertificateChain.first().publicKey, caKey), provider) + verify(data, signature, caCertificateChain.first().publicKey) + result = SignatureAndCertPath(signature, buildCertPath(*caCertificateChain)) + } + return result + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/Signer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/Signer.kt deleted file mode 100644 index 4e9a824c6e..0000000000 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/Signer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.r3.corda.networkmanage.hsm.signer - -import com.r3.corda.networkmanage.hsm.persistence.CertificateRequestData - -/** - * Encapsulates the logic related to the certificate signing process. - */ -interface Signer { - - /** - * Signs the provided list of [CertificateRequestData] - */ - fun sign(toSign: List) - -} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt index 5e8ca48f80..0360610f34 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/X509Utils.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.hsm.utils import CryptoServerJCE.CryptoServerProvider +import net.corda.core.crypto.DigitalSignature import net.corda.core.identity.CordaX500Name import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.x500Name @@ -271,6 +272,29 @@ object X509Utilities { } } + /** + * Sign data with the given private key + */ + fun signData(data: ByteArray, + keyPair: KeyPair, + provider: Provider, + signatureAlgorithm: String = SIGNATURE_ALGORITHM): DigitalSignature.WithKey { + val signer = Signature.getInstance(signatureAlgorithm, provider) + signer.initSign(keyPair.private) + signer.update(data) + return DigitalSignature.WithKey(keyPair.public, signer.sign()) + } + + fun verify(data: ByteArray, + signature: DigitalSignature, + publicKey: PublicKey, + signatureAlgorithm: String = SIGNATURE_ALGORITHM) { + val verify = Signature.getInstance(signatureAlgorithm) + verify.initVerify(publicKey) + verify.update(data) + require(verify.verify(signature.bytes)) { "Signature didn't independently verify" } + } + /** * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key */ diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt new file mode 100644 index 0000000000..f89cd1c499 --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt @@ -0,0 +1,96 @@ +package com.r3.corda.networkmanage + +import com.nhaarman.mockito_kotlin.mock +import com.r3.corda.networkmanage.common.persistence.CertificateData +import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity +import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity +import net.corda.core.crypto.SecureHash +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.utilities.seconds +import net.corda.testing.SerializationEnvironmentRule +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.junit.Rule +import java.security.cert.CertPath +import java.time.Duration +import java.time.Instant + +abstract class TestBase { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + protected fun certificateSigningRequestEntity( + requestId: String = SecureHash.randomSHA256().toString(), + status: RequestStatus = RequestStatus.New, + legalName: String = "TestLegalName", + modifiedBy: List = emptyList(), + modifiedAt: Instant = Instant.now(), + remark: String = "Test remark", + certificateData: CertificateDataEntity? = null, + requestBytes: ByteArray = ByteArray(0) + ): CertificateSigningRequestEntity { + return CertificateSigningRequestEntity( + requestId = requestId, + status = status, + legalName = legalName, + modifiedBy = modifiedBy, + modifiedAt = modifiedAt, + remark = remark, + certificateData = certificateData, + requestBytes = requestBytes + ) + } + + protected fun certificateSigningRequest( + requestId: String = SecureHash.randomSHA256().toString(), + status: RequestStatus = RequestStatus.New, + legalName: String = "TestLegalName", + remark: String = "Test remark", + request: PKCS10CertificationRequest = mock(), + certData: CertificateData = mock(), + modifiedBy: List = emptyList() + ): CertificateSigningRequest { + return CertificateSigningRequest( + requestId = requestId, + status = status, + legalName = legalName, + remark = remark, + certData = certData, + request = request, + modifiedBy = modifiedBy + ) + } + + protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(), + certStatus: CertificateStatus = CertificateStatus.VALID, + certPath: CertPath = mock()): CertificateData { + return CertificateData( + publicKeyHash = publicKeyHash, + certStatus = certStatus, + certPath = certPath + ) + } + + // TODO remove this once testNetworkParameters are updated with default parameters + protected fun createNetworkParameters(minimumPlatformVersion: Int = 1, + notaries: List = emptyList(), + eventHorizon: Duration = 1.seconds, + maxMessageSize: Int = 0, + maxTransactionSize: Int = 0, + modifiedTime: Instant = Instant.now(), + epoch: Int = 1): NetworkParameters { + return NetworkParameters( + minimumPlatformVersion = minimumPlatformVersion, + notaries = notaries, + eventHorizon = eventHorizon, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + modifiedTime = modifiedTime, + epoch = epoch + ) + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorageTest.kt index ce35c34bf4..d371ab8ef9 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBCertificateRequestStorageTest.kt @@ -1,6 +1,8 @@ package com.r3.corda.networkmanage.common.persistence +import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE +import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import net.corda.core.crypto.Crypto @@ -22,14 +24,14 @@ import java.security.KeyPair import java.util.* import kotlin.test.* -class DBCertificateRequestStorageTest { - private lateinit var storage: DBCertificateRequestStorage +class DBCertificateRequestStorageTest : TestBase() { + private lateinit var storage: PersistentCertificateRequestStorage private lateinit var persistence: CordaPersistence @Before fun startDb() { persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { throw UnsupportedOperationException() }, SchemaService()) - storage = DBCertificateRequestStorage(persistence) + storage = PersistentCertificateRequestStorage(persistence) } @After @@ -42,7 +44,7 @@ class DBCertificateRequestStorageTest { val request = createRequest("LegalName").first val requestId = storage.saveRequest(request) assertNotNull(storage.getRequest(requestId)).apply { - assertEquals(request, PKCS10CertificationRequest(this.request)) + assertEquals(request, this.request) } assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId) } @@ -55,11 +57,9 @@ class DBCertificateRequestStorageTest { // Pending request should equals to 1. assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertNull(storage.getRequest(requestId)!!.certificateData) + assertNull(storage.getRequest(requestId)!!.certData) // Store certificate to DB. - val result = storage.approveRequest(requestId, DOORMAN_SIGNATURE) - // Check request request has been approved - assertTrue(result) + storage.approveRequest(requestId, DOORMAN_SIGNATURE) // Check request is not ready yet. // assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) // New request should be empty. @@ -72,12 +72,17 @@ class DBCertificateRequestStorageTest { val (request, _) = createRequest("LegalName") // Add request to DB. val requestId = storage.saveRequest(request) - storage.approveRequest(requestId, DOORMAN_SIGNATURE) + storage.approveRequest(requestId, "ApproverA") + var thrown: Exception? = null // When subsequent approval is performed - val result = storage.approveRequest(requestId, DOORMAN_SIGNATURE) + try { + storage.approveRequest(requestId, "ApproverB") + } catch (e: IllegalArgumentException) { + thrown = e + } // Then check request has not been approved - assertFalse(result) + assertNotNull(thrown) } @Test @@ -88,7 +93,7 @@ class DBCertificateRequestStorageTest { // New request should equals to 1. assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertNull(storage.getRequest(requestId)!!.certificateData) + assertNull(storage.getRequest(requestId)!!.certData) // Store certificate to DB. storage.approveRequest(requestId, DOORMAN_SIGNATURE) // Check request is not ready yet. @@ -105,7 +110,7 @@ class DBCertificateRequestStorageTest { buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) }, listOf(DOORMAN_SIGNATURE)) // Check request is ready - assertNotNull(storage.getRequest(requestId)!!.certificateData) + assertNotNull(storage.getRequest(requestId)!!.certData) } @Test @@ -188,21 +193,15 @@ class DBCertificateRequestStorageTest { // then persistence.transaction { val auditReader = AuditReaderFactory.get(persistence.entityManagerFactory.createEntityManager()) - val newRevision = auditReader.find(CertificateSigningRequest::class.java, requestId, 1) + val newRevision = auditReader.find(CertificateSigningRequestEntity::class.java, requestId, 1) assertEquals(RequestStatus.New, newRevision.status) assertTrue(newRevision.modifiedBy.isEmpty()) - val approvedRevision = auditReader.find(CertificateSigningRequest::class.java, requestId, 2) + val approvedRevision = auditReader.find(CertificateSigningRequestEntity::class.java, requestId, 2) assertEquals(RequestStatus.Approved, approvedRevision.status) assertEquals(approver, approvedRevision.modifiedBy.first()) } } - private fun createRequest(legalName: String): Pair { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair) - return Pair(request, keyPair) - } - private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") @@ -221,3 +220,9 @@ class DBCertificateRequestStorageTest { return props } } + +internal fun createRequest(organisation: String): Pair { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = organisation, locality = "London", country = "GB"), "my@mail.com", keyPair) + return Pair(request, keyPair) +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt new file mode 100644 index 0000000000..e25d7b63ee --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/DBNetworkMapStorageTest.kt @@ -0,0 +1,164 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.signer.NetworkMap +import com.r3.corda.networkmanage.common.signer.SignatureAndCertPath +import com.r3.corda.networkmanage.common.signer.SignedNetworkMap +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.toX509Certificate +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.sign +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.node.NotaryInfo +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +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.testing.common.internal.testNetworkParameters +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.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DBNetworkMapStorageTest : TestBase() { + private lateinit var networkMapStorage: NetworkMapStorage + private lateinit var requestStorage: CertificationRequestStorage + private lateinit var nodeInfoStorage: NodeInfoStorage + private lateinit var persistence: CordaPersistence + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + 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) + + @Before + fun startDb() { + persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { throw UnsupportedOperationException() }, SchemaService()) + networkMapStorage = PersistentNetworkMapStorage(persistence) + nodeInfoStorage = PersistentNodeInfoStorage(persistence) + requestStorage = PersistentCertificateRequestStorage(persistence) + } + + @After + fun closeDb() { + persistence.close() + } + + @Test + fun `signNetworkMap creates current network map`() { + // given + // Create node info. + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfo) + // Some random bytes + val signature = keyPair.sign(nodeInfo.serialize()) + nodeInfoStorage.signNodeInfo(nodeInfoHash, signature) + + // Create network parameters + val networkParametersHash = networkMapStorage.putNetworkParameters(testNetworkParameters(emptyList())) + + val signatureData = SignatureAndCertPath(signature, certPath) + val signedNetworkMap = SignedNetworkMap(NetworkMap(listOf(nodeInfoHash.toString()), networkParametersHash.toString()), signatureData) + + // when + networkMapStorage.saveNetworkMap(signedNetworkMap) + + // then + val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() + assertEquals(signedNetworkMap, persistedSignedNetworkMap) + } + + @Test + fun `getLatestNetworkParameters returns last inserted`() { + // given + networkMapStorage.putNetworkParameters(createNetworkParameters(minimumPlatformVersion = 1)) + networkMapStorage.putNetworkParameters(createNetworkParameters(minimumPlatformVersion = 2)) + + // when + val latest = networkMapStorage.getLatestNetworkParameters() + + // then + assertEquals(2, latest.minimumPlatformVersion) + } + + @Test + fun `getCurrentNetworkParameters returns current network map parameters`() { + // given + // Create network parameters + val networkMapParametersHash = networkMapStorage.putNetworkParameters(createNetworkParameters(1)) + // Create empty network map + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Corda", locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(intermediateCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + + // Sign network map making it current network map + val hashedNetworkMap = NetworkMap(emptyList(), networkMapParametersHash.toString()) + val signatureData = SignatureAndCertPath(keyPair.sign(hashedNetworkMap.serialize()), certPath) + val signedNetworkMap = SignedNetworkMap(hashedNetworkMap, signatureData) + networkMapStorage.saveNetworkMap(signedNetworkMap) + + // Create new network parameters + networkMapStorage.putNetworkParameters(createNetworkParameters(2)) + + // when + val result = networkMapStorage.getCurrentNetworkParameters() + + // then + assertEquals(1, result.minimumPlatformVersion) + } + + @Test + fun `getDetachedSignedAndValidNodeInfoHashes returns only valid and signed node info hashes`() { + // given + // Create node info. + val organisationA = "TestA" + val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) + requestStorage.approveRequest(requestIdA, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertA = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPair.public) + val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) + val organisationB = "TestB" + val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) + requestStorage.approveRequest(requestIdB, "TestUser") + val clientCertB = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public) + val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) + val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.companyA.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) + val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.companyB.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) + val nodeInfoHashA = nodeInfoStorage.putNodeInfo(nodeInfoA) + val nodeInfoHashB = nodeInfoStorage.putNodeInfo(nodeInfoB) + // Sign node info + nodeInfoStorage.signNodeInfo(nodeInfoHashA, keyPair.sign(nodeInfoA.serialize())) + nodeInfoStorage.signNodeInfo(nodeInfoHashB, keyPair.sign(nodeInfoB.serialize())) + + // Create network parameters + val networkParametersHash = networkMapStorage.putNetworkParameters(createNetworkParameters()) + val networkMap = NetworkMap(listOf(nodeInfoHashA.toString()), networkParametersHash.toString()) + val signatureData = SignatureAndCertPath(keyPair.sign(networkMap.serialize()), certPathA) + val signedNetworkMap = SignedNetworkMap(networkMap, signatureData) + + // Sign network map + networkMapStorage.saveNetworkMap(signedNetworkMap) + + // when + val detachedHashes = networkMapStorage.getDetachedSignedAndValidNodeInfoHashes() + + // then + assertEquals(1, detachedHashes.size) + assertTrue(detachedHashes.contains(nodeInfoHashB)) + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorageTest.kt index 515d22c240..e69de29bb2 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceNodeInfoStorageTest.kt @@ -1,139 +0,0 @@ -package com.r3.corda.networkmanage.common.persistence - -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE -import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.common.utils.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.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme -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(), { throw UnsupportedOperationException() }, SchemaService()) - 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, DOORMAN_SIGNATURE) - - assertNull(nodeInfoStorage.getCertificatePath(keyPair.public.hash())) - - requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()), listOf(DOORMAN_SIGNATURE)) - - 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/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt new file mode 100644 index 0000000000..7061e2c6fe --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt @@ -0,0 +1,180 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.common.utils.hashString +import com.r3.corda.networkmanage.common.utils.toX509Certificate +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.crypto.sign +import net.corda.core.identity.CordaX500Name +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.node.utilities.CertificateType +import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.X509Utilities +import net.corda.node.utilities.configureDatabase +import net.corda.testing.node.MockServices +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class PersitenceNodeInfoStorageTest : TestBase() { + private lateinit var requestStorage: CertificationRequestStorage + private lateinit var nodeInfoStorage: NodeInfoStorage + private lateinit var persistence: CordaPersistence + private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey) + private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + 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) + + @Before + fun startDb() { + persistence = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), { throw UnsupportedOperationException() }, SchemaService()) + nodeInfoStorage = PersistentNodeInfoStorage(persistence) + requestStorage = PersistentCertificateRequestStorage(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, CertificationRequestStorage.DOORMAN_SIGNATURE) + + assertNull(nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString()))) + + requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()), listOf(CertificationRequestStorage.DOORMAN_SIGNATURE)) + + val storedCertPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(keyPair.public.hashString())) + assertNotNull(storedCertPath) + + assertEquals(clientCert.toX509Certificate(), storedCertPath!!.certificates.first()) + } + + @Test + fun `test getNodeInfoHashes`() { + // Create node info. + val organisationA = "TestA" + val requestIdA = requestStorage.saveRequest(createRequest(organisationA).first) + requestStorage.approveRequest(requestIdA, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCertA = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationA, locality = "London", country = "GB"), keyPair.public) + val certPathA = buildCertPath(clientCertA.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdA, certPathA, emptyList()) + val organisationB = "TestB" + val requestIdB = requestStorage.saveRequest(createRequest(organisationB).first) + requestStorage.approveRequest(requestIdB, "TestUser") + val clientCertB = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisationB, locality = "London", country = "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public) + val certPathB = buildCertPath(clientCertB.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestIdB, certPathB, emptyList()) + val nodeInfoA = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) + val nodeInfoSame = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathA)), 1, serial = 1L) + val nodeInfoB = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPathB)), 1, serial = 1L) + + nodeInfoStorage.putNodeInfo(nodeInfoA) + nodeInfoStorage.putNodeInfo(nodeInfoSame) + + // getNodeInfoHashes should contain 1 hash. + assertEquals(listOf(nodeInfoA.serialize().sha256()), nodeInfoStorage.getUnsignedNodeInfoHashes()) + + nodeInfoStorage.putNodeInfo(nodeInfoB) + // getNodeInfoHashes should contain 2 hash. + assertEquals(listOf(nodeInfoB.serialize().sha256(), nodeInfoA.serialize().sha256()).sorted(), nodeInfoStorage.getUnsignedNodeInfoHashes().sorted()) + + // Test retrieve NodeInfo. + assertEquals(nodeInfoA, nodeInfoStorage.getNodeInfo(nodeInfoA.serialize().sha256())) + assertEquals(nodeInfoB, nodeInfoStorage.getNodeInfo(nodeInfoB.serialize().sha256())) + } + + @Test + fun `same pub key with different node info`() { + // Create node info. + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoSamePubKey = NodeInfo(listOf(NetworkHostAndPort("my.company2.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + + nodeInfoStorage.putNodeInfo(nodeInfo) + assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256())) + + // This should replace the node info. + nodeInfoStorage.putNodeInfo(nodeInfoSamePubKey) + // Old node info should be removed. + assertNull(nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256())) + assertEquals(nodeInfoSamePubKey, nodeInfoStorage.getNodeInfo(nodeInfoSamePubKey.serialize().sha256())) + } + + @Test + fun `signNodeInfo associates signature to with node info`() { + // given + // Create node info. + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfo) + // Some random bytes + val signature = keyPair.sign(nodeInfo.serialize()) + + // when + nodeInfoStorage.signNodeInfo(nodeInfoHash, signature) + + // then + val signedNodeInfo = nodeInfoStorage.getSignedNodeInfo(nodeInfoHash) + assertEquals(signature, signedNodeInfo?.sig) + } + + @Test + fun `getUnsignedNodeInfoBytes return node info bytes`() { + // given + // Create node info. + val organisation = "Test" + val requestId = requestStorage.saveRequest(createRequest(organisation).first) + requestStorage.approveRequest(requestId, "TestUser") + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + requestStorage.putCertificatePath(requestId, certPath, emptyList()) + + val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) + val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfo) + + // when + val nodeInfoBytes = nodeInfoStorage.getUnsignedNodeInfoBytes() + + // then + assertTrue(nodeInfoBytes.containsKey(nodeInfoHash)) + assertEquals(nodeInfo, nodeInfoBytes[nodeInfoHash]?.deserialize()!!) + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt new file mode 100644 index 0000000000..7354ecb4a9 --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt @@ -0,0 +1,74 @@ +package com.r3.corda.networkmanage.common.signer + +import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.serialization.serialize +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class NetworkMapSignerTest : TestBase() { + private lateinit var signer: Signer + private lateinit var networkMapStorage: NetworkMapStorage + private lateinit var networkMapSigner: NetworkMapSigner + + @Before + fun setUp() { + signer = mock() + networkMapStorage = mock() + networkMapSigner = NetworkMapSigner(networkMapStorage, signer) + } + + @Test + fun `signNetworkMap builds and signs network map`() { + // given + val signedNodeInfoHashes = listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256()) + val detachedNodeInfoHashes = listOf(SecureHash.randomSHA256()) + val networkMapParameters = createNetworkParameters() + whenever(networkMapStorage.getCurrentNetworkMapNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes) + whenever(networkMapStorage.getDetachedSignedAndValidNodeInfoHashes()).thenReturn(detachedNodeInfoHashes) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkMapParameters) + whenever(signer.sign(any())).thenReturn(mock()) + + // when + networkMapSigner.signNetworkMap() + + // then + // Verify networkMapStorage calls + verify(networkMapStorage).getCurrentNetworkMapNodeInfoHashes(any()) + verify(networkMapStorage).getDetachedSignedAndValidNodeInfoHashes() + verify(networkMapStorage).getLatestNetworkParameters() + argumentCaptor().apply { + verify(networkMapStorage).saveNetworkMap(capture()) + val networkMap = firstValue.networkMap + assertEquals(networkMapParameters.serialize().hash.toString(), networkMap.parametersHash) + assertEquals(signedNodeInfoHashes.size + detachedNodeInfoHashes.size, networkMap.nodeInfoHashes.size) + assertTrue(networkMap.nodeInfoHashes.containsAll(signedNodeInfoHashes.map { it.toString() })) + assertTrue(networkMap.nodeInfoHashes.containsAll(detachedNodeInfoHashes.map { it.toString() })) + } + } + + @Test + fun `signNetworkMap does NOT create a new network map if there are no changes`() { + // given + val networkMapParameters = createNetworkParameters() + val networkMapParametersHash = networkMapParameters.serialize().bytes.sha256() + val networkMap = NetworkMap(emptyList(), networkMapParametersHash.toString()) + val signedNetworkMap = SignedNetworkMap(networkMap, mock()) + whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) + whenever(networkMapStorage.getCurrentNetworkMapNodeInfoHashes(any())).thenReturn(emptyList()) + whenever(networkMapStorage.getDetachedSignedAndValidNodeInfoHashes()).thenReturn(emptyList()) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkMapParameters) + + // when + networkMapSigner.signNetworkMap() + + // then + // Verify networkMapStorage is not called + verify(networkMapStorage, never()).saveNetworkMap(any()) + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt index 4a2c8d7fb0..af65ab78e5 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt @@ -4,29 +4,33 @@ import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify -import com.r3.corda.networkmanage.common.persistence.* +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.persistence.CertificateResponse +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.RequestStatus import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler -import com.r3.corda.networkmanage.doorman.signer.Signer +import com.r3.corda.networkmanage.doorman.signer.LocalSigner 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 { +class DefaultRequestProcessorTest : TestBase() { @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, remark = "Random reason")) + on { getRequest("New") }.thenReturn(certificateSigningRequest()) + on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.Signed, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert.toX509Certificate())))) + on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.Rejected, remark = "Random reason")) } - val signer: Signer = mock() + val signer: LocalSigner = mock() val requestProcessor = DefaultCsrHandler(requestStorage, signer) assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) @@ -43,17 +47,17 @@ class DefaultRequestProcessorTest { 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) + certificateSigningRequest(requestId = "1", request = request1, status = RequestStatus.Approved), + certificateSigningRequest(requestId = "2", request = request2, status = RequestStatus.Approved), + certificateSigningRequest(requestId = "3", request = request3, status = RequestStatus.Approved) )) } - val signer: Signer = mock() + val signer: LocalSigner = mock() val requestProcessor = DefaultCsrHandler(requestStorage, signer) requestProcessor.processApprovedRequests() - verify(signer, times(3)).sign(any()) + verify(signer, times(3)).createSignedClientCertificate(any()) verify(requestStorage, times(1)).getRequests(any()) } } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt index 158e882346..ff48f35471 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt @@ -1,13 +1,15 @@ package com.r3.corda.networkmanage.doorman +import com.r3.corda.networkmanage.TestBase import com.typesafe.config.ConfigException +import net.corda.core.utilities.seconds import org.junit.Test import java.io.File import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class DoormanParametersTest { +class DoormanParametersTest : TestBase() { private val testDummyPath = ".${File.separator}testDummyPath.jks" private val validInitialNetworkConfigPath = File(javaClass.getResource("/initial-network-parameters.conf").toURI()).absolutePath private val validConfigPath = File(javaClass.getResource("/doorman.conf").toURI()).absolutePath diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt index 5338769611..4344706323 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt @@ -4,7 +4,11 @@ import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.verify +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage +import com.r3.corda.networkmanage.common.signer.NetworkMap +import com.r3.corda.networkmanage.common.signer.SignedNetworkMap import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService @@ -12,19 +16,15 @@ 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 net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.testing.common.internal.testNetworkParameters 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 @@ -34,31 +34,12 @@ import javax.ws.rs.core.MediaType import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class NodeInfoWebServiceTest { +class NodeInfoWebServiceTest : TestBase() { 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. @@ -74,8 +55,7 @@ class NodeInfoWebServiceTest { on { getCertificatePath(any()) }.thenReturn(certPath) } - DoormanServer(NetworkHostAndPort("localhost", 0), - NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use { + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use { it.start() val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register") val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes @@ -101,8 +81,7 @@ class NodeInfoWebServiceTest { on { getCertificatePath(any()) }.thenReturn(certPath) } - DoormanServer(NetworkHostAndPort("localhost", 0), - NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use { + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use { it.start() val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register") val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes @@ -116,18 +95,16 @@ class NodeInfoWebServiceTest { @Test fun `get network map`() { - val networkMapList = listOf(SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString()) - val nodeInfoStorage: NodeInfoStorage = mock { - on { getNodeInfoHashes() }.thenReturn(networkMapList) + val hashedNetworkMap = NetworkMap(listOf(SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString()), SecureHash.randomSHA256().toString()) + val networkMapStorage: NetworkMapStorage = mock { + on { getCurrentNetworkMap() }.thenReturn(SignedNetworkMap(hashedNetworkMap, mock())) } - DoormanServer(NetworkHostAndPort("localhost", 0), - NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use { + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage)).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) + val signedHashedNetworkMap = conn.inputStream.readBytes().deserialize() + verify(networkMapStorage, times(1)).getCurrentNetworkMap() + assertEquals(signedHashedNetworkMap.networkMap, hashedNetworkMap) } } @@ -138,20 +115,20 @@ class NodeInfoWebServiceTest { 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 nodeInfoHash = nodeInfo.serialize().sha256() val nodeInfoStorage: NodeInfoStorage = mock { - on { getNodeInfo(nodeInfoHash) }.thenReturn(nodeInfo) + val serializedNodeInfo = nodeInfo.serialize() + on { getSignedNodeInfo(nodeInfoHash) }.thenReturn(SignedData(serializedNodeInfo, keyPair.sign(serializedNodeInfo))) } - DoormanServer(NetworkHostAndPort("localhost", 0), - NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use { + DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).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) + val nodeInfoResponse = conn.inputStream.readBytes().deserialize>() + verify(nodeInfoStorage, times(1)).getSignedNodeInfo(nodeInfoHash) + assertEquals(nodeInfo, nodeInfoResponse.verified()) assertFailsWith(FileNotFoundException::class) { URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/${SecureHash.randomSHA256()}").openConnection().getInputStream() diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt index 221e793772..a6bcec02e7 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.doorman import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.toX509Certificate @@ -36,7 +37,7 @@ import java.util.zip.ZipInputStream import javax.ws.rs.core.MediaType import kotlin.test.assertEquals -class RegistrationWebServiceTest { +class RegistrationWebServiceTest : TestBase() { 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) @@ -141,9 +142,7 @@ class RegistrationWebServiceTest { } startSigningServer(storage) - assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.processApprovedRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/authentication/AuthenticatorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/authentication/AuthenticatorTest.kt index 78f1983981..3351c85526 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/authentication/AuthenticatorTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/authentication/AuthenticatorTest.kt @@ -1,15 +1,15 @@ package com.r3.corda.networkmanage.hsm.authentication -import CryptoServerCXI.CryptoServerCXI import CryptoServerJCE.CryptoServerProvider import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase import org.junit.Before import org.junit.Test import java.io.Console import kotlin.test.assertFalse import kotlin.test.assertTrue -class AuthenticatorTest { +class AuthenticatorTest : TestBase() { private lateinit var provider: CryptoServerProvider private lateinit var console: Console @@ -17,7 +17,7 @@ class AuthenticatorTest { @Before fun setUp() { provider = mock() - whenever(provider.cryptoServer).thenReturn(mock()) + whenever(provider.cryptoServer).thenReturn(mock()) console = mock() } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/configuration/ConfigurationTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/configuration/ConfigurationTest.kt index 9c9d4aaa4b..78733b4b64 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/configuration/ConfigurationTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/hsm/configuration/ConfigurationTest.kt @@ -1,5 +1,6 @@ package com.r3.corda.networkmanage.hsm.configuration +import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.hsm.authentication.AuthMode import com.typesafe.config.ConfigException import org.junit.Test @@ -7,7 +8,7 @@ import java.io.File import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class ConfigurationTest { +class ConfigurationTest : TestBase() { private val validConfigPath = File(javaClass.getResource("/hsm.conf").toURI()).absolutePath private val invalidConfigPath = File(javaClass.getResource("/hsm_fail.conf").toURI()).absolutePath