ENT-1443 Add cert role to CSR and doorman issue cert according to the cert role (#431)

* Doorman and HSM create certificate base on requested cert role specified in the certificate signing request.
This commit is contained in:
Patrick Kuo 2018-02-23 10:57:59 +00:00 committed by GitHub
parent 81801d4566
commit 94f73920cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 705 additions and 194 deletions

3
.idea/compiler.xml generated
View File

@ -134,6 +134,9 @@
<module name="quasar-hook_test" target="1.8" />
<module name="quasar-utils_main" target="1.8" />
<module name="quasar-utils_test" target="1.8" />
<module name="registration-tool_integrationTest" target="1.8" />
<module name="registration-tool_main" target="1.8" />
<module name="registration-tool_test" target="1.8" />
<module name="rpc_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" />

View File

@ -0,0 +1,54 @@
#Network Registration Tool
The network registration tool creates a CSR (Certificate Signing Request) and sent to compatibility zone doorman for approval.
A keystore and a trust store will be created once the request is approved.
##Configuration file
The tool creates the CSR using information provided by the config file, the path to the config file should be provided
using the ``--config-file`` flag on start up.
The config file should contain the following parameters.
```
Parameter Description
--------- -----------
legalName Legal name of the requester. It can be in form of X.500 string or CordaX500Name in typesafe config object format.
email Requester's e-mail address.
compatibilityZoneURL Compatibility zone URL.
networkRootTrustStorePath Path to the network root trust store.
certRole Requested cert role, it should be one of [NODE_CA, SERVICE_IDENTITY].
networkRootTrustStorePassword Network root trust store password, to be provided by the network operator. Optional, the tool will prompt for password input if not provided.
keyStorePassword Generated keystore's password. Optional, the tool will prompt for password input if not provided.
trustStorePassword Generated trust store's password. Optional, the tool will prompt for password input if not provided.
```
Example config file
```
legalName {
organisationUnit = "R3 Corda"
organisation = "R3 LTD"
locality = "London"
country = "GB"
}
# legalName = "C=GB, L=London, O=R3 LTD, OU=R3 Corda"
email = "test@email.com"
compatibilityZoneURL = "http://doorman.url.com"
networkRootTrustStorePath = "networkRootTrustStore.jks"
certRole = "NODE_CA"
networkRootTrustStorePassword = "password"
keyStorePassword = "password"
trustStorePassword = "password"
```
##Running the registration tool
``java -jar registration-tool-<<version>>.jar --config-file <<config file path>>``

View File

@ -0,0 +1,63 @@
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'us.kirchmeier.capsule'
apply plugin: 'kotlin'
description 'Network registration tool'
version project(':network-management').version
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'http://ci-artifactory.corda.r3cev.com/artifactory/corda-dev'
}
maven {
url 'http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases'
}
}
configurations {
runtimeArtifacts.extendsFrom runtime
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
}
task buildRegistrationTool(type: FatCapsule, dependsOn: 'jar') {
group = "build"
applicationClass 'com.r3.corda.networkmanage.RegistrationToolKt'
archiveName "registration-tool-${version}.jar"
capsuleManifest {
applicationVersion = corda_release_version
systemProperties['visualvm.display.name'] = 'Corda Network Registration Tool'
minJavaVersion = '1.8.0'
}
applicationSource = files(
project(':network-management:registration-tool').configurations.runtime,
project(':network-management:registration-tool').jar
)
}
artifacts {
runtimeArtifacts buildRegistrationTool
publish buildRegistrationTool
}
jar {
classifier "ignore"
}
publish {
name 'registration-tool'
disableDefaultJar = true
}
dependencies {
compile project(':node')
testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}"
}

View File

@ -0,0 +1,68 @@
package com.r3.corda.networkmanage.registration
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser
import joptsimple.util.PathConverter
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.parseAs
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
fun main(args: Array<String>) {
val optionParser = OptionParser()
val configFileArg = optionParser
.accepts("config-file", "The path to the registration config file")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
val baseDirArg = optionParser
.accepts("baseDir", "The registration tool's base directory, default to current directory.")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
.defaultsTo(Paths.get("."))
val optionSet = optionParser.parse(*args)
val configFilePath = optionSet.valueOf(configFileArg)
val baseDir = optionSet.valueOf(baseDirArg)
val config = ConfigFactory.parseFile(configFilePath.toFile(), ConfigParseOptions.defaults().setAllowMissing(false))
.resolve()
.parseAs<RegistrationConfig>()
val sslConfig = object : SSLConfiguration {
override val keyStorePassword: String by lazy { config.keyStorePassword ?: readPassword("Node Keystore password:") }
override val trustStorePassword: String by lazy { config.trustStorePassword ?: readPassword("Node TrustStore password:") }
override val certificatesDirectory: Path = baseDir
}
NetworkRegistrationHelper(sslConfig,
config.legalName,
config.email,
HTTPNetworkRegistrationService(config.compatibilityZoneURL),
config.networkRootTrustStorePath,
config.networkRootTrustStorePassword ?: readPassword("Network trust root password:"), config.certRole).buildKeystore()
}
fun readPassword(fmt: String): String {
return if (System.console() != null) {
String(System.console().readPassword(fmt))
} else {
print(fmt)
readLine() ?: ""
}
}
data class RegistrationConfig(val legalName: CordaX500Name,
val email: String,
val compatibilityZoneURL: URL,
val networkRootTrustStorePath: Path,
val certRole: CertRole,
val keyStorePassword: String?,
val networkRootTrustStorePassword: String?,
val trustStorePassword: String?)

