mirror of
https://github.com/corda/corda.git
synced 2025-01-14 00:39:57 +00:00
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:
parent
359610ff14
commit
1ec40ee983
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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,14 +108,22 @@ class SigningServiceIntegrationTest {
|
||||
|
||||
val hsmSigner = givenSignerSigningAllRequests(signingServiceStorage)
|
||||
// Poll the database for approved requests
|
||||
timer.scheduleAtFixedRate(2.seconds.toMillis(), 1.seconds.toMillis()) {
|
||||
timer.scheduleAtFixedRate(0, 1.seconds.toMillis()) {
|
||||
// The purpose of this tests is to validate the communication between this service and Doorman
|
||||
// by the means of data in the shared database.
|
||||
// Therefore the HSM interaction logic is mocked here.
|
||||
val approved = signingServiceStorage.getApprovedRequests()
|
||||
if (approved.isNotEmpty()) {
|
||||
hsmSigner.sign(approved)
|
||||
timer.cancel()
|
||||
try {
|
||||
val approved = signingServiceStorage.getApprovedRequests()
|
||||
if (approved.isNotEmpty()) {
|
||||
hsmSigner.sign(approved)
|
||||
timer.cancel()
|
||||
}
|
||||
} catch (exception: PersistenceException) {
|
||||
// It may happen that Doorman DB is not created at the moment when the signing service polls it.
|
||||
// This is due to the fact that schema is initialized at the time first hibernate session is established.
|
||||
// Since Doorman does this at the time the first CSR arrives, which in turn happens after signing service
|
||||
// startup, the very first iteration of the signing service polling fails with
|
||||
// [org.hibernate.tool.schema.spi.SchemaManagementException] being thrown as the schema is missing.
|
||||
}
|
||||
}
|
||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore()
|
||||
@ -139,8 +147,7 @@ class SigningServiceIntegrationTest {
|
||||
// Identity service not needed doorman, corda persistence is not very generic.
|
||||
throw UnsupportedOperationException()
|
||||
}, SchemaService())
|
||||
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true,
|
||||
initialNetworkMapParameters = testNetworkParameters(emptyList()))
|
||||
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 10, initialNetworkMapParameters = testNetworkParameters(emptyList()))
|
||||
|
||||
thread(start = true, isDaemon = true) {
|
||||
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")
|
||||
|
@ -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.
|
||||
@ -59,4 +69,18 @@ sealed class CertificateResponse {
|
||||
object NotReady : CertificateResponse()
|
||||
data class Ready(val certificatePath: CertPath) : CertificateResponse()
|
||||
data class Unauthorised(val message: String) : CertificateResponse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes certificate status
|
||||
*/
|
||||
enum class CertificateStatus {
|
||||
VALID, SUSPENDED, REVOKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes entire certificate signing request status
|
||||
*/
|
||||
enum class RequestStatus {
|
||||
New, Approved, Rejected, Signed
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 = ""
|
||||
)
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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())
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
}
|
@ -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();
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,14 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.*
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.DBCertificateRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.PersistenceNodeInfoStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.SchemaService
|
||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.ShowHelpException
|
||||
import com.r3.corda.networkmanage.doorman.DoormanServer.Companion.logger
|
||||
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.signer.Signer
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
||||
import net.corda.core.crypto.Crypto
|
||||
@ -19,7 +17,6 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.utilities.*
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.eclipse.jetty.server.Server
|
||||
@ -34,6 +31,8 @@ import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@ -161,16 +160,18 @@ fun startDoorman(hostAndPort: NetworkHostAndPort,
|
||||
database: CordaPersistence,
|
||||
approveAll: Boolean,
|
||||
initialNetworkMapParameters: NetworkParameters,
|
||||
signer: Signer? = null,
|
||||
signer: LocalSigner? = null,
|
||||
approveInterval: Long,
|
||||
signInterval: Long,
|
||||
jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer {
|
||||
|
||||
logger.info("Starting Doorman server.")
|
||||
|
||||
val requestService = if (approveAll) {
|
||||
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
|
||||
ApproveAllCertificateRequestStorage(DBCertificateRequestStorage(database))
|
||||
ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
|
||||
} else {
|
||||
DBCertificateRequestStorage(database)
|
||||
PersistentCertificateRequestStorage(database)
|
||||
}
|
||||
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
@ -180,29 +181,46 @@ fun startDoorman(hostAndPort: NetworkHostAndPort,
|
||||
} else {
|
||||
DefaultCsrHandler(requestService, signer)
|
||||
}
|
||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
val nodeInfoStorage = PersistentNodeInfoStorage(database)
|
||||
|
||||
val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus),
|
||||
NodeInfoWebService(PersistenceNodeInfoStorage(database), initialNetworkMapParameters))
|
||||
val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(nodeInfoStorage, networkMapStorage, signer))
|
||||
doorman.start()
|
||||
|
||||
val networkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null
|
||||
|
||||
// Thread process approved request periodically.
|
||||
thread(name = "Approved Request Process Thread") {
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(10.seconds.toMillis())
|
||||
DoormanServer.serverStatus.lastRequestCheckTime = Instant.now()
|
||||
requestProcessor.processApprovedRequests()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
DoormanServer.logger.error("Error encountered when approving request.", e)
|
||||
}
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(2)
|
||||
val approvalThread = Runnable {
|
||||
try {
|
||||
DoormanServer.serverStatus.lastRequestCheckTime = Instant.now()
|
||||
requestProcessor.processApprovedRequests()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
DoormanServer.logger.error("Error encountered when approving request.", e)
|
||||
}
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, approveInterval, approveInterval, TimeUnit.SECONDS)
|
||||
// Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed).
|
||||
if (networkMapSigner != null) {
|
||||
val signingThread = Runnable {
|
||||
try {
|
||||
networkMapSigner.signNetworkMap()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
DoormanServer.logger.error("Error encountered when processing node info changes.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(signingThread, signInterval, signInterval, TimeUnit.SECONDS)
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||
scheduledExecutor.shutdown()
|
||||
doorman.close()
|
||||
})
|
||||
return doorman
|
||||
}
|
||||
|
||||
private fun buildLocalSigner(parameters: DoormanParameters): Signer? {
|
||||
private fun buildLocalSigner(parameters: DoormanParameters): LocalSigner? {
|
||||
return parameters.keystorePath?.let {
|
||||
// Get password from console if not in config.
|
||||
val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ")
|
||||
@ -210,7 +228,7 @@ private fun buildLocalSigner(parameters: DoormanParameters): Signer? {
|
||||
val keystore = loadOrCreateKeyStore(parameters.keystorePath, keystorePassword)
|
||||
val caKeyPair = keystore.getKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword)
|
||||
val caCertPath = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA)
|
||||
Signer(caKeyPair, caCertPath)
|
||||
LocalSigner(caKeyPair, caCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,8 +236,8 @@ private fun buildLocalSigner(parameters: DoormanParameters): Signer? {
|
||||
* This storage automatically approves all created requests.
|
||||
*/
|
||||
private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate {
|
||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
|
||||
val requestId = delegate.saveRequest(rawRequest)
|
||||
override fun saveRequest(request: PKCS10CertificationRequest): String {
|
||||
val requestId = delegate.saveRequest(request)
|
||||
approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
return requestId
|
||||
}
|
||||
@ -246,7 +264,7 @@ fun main(args: Array<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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")) {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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>)
|
||||
}
|
@ -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>)
|
||||
|
||||
}
|
@ -2,7 +2,7 @@ package com.r3.corda.networkmanage.hsm.signer
|
||||
|
||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||
import com.r3.corda.networkmanage.hsm.authentication.readPassword
|
||||
import com.r3.corda.networkmanage.hsm.persistence.CertificateRequestData
|
||||
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
|
||||
import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage
|
||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.buildCertPath
|
||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.createClientCertificate
|
||||
@ -12,24 +12,25 @@ import com.r3.corda.networkmanage.hsm.utils.X509Utilities.retrieveCertificateAnd
|
||||
/**
|
||||
* Encapsulates certificate signing logic
|
||||
*/
|
||||
class HsmSigner(private val storage: SignedCertificateRequestStorage,
|
||||
private val caCertificateName: String,
|
||||
private val caPrivateKeyPass: String?,
|
||||
private val caParentCertificateName: String,
|
||||
private val validDays: Int,
|
||||
private val keyStorePassword: String?,
|
||||
private val authenticator: Authenticator) : Signer {
|
||||
class HsmCsrSigner(private val storage: SignedCertificateRequestStorage,
|
||||
private val caCertificateName: String,
|
||||
private val caPrivateKeyPass: String?,
|
||||
private val caParentCertificateName: String,
|
||||
private val validDays: Int,
|
||||
private val keyStorePassword: String?,
|
||||
private val authenticator: Authenticator) : CertificateSigningRequestSigner {
|
||||
|
||||
/**
|
||||
* Signs the provided list of approved certificate signing requests. By signature we mean creation of the client-level certificate
|
||||
* that is accompanied with a key pair (public + private) and signed by the intermediate CA using its private key.
|
||||
* that is accompanied with a key pair (public + private) and signed by the intermediate CA (stored on the HSM)
|
||||
* using its private key.
|
||||
* That key (along with the certificate) is retrieved from the key store obtained from the provider given as a result of the
|
||||
* connectAndAuthenticate method of the authenticator.
|
||||
* The method iterates through the collection of the [ApprovedCertificateRequestData] instances passed as the method parameter
|
||||
* and sets the certificate field with an appropriate value.
|
||||
* @param toSign list of approved certificates to be signed
|
||||
*/
|
||||
override fun sign(toSign: List<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
|
@ -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
|
||||
}
|
||||
}
|
@ -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>)
|
||||
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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()))
|
||||
}
|
||||
}
|
@ -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()!!)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user