Network Map signing (#87)

* Network Map signing

* Fixing failing tests

* Fixing integration tests

* Addressing review comments

* Addressing review comments

* Rebasing to the new network parameters + network map ownership redesign

* Addressing review comments

* Addressing review comments

* Splitting the PR

* Reverting enum uppercasing

* Fixing the NetworkMapEntity comment

* Addressing review comments - round 3

* Fixing docs and adding some TODOS

* Removing nullification of the common name
This commit is contained in:
mkit 2017-11-14 08:29:07 +00:00 committed by GitHub
parent 359610ff14
commit 1ec40ee983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1855 additions and 684 deletions

View File

@ -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.

View File

@ -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(

View File

@ -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<CertificateRequestData>
val toSign: List<ApprovedCertificateRequestData> = 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,15 +108,23 @@ 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.
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()
verify(hsmSigner).sign(any())
@ -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")

View File

@ -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<String>,
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<CertificateSigningRequest>
@ -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.
@ -60,3 +70,17 @@ sealed class 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
}

View File

@ -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<String>) {
return database.transaction(Connection.TRANSACTION_SERIALIZABLE) {
val request = singleRequestWhere { builder, path ->
val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
val statusEq = builder.equal(path.get<String>(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<String>(CertificateSigningRequest::legalName.name), legalName.toString())
val statusNewOrApproved = get<String>(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<String>(CertificateSigningRequest::requestId.name), requestId),
builder.equal(path.get<String>(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<String>(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<String>(CertificateSigningRequest::requestId.name), requestId)
}
}
}
override fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest> {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(CertificateSigningRequest::class.java).run {
from(CertificateSigningRequest::class.java).run {
where(builder.equal(get<RequestStatus>(CertificateSigningRequest::status.name), requestStatus))
}
}
session.createQuery(query).resultList
}
}
private fun DatabaseTransaction.singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> 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)
}
}

View File

@ -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<String> = 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 = ""
)

View File