View File

@ -0,0 +1,15 @@
legalName {
organisationUnit = "R3 Corda"
organisation = "R3 LTD"
locality = "London"
country = "GB"
}
# legalName = "C=GB, L=London, O=R3 LTD, OU=R3 Corda"
email = "test@email.com"
compatibilityZoneURL = "http://doorman.url.com"
networkRootTrustStorePath = "networkRootTrustStore.jks"
certRole = "NODE_CA"
networkRootTrustStorePassword = "password"
keyStorePassword = "password"
trustStorePassword = "password"

View File

@ -0,0 +1,44 @@
package com.r3.corda.networkmanage.registration
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.config.parseAs
import org.junit.Assert.assertEquals
import org.junit.Test
import java.nio.file.Paths
class RegistrationConfigTest {
@Test
fun `parse config file correctly`() {
val testConfig = """
legalName {
organisationUnit = "R3 Corda"
organisation = "R3 LTD"
locality = "London"
country = "GB"
}
email = "test@email.com"
compatibilityZoneURL = "http://doorman.url.com"
networkRootTrustStorePath = "networkRootTrustStore.jks"
certRole = "NODE_CA"
networkRootTrustStorePassword = "password"
keyStorePassword = "password"
trustStorePassword = "password"
""".trimIndent()
val config = ConfigFactory.parseString(testConfig, ConfigParseOptions.defaults().setAllowMissing(false))
.resolve()
.parseAs<RegistrationConfig>()
assertEquals(CertRole.NODE_CA, config.certRole)
assertEquals(CordaX500Name.parse("OU=R3 Corda, O=R3 LTD, L=London, C=GB"), config.legalName)
assertEquals("http://doorman.url.com", config.compatibilityZoneURL.toString())
assertEquals("test@email.com", config.email)
assertEquals(Paths.get("networkRootTrustStore.jks"), config.networkRootTrustStorePath)
assertEquals("password", config.networkRootTrustStorePassword)
}
}

View File

@ -14,49 +14,32 @@ import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
import com.r3.corda.networkmanage.hsm.signer.HsmSigner
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name.Companion.parse
import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificateSigningRequest
import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
class HsmSigningServiceTest : HsmBaseTest() {
@Before
fun setUp() {
loadOrCreateKeyStore(rootKeyStoreFile, TRUSTSTORE_PASSWORD)
}
@Test
fun `HSM signing service can sign CSR data`() {
// when root cert is created
run(createGeneratorParameters(
keyGroup = ROOT_CERT_KEY_GROUP,
rootKeyGroup = null,
certificateType = CertificateType.ROOT_CA,
subject = ROOT_CERT_SUBJECT
))
// when network map cert is created
run(createGeneratorParameters(
keyGroup = NETWORK_MAP_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.NETWORK_MAP,
subject = NETWORK_MAP_CERT_SUBJECT
))
// when doorman cert is created
run(createGeneratorParameters(
keyGroup = DOORMAN_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.INTERMEDIATE_CA,
subject = DOORMAN_CERT_SUBJECT
))
fun `HSM signing service can sign node CSR data`() {
setupCertificates()
// given authenticated user
val userInput = givenHsmUserAuthenticationInput()
@ -92,31 +75,59 @@ class HsmSigningServiceTest : HsmBaseTest() {
assertNotNull(toSign.certPath)
val certificates = toSign.certPath!!.certificates
assertEquals(3, certificates.size)
// Is a CA
assertNotEquals(-1, certificates.first().x509.basicConstraints)
assertEquals(CertRole.NODE_CA, CertRole.extract(certificates.first().x509))
}
@Test
fun `HSM signing service can sign service identity CSR data`() {
setupCertificates()
// given authenticated user
val userInput = givenHsmUserAuthenticationInput()
// given HSM CSR signer
val hsmSigningServiceConfig = createHsmSigningServiceConfig()
val doormanCertificateConfig = hsmSigningServiceConfig.csrSigning!!
val signer = HsmCsrSigner(
mock(),
doormanCertificateConfig.loadRootKeyStore(),
"",
null,
3650,
Authenticator(
provider = createProvider(
doormanCertificateConfig.keyGroup,
hsmSigningServiceConfig.keySpecifier,
hsmSigningServiceConfig.device),
inputReader = userInput)
)
// give random data to sign
val toSign = ApprovedCertificateRequestData(
"test",
createCertificateSigningRequest(
parse("O=R3Cev,L=London,C=GB").x500Principal,
"my@mail.com",
generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME),
certRole = CertRole.SERVICE_IDENTITY))
// when
signer.sign(listOf(toSign))
// then
assertNotNull(toSign.certPath)
val certificates = toSign.certPath!!.certificates
assertEquals(3, certificates.size)
// Not a CA
assertEquals(-1, certificates.first().x509.basicConstraints)
assertEquals(CertRole.SERVICE_IDENTITY, CertRole.extract(certificates.first().x509))
}
@Test
fun `HSM signing service can sign and serialize network map data to the Doorman DB`() {
// when root cert is created
run(createGeneratorParameters(
keyGroup = ROOT_CERT_KEY_GROUP,
rootKeyGroup = null,
certificateType = CertificateType.ROOT_CA,
subject = ROOT_CERT_SUBJECT
))
// when network map cert is created
run(createGeneratorParameters(
keyGroup = NETWORK_MAP_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.NETWORK_MAP,
subject = NETWORK_MAP_CERT_SUBJECT
))
// when doorman cert is created
run(createGeneratorParameters(
keyGroup = DOORMAN_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.INTERMEDIATE_CA,
subject = DOORMAN_CERT_SUBJECT
))
setupCertificates()
// given authenticated user
val userInput = givenHsmUserAuthenticationInput()
@ -150,4 +161,28 @@ class HsmSigningServiceTest : HsmBaseTest() {
assertEquals(networkMapParameters.serialize().hash, persistedNetworkMap.networkParameterHash)
assertThat(persistedNetworkMap.nodeInfoHashes).isEmpty()
}
private fun setupCertificates(){
// when root cert is created
run(createGeneratorParameters(
keyGroup = ROOT_CERT_KEY_GROUP,
rootKeyGroup = null,
certificateType = CertificateType.ROOT_CA,
subject = ROOT_CERT_SUBJECT
))
// when network map cert is created
run(createGeneratorParameters(
keyGroup = NETWORK_MAP_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.NETWORK_MAP,
subject = NETWORK_MAP_CERT_SUBJECT
))
// when doorman cert is created
run(createGeneratorParameters(
keyGroup = DOORMAN_CERT_KEY_GROUP,
rootKeyGroup = ROOT_CERT_KEY_GROUP,
certificateType = CertificateType.INTERMEDIATE_CA,
subject = DOORMAN_CERT_SUBJECT
))
}
}

View File

@ -17,6 +17,7 @@ import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
@ -137,7 +138,8 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any())
doReturn(sslKeyStore).whenever(it).loadSslKeyStore(any())
}
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!), networkTrustStorePath, networkTrustStorePassword).buildKeystore()
val regConfig = NodeRegistrationOption(networkTrustStorePath, networkTrustStorePassword)
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!), regConfig).buildKeystore()
verify(hsmSigner).sign(any())
}
}

