diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 3bae340100..6061214308 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -134,6 +134,9 @@
+
+
+
diff --git a/network-management/registration-tool/README.md b/network-management/registration-tool/README.md
new file mode 100644
index 0000000000..c64c110711
--- /dev/null
+++ b/network-management/registration-tool/README.md
@@ -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-<>.jar --config-file <>``
\ No newline at end of file
diff --git a/network-management/registration-tool/build.gradle b/network-management/registration-tool/build.gradle
new file mode 100644
index 0000000000..559c6bff55
--- /dev/null
+++ b/network-management/registration-tool/build.gradle
@@ -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}"
+}
diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt
new file mode 100644
index 0000000000..f33a3eb9e5
--- /dev/null
+++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt
@@ -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) {
+ 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()
+
+ 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?)
diff --git a/network-management/registration-tool/src/main/resources/registration.conf b/network-management/registration-tool/src/main/resources/registration.conf
new file mode 100644
index 0000000000..b9912d44fa
--- /dev/null
+++ b/network-management/registration-tool/src/main/resources/registration.conf
@@ -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"
diff --git a/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/RegistrationConfigTest.kt b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/RegistrationConfigTest.kt
new file mode 100644
index 0000000000..f0e1a2886b
--- /dev/null
+++ b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/RegistrationConfigTest.kt
@@ -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()
+
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmSigningServiceTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmSigningServiceTest.kt
index 3f035df612..2a0416c2d4 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmSigningServiceTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmSigningServiceTest.kt
@@ -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
+ ))
+ }
}
\ No newline at end of file
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
index f474603977..4f2279c288 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
@@ -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())
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
index 5a4b1011bf..532a67397f 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
@@ -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) {
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(
- 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
- ))
+ val requestEntity = try {
+ val legalName = validateRequestAndParseLegalName(request)
+ CertificateSigningRequestEntity(
+ requestId = requestId,
+ legalName = legalName,
+ publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
+ requestBytes = request.encoded,
+ modifiedBy = emptyList(),
+ 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 {
+ 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(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(CertificateSigningRequestEntity::publicKeyHash.name), publicKey))
+ val valueQuery = equal(get(columnName), value)
+ val statusQuery = notEqual(get(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED)
+ criteriaQuery.where(and(valueQuery, statusQuery))
}
}
-
- //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")
+ return session.createQuery(query).setMaxResults(1).resultList.isNotEmpty()
}
+
+ private class RequestValidationException(val parsedLegalName: String, val rejectMessage: String) : Exception("Validation failed for $parsedLegalName. $rejectMessage.")
}
\ No newline at end of file
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
index 5f0213acdd..8f26c97a09 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
@@ -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
@@ -75,4 +81,25 @@ private fun String.toCamelcase(): String {
return if (contains('_') || contains('-')) {
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
} else this
-}
\ No newline at end of file
+}
+
+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()
+}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
index 9cc722a325..5768b605a6 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
@@ -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()
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
index 2048459c75..791b139432 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
@@ -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),
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt
index f7c0771f00..52e8646641 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmCsrSigner.kt
@@ -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,
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
index 775a7c324c..13d76bb439 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
@@ -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 {
- 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 {
+ 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)
}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
index ee706d7a2f..6d4b2eaf97 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
@@ -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 {
- 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")
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt
index ba886744a6..24fcd7771b 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt
@@ -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()
+
+ // 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)
}
}
}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt
index 94997d34b8..544f1c1f7e 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt
@@ -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) {
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt
index 63d8d4fcf5..60620eb1b7 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt
@@ -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): 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 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 CertPath.x509Certificates: List
+ 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" }
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt
index a6fa2ca169..7fbc481321 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt
@@ -87,10 +87,15 @@ class ConfigParsingTest {
@Test
fun CordaX500Name() {
+ val name1 = CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB")
testPropertyType(
- 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().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")
diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt
index 5ac9cb01ef..63ed947943 100644
--- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt
+++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt
@@ -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
}
diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
index 0aef5f1f2d..7442049200 100644
--- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
@@ -94,9 +94,9 @@ open class NodeStartup(val args: Array) {
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) {
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) {
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()
diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt
index 287f4b41c9..1a253a701a 100644
--- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt
+++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt
@@ -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.")
- // 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.
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()
diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt
index 96f98d8c33..8ddb9707e9 100644
--- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt
+++ b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt
@@ -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
diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt
index 2371ab4437..d77e61bcae 100644
--- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt
+++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt
@@ -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 {
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 {
+ 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): NetworkRegistrationHelper {
val certService = rigorousMock().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) {
diff --git a/settings.gradle b/settings.gradle
index f9a1119927..aff7d45bc3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -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'
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
index 290c843a0c..787ce5dcad 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
@@ -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 {