@ -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<CertificateStatus>): List<SecureHash>
/**
* 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<SecureHash>
/**
* 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
}

View File

@ -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<String>
fun getUnsignedNodeInfoHashes(): List<SecureHash>
/**
* 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<SecureHash, ByteArray>
/**
* 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<NodeInfo>?
/**
* The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
* @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)
}

View File

@ -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<String, NodeInfo, NodeInfoEntity, String>(
toPersistentEntityKey = { it },
toPersistentEntity = { key, nodeInfo ->
val serializedNodeInfo = nodeInfo.serialize()
NodeInfoEntity(key, serializedNodeInfo.bytes)
},
fromPersistentEntity = {
val nodeInfo = it.nodeInfo.deserialize<NodeInfo>()
it.nodeInfoHash to nodeInfo
},
persistentEntityClass = NodeInfoEntity::class.java
)
fun makePublicKeyMap() = PersistentMap<String, String, PublicKeyNodeInfoLink, String>(
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<String> = 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<CertificateData>(CertificateSigningRequest::certificateData.name).get<ByteArray>(CertificateData::certificatePath.name))
where(builder.equal(get<CertificateData>(CertificateSigningRequest::certificateData.name).get<String>(CertificateData::publicKeyHash.name), publicKeyHash))
}
}
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
}
}
}

View File

@ -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 <T> DatabaseTransaction.singleRequestWhere(clazz: Class<T>, predicate: (CriteriaBuilder, Path<T>) -> 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 <T> DatabaseTransaction.deleteRequest(clazz: Class<T>, predicate: (CriteriaBuilder, Path<T>) -> 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()

View File

@ -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<String>) {
return database.transaction(Connection.TRANSACTION_SERIALIZABLE) {
val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId)
val statusEq = builder.equal(path.get<String>(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<String>(CertificateSigningRequestEntity::requestId.name), requestId),
builder.equal(path.get<String>(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<String>(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<String>(CertificateSigningRequestEntity::requestId.name), requestId)
}?.toCertificateSigningRequest()
}
}
override fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest> {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(CertificateSigningRequestEntity::class.java).run {
from(CertificateSigningRequestEntity::class.java).run {
where(builder.equal(get<RequestStatus>(CertificateSigningRequestEntity::status.name), requestStatus))
}
}
session.createQuery(query).resultList.map { it.toCertificateSigningRequest() }
}
}
private fun parseAndValidateLegalName(request: PKCS10CertificationRequest, session: Session): Pair<X500Name, String?> {
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<String>(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")
}
}
}

View File

@ -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<CertificateStatus>): List<SecureHash> = 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<String>(NetworkParametersEntity::version.name)))
}
}
// We just want the last signed entry
session.createQuery(query).resultList.first()
}
override fun getDetachedSignedAndValidNodeInfoHashes(): List<SecureHash> = 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<ByteArray>(NodeInfoEntity::networkMap.name)),
builder.isNotNull(get<ByteArray>(NodeInfoEntity::signatureBytes.name))))
}
}
session.createQuery(query).resultList.map { SecureHash.parse(it.nodeInfoHash) }
}
private fun getCurrentNetworkMapEntity(hint: Pair<String, Any>): NetworkMapEntity? = database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(NetworkMapEntity::class.java).run {
from(NetworkMapEntity::class.java).run {
where(builder.isNotNull(get<ByteArray?>(NetworkMapEntity::signatureBytes.name)))
orderBy(builder.desc(get<String>(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<String>(NetworkParametersEntity::parametersHash.name), parameterHash)
}
}
/**
* Creates Hibernate query hint for pulling [NetworkParametersEntity] when querying for [NetworkMapEntity]
*/
private fun getNetworkMapWithParametersHint(session: Session): Pair<String, Any> {
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<String, Any> {
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<String, Any> {
val graph = session.createEntityGraph(NetworkMapEntity::class.java)
graph.addAttributeNodes(NetworkMapEntity::nodeInfoList.name)
graph.addAttributeNodes(NetworkMapEntity::parameters.name)
return QueryHints.HINT_LOADGRAPH to graph
}
}

View File

@ -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<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash)
val certStatusValid = builder.equal(path.get<CertificateStatus>(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<CertificateSigningRequestEntity>(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<NodeInfo>? = 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<SecureHash, ByteArray> {
return getUnsignedNodeInfoEntities().associate { SecureHash.parse(it.nodeInfoHash) to it.nodeInfoBytes }
}
override fun getUnsignedNodeInfoHashes(): List<SecureHash> {
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<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
.get<ByteArray>(CertificateDataEntity::certificatePathBytes.name))
where(builder.equal(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
.get<String>(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<NodeInfoEntity> = 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<ByteArray>(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<String, Any> {
val graph = session.createEntityGraph(NodeInfoEntity::class.java)
graph.addAttributeNodes(NodeInfoEntity::certificateSigningRequest.name)
return QueryHints.HINT_LOADGRAPH to graph
}
}

View File

@ -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<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions()))
override var schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(NetworkServicesV1, SchemaService.SchemaOptions()))
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(DoormanServicesV1)
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(NetworkServicesV1)
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException()

View File

@ -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<String> = 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<String> = 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())
}

View File

@ -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<NodeInfoEntity> = 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())
}
}

View File

@ -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()
}

View File