View File

@ -2,15 +2,16 @@ 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.getCertRole
import com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.Crypto.toSupportedPublicKey
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.hibernate.Session
import java.security.cert.CertPath
import java.time.Instant
import javax.security.auth.x500.X500Principal
@ -19,6 +20,11 @@ import javax.security.auth.x500.X500Principal
* Database implementation of the [CertificationRequestStorage] interface.
*/
class PersistentCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage {
companion object {
// TODO: make this configurable?
private val allowedCertRoles = setOf(CertRole.NODE_CA, CertRole.SERVICE_IDENTITY)
}
override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String>) {
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
@ -43,16 +49,28 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = SecureHash.randomSHA256().toString()
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val (legalName, rejectReason) = parseAndValidateLegalName(request, session)
session.save(CertificateSigningRequestEntity(
val requestEntity = try {
val legalName = validateRequestAndParseLegalName(request)
CertificateSigningRequestEntity(
requestId = requestId,
legalName = legalName,
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
requestBytes = request.encoded,
remark = rejectReason,
modifiedBy = emptyList(),
status = if (rejectReason == null) RequestStatus.NEW else RequestStatus.REJECTED
))
status = RequestStatus.NEW
)
} catch (e: RequestValidationException) {
CertificateSigningRequestEntity(
requestId = requestId,
legalName = e.parsedLegalName,
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
requestBytes = request.encoded,
remark = e.rejectMessage,
modifiedBy = emptyList(),
status = RequestStatus.REJECTED
)
}
session.save(requestEntity)
}
return requestId
}
@ -125,46 +143,41 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
}
}
private fun parseAndValidateLegalName(request: PKCS10CertificationRequest, session: Session): Pair<String, String?> {
private fun DatabaseTransaction.validateRequestAndParseLegalName(request: PKCS10CertificationRequest): String {
// It's important that we always use the toString() output of CordaX500Name as it standardises the string format
// to make querying possible.
val legalName = try {
CordaX500Name.build(X500Principal(request.subject.encoded)).toString()
CordaX500Name.build(X500Principal(request.subject.encoded))
} catch (e: IllegalArgumentException) {
return Pair(request.subject.toString(), "Name validation failed: ${e.message}")
throw RequestValidationException(request.subject.toString(), "Name validation failed: ${e.message}")
}
val duplicateNameQuery = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName))
}
}
return when {
// Check if requested role is valid.
request.getCertRole() !in allowedCertRoles -> throw RequestValidationException(legalName.toString(), "Requested certificate role ${request.getCertRole()} is not allowed.")
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
// What if we approved something by mistake.
val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter {
it.status != RequestStatus.REJECTED
nonRejectedRequestExists(CertificateSigningRequestEntity::legalName.name, legalName.toString()) -> throw RequestValidationException(legalName.toString(), "Duplicate legal name")
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
nonRejectedRequestExists(CertificateSigningRequestEntity::publicKeyHash.name, toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()) -> throw RequestValidationException(legalName.toString(), "Duplicate public key")
else -> legalName.toString()
}
}
if (nameDuplicates.isNotEmpty()) {
return Pair(legalName, "Duplicate legal name")
}
val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()
val duplicatePkQuery = session.criteriaBuilder.run {
/**
* Check if "non-rejected" request exists with provided column and value.
*/
private fun DatabaseTransaction.nonRejectedRequestExists(columnName: String, value: String): Boolean {
val query = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKey))
val valueQuery = equal(get<String>(columnName), value)
val statusQuery = notEqual(get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED)
criteriaQuery.where(and(valueQuery, statusQuery))
}
}
return session.createQuery(query).setMaxResults(1).resultList.isNotEmpty()
}
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
val pkDuplicates = session.createQuery(duplicatePkQuery).resultList.filter {
it.status != RequestStatus.REJECTED
}
return Pair(legalName, if (pkDuplicates.isEmpty()) null else "Duplicate public key")
}
private class RequestValidationException(val parsedLegalName: String, val rejectMessage: String) : Exception("Validation failed for $parsedLegalName. $rejectMessage.")
}

