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-hook_test" target="1.8" />
<module name="quasar-utils_main" target="1.8" /> <module name="quasar-utils_main" target="1.8" />
<module name="quasar-utils_test" 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_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" /> <module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" 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 com.r3.corda.networkmanage.hsm.signer.HsmSigner
import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name.Companion.parse import net.corda.core.identity.CordaX500Name.Companion.parse
import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.crypto.CertificateType 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.DEFAULT_TLS_SIGNATURE_SCHEME
import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificateSigningRequest import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificateSigningRequest
import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
class HsmSigningServiceTest : HsmBaseTest() { class HsmSigningServiceTest : HsmBaseTest() {
@Before @Before
fun setUp() { fun setUp() {
loadOrCreateKeyStore(rootKeyStoreFile, TRUSTSTORE_PASSWORD) loadOrCreateKeyStore(rootKeyStoreFile, TRUSTSTORE_PASSWORD)
} }
@Test @Test
fun `HSM signing service can sign CSR data`() { fun `HSM signing service can sign node CSR data`() {
// when root cert is created setupCertificates()
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
))
// given authenticated user // given authenticated user
val userInput = givenHsmUserAuthenticationInput() val userInput = givenHsmUserAuthenticationInput()
@ -92,31 +75,59 @@ class HsmSigningServiceTest : HsmBaseTest() {
assertNotNull(toSign.certPath) assertNotNull(toSign.certPath)
val certificates = toSign.certPath!!.certificates val certificates = toSign.certPath!!.certificates
assertEquals(3, certificates.size) 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 @Test
fun `HSM signing service can sign and serialize network map data to the Doorman DB`() { fun `HSM signing service can sign and serialize network map data to the Doorman DB`() {
// when root cert is created setupCertificates()
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
))
// given authenticated user // given authenticated user
val userInput = givenHsmUserAuthenticationInput() val userInput = givenHsmUserAuthenticationInput()
@ -150,4 +161,28 @@ class HsmSigningServiceTest : HsmBaseTest() {
assertEquals(networkMapParameters.serialize().hash, persistedNetworkMap.networkParameterHash) assertEquals(networkMapParameters.serialize().hash, persistedNetworkMap.networkParameterHash)
assertThat(persistedNetworkMap.nodeInfoHashes).isEmpty() 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.internal.uncheckedCast
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.node.utilities.registration.NetworkRegistrationHelper
@ -137,7 +138,8 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any()) doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any())
doReturn(sslKeyStore).whenever(it).loadSslKeyStore(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()) 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.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity 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 com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.Crypto.toSupportedPublicKey import net.corda.core.crypto.Crypto.toSupportedPublicKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name 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.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.hibernate.Session
import java.security.cert.CertPath import java.security.cert.CertPath
import java.time.Instant import java.time.Instant
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@ -19,6 +20,11 @@ import javax.security.auth.x500.X500Principal
* Database implementation of the [CertificationRequestStorage] interface. * Database implementation of the [CertificationRequestStorage] interface.
*/ */
class PersistentCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { 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>) { override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String>) {
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
@ -43,16 +49,28 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
override fun saveRequest(request: PKCS10CertificationRequest): String { override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = SecureHash.randomSHA256().toString() val requestId = SecureHash.randomSHA256().toString()
database.transaction(TransactionIsolationLevel.SERIALIZABLE) { database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val (legalName, rejectReason) = parseAndValidateLegalName(request, session) val requestEntity = try {
session.save(CertificateSigningRequestEntity( val legalName = validateRequestAndParseLegalName(request)
requestId = requestId, CertificateSigningRequestEntity(
legalName = legalName, requestId = requestId,
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(), legalName = legalName,
requestBytes = request.encoded, publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
remark = rejectReason, requestBytes = request.encoded,
modifiedBy = emptyList(), 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 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 // It's important that we always use the toString() output of CordaX500Name as it standardises the string format
// to make querying possible. // to make querying possible.
val legalName = try { val legalName = try {
CordaX500Name.build(X500Principal(request.subject.encoded)).toString() CordaX500Name.build(X500Principal(request.subject.encoded))
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
return Pair(request.subject.toString(), "Name validation failed: ${e.message}") throw RequestValidationException(request.subject.toString(), "Name validation failed: ${e.message}")
} }
return when {
val duplicateNameQuery = session.criteriaBuilder.run { // Check if requested role is valid.
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) request.getCertRole() !in allowedCertRoles -> throw RequestValidationException(legalName.toString(), "Requested certificate role ${request.getCertRole()} is not allowed.")
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName))
}
}
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked // 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. // Also, at the moment we assume that once the CSR is approved it cannot be rejected.
// What if we approved something by mistake. // What if we approved something by mistake.
val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter { nonRejectedRequestExists(CertificateSigningRequestEntity::legalName.name, legalName.toString()) -> throw RequestValidationException(legalName.toString(), "Duplicate legal name")
it.status != RequestStatus.REJECTED //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") * Check if "non-rejected" request exists with provided column and value.
} */
private fun DatabaseTransaction.nonRejectedRequestExists(columnName: String, value: String): Boolean {
val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString() val query = session.criteriaBuilder.run {
val duplicatePkQuery = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run { 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 com.typesafe.config.ConfigFactory
import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser import joptsimple.OptionParser
import net.corda.core.CordaOID
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.CertRole
import net.corda.core.internal.SignedDataWithCert import net.corda.core.internal.SignedDataWithCert
import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv 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.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme 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.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
@ -76,3 +82,24 @@ private fun String.toCamelcase(): String {
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_")) CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
} else this } 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.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput 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.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject 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 canceledTransitionId: Int = -1
private var startProgressTransitionId: Int = -1 private var startProgressTransitionId: Int = -1
// TODO: Pass in a parsed object instead of raw PKCS10 request.
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) { fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request. // Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) { 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 // 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. // have to do it again here.
val subject = CordaX500Name.build(X500Principal(signingRequest.subject.encoded)) 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) val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode) .setProjectKey(projectCode)
.setDescription("Organisation: ${subject.organisation}\nNearest City: ${subject.locality}\nCountry: ${subject.country}\nEmail: $email\n\n{code}$request{code}") .setDescription(ticketDescription)
.setSummary(subject.organisation) .setSummary(ticketSummary)
.setFieldValue(requestIdField.id, requestId) .setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created. // This will block until the issue is created.
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() 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.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.persistence.RequestStatus import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.utils.CertPathAndKey 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.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities 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.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints 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. // We assume all attributes in the subject name has been checked prior approval.
// TODO: add validation to subject name. // TODO: add validation to subject name.
val request = JcaPKCS10CertificationRequest(certificationRequest) val request = JcaPKCS10CertificationRequest(certificationRequest)
val certRole = request.getCertRole()
val nameConstraints = NameConstraints( val nameConstraints = NameConstraints(
arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
arrayOf()) arrayOf())
val nodeCaCert = X509Utilities.createCertificate( val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA, certRole.certificateType,
csrCertPathAndKey.certPath[0], csrCertPathAndKey.certPath[0],
csrCertPathAndKey.toKeyPair(), csrCertPathAndKey.toKeyPair(),
X500Principal(request.subject.encoded), X500Principal(request.subject.encoded),

View File

@ -1,17 +1,20 @@
package com.r3.corda.networkmanage.hsm.signer 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.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage 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.createClientCertificate
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getAndInitializeKeyStore import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.retrieveCertAndKeyPair import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.retrieveCertAndKeyPair
import net.corda.core.internal.CertRole
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore 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_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_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.X509Utilities.buildCertPath
import net.corda.nodeapi.internal.crypto.certificateType
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.io.PrintStream 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...") logger.debug("Retrieving the doorman certificate $CORDA_INTERMEDIATE_CA from HSM...")
val doormanCertAndKey = retrieveCertAndKeyPair(CORDA_INTERMEDIATE_CA, keyStore) val doormanCertAndKey = retrieveCertAndKeyPair(CORDA_INTERMEDIATE_CA, keyStore)
toSign.forEach { toSign.forEach {
val certRole = it.request.getCertRole()
val nodeCaCert = createClientCertificate( val nodeCaCert = createClientCertificate(
CertificateType.NODE_CA, certRole.certificateType,
doormanCertAndKey, doormanCertAndKey,
it.request, it.request,
validDays, 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.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
@ -40,7 +41,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `valid request`() { fun `valid request`() {
val request = createRequest("LegalName").first val request = createRequest("LegalName", certRole = CertRole.NODE_CA).first
val requestId = storage.saveRequest(request) val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply { assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request, this.request) assertEquals(request, this.request)
@ -48,9 +49,29 @@ class PersistentCertificateRequestStorageTest : TestBase() {
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId) 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 @Test
fun `approve request`() { fun `approve request`() {
val (request, _) = createRequest("LegalName") val (request, _) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(request) val requestId = storage.saveRequest(request)
// Pending request should equals to 1. // Pending request should equals to 1.
@ -69,7 +90,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `sign request`() { fun `sign request`() {
val (csr, nodeKeyPair) = createRequest("LegalName") val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// New request should equals to 1. // New request should equals to 1.
@ -95,7 +116,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `sign request ignores subsequent sign requests`() { 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. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// Store certificate to DB. // Store certificate to DB.
@ -118,7 +139,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `sign request rejects requests with the same public key`() { 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. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// Store certificate to DB. // Store certificate to DB.
@ -132,7 +153,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
) )
// Sign certificate // Sign certificate
// When request with the same public key is requested // 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) val duplicateRequestId = storage.saveRequest(newCsr)
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty() assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
val duplicateRequest = storage.getRequest(duplicateRequestId) val duplicateRequest = storage.getRequest(duplicateRequestId)
@ -142,7 +163,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `reject request`() { 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!") storage.rejectRequest(requestId, DOORMAN_SIGNATURE, "Because I said so!")
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty() assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
assertThat(storage.getRequest(requestId)!!.remark).isEqualTo("Because I said so!") assertThat(storage.getRequest(requestId)!!.remark).isEqualTo("Because I said so!")
@ -150,9 +171,9 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `request with the same legal name as a pending request`() { 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) 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) assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId1)
assertEquals(RequestStatus.REJECTED, storage.getRequest(requestId2)!!.status) assertEquals(RequestStatus.REJECTED, storage.getRequest(requestId2)!!.status)
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate") assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
@ -164,16 +185,16 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test @Test
fun `request with the same legal name as a previously approved request`() { 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.markRequestTicketCreated(requestId1)
storage.approveRequest(requestId1, DOORMAN_SIGNATURE) 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") assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
} }
@Test @Test
fun `request with the same legal name as a previously signed request`() { 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) val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE) storage.approveRequest(requestId, DOORMAN_SIGNATURE)
@ -183,15 +204,15 @@ class PersistentCertificateRequestStorageTest : TestBase() {
generateSignedCertPath(csr, nodeKeyPair), generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE) 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") assertThat(storage.getRequest(rejectedRequestId)!!.remark).containsIgnoringCase("duplicate")
} }
@Test @Test
fun `request with the same legal name as a previously rejected request`() { 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!") 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) assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId2)
storage.markRequestTicketCreated(requestId2) storage.markRequestTicketCreated(requestId2)
storage.approveRequest(requestId2, DOORMAN_SIGNATURE) storage.approveRequest(requestId2, DOORMAN_SIGNATURE)
@ -204,7 +225,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
val approver = "APPROVER" val approver = "APPROVER"
// when // when
val requestId = storage.saveRequest(createRequest("BankA").first) val requestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, approver) 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> { 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) val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair, certRole = certRole)
return Pair(request, keyPair) // 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.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
@ -132,7 +133,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
internal fun createValidSignedNodeInfo(organisation: String, internal fun createValidSignedNodeInfo(organisation: String,
storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> { storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
val (csr, nodeKeyPair) = createRequest(organisation) val (csr, nodeKeyPair) = createRequest(organisation, certRole = CertRole.NODE_CA)
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, "TestUser") 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.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.persistence.RequestStatus import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.utils.CertPathAndKey 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.crypto.Crypto
import net.corda.core.internal.CertRole import net.corda.core.internal.CertRole
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
@ -83,6 +82,49 @@ class DefaultCsrHandlerTest : TestBase() {
assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA) assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA)
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo)) assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB")) 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 package net.corda.nodeapi.internal.config
import com.typesafe.config.Config import com.typesafe.config.*
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigUtil
import com.typesafe.config.ConfigValueFactory
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.noneOrSingle import net.corda.core.internal.noneOrSingle
import net.corda.core.internal.uncheckedCast 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)) NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path))
Path::class -> Paths.get(getString(path)) Path::class -> Paths.get(getString(path))
URL::class -> URL(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() Properties::class -> getConfig(path).toProperties()
Config::class -> getConfig(path) Config::class -> getConfig(path)
else -> if (typeClass.java.isEnum) { else -> if (typeClass.java.isEnum) {

View File

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

View File

@ -87,10 +87,15 @@ class ConfigParsingTest {
@Test @Test
fun CordaX500Name() { fun CordaX500Name() {
val name1 = CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB")
testPropertyType<CordaX500NameData, CordaX500NameListData, CordaX500Name>( testPropertyType<CordaX500NameData, CordaX500NameListData, CordaX500Name>(
CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), name1,
CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"),
valuesToString = true) 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 @Test
@ -273,6 +278,7 @@ class ConfigParsingTest {
data class OldData( data class OldData(
@OldConfig("oldValue") @OldConfig("oldValue")
val newValue: String) val newValue: String)
data class DataWithCompanion(val value: Int) { data class DataWithCompanion(val value: Int) {
companion object { companion object {
@Suppress("unused") @Suppress("unused")

View File

@ -2,7 +2,10 @@ package net.corda.node
import joptsimple.OptionParser import joptsimple.OptionParser
import joptsimple.util.EnumConverter 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.div
import net.corda.core.internal.exists
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.node.services.config.parseAsNodeConfiguration
@ -10,8 +13,6 @@ import org.slf4j.event.Level
import java.io.PrintStream import java.io.PrintStream
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths 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. // NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
class ArgsParser { class ArgsParser {
@ -35,9 +36,11 @@ class ArgsParser {
private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.") 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 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 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() .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() .withRequiredArg()
private val isVersionArg = optionParser.accepts("version", "Print the version and exit") private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
@ -61,16 +64,23 @@ class ArgsParser {
val sshdServer = optionSet.has(sshdServerArg) val sshdServer = optionSet.has(sshdServerArg)
val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg)
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg) val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
val networkRootTruststorePath = optionSet.valueOf(networkRootTruststorePathArg)?.let { Paths.get(it).normalize().toAbsolutePath() } val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
val networkRootTruststorePassword = optionSet.valueOf(networkRootTruststorePasswordArg) 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, return CmdLineOptions(baseDirectory,
configFile, configFile,
help, help,
loggingLevel, loggingLevel,
logToConsole, logToConsole,
isRegistration, registrationConfig,
networkRootTruststorePath,
networkRootTruststorePassword,
isVersion, isVersion,
noLocalShell, noLocalShell,
sshdServer, sshdServer,
@ -81,14 +91,14 @@ class ArgsParser {
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
} }
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
data class CmdLineOptions(val baseDirectory: Path, data class CmdLineOptions(val baseDirectory: Path,
val configFile: Path, val configFile: Path,
val help: Boolean, val help: Boolean,
val loggingLevel: Level, val loggingLevel: Level,
val logToConsole: Boolean, val logToConsole: Boolean,
val isRegistration: Boolean, val nodeRegistrationConfig: NodeRegistrationOption?,
val networkRootTruststorePath: Path?,
val networkRootTruststorePassword: String?,
val isVersion: Boolean, val isVersion: Boolean,
val noLocalShell: Boolean, val noLocalShell: Boolean,
val sshdServer: Boolean, val sshdServer: Boolean,
@ -96,10 +106,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val bootstrapRaftCluster: Boolean) { val bootstrapRaftCluster: Boolean) {
fun loadConfig(): NodeConfiguration { fun loadConfig(): NodeConfiguration {
val config = ConfigHelper.loadConfig(baseDirectory, configFile).parseAsNodeConfiguration() 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(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 return config
} }

View File

@ -94,9 +94,9 @@ open class NodeStartup(val args: Array<String>) {
try { try {
banJavaSerialisation(conf) banJavaSerialisation(conf)
preNetworkRegistration(conf) preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { if (cmdlineOptions.nodeRegistrationConfig != null) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] // 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 return true
} }
logStartupInfo(versionInfo, cmdlineOptions, conf) logStartupInfo(versionInfo, cmdlineOptions, conf)
@ -180,12 +180,7 @@ open class NodeStartup(val args: Array<String>) {
logger.info("Starting as node on ${conf.p2pAddress}") logger.info("Starting as node on ${conf.p2pAddress}")
} }
private fun shouldRegisterWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration): Boolean { open protected fun registerWithNetwork(conf: NodeConfiguration, nodeRegistrationConfig: NodeRegistrationOption) {
val compatibilityZoneURL = conf.compatibilityZoneURL
return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null)
}
open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) {
val compatibilityZoneURL = conf.compatibilityZoneURL!! val compatibilityZoneURL = conf.compatibilityZoneURL!!
println() println()
println("******************************************************************") println("******************************************************************")
@ -193,7 +188,7 @@ open class NodeStartup(val args: Array<String>) {
println("* Registering as a new participant with Corda network *") println("* Registering as a new participant with Corda network *")
println("* *") println("* *")
println("******************************************************************") println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore() NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore()
} }
open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig() 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.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration 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.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities 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 * Helper for managing the node registration process, which checks for any existing certificates and requests them if
* needed. * 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, private val certService: NetworkRegistrationService,
networkRootTrustStorePath: Path, private val networkRootTrustStorePath: Path,
networkRootTruststorePassword: String) { 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 { private companion object {
const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" 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. " + "$networkRootTrustStorePath does not exist. This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator." "Please contact your CZ operator."
} }
rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTruststorePassword) rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTrustStorePassword)
rootCert = rootTrustStore.getCertificate(CORDA_ROOT_CA) 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. // 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) { if (SELF_SIGNED_PRIVATE_KEY !in nodeKeyStore) {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) 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. // Save to the key store.
nodeKeyStore.setPrivateKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) nodeKeyStore.setPrivateKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword)
nodeKeyStore.save() nodeKeyStore.save()
@ -87,36 +98,59 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
throw certificateRequestException throw certificateRequestException
} }
val nodeCaCert = certificates[0] val certificate = certificates.first()
val nodeCaSubject = try { val nodeCaSubject = try {
CordaX500Name.build(nodeCaCert.subjectX500Principal) CordaX500Name.build(certificate.subjectX500Principal)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
throw CertificateRequestException("Received node CA cert has invalid subject name: ${e.message}") 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") throw CertificateRequestException("Subject of received node CA cert doesn't match with node legal name: $nodeCaSubject")
} }
val nodeCaCertRole = try { val nodeCaCertRole = try {
CertRole.extract(nodeCaCert) CertRole.extract(certificate)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
throw CertificateRequestException("Unable to extract cert role from received node CA cert: ${e.message}") 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. // 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) X509Utilities.validateCertificateChain(rootCert, certificates)
println("Certificate signing request approved, storing private key with the certificate chain.") println("Certificate signing request approved, storing private key with the certificate chain.")
// 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}.")
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. // Save root certificates to trust store.
config.loadTrustStore(createNew = true).update { config.loadTrustStore(createNew = true).update {
println("Generating trust store for corda node.") println("Generating trust store for corda node.")
@ -124,20 +158,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
setCertificate(CORDA_ROOT_CA, certificates.last()) setCertificate(CORDA_ROOT_CA, certificates.last())
} }
println("Node trust store stored in ${config.trustStoreFile}.") 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. // All done, clean up temp files.
requestIdStore.deleteIfExists() requestIdStore.deleteIfExists()
} }
@ -169,15 +189,15 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String { private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String {
// Retrieve request id from file if exists, else post a request to server. // Retrieve request id from file if exists, else post a request to server.
return if (!requestIdStore.exists()) { 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() val writer = StringWriter()
JcaPEMWriter(writer).use { JcaPEMWriter(writer).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) 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("Certificate signing request with the following information will be submitted to the Corda certificate signing server.")
println() println()
println("Legal Name: ${config.myLegalName}") println("Legal Name: $myLegalName")
println("Email: ${config.emailAddress}") println("Email: $emailAddress")
println() println()
println("Public Key: ${keyPair.public}") println("Public Key: ${keyPair.public}")
println() println()

View File

@ -2,12 +2,14 @@ package net.corda.node
import joptsimple.OptionException import joptsimple.OptionException
import net.corda.core.internal.div 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.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test import org.junit.Test
import org.slf4j.event.Level import org.slf4j.event.Level
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class ArgsParserTest { class ArgsParserTest {
private val parser = ArgsParser() private val parser = ArgsParser()
@ -21,14 +23,12 @@ class ArgsParserTest {
help = false, help = false,
logToConsole = false, logToConsole = false,
loggingLevel = Level.INFO, loggingLevel = Level.INFO,
isRegistration = false, nodeRegistrationConfig = null,
isVersion = false, isVersion = false,
noLocalShell = false, noLocalShell = false,
sshdServer = false, sshdServer = false,
justGenerateNodeInfo = false, justGenerateNodeInfo = false,
bootstrapRaftCluster = false, bootstrapRaftCluster = false))
networkRootTruststorePassword = null,
networkRootTruststorePath = null))
} }
@Test @Test
@ -113,11 +113,17 @@ class ArgsParserTest {
@Test @Test
fun `initial-registration`() { 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") val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
assertThat(cmdLineOptions.isRegistration).isTrue() assertNotNull(cmdLineOptions.nodeRegistrationConfig)
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.networkRootTruststorePath) assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePath)
assertEquals("password-test", cmdLineOptions.networkRootTruststorePassword) assertEquals("password-test", cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePassword)
} }
@Test @Test

View File

@ -13,7 +13,9 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.x500Name import net.corda.core.internal.x500Name
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration 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.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
@ -141,6 +143,38 @@ class NetworkRegistrationHelperTest {
}.isInstanceOf(CertPathValidatorException::class.java) }.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, private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA,
legalName: CordaX500Name = nodeLegalName): List<X509Certificate> { legalName: CordaX500Name = nodeLegalName): List<X509Certificate> {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
@ -156,12 +190,25 @@ class NetworkRegistrationHelperTest {
return listOf(nodeCaCert, intermediateCa.certificate, rootCa.certificate) 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 { private fun createRegistrationHelper(response: List<X509Certificate>): NetworkRegistrationHelper {
val certService = rigorousMock<NetworkRegistrationService>().also { val certService = rigorousMock<NetworkRegistrationService>().also {
doReturn(requestId).whenever(it).submitRequest(any()) doReturn(requestId).whenever(it).submitRequest(any())
doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId)) 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) { private fun saveNetworkTrustStore(rootCert: X509Certificate) {

View File

@ -35,6 +35,7 @@ include 'network-management'
include 'network-management:capsule' include 'network-management:capsule'
include 'network-management:capsule-hsm' include 'network-management:capsule-hsm'
include 'network-management:capsule-hsm-cert-generator' include 'network-management:capsule-hsm-cert-generator'
include 'network-management:registration-tool'
include 'tools:jmeter' include 'tools:jmeter'
include 'tools:explorer' include 'tools:explorer'
include 'tools:explorer:capsule' 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.contextLogger
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis import net.corda.core.utilities.millis
import net.corda.node.NodeRegistrationOption
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.NodeStartup import net.corda.node.internal.NodeStartup
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
@ -250,7 +251,7 @@ class DriverDSLImpl(
return if (startNodesInProcess) { return if (startNodesInProcess) {
executorService.fork { executorService.fork {
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), rootTruststorePath, rootTruststorePassword).buildKeystore() NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)).buildKeystore()
config config
} }
} else { } else {