mirror of
https://github.com/corda/corda.git
synced 2025-01-14 08:49:47 +00:00
ENT-1443 Add cert role to CSR and doorman issue cert according to the cert role (#431)
* Doorman and HSM create certificate base on requested cert role specified in the certificate signing request.
This commit is contained in:
parent
81801d4566
commit
94f73920cc
3
.idea/compiler.xml
generated
3
.idea/compiler.xml
generated
@ -134,6 +134,9 @@
|
||||
<module name="quasar-hook_test" target="1.8" />
|
||||
<module name="quasar-utils_main" target="1.8" />
|
||||
<module name="quasar-utils_test" target="1.8" />
|
||||
<module name="registration-tool_integrationTest" target="1.8" />
|
||||
<module name="registration-tool_main" target="1.8" />
|
||||
<module name="registration-tool_test" target="1.8" />
|
||||
<module name="rpc_integrationTest" target="1.8" />
|
||||
<module name="rpc_main" target="1.8" />
|
||||
<module name="rpc_smokeTest" target="1.8" />
|
||||
|
54
network-management/registration-tool/README.md
Normal file
54
network-management/registration-tool/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
#Network Registration Tool
|
||||
|
||||
The network registration tool creates a CSR (Certificate Signing Request) and sent to compatibility zone doorman for approval.
|
||||
A keystore and a trust store will be created once the request is approved.
|
||||
|
||||
##Configuration file
|
||||
The tool creates the CSR using information provided by the config file, the path to the config file should be provided
|
||||
using the ``--config-file`` flag on start up.
|
||||
|
||||
The config file should contain the following parameters.
|
||||
|
||||
```
|
||||
Parameter Description
|
||||
--------- -----------
|
||||
legalName Legal name of the requester. It can be in form of X.500 string or CordaX500Name in typesafe config object format.
|
||||
|
||||
email Requester's e-mail address.
|
||||
|
||||
compatibilityZoneURL Compatibility zone URL.
|
||||
|
||||
networkRootTrustStorePath Path to the network root trust store.
|
||||
|
||||
certRole Requested cert role, it should be one of [NODE_CA, SERVICE_IDENTITY].
|
||||
|
||||
networkRootTrustStorePassword Network root trust store password, to be provided by the network operator. Optional, the tool will prompt for password input if not provided.
|
||||
|
||||
keyStorePassword Generated keystore's password. Optional, the tool will prompt for password input if not provided.
|
||||
|
||||
trustStorePassword Generated trust store's password. Optional, the tool will prompt for password input if not provided.
|
||||
```
|
||||
|
||||
Example config file
|
||||
```
|
||||
legalName {
|
||||
organisationUnit = "R3 Corda"
|
||||
organisation = "R3 LTD"
|
||||
locality = "London"
|
||||
country = "GB"
|
||||
}
|
||||
# legalName = "C=GB, L=London, O=R3 LTD, OU=R3 Corda"
|
||||
email = "test@email.com"
|
||||
compatibilityZoneURL = "http://doorman.url.com"
|
||||
networkRootTrustStorePath = "networkRootTrustStore.jks"
|
||||
certRole = "NODE_CA"
|
||||
|
||||
networkRootTrustStorePassword = "password"
|
||||
keyStorePassword = "password"
|
||||
trustStorePassword = "password"
|
||||
|
||||
```
|
||||
|
||||
##Running the registration tool
|
||||
|
||||
``java -jar registration-tool-<<version>>.jar --config-file <<config file path>>``
|
63
network-management/registration-tool/build.gradle
Normal file
63
network-management/registration-tool/build.gradle
Normal file
@ -0,0 +1,63 @@
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'us.kirchmeier.capsule'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
description 'Network registration tool'
|
||||
|
||||
version project(':network-management').version
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://ci-artifactory.corda.r3cev.com/artifactory/corda-dev'
|
||||
}
|
||||
maven {
|
||||
url 'http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases'
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
runtimeArtifacts.extendsFrom runtime
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
task buildRegistrationTool(type: FatCapsule, dependsOn: 'jar') {
|
||||
group = "build"
|
||||
applicationClass 'com.r3.corda.networkmanage.RegistrationToolKt'
|
||||
archiveName "registration-tool-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
systemProperties['visualvm.display.name'] = 'Corda Network Registration Tool'
|
||||
minJavaVersion = '1.8.0'
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':network-management:registration-tool').configurations.runtime,
|
||||
project(':network-management:registration-tool').jar
|
||||
)
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildRegistrationTool
|
||||
publish buildRegistrationTool
|
||||
}
|
||||
|
||||
jar {
|
||||
classifier "ignore"
|
||||
}
|
||||
|
||||
publish {
|
||||
name 'registration-tool'
|
||||
disableDefaultJar = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.util.PathConverter
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val optionParser = OptionParser()
|
||||
val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the registration config file")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter())
|
||||
|
||||
val baseDirArg = optionParser
|
||||
.accepts("baseDir", "The registration tool's base directory, default to current directory.")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter())
|
||||
.defaultsTo(Paths.get("."))
|
||||
|
||||
val optionSet = optionParser.parse(*args)
|
||||
val configFilePath = optionSet.valueOf(configFileArg)
|
||||
val baseDir = optionSet.valueOf(baseDirArg)
|
||||
|
||||
val config = ConfigFactory.parseFile(configFilePath.toFile(), ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
.resolve()
|
||||
.parseAs<RegistrationConfig>()
|
||||
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val keyStorePassword: String by lazy { config.keyStorePassword ?: readPassword("Node Keystore password:") }
|
||||
override val trustStorePassword: String by lazy { config.trustStorePassword ?: readPassword("Node TrustStore password:") }
|
||||
override val certificatesDirectory: Path = baseDir
|
||||
}
|
||||
|
||||
NetworkRegistrationHelper(sslConfig,
|
||||
config.legalName,
|
||||
config.email,
|
||||
HTTPNetworkRegistrationService(config.compatibilityZoneURL),
|
||||
config.networkRootTrustStorePath,
|
||||
config.networkRootTrustStorePassword ?: readPassword("Network trust root password:"), config.certRole).buildKeystore()
|
||||
}
|
||||
|
||||
fun readPassword(fmt: String): String {
|
||||
return if (System.console() != null) {
|
||||
String(System.console().readPassword(fmt))
|
||||
} else {
|
||||
print(fmt)
|
||||
readLine() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
data class RegistrationConfig(val legalName: CordaX500Name,
|
||||
val email: String,
|
||||
val compatibilityZoneURL: URL,
|
||||
val networkRootTrustStorePath: Path,
|
||||
val certRole: CertRole,
|
||||
val keyStorePassword: String?,
|
||||
val networkRootTrustStorePassword: String?,
|
||||
val trustStorePassword: String?)
|
@ -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"
|
@ -0,0 +1,44 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
|
||||
class RegistrationConfigTest {
|
||||
|
||||
@Test
|
||||
fun `parse config file correctly`() {
|
||||
val testConfig = """
|
||||
legalName {
|
||||
organisationUnit = "R3 Corda"
|
||||
organisation = "R3 LTD"
|
||||
locality = "London"
|
||||
country = "GB"
|
||||
}
|
||||
email = "test@email.com"
|
||||
compatibilityZoneURL = "http://doorman.url.com"
|
||||
networkRootTrustStorePath = "networkRootTrustStore.jks"
|
||||
certRole = "NODE_CA"
|
||||
|
||||
networkRootTrustStorePassword = "password"
|
||||
keyStorePassword = "password"
|
||||
trustStorePassword = "password"
|
||||
""".trimIndent()
|
||||
|
||||
val config = ConfigFactory.parseString(testConfig, ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
.resolve()
|
||||
.parseAs<RegistrationConfig>()
|
||||
|
||||
assertEquals(CertRole.NODE_CA, config.certRole)
|
||||
assertEquals(CordaX500Name.parse("OU=R3 Corda, O=R3 LTD, L=London, C=GB"), config.legalName)
|
||||
assertEquals("http://doorman.url.com", config.compatibilityZoneURL.toString())
|
||||
assertEquals("test@email.com", config.email)
|
||||
assertEquals(Paths.get("networkRootTrustStore.jks"), config.networkRootTrustStorePath)
|
||||
assertEquals("password", config.networkRootTrustStorePassword)
|
||||
}
|
||||
}
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,16 @@ package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||
import com.r3.corda.networkmanage.common.utils.getCertRole
|
||||
import com.r3.corda.networkmanage.common.utils.hashString
|
||||
import net.corda.core.crypto.Crypto.toSupportedPublicKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.hibernate.Session
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
import javax.security.auth.x500.X500Principal
|
||||
@ -19,6 +20,11 @@ import javax.security.auth.x500.X500Principal
|
||||
* Database implementation of the [CertificationRequestStorage] interface.
|
||||
*/
|
||||
class PersistentCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage {
|
||||
companion object {
|
||||
// TODO: make this configurable?
|
||||
private val allowedCertRoles = setOf(CertRole.NODE_CA, CertRole.SERVICE_IDENTITY)
|
||||
}
|
||||
|
||||
override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String>) {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
|
||||
@ -43,16 +49,28 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
||||
override fun saveRequest(request: PKCS10CertificationRequest): String {
|
||||
val requestId = SecureHash.randomSHA256().toString()
|
||||
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val (legalName, rejectReason) = parseAndValidateLegalName(request, session)
|
||||
session.save(CertificateSigningRequestEntity(
|
||||
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<String, String?> {
|
||||
private fun DatabaseTransaction.validateRequestAndParseLegalName(request: PKCS10CertificationRequest): String {
|
||||
// It's important that we always use the toString() output of CordaX500Name as it standardises the string format
|
||||
// to make querying possible.
|
||||
val legalName = try {
|
||||
CordaX500Name.build(X500Principal(request.subject.encoded)).toString()
|
||||
CordaX500Name.build(X500Principal(request.subject.encoded))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return Pair(request.subject.toString(), "Name validation failed: ${e.message}")
|
||||
throw RequestValidationException(request.subject.toString(), "Name validation failed: ${e.message}")
|
||||
}
|
||||
|
||||
val duplicateNameQuery = session.criteriaBuilder.run {
|
||||
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
|
||||
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
||||
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName))
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
// Check if requested role is valid.
|
||||
request.getCertRole() !in allowedCertRoles -> throw RequestValidationException(legalName.toString(), "Requested certificate role ${request.getCertRole()} is not allowed.")
|
||||
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
|
||||
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
|
||||
// What if we approved something by mistake.
|
||||
val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter {
|
||||
it.status != RequestStatus.REJECTED
|
||||
nonRejectedRequestExists(CertificateSigningRequestEntity::legalName.name, legalName.toString()) -> throw RequestValidationException(legalName.toString(), "Duplicate legal name")
|
||||
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
|
||||
nonRejectedRequestExists(CertificateSigningRequestEntity::publicKeyHash.name, toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()) -> throw RequestValidationException(legalName.toString(), "Duplicate public key")
|
||||
else -> legalName.toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (nameDuplicates.isNotEmpty()) {
|
||||
return Pair(legalName, "Duplicate legal name")
|
||||
}
|
||||
|
||||
val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()
|
||||
val duplicatePkQuery = session.criteriaBuilder.run {
|
||||
/**
|
||||
* Check if "non-rejected" request exists with provided column and value.
|
||||
*/
|
||||
private fun DatabaseTransaction.nonRejectedRequestExists(columnName: String, value: String): Boolean {
|
||||
val query = session.criteriaBuilder.run {
|
||||
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
|
||||
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
||||
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKey))
|
||||
val valueQuery = equal(get<String>(columnName), value)
|
||||
val statusQuery = notEqual(get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED)
|
||||
criteriaQuery.where(and(valueQuery, statusQuery))
|
||||
}
|
||||
}
|
||||
|
||||
//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.")
|
||||
}
|
@ -5,7 +5,9 @@ import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import joptsimple.ArgumentAcceptingOptionSpec
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.CordaOID
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
@ -16,6 +18,10 @@ import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
@ -76,3 +82,24 @@ private fun String.toCamelcase(): String {
|
||||
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
|
||||
} else this
|
||||
}
|
||||
|
||||
private fun PKCS10CertificationRequest.firstAttributeValue(identifier: ASN1ObjectIdentifier): ASN1Encodable? {
|
||||
return getAttributes(identifier).firstOrNull()?.attrValues?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract cert role from certificate signing request. Default to NODE_CA if not exist for backward compatibility.
|
||||
*/
|
||||
fun PKCS10CertificationRequest.getCertRole(): CertRole {
|
||||
// Default cert role to Node_CA for backward compatibility.
|
||||
val encoded = firstAttributeValue(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE))?.toASN1Primitive()?.encoded ?: return CertRole.NODE_CA
|
||||
return CertRole.getInstance(encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract email from certificate signing request.
|
||||
*/
|
||||
fun PKCS10CertificationRequest.getEmail(): String {
|
||||
// TODO: Add basic email check?
|
||||
return firstAttributeValue(BCStyle.E).toString()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -6,6 +6,7 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRe
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
@ -40,7 +41,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `valid request`() {
|
||||
val request = createRequest("LegalName").first
|
||||
val request = createRequest("LegalName", certRole = CertRole.NODE_CA).first
|
||||
val requestId = storage.saveRequest(request)
|
||||
assertNotNull(storage.getRequest(requestId)).apply {
|
||||
assertEquals(request, this.request)
|
||||
@ -48,9 +49,29 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `valid service identity request`() {
|
||||
val request = createRequest("LegalName", certRole = CertRole.SERVICE_IDENTITY).first
|
||||
val requestId = storage.saveRequest(request)
|
||||
assertNotNull(storage.getRequest(requestId)).apply {
|
||||
assertEquals(request, this.request)
|
||||
}
|
||||
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid cert role request`() {
|
||||
val request = createRequest("LegalName", certRole = CertRole.INTERMEDIATE_CA).first
|
||||
val requestId = storage.saveRequest(request)
|
||||
assertNotNull(storage.getRequest(requestId)).apply {
|
||||
assertEquals(request, this.request)
|
||||
}
|
||||
assertThat(storage.getRequests(RequestStatus.REJECTED).map { it.requestId }).containsOnly(requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approve request`() {
|
||||
val (request, _) = createRequest("LegalName")
|
||||
val (request, _) = createRequest("LegalName", certRole = CertRole.NODE_CA)
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(request)
|
||||
// Pending request should equals to 1.
|
||||
@ -69,7 +90,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `sign request`() {
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// New request should equals to 1.
|
||||
@ -95,7 +116,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `sign request ignores subsequent sign requests`() {
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// Store certificate to DB.
|
||||
@ -118,7 +139,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `sign request rejects requests with the same public key`() {
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// Store certificate to DB.
|
||||
@ -132,7 +153,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
)
|
||||
// Sign certificate
|
||||
// When request with the same public key is requested
|
||||
val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair)
|
||||
val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair, certRole = CertRole.NODE_CA)
|
||||
val duplicateRequestId = storage.saveRequest(newCsr)
|
||||
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
|
||||
val duplicateRequest = storage.getRequest(duplicateRequestId)
|
||||
@ -142,7 +163,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `reject request`() {
|
||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
storage.rejectRequest(requestId, DOORMAN_SIGNATURE, "Because I said so!")
|
||||
assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
|
||||
assertThat(storage.getRequest(requestId)!!.remark).isEqualTo("Because I said so!")
|
||||
@ -150,9 +171,9 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a pending request`() {
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId1)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId1)
|
||||
assertEquals(RequestStatus.REJECTED, storage.getRequest(requestId2)!!.status)
|
||||
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
|
||||
@ -164,16 +185,16 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a previously approved request`() {
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
storage.markRequestTicketCreated(requestId1)
|
||||
storage.approveRequest(requestId1, DOORMAN_SIGNATURE)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a previously signed request`() {
|
||||
val (csr, nodeKeyPair) = createRequest("BankA")
|
||||
val (csr, nodeKeyPair) = createRequest("BankA", certRole = CertRole.NODE_CA)
|
||||
val requestId = storage.saveRequest(csr)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
@ -183,15 +204,15 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
generateSignedCertPath(csr, nodeKeyPair),
|
||||
listOf(DOORMAN_SIGNATURE)
|
||||
)
|
||||
val rejectedRequestId = storage.saveRequest(createRequest("BankA").first)
|
||||
val rejectedRequestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
assertThat(storage.getRequest(rejectedRequestId)!!.remark).containsIgnoringCase("duplicate")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a previously rejected request`() {
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
storage.rejectRequest(requestId1, DOORMAN_SIGNATURE, "Because I said so!")
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
assertThat(storage.getRequests(RequestStatus.NEW).map { it.requestId }).containsOnly(requestId2)
|
||||
storage.markRequestTicketCreated(requestId2)
|
||||
storage.approveRequest(requestId2, DOORMAN_SIGNATURE)
|
||||
@ -204,7 +225,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
val approver = "APPROVER"
|
||||
|
||||
// when
|
||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||
val requestId = storage.saveRequest(createRequest("BankA", certRole = CertRole.NODE_CA).first)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, approver)
|
||||
|
||||
@ -242,7 +263,8 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
|
||||
return Pair(request, keyPair)
|
||||
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), certRole: CertRole): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair, certRole = certRole)
|
||||
// encode and decode the request to make sure class information (CertRole) etc are not passed into the test.
|
||||
return Pair(JcaPKCS10CertificationRequest(request.encoded), keyPair)
|
||||
}
|
@ -5,6 +5,7 @@ import com.r3.corda.networkmanage.common.utils.hashString
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
@ -132,7 +133,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
||||
|
||||
internal fun createValidSignedNodeInfo(organisation: String,
|
||||
storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
|
||||
val (csr, nodeKeyPair) = createRequest(organisation)
|
||||
val (csr, nodeKeyPair) = createRequest(organisation, certRole = CertRole.NODE_CA)
|
||||
val requestId = storage.saveRequest(csr)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, "TestUser")
|
||||
|
@ -8,7 +8,6 @@ import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
@ -83,6 +82,49 @@ class DefaultCsrHandlerTest : TestBase() {
|
||||
assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA)
|
||||
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
|
||||
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
|
||||
// Is CA
|
||||
assertThat(basicConstraints != -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process approved service identity request`() {
|
||||
val requests = (1..3).map {
|
||||
X509Utilities.createCertificateSigningRequest(
|
||||
X500Principal("O=Test$it,L=London,C=GB"),
|
||||
"my@email.com",
|
||||
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), certRole = CertRole.SERVICE_IDENTITY)
|
||||
}
|
||||
|
||||
val requestStorage: CertificationRequestStorage = mock {
|
||||
on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
|
||||
certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED)
|
||||
))
|
||||
}
|
||||
|
||||
val (rootCa, csrCa) = createDevIntermediateCaCertPath()
|
||||
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
|
||||
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
|
||||
|
||||
requestProcessor.processRequests()
|
||||
|
||||
val certPathCapture = argumentCaptor<CertPath>()
|
||||
|
||||
// Verify only the approved requests are taken
|
||||
verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED)
|
||||
verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
|
||||
|
||||
// Then make sure the generated node cert paths are correct
|
||||
certPathCapture.allValues.forEachIndexed { index, certPath ->
|
||||
X509Utilities.validateCertificateChain(rootCa.certificate, certPath.x509Certificates)
|
||||
assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate)
|
||||
(certPath.certificates[0] as X509Certificate).apply {
|
||||
assertThat(CertRole.extract(this)).isEqualTo(CertRole.SERVICE_IDENTITY)
|
||||
assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
|
||||
assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
|
||||
// Not a CA
|
||||
assertThat(basicConstraints == -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -262,13 +262,17 @@ object X509Utilities {
|
||||
private fun createCertificateSigningRequest(subject: X500Principal,
|
||||
email: String,
|
||||
keyPair: KeyPair,
|
||||
signatureScheme: SignatureScheme): PKCS10CertificationRequest {
|
||||
signatureScheme: SignatureScheme,
|
||||
certRole: CertRole): PKCS10CertificationRequest {
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
|
||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
|
||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public)
|
||||
.addAttribute(BCStyle.E, DERUTF8String(email))
|
||||
.addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole)
|
||||
.build(signer)
|
||||
}
|
||||
|
||||
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
|
||||
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest {
|
||||
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME, certRole)
|
||||
}
|
||||
|
||||
fun buildCertPath(first: X509Certificate, remaining: List<X509Certificate>): CertPath {
|
||||
@ -287,19 +291,24 @@ object X509Utilities {
|
||||
}
|
||||
}
|
||||
|
||||
// Assuming cert type to role is 1:1
|
||||
val CertRole.certificateType: CertificateType get() = CertificateType.values().first { it.role == this }
|
||||
|
||||
/**
|
||||
* Convert a [X509Certificate] into Bouncycastle's [X509CertificateHolder].
|
||||
*
|
||||
* NOTE: To avoid unnecessary copying use [X509Certificate] where possible.
|
||||
*/
|
||||
fun X509Certificate.toBc() = X509CertificateHolder(encoded)
|
||||
|
||||
fun X509CertificateHolder.toJca(): X509Certificate = X509CertificateFactory().generateCertificate(encoded.inputStream())
|
||||
|
||||
val CertPath.x509Certificates: List<X509Certificate> get() {
|
||||
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<X509Certificate>
|
||||
get() {
|
||||
require(type == "X.509") { "Not an X.509 cert path: $this" }
|
||||
// We're not mapping the list to avoid creating a new one.
|
||||
return uncheckedCast(certificates)
|
||||
}
|
||||
|
||||
val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certificate) { "Not an X.509 certificate: $this" }
|
||||
|
||||
|
@ -87,10 +87,15 @@ class ConfigParsingTest {
|
||||
|
||||
@Test
|
||||
fun CordaX500Name() {
|
||||
val name1 = CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB")
|
||||
testPropertyType<CordaX500NameData, CordaX500NameListData, CordaX500Name>(
|
||||
CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"),
|
||||
name1,
|
||||
CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"),
|
||||
valuesToString = true)
|
||||
|
||||
// Test with config object.
|
||||
val config = config("value" to mapOf("organisation" to "Mock Party", "locality" to "London", "country" to "GB"))
|
||||
assertThat(config.parseAs<CordaX500NameData>().value).isEqualTo(name1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -273,6 +278,7 @@ class ConfigParsingTest {
|
||||
data class OldData(
|
||||
@OldConfig("oldValue")
|
||||
val newValue: String)
|
||||
|
||||
data class DataWithCompanion(val value: Int) {
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -94,9 +94,9 @@ open class NodeStartup(val args: Array<String>) {
|
||||
try {
|
||||
banJavaSerialisation(conf)
|
||||
preNetworkRegistration(conf)
|
||||
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
|
||||
if (cmdlineOptions.nodeRegistrationConfig != null) {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
registerWithNetwork(conf, cmdlineOptions.networkRootTruststorePath!!, cmdlineOptions.networkRootTruststorePassword!!)
|
||||
registerWithNetwork(conf, cmdlineOptions.nodeRegistrationConfig)
|
||||
return true
|
||||
}
|
||||
logStartupInfo(versionInfo, cmdlineOptions, conf)
|
||||
@ -180,12 +180,7 @@ open class NodeStartup(val args: Array<String>) {
|
||||
logger.info("Starting as node on ${conf.p2pAddress}")
|
||||
}
|
||||
|
||||
private fun shouldRegisterWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration): Boolean {
|
||||
val compatibilityZoneURL = conf.compatibilityZoneURL
|
||||
return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null)
|
||||
}
|
||||
|
||||
open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) {
|
||||
open protected fun registerWithNetwork(conf: NodeConfiguration, nodeRegistrationConfig: NodeRegistrationOption) {
|
||||
val compatibilityZoneURL = conf.compatibilityZoneURL!!
|
||||
println()
|
||||
println("******************************************************************")
|
||||
@ -193,7 +188,7 @@ open class NodeStartup(val args: Array<String>) {
|
||||
println("* Registering as a new participant with Corda network *")
|
||||
println("* *")
|
||||
println("******************************************************************")
|
||||
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore()
|
||||
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore()
|
||||
}
|
||||
|
||||
open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -13,7 +13,9 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.x500Name
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
@ -141,6 +143,38 @@ class NetworkRegistrationHelperTest {
|
||||
}.isInstanceOf(CertPathValidatorException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create service identity cert`() {
|
||||
assertThat(config.nodeKeystore).doesNotExist()
|
||||
assertThat(config.sslKeystore).doesNotExist()
|
||||
assertThat(config.trustStoreFile).doesNotExist()
|
||||
|
||||
val serviceIdentityCertPath = createServiceIdentityCertPath()
|
||||
|
||||
saveNetworkTrustStore(serviceIdentityCertPath.last())
|
||||
createRegistrationHelper(serviceIdentityCertPath).buildKeystore()
|
||||
|
||||
val nodeKeystore = config.loadNodeKeyStore()
|
||||
val trustStore = config.loadTrustStore()
|
||||
assertThat(config.sslKeystore).doesNotExist()
|
||||
|
||||
val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key"
|
||||
|
||||
nodeKeystore.run {
|
||||
assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||
assertFalse(contains(X509Utilities.CORDA_ROOT_CA))
|
||||
assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS))
|
||||
assertFalse(contains(X509Utilities.CORDA_CLIENT_CA))
|
||||
assertThat(getCertificateChain(serviceIdentityAlias)).containsExactlyElementsOf(serviceIdentityCertPath)
|
||||
}
|
||||
|
||||
trustStore.run {
|
||||
assertFalse(contains(X509Utilities.CORDA_CLIENT_CA))
|
||||
assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||
assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(serviceIdentityCertPath.last())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA,
|
||||
legalName: CordaX500Name = nodeLegalName): List<X509Certificate> {
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
@ -156,12 +190,25 @@ class NetworkRegistrationHelperTest {
|
||||
return listOf(nodeCaCert, intermediateCa.certificate, rootCa.certificate)
|
||||
}
|
||||
|
||||
private fun createServiceIdentityCertPath(type: CertificateType = CertificateType.SERVICE_IDENTITY,
|
||||
legalName: CordaX500Name = nodeLegalName): List<X509Certificate> {
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
val keyPair = Crypto.generateKeyPair()
|
||||
val serviceIdentityCert = X509Utilities.createCertificate(
|
||||
type,
|
||||
intermediateCa.certificate,
|
||||
intermediateCa.keyPair,
|
||||
legalName.x500Principal,
|
||||
keyPair.public)
|
||||
return listOf(serviceIdentityCert, intermediateCa.certificate, rootCa.certificate)
|
||||
}
|
||||
|
||||
private fun createRegistrationHelper(response: List<X509Certificate>): NetworkRegistrationHelper {
|
||||
val certService = rigorousMock<NetworkRegistrationService>().also {
|
||||
doReturn(requestId).whenever(it).submitRequest(any())
|
||||
doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId))
|
||||
}
|
||||
return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)
|
||||
return NetworkRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword))
|
||||
}
|
||||
|
||||
private fun saveNetworkTrustStore(rootCert: X509Certificate) {
|
||||
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user