View File

@ -5,7 +5,9 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser
import net.corda.core.CordaOID
import net.corda.core.crypto.sha256
import net.corda.core.internal.CertRole
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv
@ -16,6 +18,10 @@ import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
@ -76,3 +82,24 @@ private fun String.toCamelcase(): String {
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
} else this
}
private fun PKCS10CertificationRequest.firstAttributeValue(identifier: ASN1ObjectIdentifier): ASN1Encodable? {
return getAttributes(identifier).firstOrNull()?.attrValues?.firstOrNull()
}
/**
* Helper method to extract cert role from certificate signing request. Default to NODE_CA if not exist for backward compatibility.
*/
fun PKCS10CertificationRequest.getCertRole(): CertRole {
// Default cert role to Node_CA for backward compatibility.
val encoded = firstAttributeValue(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE))?.toASN1Primitive()?.encoded ?: return CertRole.NODE_CA
return CertRole.getInstance(encoded)
}
/**
* Helper method to extract email from certificate signing request.
*/
fun PKCS10CertificationRequest.getEmail(): String {
// TODO: Add basic email check?
return firstAttributeValue(BCStyle.E).toString()
}

View File

@ -8,10 +8,12 @@ import com.atlassian.jira.rest.client.api.domain.Issue
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.networkmanage.common.utils.getCertRole
import com.r3.corda.networkmanage.common.utils.getEmail
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject
@ -33,6 +35,7 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
private var canceledTransitionId: Int = -1
private var startProgressTransitionId: Int = -1
// TODO: Pass in a parsed object instead of raw PKCS10 request.
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) {
@ -49,13 +52,29 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
// TODO The subject of the signing request has already been validated and parsed into a CordaX500Name. We shouldn't
// have to do it again here.
val subject = CordaX500Name.build(X500Principal(signingRequest.subject.encoded))
val email = signingRequest.getEmail()
val email = signingRequest.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
val certRole = signingRequest.getCertRole()
val ticketSummary = if (subject.organisationUnit != null) {
"${subject.organisationUnit}, ${subject.organisation}"
} else {
subject.organisation
}
val data = mapOf("Requested Role Type" to certRole.name,
"Organisation" to subject.organisation,
"Organisation Unit" to subject.organisationUnit,
"Nearest City" to subject.locality,
"Country" to subject.country,
"Email" to email)
val ticketDescription = data.filter { it.value != null }.map { "${it.key}: ${it.value}" }.joinToString("\n") + "\n\n{code}$request{code}"
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription("Organisation: ${subject.organisation}\nNearest City: ${subject.locality}\nCountry: ${subject.country}\nEmail: $email\n\n{code}$request{code}")
.setSummary(subject.organisation)
.setDescription(ticketDescription)
.setSummary(ticketSummary)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()

View File

@ -5,9 +5,12 @@ 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.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.getCertRole
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.certificateType
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
@ -54,11 +57,12 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage,
// We assume all attributes in the subject name has been checked prior approval.
// TODO: add validation to subject name.
val request = JcaPKCS10CertificationRequest(certificationRequest)
val certRole = request.getCertRole()
val nameConstraints = NameConstraints(
arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
arrayOf())
val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
certRole.certificateType,
csrCertPathAndKey.certPath[0],
csrCertPathAndKey.toKeyPair(),
X500Principal(request.subject.encoded),

View File

@ -1,17 +1,20 @@
package com.r3.corda.networkmanage.hsm.signer
import com.r3.corda.networkmanage.common.utils.getCertRole
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.createClientCertificate
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.retrieveCertAndKeyPair
import net.corda.core.internal.CertRole
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.buildCertPath
import net.corda.nodeapi.internal.crypto.certificateType
import org.bouncycastle.asn1.x500.X500Name
import java.io.PrintStream
@ -49,8 +52,9 @@ class HsmCsrSigner(private val storage: SignedCertificateRequestStorage,
logger.debug("Retrieving the doorman certificate $CORDA_INTERMEDIATE_CA from HSM...")
val doormanCertAndKey = retrieveCertAndKeyPair(CORDA_INTERMEDIATE_CA, keyStore)
toSign.forEach {
val certRole = it.request.getCertRole()
val nodeCaCert = createClientCertificate(
CertificateType.NODE_CA,
certRole.certificateType,
doormanCertAndKey,
it.request,
validDays,

View File

@ -6,6 +6,7 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRe
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
@ -40,7 +41,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `valid request`() {
val request = createRequest("LegalName").first
val request = createRequest("LegalName", certRole = CertRole.NODE_CA).first
val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request, this.request)
@ -48,9 +49,29 @@ class PersistentCertificateRequestStorageTest : TestBase() {
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId)
}
@Test
fun `valid service identity request`() {
val request = createRequest("LegalName", certRole = CertRole.SERVICE_IDENTITY).first
val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request, this.request)
}
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId)
}
@Test
fun `invalid cert role request`() {
val request = createRequest("LegalName", certRole = CertRole.INTERMEDIATE_CA).first
val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request, this.request)
}
assertThat(storage.getRequests(RequestStatus.REJECTED).map { it.requestId }).containsOnly(requestId)
}
@Test
fun `approve request`() {
val (request, _) = createRequest("LegalName")
val (request, _) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB.
val requestId = storage.saveRequest(request)
// Pending request should equals to 1.
@ -69,7 +90,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `sign request`() {
val (csr, nodeKeyPair) = createRequest("LegalName")
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB.
val requestId = storage.saveRequest(csr)
// New request should equals to 1.
@ -95,7 +116,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `sign request ignores subsequent sign requests`() {
val (csr, nodeKeyPair) = createRequest("LegalName")
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB.
val requestId = storage.saveRequest(csr)
// Store certificate to DB.
@ -118,7 +139,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `sign request rejects requests with the same public key`() {
val (csr, nodeKeyPair) = createRequest("LegalName")
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB.
val requestId = storage.saveRequest(csr)
// Store certificate to DB.
@ -132,7 +153,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
)
// Sign certificate
// When request with the same public key is requested
val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair)
val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair, certRole = CertRole.NODE_CA)
val duplicateRequestId = storage.saveRequest(newCsr)
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
val duplicateRequest = storage.getRequest(duplicateRequestId)
@ -142,7 +163,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `reject request`() {
val requestId = storage.saveRequest(createRequest("BankA").first)
val requestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
storage.rejectRequest(requestId, DOORMAN_SIGNATURE, "Because I said so!")
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
assertThat(storage.getRequest(requestId)!!.remark).isEqualTo("Because I said so!")
@ -150,9 +171,9 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `request with the same legal name as a pending request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first)
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId1)
val requestId2 = storage.saveRequest(createRequest("BankA").first)
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId1)
assertEquals(RequestStatus.REJECTED, storage.getRequest(requestId2)!!.status)
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
@ -164,16 +185,16 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `request with the same legal name as a previously approved request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first)
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
storage.markRequestTicketCreated(requestId1)
storage.approveRequest(requestId1, DOORMAN_SIGNATURE)
val requestId2 = storage.saveRequest(createRequest("BankA").first)
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
}
@Test
fun `request with the same legal name as a previously signed request`() {
val (csr, nodeKeyPair) = createRequest("BankA")
val (csr, nodeKeyPair) = createRequest("BankA", certRole = CertRole.NODE_CA)
val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
@ -183,15 +204,15 @@ class PersistentCertificateRequestStorageTest : TestBase() {
generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE)
)
val rejectedRequestId = storage.saveRequest(createRequest("BankA").first)
val rejectedRequestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
assertThat(storage.getRequest(rejectedRequestId)!!.remark).containsIgnoringCase("duplicate")
}
@Test
fun `request with the same legal name as a previously rejected request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first)
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
storage.rejectRequest(requestId1, DOORMAN_SIGNATURE, "Because I said so!")
val requestId2 = storage.saveRequest(createRequest("BankA").first)
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId2)
storage.markRequestTicketCreated(requestId2)
storage.approveRequest(requestId2, DOORMAN_SIGNATURE)
@ -204,7 +225,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
val approver = "APPROVER"
// when
val requestId = storage.saveRequest(createRequest("BankA").first)
val requestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, approver)
@ -242,7 +263,8 @@ class PersistentCertificateRequestStorageTest : TestBase() {
}
}
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PKCS10CertificationRequest, KeyPair> {
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
return Pair(request, keyPair)
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), certRole: CertRole): Pair<PKCS10CertificationRequest, KeyPair> {
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair, certRole = certRole)
// encode and decode the request to make sure class information (CertRole) etc are not passed into the test.
return Pair(JcaPKCS10CertificationRequest(request.encoded), keyPair)
}

View File

@ -5,6 +5,7 @@ import com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
@ -132,7 +133,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
internal fun createValidSignedNodeInfo(organisation: String,
storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
val (csr, nodeKeyPair) = createRequest(organisation)
val (csr, nodeKeyPair) = createRequest(organisation, certRole = CertRole.NODE_CA)
val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, "TestUser")

View File

@ -8,7 +8,6 @@ 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.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.X509Utilities
@ -83,6 +82,49 @@ class DefaultCsrHandlerTest : TestBase() {
assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA)
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
// Is CA
assertThat(basicConstraints != -1)
}
}
}
@Test
fun `process approved service identity request`() {
val requests = (1..3).map {
X509Utilities.createCertificateSigningRequest(
X500Principal("O=Test$it,L=London,C=GB"),
"my@email.com",
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), certRole = CertRole.SERVICE_IDENTITY)
}
val requestStorage: CertificationRequestStorage = mock {
on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED)
))
}
val (rootCa, csrCa) = createDevIntermediateCaCertPath()
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
requestProcessor.processRequests()
val certPathCapture = argumentCaptor<CertPath>()
// Verify only the approved requests are taken
verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED)
verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
// Then make sure the generated node cert paths are correct
certPathCapture.allValues.forEachIndexed { index, certPath ->
X509Utilities.validateCertificateChain(rootCa.certificate, certPath.x509Certificates)
assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate)
(certPath.certificates[0] as X509Certificate).apply {
assertThat(CertRole.extract(this)).isEqualTo(CertRole.SERVICE_IDENTITY)
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
// Not a CA
assertThat(basicConstraints == -1)
}
}
}

View File

@ -2,10 +2,7 @@
package net.corda.nodeapi.internal.config
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigUtil
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.noneOrSingle
import net.corda.core.internal.uncheckedCast
@ -78,7 +75,12 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path))
Path::class -> Paths.get(getString(path))
URL::class -> URL(getString(path))
CordaX500Name::class -> CordaX500Name.parse(getString(path))
CordaX500Name::class -> {
when (getValue(path).valueType()) {
ConfigValueType.OBJECT -> getConfig(path).parseAs()
else -> CordaX500Name.parse(getString(path))
}
}
Properties::class -> getConfig(path).toProperties()
Config::class -> getConfig(path)
else -> if (typeClass.java.isEnum) {

View File

@ -262,13 +262,17 @@ object X509Utilities {
private fun createCertificateSigningRequest(subject: X500Principal,
email: String,
keyPair: KeyPair,
signatureScheme: SignatureScheme): PKCS10CertificationRequest {
signatureScheme: SignatureScheme,
certRole: CertRole): PKCS10CertificationRequest {
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public)
.addAttribute(BCStyle.E, DERUTF8String(email))
.addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole)
.build(signer)
}
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest {
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME, certRole)
}
fun buildCertPath(first: X509Certificate, remaining: List<X509Certificate>): CertPath {
@ -287,19 +291,24 @@ object X509Utilities {
}
}
// Assuming cert type to role is 1:1
val CertRole.certificateType: CertificateType get() = CertificateType.values().first { it.role == this }
/**
* Convert a [X509Certificate] into Bouncycastle's [X509CertificateHolder].
*
* NOTE: To avoid unnecessary copying use [X509Certificate] where possible.
*/
fun X509Certificate.toBc() = X509CertificateHolder(encoded)
fun X509CertificateHolder.toJca(): X509Certificate = X509CertificateFactory().generateCertificate(encoded.inputStream())
val CertPath.x509Certificates: List<X509Certificate> get() {
val CertPath.x509Certificates: List<X509Certificate>
get() {
require(type == "X.509") { "Not an X.509 cert path: $this" }
// We're not mapping the list to avoid creating a new one.
return uncheckedCast(certificates)
}
}
val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certificate) { "Not an X.509 certificate: $this" }

View File

@ -87,10 +87,15 @@ class ConfigParsingTest {
@Test
fun CordaX500Name() {
val name1 = CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB")
testPropertyType<CordaX500NameData, CordaX500NameListData, CordaX500Name>(
CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"),
name1,
CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"),
valuesToString = true)
// Test with config object.
val config = config("value" to mapOf("organisation" to "Mock Party", "locality" to "London", "country" to "GB"))
assertThat(config.parseAs<CordaX500NameData>().value).isEqualTo(name1)
}
@Test
@ -273,6 +278,7 @@ class ConfigParsingTest {
data class OldData(
@OldConfig("oldValue")
val newValue: String)
data class DataWithCompanion(val value: Int) {
companion object {
@Suppress("unused")

View File

@ -2,7 +2,10 @@ package net.corda.node
import joptsimple.OptionParser
import joptsimple.util.EnumConverter
import joptsimple.util.PathConverter
import net.corda.core.internal.CertRole
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.parseAsNodeConfiguration
@ -10,8 +13,6 @@ import org.slf4j.event.Level
import java.io.PrintStream
import java.nio.file.Path
import java.nio.file.Paths
import java.text.SimpleDateFormat
import java.util.*
// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
class ArgsParser {
@ -35,9 +36,11 @@ class ArgsParser {
private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.")
private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.")
private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
private val networkRootTruststorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.")
private val networkRootTrustStorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.")
.withRequiredArg()
private val networkRootTruststorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
.withValuesConvertedBy(PathConverter())
.defaultsTo((Paths.get("certificates") / "network-root-truststore.jks"))
private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
.withRequiredArg()
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
@ -61,16 +64,23 @@ class ArgsParser {
val sshdServer = optionSet.has(sshdServerArg)
val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg)
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
val networkRootTruststorePath = optionSet.valueOf(networkRootTruststorePathArg)?.let { Paths.get(it).normalize().toAbsolutePath() }
val networkRootTruststorePassword = optionSet.valueOf(networkRootTruststorePasswordArg)
val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg)
val registrationConfig = if (isRegistration) {
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode." }
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
} else {
null
}
return CmdLineOptions(baseDirectory,
configFile,
help,
loggingLevel,
logToConsole,
isRegistration,
networkRootTruststorePath,
networkRootTruststorePassword,
registrationConfig,
isVersion,
noLocalShell,
sshdServer,
@ -81,14 +91,14 @@ class ArgsParser {
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
}
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
data class CmdLineOptions(val baseDirectory: Path,
val configFile: Path,
val help: Boolean,
val loggingLevel: Level,
val logToConsole: Boolean,
val isRegistration: Boolean,
val networkRootTruststorePath: Path?,
val networkRootTruststorePassword: String?,
val nodeRegistrationConfig: NodeRegistrationOption?,
val isVersion: Boolean,
val noLocalShell: Boolean,
val sshdServer: Boolean,
@ -96,10 +106,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val bootstrapRaftCluster: Boolean) {
fun loadConfig(): NodeConfiguration {
val config = ConfigHelper.loadConfig(baseDirectory, configFile).parseAsNodeConfiguration()
if (isRegistration) {
if (nodeRegistrationConfig != null) {
requireNotNull(config.compatibilityZoneURL) { "Compatibility Zone Url must be provided in registration mode." }
requireNotNull(networkRootTruststorePath) { "Network root trust store path must be provided in registration mode." }
requireNotNull(networkRootTruststorePassword) { "Network root trust store password must be provided in registration mode." }
}
return config
}

View File

@ -94,9 +94,9 @@ open class NodeStartup(val args: Array<String>) {
try {
banJavaSerialisation(conf)
preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
if (cmdlineOptions.nodeRegistrationConfig != null) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
registerWithNetwork(conf, cmdlineOptions.networkRootTruststorePath!!, cmdlineOptions.networkRootTruststorePassword!!)
registerWithNetwork(conf, cmdlineOptions.nodeRegistrationConfig)
return true
}
logStartupInfo(versionInfo, cmdlineOptions, conf)
@ -180,12 +180,7 @@ open class NodeStartup(val args: Array<String>) {
logger.info("Starting as node on ${conf.p2pAddress}")
}
private fun shouldRegisterWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration): Boolean {
val compatibilityZoneURL = conf.compatibilityZoneURL
return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null)
}
open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) {
open protected fun registerWithNetwork(conf: NodeConfiguration, nodeRegistrationConfig: NodeRegistrationOption) {
val compatibilityZoneURL = conf.compatibilityZoneURL!!
println()
println("******************************************************************")
@ -193,7 +188,7 @@ open class NodeStartup(val args: Array<String>) {
println("* Registering as a new participant with Corda network *")
println("* *")
println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore()
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore()
}
open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig()

View File

@ -3,7 +3,10 @@ package net.corda.node.utilities.registration
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
@ -22,10 +25,18 @@ import java.security.cert.X509Certificate
* Helper for managing the node registration process, which checks for any existing certificates and requests them if
* needed.
*/
class NetworkRegistrationHelper(private val config: NodeConfiguration,
class NetworkRegistrationHelper(private val config: SSLConfiguration,
private val myLegalName: CordaX500Name,
private val emailAddress: String,
private val certService: NetworkRegistrationService,
networkRootTrustStorePath: Path,
networkRootTruststorePassword: String) {
private val networkRootTrustStorePath: Path,
networkRootTrustStorePassword: String,
private val certRole: CertRole) {
// Constructor for corda node, cert role is restricted to [CertRole.NODE_CA].
constructor(config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) :
this(config, config.myLegalName, config.emailAddress, certService, regConfig.networkRootTrustStorePath, regConfig.networkRootTrustStorePassword, CertRole.NODE_CA)
private companion object {
const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key"
}
@ -41,7 +52,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
"$networkRootTrustStorePath does not exist. This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator."
}
rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTruststorePassword)
rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTrustStorePassword)
rootCert = rootTrustStore.getCertificate(CORDA_ROOT_CA)
}
@ -68,7 +79,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
// We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval.
if (SELF_SIGNED_PRIVATE_KEY !in nodeKeyStore) {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName.x500Principal, keyPair)
val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair)
// Save to the key store.
nodeKeyStore.setPrivateKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword)
nodeKeyStore.save()
@ -87,36 +98,59 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
throw certificateRequestException
}
val nodeCaCert = certificates[0]
val certificate = certificates.first()
val nodeCaSubject = try {
CordaX500Name.build(nodeCaCert.subjectX500Principal)
CordaX500Name.build(certificate.subjectX500Principal)
} catch (e: IllegalArgumentException) {
throw CertificateRequestException("Received node CA cert has invalid subject name: ${e.message}")
}
if (nodeCaSubject != config.myLegalName) {
if (nodeCaSubject != myLegalName) {
throw CertificateRequestException("Subject of received node CA cert doesn't match with node legal name: $nodeCaSubject")
}
val nodeCaCertRole = try {
CertRole.extract(nodeCaCert)
CertRole.extract(certificate)
} catch (e: IllegalArgumentException) {
throw CertificateRequestException("Unable to extract cert role from received node CA cert: ${e.message}")
}
if (nodeCaCertRole != CertRole.NODE_CA) {
throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole")
}
// Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server.
X509Utilities.validateCertificateChain(rootCert, certificates)
println("Certificate signing request approved, storing private key with the certificate chain.")
when (nodeCaCertRole) {
CertRole.NODE_CA -> {
// Save private key and certificate chain to the key store.
nodeKeyStore.setPrivateKey(CORDA_CLIENT_CA, keyPair.private, certificates, keyPassword = privateKeyPassword)
nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
nodeKeyStore.save()
println("Node private key and certificate stored in ${config.nodeKeystore}.")
config.loadSslKeyStore(createNew = true).update {
println("Generating SSL certificate for node messaging service.")
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val sslCert = X509Utilities.createCertificate(
CertificateType.TLS,
certificate,
keyPair,
myLegalName.x500Principal,
sslKeyPair.public)
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
}
println("SSL private key and certificate stored in ${config.sslKeystore}.")
}
// TODO: Fix this, this is not needed in corda node.
CertRole.SERVICE_IDENTITY -> {
// Only create keystore containing notary's key for service identity role.
nodeKeyStore.setPrivateKey("${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", keyPair.private, certificates, keyPassword = privateKeyPassword)
nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
nodeKeyStore.save()
println("Service identity private key and certificate stored in ${config.nodeKeystore}.")
}
else -> throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole")
}
// Save root certificates to trust store.
config.loadTrustStore(createNew = true).update {
println("Generating trust store for corda node.")
@ -124,20 +158,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
setCertificate(CORDA_ROOT_CA, certificates.last())
}
println("Node trust store stored in ${config.trustStoreFile}.")
config.loadSslKeyStore(createNew = true).update {
println("Generating SSL certificate for node messaging service.")
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val sslCert = X509Utilities.createCertificate(
CertificateType.TLS,
nodeCaCert,
keyPair,
config.myLegalName.x500Principal,
sslKeyPair.public)
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
}
println("SSL private key and certificate stored in ${config.sslKeystore}.")
// All done, clean up temp files.
requestIdStore.deleteIfExists()
}
@ -169,15 +189,15 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String {
// Retrieve request id from file if exists, else post a request to server.
return if (!requestIdStore.exists()) {
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName.x500Principal, config.emailAddress, keyPair)
val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, keyPair, certRole)
val writer = StringWriter()
JcaPEMWriter(writer).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded))
}
println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.")
println()
println("Legal Name: ${config.myLegalName}")
println("Email: ${config.emailAddress}")
println("Legal Name: $myLegalName")
println("Email: $emailAddress")
println()
println("Public Key: ${keyPair.public}")
println()

View File

@ -2,12 +2,14 @@ package net.corda.node
import joptsimple.OptionException
import net.corda.core.internal.div
import net.corda.nodeapi.internal.crypto.X509KeyStore
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
import org.slf4j.event.Level
import java.nio.file.Paths
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class ArgsParserTest {
private val parser = ArgsParser()
@ -21,14 +23,12 @@ class ArgsParserTest {
help = false,
logToConsole = false,
loggingLevel = Level.INFO,
isRegistration = false,
nodeRegistrationConfig = null,
isVersion = false,
noLocalShell = false,
sshdServer = false,
justGenerateNodeInfo = false,
bootstrapRaftCluster = false,
networkRootTruststorePassword = null,
networkRootTruststorePath = null))
bootstrapRaftCluster = false))
}
@Test
@ -113,11 +113,17 @@ class ArgsParserTest {
@Test
fun `initial-registration`() {
val truststorePath = Paths.get("truststore") / "file.jks"
val truststorePath = workingDirectory / "truststore" / "file.jks"
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
}.withMessageContaining("Network root trust store path").withMessageContaining("doesn't exist")
X509KeyStore.fromFile(truststorePath, "dummy_password", createNew = true)
val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
assertThat(cmdLineOptions.isRegistration).isTrue()
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.networkRootTruststorePath)
assertEquals("password-test", cmdLineOptions.networkRootTruststorePassword)
assertNotNull(cmdLineOptions.nodeRegistrationConfig)
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePath)
assertEquals("password-test", cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePassword)
}
@Test

View File

@ -13,7 +13,9 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.x500Name
import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
@ -141,6 +143,38 @@ class NetworkRegistrationHelperTest {
}.isInstanceOf(CertPathValidatorException::class.java)
}
@Test
fun `create service identity cert`() {
assertThat(config.nodeKeystore).doesNotExist()
assertThat(config.sslKeystore).doesNotExist()
assertThat(config.trustStoreFile).doesNotExist()
val serviceIdentityCertPath = createServiceIdentityCertPath()
saveNetworkTrustStore(serviceIdentityCertPath.last())
createRegistrationHelper(serviceIdentityCertPath).buildKeystore()
val nodeKeystore = config.loadNodeKeyStore()
val trustStore = config.loadTrustStore()
assertThat(config.sslKeystore).doesNotExist()
val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key"
nodeKeystore.run {
assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA))
assertFalse(contains(X509Utilities.CORDA_ROOT_CA))
assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS))
assertFalse(contains(X509Utilities.CORDA_CLIENT_CA))
assertThat(getCertificateChain(serviceIdentityAlias)).containsExactlyElementsOf(serviceIdentityCertPath)
}
trustStore.run {
assertFalse(contains(X509Utilities.CORDA_CLIENT_CA))
assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA))
assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(serviceIdentityCertPath.last())
}
}
private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA,
legalName: CordaX500Name = nodeLegalName): List<X509Certificate> {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
@ -156,12 +190,25 @@ class NetworkRegistrationHelperTest {
return listOf(nodeCaCert, intermediateCa.certificate, rootCa.certificate)
}
private fun createServiceIdentityCertPath(type: CertificateType = CertificateType.SERVICE_IDENTITY,
legalName: CordaX500Name = nodeLegalName): List<X509Certificate> {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
val keyPair = Crypto.generateKeyPair()
val serviceIdentityCert = X509Utilities.createCertificate(
type,
intermediateCa.certificate,
intermediateCa.keyPair,
legalName.x500Principal,
keyPair.public)
return listOf(serviceIdentityCert, intermediateCa.certificate, rootCa.certificate)
}
private fun createRegistrationHelper(response: List<X509Certificate>): NetworkRegistrationHelper {
val certService = rigorousMock<NetworkRegistrationService>().also {
doReturn(requestId).whenever(it).submitRequest(any())
doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId))
}
return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)
return NetworkRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword))
}
private fun saveNetworkTrustStore(rootCert: X509Certificate) {

View File

@ -35,6 +35,7 @@ include 'network-management'
include 'network-management:capsule'
include 'network-management:capsule-hsm'
include 'network-management:capsule-hsm-cert-generator'
include 'network-management:registration-tool'
include 'tools:jmeter'
include 'tools:explorer'
include 'tools:explorer:capsule'

View File

@ -21,6 +21,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
import net.corda.node.NodeRegistrationOption
import net.corda.node.internal.Node
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.StartedNode
@ -250,7 +251,7 @@ class DriverDSLImpl(
return if (startNodesInProcess) {
executorService.fork {
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), rootTruststorePath, rootTruststorePassword).buildKeystore()
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)).buildKeystore()
config
}
} else {