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 {