@ -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<NodeInfo>()
/**
* 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
)
}
}

View File

@ -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<String>, 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)
}
}
}

View File

@ -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?
}

View File

@ -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<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config {
val parser = OptionParser()
val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp();

View File

@ -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")
}

View File

@ -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,16 +181,18 @@ 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) {
val scheduledExecutor = Executors.newScheduledThreadPool(2)
val approvalThread = Runnable {
try {
Thread.sleep(10.seconds.toMillis())
DoormanServer.serverStatus.lastRequestCheckTime = Instant.now()
requestProcessor.processApprovedRequests()
} catch (e: Exception) {
@ -197,12 +200,27 @@ fun startDoorman(hostAndPort: NetworkHostAndPort,
DoormanServer.logger.error("Error encountered when approving request.", e)
}
}
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)
}
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })
}
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<String>) {
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)
}
}
}

View File

@ -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)
}

View File

@ -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<Certificate>) {
fun sign(certificationRequest: PKCS10CertificationRequest): CertPath {
class LocalSigner(private val caKeyPair: KeyPair, private val caCertPath: Array<Certificate>) : 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<Certi
nameConstraints = nameConstraints).toX509Certificate()
return buildCertPath(clientCertificate, *caCertPath)
}
override fun sign(data: ByteArray): SignatureAndCertPath {
return SignatureAndCertPath(caKeyPair.sign(data), buildCertPath(*caCertPath))
}
}

View File

@ -1,15 +1,17 @@
package com.r3.corda.networkmanage.doorman.webservice
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.common.utils.hashString
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.networkMapPath
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.toSHA256Bytes
import org.codehaus.jackson.map.ObjectMapper
import java.io.InputStream
import java.security.InvalidKeyException
import java.security.SignatureException
@ -22,7 +24,9 @@ import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status
@Path(networkMapPath)
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private val networkParameters: NetworkParameters) {
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapStorage: NetworkMapStorage,
private val signer: LocalSigner? = null) {
companion object {
const val networkMapPath = "network-map"
}
@ -37,13 +41,18 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private v
val nodeInfo = registrationData.verified()
val digitalSignature = registrationData.sig
val certPath = nodeInfoStorage.getCertificatePath(nodeInfo.legalIdentities.first().owningKey.toSHA256Bytes().toString())
val certPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(digitalSignature.by.hashString()))
return if (certPath != null) {
try {
require(Crypto.doVerify(certPath.certificates.first().publicKey, digitalSignature.bytes, nodeInfo.serialize().bytes))
// Store the NodeInfo
// TODO: Does doorman need to sign the nodeInfo?
nodeInfoStorage.putNodeInfo(nodeInfo)
val serializedNodeInfo = nodeInfo.serialize().bytes
val nodeCAPubKey = certPath.certificates.first().publicKey
// Validate node public key
nodeInfo.legalIdentitiesAndCerts.forEach {
require(it.certPath.certificates.any { it.publicKey == nodeCAPubKey })
}
require(Crypto.doVerify(nodeCAPubKey, digitalSignature.bytes, serializedNodeInfo))
// Store the NodeInfo and notify registration listener
nodeInfoStorage.putNodeInfo(nodeInfo, signer?.sign(serializedNodeInfo)?.signature)
ok()
} catch (e: Exception) {
// Catch exceptions thrown by signature verification.
@ -61,15 +70,13 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private v
@GET
fun getNetworkMap(): Response {
// TODO: Cache the response?
// TODO: Add the networkParamters to this returned response.
return ok(ObjectMapper().writeValueAsString(nodeInfoStorage.getNodeInfoHashes())).build()
return ok(networkMapStorage.getCurrentNetworkMap().serialize().bytes).build()
}
@GET
@Path("{var}")
fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response {
// TODO: Use JSON instead.
return nodeInfoStorage.getNodeInfo(nodeInfoHash)?.let {
@Path("{nodeInfoHash}")
fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response {
return nodeInfoStorage.getSignedNodeInfo(SecureHash.parse(nodeInfoHash))?.let {
ok(it.serialize().bytes).build()
} ?: status(Response.Status.NOT_FOUND).build()
}

View File

@ -1,15 +1,20 @@
package com.r3.corda.networkmanage.hsm
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage
import com.r3.corda.networkmanage.common.persistence.SchemaService
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.authentication.createProvider
import com.r3.corda.networkmanage.hsm.configuration.Parameters
import com.r3.corda.networkmanage.hsm.configuration.parseParameters
import com.r3.corda.networkmanage.hsm.generator.CertificateNameAndPass
import com.r3.corda.networkmanage.hsm.generator.KeyCertificateGenerator
import com.r3.corda.networkmanage.hsm.menu.Menu
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.signer.HsmSigner
import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
import net.corda.node.utilities.configureDatabase
@ -25,30 +30,42 @@ fun run(parameters: Parameters) {
// Identity service not needed
throw UnsupportedOperationException()
}, SchemaService())
val storage = DBSignedCertificateRequestStorage(database)
val provider = createProvider()
val sign: (List<CertificateRequestData>) -> 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<ApprovedCertificateRequestData>) -> 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<CertificateRequestData>): Boolean {
private fun confirmedSign(selectedItems: List<ApprovedCertificateRequestData>): 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<CertificateRequestData>): List<CertificateRequestData> {
private fun getSelection(toSelect: List<ApprovedCertificateRequestData>): List<ApprovedCertificateRequestData> {
print("CSRs to be signed (comma separated list): ")
val line = readLine()
if (line == null) {

View File

@ -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: <<current directory>>/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")) {

View File

@ -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<CertificateNameAndPass>,
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")
}

View File

@ -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<CertificateRequestData>, signers: List<String>) {
override fun store(requests: List<ApprovedCertificateRequestData>, signers: List<String>) {
for ((requestId, _, certPath) in requests) {
storage.putCertificatePath(requestId, certPath!!, signers)
}
}
override fun getApprovedRequests(): List<CertificateRequestData> {
override fun getApprovedRequests(): List<ApprovedCertificateRequestData> {
return storage.getRequests(RequestStatus.Approved).map { it.toRequestData() }
}
private fun CertificateSigningRequest.toRequestData() = CertificateRequestData(requestId, PKCS10CertificationRequest(request))
private fun CertificateSigningRequest.toRequestData() = ApprovedCertificateRequestData(requestId, request)
}

View File

@ -8,7 +8,7 @@ interface SignedCertificateRequestStorage {
/**
* Returns all certificate signing requests that have been approved for signing.
*/
fun getApprovedRequests(): List<CertificateRequestData>
fun getApprovedRequests(): List<ApprovedCertificateRequestData>
/**
* 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<CertificateRequestData>, signers: List<String>)
fun store(requests: List<ApprovedCertificateRequestData>, signers: List<String>)
}

View File

@ -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<ApprovedCertificateRequestData>)
}

View File

@ -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,
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) : Signer {
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<CertificateRequestData>) {
override fun sign(toSign: List<ApprovedCertificateRequestData>) {
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

View File

@ -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<HsmNetworkMapSigner>()
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
}
}

View File

@ -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<CertificateRequestData>)
}

View File

@ -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
*/

View File

@ -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<String> = 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<String> = 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<NotaryInfo> = 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
)
}
}

View File

@ -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<PKCS10CertificationRequest, KeyPair> {
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<PKCS10CertificationRequest, KeyPair> {
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)
}

View File

@ -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<NotaryInfo>()))
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))
}
}

View File

@ -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()))
}
}

View File

@ -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()!!)
}
}

View File

@ -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<SignedNetworkMap>().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())
}
}

View File

@ -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())
}
}

View File

@ -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

View File

@ -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<SignedNetworkMap>()
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<NodeInfo>()
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
assertEquals(nodeInfo, nodeInfoResponse)
val nodeInfoResponse = conn.inputStream.readBytes().deserialize<SignedData<NodeInfo>>()
verify(nodeInfoStorage, times(1)).getSignedNodeInfo(nodeInfoHash)
assertEquals(nodeInfo, nodeInfoResponse.verified())
assertFailsWith(FileNotFoundException::class) {
URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/${SecureHash.randomSHA256()}").openConnection().getInputStream()

View File

@ -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

View File

@ -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<CryptoServerCXI>())
whenever(provider.cryptoServer).thenReturn(mock())
console = mock()
}

View File

@ -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