Initial implementation of the certificate generation tool (#148)

* Initial implementation of the certificate generation tool

* Adding trust store persisting

* Addressing review comments

* Adding certificate type to the certificate generation process.

* Addressing review comments

* Fixing typos

* Changing keyOverride to 0 in examples and tests

* Addressing review comments + rebasing

* Adding CRL information to the certificate generation process

* Generation tool refactoring

* Addressing review comments
This commit is contained in:
Michal Kit 2018-01-23 11:50:03 +00:00 committed by GitHub
parent d2795954cb
commit 789ce5d44a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 798 additions and 199 deletions

View File

@ -0,0 +1,28 @@
HSM Certificate Generation Tool
===============================
The purpose of the HSM Certificate Generation Tool is to provide means for corda certificate hierarchy generation on an HSM infrastructure.
The tool is intended to be executed automatically, therefore no human interaction is required. All necessary data is fed
to the tool from the provided configuration file.
In the configuration file, the tool expects 2 main parts to be configured:
* HSM device location
* HSM user(s) credentials, that will be used to gain access to the HSM device and to its functionality related to key/certificate generation.
As such, it is assumed that the relevant privileged users are already set-up on the HSM prior to the script execution.
The tool supports multi-user authentication, so it works with more strict policies, where there is a need for distributed privileges.
* Certificate configuration.
The tool is designed to generate a single certificate in one execution. The type of the generated certificate is driven by the certificateType parameter.
Currently in Corda there is a 2-level certificate hierarchy composed of a root certificate and 2 intermediate certificates (one for CSR and one for network map/parameters signing).
Most attributes for those certificates are taken from the configuration file provided to the tool during the execution time. Some of them (e.g. aliases), however,
are fixed in the code due to compatibility aspects.
In addition to the certificate hierarchy creation on the HSM side, the tool stores the created root CA certificate in a trust store, which is intended for distribution across the nodes.
IMPORTANT NOTE:
===============
Caution is required when the tool is used, especially in case where certificate attributes needs to be altered.
Incorrect usage of the tool may result in existing certificates being overriden and possible network corruption.

View File

@ -0,0 +1,87 @@
Running the HSM Certificate Generation tool
===========================================
The purpose of this tool is to facilitate the process of certificate generation on the HSM infrastructure.
See :doc:`hsm-cert-generator` for more details.
See the Readme under ``network-management`` for detailed building instructions.
Configuration file
------------------
At startup, the HSM Certificate Generation Tool reads a configuration file, passed with ``--config-file`` on the command line.
This is an example of what a tool configuration file might look like:
.. literalinclude:: ../../network-management/generator.conf
Invoke doorman with ``-?`` for a full list of supported command-line arguments.
General configuration parameters
--------------------------------
Allowed parameters are:
:hsmHost: IP address of the HSM device.
:hsmPort: Port number of the HSM device.
:userConfigs: List of user authentication configurations. See below section on User Authentication Configuration.
:certConfig: Certificate specific configuration. See below section on Certificate Configuration.
:trustStoreDirectory: Path to the directory where the generated trust store should be placed.
The name of the generated file is "truststore.jks".
If the trust store file does not exist, it will be created.
IMPORTANT - This trust store is intended to be distributed across the nodes.
Nodes are hardcoded to use "truststore.jks" file as the trust store name.
As such, it is required that the file name is as the one expected by nodes.
:trustStorePassword: Password for the generated trust store.
Certificate Configuration
-------------------------
:certificateType: Type of the certificate to be created. Allowed values are:
ROOT_CA, INTERMEDIATE_CA, NETWORK_MAP.
:rootPrivateKeyPassword: Private key of the root certificate.
:privateKeyPassword: Private key password to be used during the key generation process.
:subject: X500Name formatted string to be used as the certificate public key subject.
:validDays: Days number for certificate validity.
:crlDistributionUrl: Url to the certificate revocation list of this certificate. If not defined the CRL information will not be added to the certificate.
:crlIssuer: X500 name of the certificate revocation list issuer - e.g. "L=London, C=GB, OU=Org Unit, CN=Service Name". If the crlDistributionUrl configuration option is specified but this parameter is not, then the certificate issuing authority is considered to be the CRL issuer for this certificate.
:keyCurve: Key algorithm curve type. See Utimaco supported values. "NIST-P256" has been used for experiments.
:keyExport: Enables key exporting. 1 for allow, 0 for deny.
:keyGenMechanism: HSM key generation process specific options. In the experiments the integer value being the logic OR of the two following (MECH_KEYGEN_UNCOMP = 4 or MECH_RND_REAL = 0) has been used. See Utimaco documentation for more details.
:keyOverride: Whether to override the key if already exists or not. 1 for override and 0 for NOT override.
:keySpecifier: This is an HSM specific parameter that corresponds to key name spacing. See Utimaco documentation for more details.
:keyGroup: This is an HSM specific parameter that corresponds to key name spacing. See Utimaco documentation for more details.
User Authentication Configuration
---------------------------------
Allowed parameters are:
:username: HSM username. This user needs to be allowed to generate keys/certificates and store them in HSM.
:authMode: One of the 2 possible authentication modes:
PASSWORD - User's password as set-up in the HSM
CARD_READER - Smart card reader authentication
KEY_FILE - Key file based authentication.
:authToken: Depending on the authMode it is either user's password or path to the authentication key file.
:keyFilePassword: Only relevant, if authMode == KEY_FILE. It is the key file password.

View File

@ -30,15 +30,20 @@ Allowed parameters are:
:keyGroup: HSM key group. This parameter is vendor specific (see Utimaco docs).
:keySpecifier: HSM key specifier. This parameter is vendor specific (see Utimaco docs). Default value: 1
:keySpecifier: HSM key specifier. This parameter is vendor specific (see Utimaco docs). Default value: 1.
:rootPrivateKeyPassword: Private key password for the root certificate.
:rootCertificateName: Root certificate name. Default value: "cordarootca"
:rootCertificateName: Root certificate name. Default value: "cordarootca".
:csrPrivateKeyPassword: Private key password for the intermediate certificate used to sign certficate signing requests.
:csrCertificateName: Certificate signing requests intermediate certificate name. Default value: "cordaintermediateca"
:csrCertificateName: Certificate signing requests intermediate certificate name. Default value: "cordaintermediateca".
:csrCertCrlDistPoint: Certificate revocation list location for the node CA certificate.
:csrCertCrlIssuer: Certificate revocation list issuer. The expected value is of the X500 name format - e.g. "L=London, C=GB, OU=Org Unit, CN=Service Name".
If not specified, the node CA certificate issuer is considered also as the CRL issuer.
:databaseProperties: Database properties.

View File

@ -36,6 +36,18 @@ For a list of options the HSM signing server takes, run with the `--help` option
java -jar capsule-hsm/build/libs/hsm-<version>.jar --help
```
## HSM Certificate Generator
To build a fat jar containing all the hsm certificate generator code you can simply invoke
```
./gradlew network-management:capsule-hsm-cert-generator:buildHsmCertGeneratorJAR
```
The built file will appear in
```
network-management/capsule-hsm-cert-generator/build/libs/hsm-cert-generator-<VERSION>.jar
```
#Configuring network management service
### Local signing
@ -135,7 +147,7 @@ networkMapConfig {
### 1. Create keystore for local signer
If local signer is enabled, the server will look for key stores in the certificate folder on start up.
If local signer is enabled, the server will look for key stores in the certificate folder on start up.
The key stores can be created using `--mode` flag.
```
java -jar doorman-<version>.jar --mode ROOT_KEYGEN
@ -146,11 +158,11 @@ networkMapConfig {
```
A trust store file containing the root trust certificate will be produced in the location `distribute-nodes / truststore.jks`
(relative to `rootStorePath`). `truststore.jks` must be copied to the `certificates` directory of each node before
(relative to `rootStorePath`). `truststore.jks` must be copied to the `certificates` directory of each node before
they attempt to register. The trust store password is `trustpass`.
### 2. Start Doorman service for notary registration
Start the network management server with the doorman service for initial bootstrapping. Network map service (`networkMapConfig`) should be **disabled** at this point.
Start the network management server with the doorman service for initial bootstrapping. Network map service (`networkMapConfig`) should be **disabled** at this point.
**Comment out** network map config in the config file and start the server by running :
```
java -jar doorman-<version>.jar
@ -185,4 +197,4 @@ A network parameters file is required to start the network map service for the f
We can now restart the network management server with both doorman and network map service.
```
java -jar doorman-<version>.jar --update-network-parameters network-parameters.conf
```
```

View File

@ -0,0 +1,39 @@
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'us.kirchmeier.capsule'
description 'HSM Certificate Generator'
version project(':network-management').version
configurations {
runtimeArtifacts.extendsFrom runtime
}
task buildHsmCertGeneratorJAR(type: FatCapsule, dependsOn: 'jar') {
applicationClass 'com.r3.corda.networkmanage.hsm.generator.MainKt'
archiveName "hsm-cert-generator-${version}.jar"
capsuleManifest {
applicationVersion = corda_release_version
systemProperties['visualvm.display.name'] = 'HSM Certificate Generator'
minJavaVersion = '1.8.0'
jvmArgs = ['-XX:+UseG1GC']
}
applicationSource = files(
project(':network-management').configurations.runtime,
project(':network-management').jar
)
}
artifacts {
runtimeArtifacts buildHsmCertGeneratorJAR
publish buildHsmCertGeneratorJAR
}
jar {
classifier "ignore"
}
publish {
name 'hsm-cert-generator'
disableDefaultJar = true
}

View File

@ -0,0 +1,30 @@
hsmHost = 127.0.0.1
hsmPort = 3001
trustStoreDirectory = "."
trustStorePassword = "trustpass"
certConfig {
subject = "CN=Corda Root, O=R3Cev, L=London, C=GB"
certificateType = ROOT_CA
privateKeyPassword = "PASSWORD"
rootPrivateKeyPassword = "PASSWORD"
validDays = 3650
keyOverride = 0
keyAlgorithm = 4
keyExport = 0
keyCurve = "NIST-P256"
keyGenMechanism = 4
keyGroup = "DEV.DOORMAN"
keySpecifier = 1
storeKeysExternal = false
crlDistributionUrl = "http://r3.com/revoked.crl"
crlIssuer = "CN=Corda Test, O=R3Cev, L=London, C=GB"
}
userConfigs = [
{
username = "INTEGRATION_TEST"
authMode = PASSWORD
authToken = "INTEGRATION_TEST"
}
]

View File

@ -7,6 +7,7 @@ rootCertificateName = "corda_root_ca"
rootPrivateKeyPassword = "Password"
csrPrivateKeyPassword = "Password"
csrCertificateName = "intermediate_ca"
csrCertCrlDistPoint = "http://test.com/revoked.crl"
networkMapCertificateName = "intermediate_ca"
networkMapPrivateKeyPassword = "Password"
validDays = 3650
@ -16,7 +17,6 @@ authKeyFilePath = "./Administrator.key"
authKeyFilePassword = "Password"
autoUsername = "AUTO_USER"
signInterval = 600
h2port = 0
dataSourceProperties {
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource

View File

@ -33,6 +33,7 @@ data class CryptoUserCredentials(val username: String, val password: String)
class HsmSimulator(private val serverAddress: String = DEFAULT_SERVER_ADDRESS,
private val imageRepoTag: String = DEFAULT_IMAGE_REPO_TAG,
private val imageVersion: String = DEFAULT_IMAGE_VERSION,
private val pullImage: Boolean = DEFAULT_PULL_IMAGE,
private val registryUser: String? = REGISTRY_USERNAME,
private val registryPass: String? = REGISTRY_PASSWORD) : ExternalResource() {
@ -40,6 +41,7 @@ class HsmSimulator(private val serverAddress: String = DEFAULT_SERVER_ADDRESS,
val DEFAULT_SERVER_ADDRESS = "corda.azurecr.io"
val DEFAULT_IMAGE_REPO_TAG = "corda.azurecr.io/network-management/hsm-simulator"
val DEFAULT_IMAGE_VERSION = "latest"
val DEFAULT_PULL_IMAGE = true
val HSM_SIMULATOR_PORT = "3001/tcp"
val CONTAINER_KILL_TIMEOUT_SECONDS = 10
@ -63,7 +65,10 @@ class HsmSimulator(private val serverAddress: String = DEFAULT_SERVER_ADDRESS,
override fun before() {
assumeFalse("Docker registry username is not set!. Skipping the test.", registryUser.isNullOrBlank())
assumeFalse("Docker registry password is not set!. Skipping the test.", registryPass.isNullOrBlank())
docker = DefaultDockerClient.fromEnv().build().pullHsmSimulatorImageFromRepository()
docker = DefaultDockerClient.fromEnv().build()
if (pullImage) {
docker.pullHsmSimulatorImageFromRepository()
}
containerId = docker.createContainer()
docker.startHsmSimulatorContainer()
}

View File

@ -0,0 +1,117 @@
package com.r3.corda.networkmanage.hsm
import com.r3.corda.networkmanage.HsmSimulator
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
import com.r3.corda.networkmanage.hsm.generator.*
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.security.cert.X509Certificate
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class HsmKeyGenerationTest {
companion object {
val KEY_PASSWORD = "PASSWORD"
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Rule
@JvmField
val hsmSimulator: HsmSimulator = HsmSimulator()
private val rootCertParameters: GeneratorParameters by lazy {
GeneratorParameters(
hsmHost = hsmSimulator.host,
hsmPort = hsmSimulator.port,
trustStoreDirectory = tempFolder.root.toPath(),
trustStorePassword = "",
userConfigs = listOf(UserAuthenticationParameters(
username = "INTEGRATION_TEST",
authMode = AuthMode.PASSWORD,
authToken = "INTEGRATION_TEST",
keyFilePassword = null
)),
certConfig = CertificateConfiguration(
keySpecifier = 1,
keyGroup = "DEV.DOORMAN",
storeKeysExternal = false,
privateKeyPassword = KEY_PASSWORD,
rootPrivateKeyPassword = KEY_PASSWORD,
subject = "CN=Corda Root, O=R3Cev, L=London, C=GB",
validDays = 3650,
keyCurve = "NIST-P256",
certificateType = CertificateType.ROOT_CA,
keyExport = 0,
keyGenMechanism = 4,
keyOverride = 0,
crlIssuer = null,
crlDistributionUrl = null
)
)
}
private val providerConfig: CryptoServerProviderConfig by lazy {
CryptoServerProviderConfig(
Device = "${rootCertParameters.hsmPort}@${rootCertParameters.hsmHost}",
KeySpecifier = rootCertParameters.certConfig.keySpecifier,
KeyGroup = rootCertParameters.certConfig.keyGroup,
StoreKeysExternal = rootCertParameters.certConfig.storeKeysExternal)
}
@Test
fun `Authenticator executes the block once user is successfully authenticated`() {
// given
val authenticator = AutoAuthenticator(providerConfig, rootCertParameters.userConfigs)
val rootCertGenerator = KeyCertificateGenerator(rootCertParameters)
// when root cert is created
authenticator.connectAndAuthenticate { provider ->
rootCertGenerator.generate(provider)
// then
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
assertEquals(rootCert.issuerX500Principal, rootCert.subjectX500Principal)
}
// when network map cert is created
val networkMapCertGenerator = KeyCertificateGenerator(rootCertParameters.copy(
certConfig = rootCertParameters.certConfig.copy(
certificateType = CertificateType.NETWORK_MAP,
subject = "CN=Corda NM, O=R3Cev, L=London, C=GB"
)
))
authenticator.connectAndAuthenticate { provider ->
networkMapCertGenerator.generate(provider)
// then
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
val networkMapCert = keyStore.getCertificate(CORDA_NETWORK_MAP) as X509Certificate
assertNotNull(networkMapCert)
assertEquals(rootCert.subjectX500Principal, networkMapCert.issuerX500Principal)
}
// when csr cert is created
val csrCertGenerator = KeyCertificateGenerator(rootCertParameters.copy(
certConfig = rootCertParameters.certConfig.copy(
certificateType = CertificateType.INTERMEDIATE_CA,
subject = "CN=Corda CSR, O=R3Cev, L=London, C=GB"
)
))
authenticator.connectAndAuthenticate { provider ->
csrCertGenerator.generate(provider)
// then
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
val csrCert = keyStore.getCertificate(CORDA_INTERMEDIATE_CA) as X509Certificate
assertNotNull(csrCert)
assertEquals(rootCert.subjectX500Principal, csrCert.issuerX500Principal)
}
}
}

View File

@ -33,7 +33,8 @@ class HsmTest {
networkMapPrivateKeyPassword = "",
rootPrivateKeyPassword = "",
keyGroup = "DEV.DOORMAN",
validDays = 3650
validDays = 3650,
csrCertCrlDistPoint = "http://test.com/revoked.crl"
)
private lateinit var inputReader: InputReader

View File

@ -16,6 +16,8 @@ import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
const val CORDA_NETWORK_MAP = "cordanetworkmap"
// TODO These should be defined in node-api
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>

View File

@ -3,11 +3,11 @@ package com.r3.corda.networkmanage.doorman
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.configureDatabase
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.identity.CordaX500Name
@ -127,7 +127,7 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor
X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
storeCertIfAbsent(
DEFAULT_NETWORK_MAP_CERTIFICATE_NAME,
CORDA_NETWORK_MAP,
CertificateType.NETWORK_MAP,
X500Principal("CN=Corda Network Map,OU=Corda,O=R3 Ltd,L=London,C=GB"),
Crypto.EDDSA_ED25519_SHA512)
@ -149,7 +149,7 @@ private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair
)
}
val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, privateKeyPassword))
val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(CORDA_NETWORK_MAP, privateKeyPassword))
return Pair(csrCertPathAndKey, networkMapSigner)
}

View File

@ -11,8 +11,6 @@ import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.authentication.createProvider
import com.r3.corda.networkmanage.hsm.configuration.Parameters
import com.r3.corda.networkmanage.hsm.configuration.parseParameters
import com.r3.corda.networkmanage.hsm.generator.CertificateNameAndPass
import com.r3.corda.networkmanage.hsm.generator.KeyCertificateGenerator
import com.r3.corda.networkmanage.hsm.menu.Menu
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStorage
@ -59,7 +57,6 @@ fun run(parameters: Parameters) {
val database = configureDatabase(dataSourceProperties, databaseConfig)
val csrStorage = DBSignedCertificateRequestStorage(database)
val hsmSigner = HsmNetworkMapSigner(
networkMapCertificateName,
networkMapPrivateKeyPassword,
Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
@ -72,25 +69,14 @@ fun run(parameters: Parameters) {
csrStorage,
csrCertificateName,
csrPrivateKeyPassword,
csrCertCrlDistPoint,
csrCertCrlIssuer,
rootCertificateName,
validDays,
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
signer.sign(it)
}
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
if (confirmedKeyGen()) {
val generator = KeyCertificateGenerator(
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, keyGenAuthThreshold),
keySpecifier,
keyGroup)
generator.generateAllCertificates(
listOf(CertificateNameAndPass(csrCertificateName, csrPrivateKeyPassword), CertificateNameAndPass(networkMapCertificateName, networkMapPrivateKeyPassword)),
rootCertificateName,
rootPrivateKeyPassword,
validDays)
}
}).addItem("2", "Sign all approved and unsigned CSRs", {
Menu().withExceptionHandler(::processError).addItem("1", "Sign all approved and unsigned CSRs", {
val approved = csrStorage.getApprovedRequests()
if (approved.isNotEmpty()) {
if (confirmedSign(approved)) {
@ -99,7 +85,7 @@ fun run(parameters: Parameters) {
} else {
println("There is no approved CSR")
}
}).addItem("3", "List all approved and unsigned CSRs", {
}).addItem("2", "List all approved and unsigned CSRs", {
val approved = csrStorage.getApprovedRequests()
if (approved.isNotEmpty()) {
println("Approved CSRs:")

View File

@ -19,13 +19,15 @@ import java.util.*
data class Parameters(val dataSourceProperties: Properties,
val databaseConfig: DatabaseConfig = DatabaseConfig(),
val device: String = DEFAULT_DEVICE,
// TODO this needs cleaning up after the config-file-only support is implemented
// TODO this needs cleaning up after the config-file-only support is implemented
val keyGroup: String,
val keySpecifier: Int = DEFAULT_KEY_SPECIFIER,
val rootPrivateKeyPassword: String,
val csrPrivateKeyPassword: String,
val csrCertificateName: String = DEFAULT_CSR_CERTIFICATE_NAME,
val networkMapCertificateName: String = DEFAULT_NETWORK_MAP_CERTIFICATE_NAME,
val csrCertCrlDistPoint: String,
val csrCertCrlIssuer: String? = DEFAULT_CSR_CERT_CRL_ISSUER, // X500 name of the issuing authority e.g. "L=New York, C=US, OU=Org Unit, CN=Service Name",
// if null parent CA is is considered as an issuer.
val networkMapPrivateKeyPassword: String,
val rootCertificateName: String = DEFAULT_ROOT_CERTIFICATE_NAME,
val validDays: Int,
@ -35,7 +37,7 @@ data class Parameters(val dataSourceProperties: Properties,
val authKeyFilePath: Path? = DEFAULT_KEY_FILE_PATH,
val authKeyFilePassword: String? = DEFAULT_KEY_FILE_PASSWORD,
val autoUsername: String? = DEFAULT_AUTO_USERNAME,
// TODO Change this to Duration in the future.
// TODO Change this to Duration in the future.
val signInterval: Long = DEFAULT_SIGN_INTERVAL) {
companion object {
val DEFAULT_DEVICE = "3001@127.0.0.1"
@ -48,8 +50,8 @@ data class Parameters(val dataSourceProperties: Properties,
val DEFAULT_KEY_FILE_PATH: Path? = null //Paths.get("/Users/michalkit/WinDev1706Eval/Shared/TEST4.key")
val DEFAULT_KEY_FILE_PASSWORD: String? = null
val DEFAULT_AUTO_USERNAME: String? = null
val DEFAULT_NETWORK_MAP_CERTIFICATE_NAME = "cordaintermediateca_nm" // TODO Change the value to "cordanetworkmap" since this is not a CA
val DEFAULT_SIGN_INTERVAL = 600L // in seconds (10 minutes)
val DEFAULT_CSR_CERT_CRL_ISSUER: String? = null
}
}

View File

@ -0,0 +1,38 @@
package com.r3.corda.networkmanage.hsm.generator
import CryptoServerJCE.CryptoServerProvider
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
import com.r3.corda.networkmanage.hsm.authentication.createProvider
/**
* Performs user authentication against the HSM
*/
class AutoAuthenticator(providerConfig: CryptoServerProviderConfig,
private val userConfigs: List<UserAuthenticationParameters>) {
private val provider = createProvider(providerConfig)
/**
* Interactively (using console) authenticates a user against the HSM. Once authentication is completed successfully
* the [block] is executed.
* @param block to be executed once the authentication process succeeds. The block should take a [CryptoServerProvider] instance as the parameter.
*/
fun connectAndAuthenticate(block: (CryptoServerProvider) -> Unit) {
try {
for (userConfig in userConfigs) {
when(userConfig.authMode) {
AuthMode.PASSWORD -> provider.loginPassword(userConfig.username, userConfig.authToken)
AuthMode.CARD_READER -> provider.loginSign(userConfig.username, ":cs2:cyb:USB0", null)
AuthMode.KEY_FILE -> provider.loginSign(userConfig.username, userConfig.keyFilePassword, userConfig.authToken)
}
}
block(provider)
} finally {
try {
provider.logoff()
} catch (throwable: Throwable) {
println("WARNING Exception while logging off")
throwable.printStackTrace(System.out)
}
}
}
}

View File

@ -0,0 +1,95 @@
package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser
import net.corda.core.internal.isRegularFile
import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.crypto.CertificateType
import java.nio.file.Path
import java.nio.file.Paths
/**
* Holds configuration necessary for user's authentication against HSM.
*/
data class UserAuthenticationParameters(val username: String,
val authMode: AuthMode,
val authToken: String, // password or path to the key file, depending on the [authMode]
val keyFilePassword: String?) // used only if authMode == [AuthMode.KEY_FILE]
/**
* Supported authentication modes.
*/
enum class AuthMode {
PASSWORD, CARD_READER, KEY_FILE
}
/**
* Holds generator parameters.
*/
data class GeneratorParameters(val hsmHost: String,
val hsmPort: Int,
val trustStoreDirectory: Path,
val trustStorePassword: String,
val userConfigs: List<UserAuthenticationParameters>,
val certConfig: CertificateConfiguration)
/**
* Holds certificate specific configuration.
*/
data class CertificateConfiguration(val keyGroup: String,
val keySpecifier: Int,
val storeKeysExternal: Boolean,
val certificateType: CertificateType,
val rootPrivateKeyPassword: String,
val privateKeyPassword: String,
val subject: String, // it is certificate [X500Name] subject
val validDays: Int,
val crlDistributionUrl: String?,
val crlIssuer: String?, // X500 name of the issuing authority e.g. "L=New York, C=US, OU=Org Unit, CN=Service Name"
val keyOverride: Int, // 1 for allow and 0 deny
val keyExport: Int, // 1 for allow, 0 for deny
val keyCurve: String, // we use "NIST-P256", check Utimaco docs for other options
val keyGenMechanism: Int) // MECH_KEYGEN_UNCOMP = 4 or MECH_RND_REAL = 0
/**
* Holds arguments for command line options.
*/
data class CommandLineOptions(val configFile: Path) {
init {
check(configFile.isRegularFile()) { "Config file $configFile does not exist" }
}
}
/**
* Parses key generator command line options.
*/
fun parseCommandLine(vararg args: String): CommandLineOptions {
val optionParser = OptionParser()
val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.describedAs("filepath")
val helpOption = optionParser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp();
val optionSet = optionParser.parse(*args)
// Print help and exit on help option or if there are missing options.
if (optionSet.has(helpOption) || !optionSet.has(configFileArg)) {
throw ShowHelpException(optionParser)
}
val configFile = Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath()
return CommandLineOptions(configFile)
}
/**
* Parses a configuration file, which contains all the configuration - i.e. for user and certificate parameters.
*/
fun parseParameters(configFile: Path): GeneratorParameters {
return ConfigFactory
.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))
.resolve()
.parseAs()
}

View File

@ -1,101 +1,116 @@
package com.r3.corda.networkmanage.hsm.generator
import CryptoServerCXI.CryptoServerCXI
import CryptoServerCXI.CryptoServerCXI.KEY_ALGO_ECDSA
import CryptoServerCXI.CryptoServerCXI.KeyAttributes
import CryptoServerJCE.CryptoServerProvider
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.createIntermediateCert
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.createSelfSignedCACert
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getCleanEcdsaKeyPair
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.retrieveCertificateAndKeys
import net.corda.nodeapi.internal.crypto.addOrReplaceKey
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.createIntermediateCert
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.createSelfSignedCACert
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getCleanEcdsaKeyPair
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.retrieveKeysAndCertificateChain
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.internal.isDirectory
import net.corda.core.internal.x500Name
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.crypto.CertificateType.*
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import java.nio.file.Path
import java.security.KeyPair
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
data class CertificateNameAndPass(val certificateName: String, val privateKeyPassword: String)
/**
* Encapsulates logic for root and intermediate key/certificate generation.
* Encapsulates logic for key and certificate generation.
*
*/
class KeyCertificateGenerator(private val authenticator: Authenticator,
private val keySpecifier: Int,
private val keyGroup: String) {
/**
* Generates root and intermediate key and certificates and stores them in the key store given by provider.
* If the keys and certificates already exists they will be overwritten.
* @param intermediateCertificatesCredentials name and password for the intermediate key/certificate
* @param parentCertificateName name of the parent key/certificate
* @param parentPrivateKeyPassword password for the parent private key
* @param validDays days of certificate validity
*/
fun generateAllCertificates(intermediateCertificatesCredentials: List<CertificateNameAndPass>,
parentCertificateName: String,
parentPrivateKeyPassword: String,
validDays: Int) {
authenticator.connectAndAuthenticate { provider, _ ->
val keyStore = getAndInitializeKeyStore(provider)
generateRootCertificate(provider, keyStore, parentCertificateName, parentPrivateKeyPassword, validDays)
intermediateCertificatesCredentials.forEach {
generateIntermediateCertificate(provider, keyStore, it.certificateName, it.privateKeyPassword, parentCertificateName, parentPrivateKeyPassword, validDays)
class KeyCertificateGenerator(private val parameters: GeneratorParameters) {
companion object {
val logger = contextLogger()
}
fun generate(provider: CryptoServerProvider) {
parameters.run {
require(trustStoreDirectory.isDirectory()) { "trustStoreDirectory must point to a directory." }
val keyName = when (certConfig.certificateType) {
ROOT_CA -> CORDA_ROOT_CA
INTERMEDIATE_CA -> CORDA_INTERMEDIATE_CA
NETWORK_MAP -> CORDA_NETWORK_MAP
else -> throw IllegalArgumentException("Invalid certificate type. ${certConfig.certificateType}")
}
val keyStore = getAndInitializeKeyStore(provider)
val keyPair = certConfig.generateEcdsaKeyPair(keyName, provider, keyStore)
val certChain = if (certConfig.certificateType == ROOT_CA) {
certConfig.generateRootCert(provider, keyPair, trustStoreDirectory, trustStorePassword)
} else {
certConfig.generateIntermediateCert(provider, keyPair, keyStore)
}
keyStore.addOrReplaceKey(keyName, keyPair.private, certConfig.privateKeyPassword.toCharArray(), certChain)
logger.info("New certificate and key pair named $keyName have been generated and stored in HSM")
}
}
/**
* Generates a root certificate
*/
private fun generateRootCertificate(provider: CryptoServerProvider,
keyStore: KeyStore,
certificateKeyName: String,
privateKeyPassword: String,
validDays: Int) {
val keyPair = generateEcdsaKeyPair(provider, keyStore, certificateKeyName, privateKeyPassword)
val selfSignedRootCertificate = createSelfSignedCACert("R3", keyPair, validDays, provider).certificate
keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignedRootCertificate))
println("New certificate and key pair named $certificateKeyName have been generated")
private fun CertificateConfiguration.generateRootCert(provider: CryptoServerProvider,
keyPair: KeyPair,
trustStoreDirectory: Path,
trustStorePassword: String): Array<X509Certificate> {
val certificate = createSelfSignedCACert(ROOT_CA,
CordaX500Name.parse(subject).x500Name,
keyPair,
validDays,
provider,
crlDistributionUrl,
crlIssuer).certificate
val trustStorePath = trustStoreDirectory / "truststore.jks"
val trustStore = loadOrCreateKeyStore(trustStorePath, trustStorePassword)
logger.info("Trust store for distribution to nodes created in $trustStore")
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificate)
logger.info("Certificate $CORDA_ROOT_CA has been added to $trustStore")
trustStore.save(trustStorePath, trustStorePassword)
logger.info("Trust store has been persisted. Ready for distribution.")
return arrayOf(certificate)
}
/**
* Generates an intermediate certificate
*/
private fun generateIntermediateCertificate(provider: CryptoServerProvider,
keyStore: KeyStore,
certificateKeyName: String,
privateKeyPassword: String,
parentCertificateName: String,
parentPrivateKeyPassword: String,
validDays: Int) {
val parentCACertKey = retrieveCertificateAndKeys(parentCertificateName, parentPrivateKeyPassword, keyStore)
val keyPair = generateEcdsaKeyPair(provider, keyStore, certificateKeyName, privateKeyPassword)
val intermediateCertificate = createIntermediateCert("R3 Intermediate", parentCACertKey, keyPair, validDays, provider)
keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(intermediateCertificate.certificate, parentCACertKey.certificate))
println("New certificate and key pair named $certificateKeyName have been generated")
private fun CertificateConfiguration.generateIntermediateCert(
provider: CryptoServerProvider,
keyPair: KeyPair,
keyStore: KeyStore): Array<X509Certificate> {
val rootKeysAndCertChain = retrieveKeysAndCertificateChain(CORDA_ROOT_CA,
rootPrivateKeyPassword,
keyStore)
val certificateAndKeyPair = createIntermediateCert(
certificateType,
CordaX500Name.parse(subject).x500Name,
CertificateAndKeyPair(rootKeysAndCertChain.certificateChain.first(), rootKeysAndCertChain.keyPair),
keyPair,
validDays,
provider,
crlDistributionUrl,
crlIssuer)
return arrayOf(certificateAndKeyPair.certificate, *rootKeysAndCertChain.certificateChain)
}
private fun generateECDSAKey(keySpecifier: Int, keyName: String, keyGroup: String, provider: CryptoServerProvider, overwrite: Boolean = true) {
val generateFlag = if (overwrite) {
println("!!! WARNING: OVERWRITING KEY NAMED $keyName !!!")
CryptoServerCXI.FLAG_OVERWRITE
} else {
0
}
val keyAttributes = CryptoServerCXI.KeyAttributes()
private fun CertificateConfiguration.generateECDSAKey(keyName: String, provider: CryptoServerProvider) {
val keyAttributes = KeyAttributes()
keyAttributes.apply {
algo = CryptoServerCXI.KEY_ALGO_ECDSA
algo = KEY_ALGO_ECDSA
group = keyGroup
specifier = keySpecifier
export = 0 // deny export
export = keyExport
name = keyName
setCurve("NIST-P256")
setCurve(keyCurve)
}
println("Generating key...")
val mechanismFlag = CryptoServerCXI.MECH_RND_REAL or CryptoServerCXI.MECH_KEYGEN_UNCOMP
provider.cryptoServer.generateKey(generateFlag, keyAttributes, mechanismFlag)
logger.info("Generating key $keyName")
provider.cryptoServer.generateKey(keyOverride, keyAttributes, keyGenMechanism)
}
private fun generateEcdsaKeyPair(provider: CryptoServerProvider, keyStore: KeyStore, keyName: String, privateKeyPassword: String): KeyPair {
generateECDSAKey(keySpecifier, keyName, keyGroup, provider)
private fun CertificateConfiguration.generateEcdsaKeyPair(keyName: String, provider: CryptoServerProvider, keyStore: KeyStore): KeyPair {
generateECDSAKey(keyName, provider)
val privateKey = keyStore.getKey(keyName, privateKeyPassword.toCharArray()) as PrivateKey
val publicKey = keyStore.getCertificate(keyName).publicKey
return getCleanEcdsaKeyPair(publicKey, privateKey)

View File

@ -0,0 +1,28 @@
package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
import org.apache.logging.log4j.LogManager
import java.nio.file.Paths
private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main")
fun main(args: Array<String>) {
val commandLineOptions = parseCommandLine(*args)
parseParameters(commandLineOptions.configFile).run {
val providerConfig = CryptoServerProviderConfig(
Device = "$hsmPort@$hsmHost",
KeySpecifier = certConfig.keySpecifier,
KeyGroup = certConfig.keyGroup,
StoreKeysExternal = certConfig.storeKeysExternal)
try {
val authenticator = AutoAuthenticator(providerConfig, userConfigs)
authenticator.connectAndAuthenticate { provider ->
val generator = KeyCertificateGenerator(this)
generator.generate(provider)
}
} catch (e: Exception) {
log.error(mapCryptoServerException(e))
}
}
}

View File

@ -3,10 +3,11 @@ package com.r3.corda.networkmanage.hsm.signer
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.X509Utilities.buildCertPath
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.createClientCertificate
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.retrieveCertificateAndKeys
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.buildCertPath
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.retrieveCertificateAndKeys
import net.corda.nodeapi.internal.crypto.CertificateType
/**
* Encapsulates certificate signing logic
@ -14,6 +15,8 @@ import com.r3.corda.networkmanage.hsm.utils.X509Utilities.retrieveCertificateAnd
class HsmCsrSigner(private val storage: SignedCertificateRequestStorage,
private val intermediateCertAlias: String,
private val intermediateCertPrivateKeyPass: String?,
private val csrCertCrlDistPoint: String,
private val csrCertCrlIssuer: String?,
private val rootCertAlias: String,
private val validDays: Int,
private val authenticator: Authenticator) : CertificateSigningRequestSigner {
@ -37,10 +40,14 @@ class HsmCsrSigner(private val storage: SignedCertificateRequestStorage,
val intermediatePrivateKeyPass = intermediateCertPrivateKeyPass ?: authenticator.readPassword("CA Private Key Password: ")
val intermediateCertAndKey = retrieveCertificateAndKeys(intermediateCertAlias, intermediatePrivateKeyPass, keyStore)
toSign.forEach {
it.certPath = buildCertPath(
createClientCertificate(intermediateCertAndKey, it.request, validDays, provider),
intermediateCertAndKey.certificate,
rootCert)
it.certPath = buildCertPath(createClientCertificate(
CertificateType.NODE_CA,
intermediateCertAndKey,
it.request,
validDays,
provider,
csrCertCrlDistPoint,
csrCertCrlIssuer), rootCert)
}
storage.store(toSign, signers)
println("The following certificates have been signed by $signers:")

View File

@ -1,10 +1,11 @@
package com.r3.corda.networkmanage.hsm.signer
import com.r3.corda.networkmanage.common.signer.Signer
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.utils.X509Utilities
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities.verify
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.nodeapi.internal.crypto.getX509Certificate
import java.security.PrivateKey
@ -14,8 +15,7 @@ import java.security.Signature
* Signer which connects to a HSM using the given [authenticator] to sign bytes.
*/
// TODO Rename this to HsmSigner
class HsmNetworkMapSigner(private val certificateKeyName: String,
private val privateKeyPassword: String,
class HsmNetworkMapSigner(private val privateKeyPassword: String,
private val authenticator: Authenticator) : Signer {
/**
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
@ -23,11 +23,11 @@ class HsmNetworkMapSigner(private val certificateKeyName: String,
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
return authenticator.connectAndAuthenticate { provider, _ ->
val keyStore = getAndInitializeKeyStore(provider)
val certificate = keyStore.getX509Certificate(certificateKeyName)
val certificate = keyStore.getX509Certificate(CORDA_NETWORK_MAP)
// Don't worry this is not a real private key but a pointer to one that resides in the HSM. It only works
// when used with the given provider.
val key = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey
val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run {
val key = keyStore.getKey(CORDA_NETWORK_MAP, privateKeyPassword.toCharArray()) as PrivateKey
val signature = Signature.getInstance(HsmX509Utilities.SIGNATURE_ALGORITHM, provider).run {
initSign(key)
update(data)
sign()

View File

@ -1,24 +1,24 @@
package com.r3.corda.networkmanage.hsm.utils
import CryptoServerJCE.CryptoServerProvider
import net.corda.core.CordaOID
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.x500Name
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.getX509Certificate
import net.corda.nodeapi.internal.crypto.toJca
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
@ -32,47 +32,48 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
object X509Utilities {
object HsmX509Utilities {
val SIGNATURE_ALGORITHM = "SHA256withECDSA"
/**
* Create a de novo root self-signed X509 v3 CA cert for the specified [KeyPair].
* @param legalName The Common (CN) field of the cert Subject will be populated with the domain string
* @param type type of the certificate to be created
* @param subject X500 name of the certificate subject
* @param keyPair public and private keys to be associated with the generated certificate
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @param crlDistPoint url to the certificate revocation list of this certificate
* @param crlIssuer issuer of the certificate revocation list of this certificate
* @return an instance of [CertificateAndKeyPair] class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
*/
fun createSelfSignedCACert(legalName: String, keyPair: KeyPair, validDays: Int, provider: Provider): CertificateAndKeyPair {
fun createSelfSignedCACert(type: CertificateType,
subject: X500Name,
keyPair: KeyPair,
validDays: Int,
provider: Provider,
crlDistPoint: String? = null,
crlIssuer: String? = null): CertificateAndKeyPair {
// TODO this needs to be chaneged
val issuer = getDevX509Name(legalName)
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = issuer
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, validDays)
val keyPurposes = DERSequence(ASN1EncodableVector().apply { type.purposes.forEach { add(it) } })
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
// TODO to remove once we allow for longer certificate chains
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(2))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false, DERSequence(purposes).toASN1Primitive())
val builder = JcaX509v3CertificateBuilder(subject, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(pubKey))
builder.addExtension(Extension.basicConstraints, true, BasicConstraints(type.isCA))
builder.addExtension(Extension.keyUsage, false, type.keyUsage)
builder.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
builder.addExtension(Extension.authorityKeyIdentifier, false, JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.public))
if (type.role != null) {
builder.addExtension(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), false, type.role)
}
addCrlInfo(builder, crlDistPoint, crlIssuer)
val cert = signCertificate(builder, keyPair.private, provider)
@ -111,85 +112,110 @@ object X509Utilities {
return CertificateAndKeyPair(certificate, getCleanEcdsaKeyPair(publicKey, privateKey))
}
/**
* Retrieves key pair and certificate chain from the given key store. Also, the keys retrieved are cleaned in a sense of the
* [getCleanEcdsaKeyPair] method.
* @param certificateKeyName certificate and key name (alias) to be used when querying the key store.
* @param privateKeyPassword password for the private key.
* @param keyStore key store that holds the certificate with its keys.
* @return instance of [KeyPairAndCertificateChain] holding the key pair and the certificate chain.
*/
fun retrieveKeysAndCertificateChain(certificateKeyName: String, privateKeyPassword: String, keyStore: KeyStore): KeyPairAndCertificateChain {
val privateKey = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey
val publicKey = keyStore.getCertificate(certificateKeyName).publicKey
val certificateChain = keyStore.getCertificateChain(certificateKeyName).map { it as X509Certificate }
return KeyPairAndCertificateChain(getCleanEcdsaKeyPair(publicKey, privateKey), certificateChain.toTypedArray())
}
/**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param commonName The Common (CN) field of the cert Subject will be populated with the domain string.
* @param type type of the certificate to be created
* @param subject X500 name of the certificate subject
* @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it.
* @param keyPair public and private keys to be associated with the generated certificate
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @param crlDistPoint url to the certificate revocation list of this certificate
* @param crlIssuer issuer of the certificate revocation list of this certificate
* @return an instance of [CertificateAndKeyPair] class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
*/
fun createIntermediateCert(commonName: String,
fun createIntermediateCert(type: CertificateType,
subject: X500Name,
certificateAuthority: CertificateAndKeyPair,
keyPair: KeyPair, validDays: Int, provider: Provider): CertificateAndKeyPair {
keyPair: KeyPair,
validDays: Int,
provider: Provider,
crlDistPoint: String?,
crlIssuer: String?): CertificateAndKeyPair {
val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = getDevX509Name(commonName)
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, validDays, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val keyPurposes = DERSequence(ASN1EncodableVector().apply { type.purposes.forEach { add(it) } })
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
// TODO to remove onece we allow for longer certificate chains
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(1))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false,
DERSequence(purposes))
val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(pubKey))
builder.addExtension(Extension.basicConstraints, true, BasicConstraints(type.isCA))
builder.addExtension(Extension.keyUsage, false, type.keyUsage)
builder.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
builder.addExtension(Extension.authorityKeyIdentifier, false, JcaX509ExtensionUtils().createAuthorityKeyIdentifier(certificateAuthority.keyPair.public))
if (type.role != null) {
builder.addExtension(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), false, type.role)
}
addCrlInfo(builder, crlDistPoint, crlIssuer)
val cert = signCertificate(builder, certificateAuthority.keyPair.private, provider)
cert.checkValidity(Date())
cert.verify(certificateAuthority.keyPair.public)
return CertificateAndKeyPair(cert, KeyPair(pubKey, keyPair.private))
return CertificateAndKeyPair(cert, keyPair)
}
/**
* Creates and signs a X509 v3 client certificate.
* @param type type of the certificate to be created
* @param caCertAndKey signing certificate authority certificate and its keys
* @param request certficate signing request
* @param request certificate signing request
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @param crlDistPoint url to the certificate revocation list of this certificate
* @param crlIssuer issuer of the certificate revocation list of this certificate
* @return an instance of [CertificateAndKeyPair] class is returned containing the signed client certificate.
*/
fun createClientCertificate(caCertAndKey: CertificateAndKeyPair,
fun createClientCertificate(type: CertificateType,
caCertAndKey: CertificateAndKeyPair,
request: PKCS10CertificationRequest,
validDays: Int,
provider: Provider): Certificate {
provider: Provider,
crlDistPoint: String?,
crlIssuer: String?): Certificate {
val jcaRequest = JcaPKCS10CertificationRequest(request)
// This can be adjusted more to our future needs.
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(jcaRequest.subject.toString()).copy(commonName = null).x500Name))), arrayOf())
val issuerCertificate = caCertAndKey.certificate
val issuerKeyPair = caCertAndKey.keyPair
val certificateType = CertificateType.NODE_CA
val validityWindow = getCertificateValidityWindow(0, validDays, issuerCertificate.notBefore, issuerCertificate.notAfter)
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = CordaX500Name.parse(jcaRequest.subject.toString()).x500Name
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(jcaRequest.publicKey.encoded))
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
val keyPurposes = DERSequence(ASN1EncodableVector().apply { type.purposes.forEach { add(it) } })
val builder = JcaX509v3CertificateBuilder(issuerCertificate, serial, validityWindow.first, validityWindow.second, subject, jcaRequest.publicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo))
.addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA))
.addExtension(Extension.keyUsage, false, certificateType.keyUsage)
.addExtension(Extension.basicConstraints, type.isCA, BasicConstraints(type.isCA))
.addExtension(Extension.keyUsage, false, type.keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
.addExtension(Extension.nameConstraints, true, nameConstraints)
.addExtension(Extension.authorityKeyIdentifier, false, JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.public))
if (type.role != null) {
builder.addExtension(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), false, type.role)
}
addCrlInfo(builder, crlDistPoint, crlIssuer)
val certificate = signCertificate(builder, issuerKeyPair.private, provider)
certificate.checkValidity(Date())
certificate.verify(issuerKeyPair.public)
@ -285,26 +311,23 @@ object X509Utilities {
provider: Provider,
signatureAlgorithm: String = SIGNATURE_ALGORITHM): X509Certificate {
val signer = JcaContentSignerBuilder(signatureAlgorithm).setProvider(provider).build(signedWithPrivateKey)
return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateBuilder.build(signer))
return certificateBuilder.build(signer).toJca()
}
/**
* Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
*/
private fun getDevX509Name(commonName: String): X500Name {
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
nameBuilder.addRDN(BCStyle.CN, commonName)
nameBuilder.addRDN(BCStyle.O, "R3")
nameBuilder.addRDN(BCStyle.OU, "corda")
nameBuilder.addRDN(BCStyle.L, "London")
nameBuilder.addRDN(BCStyle.C, "UK")
return nameBuilder.build()
private fun addCrlInfo(builder: X509v3CertificateBuilder, crlDistPoint: String?, crlIssuer: String?) {
if (crlDistPoint != null) {
val distPointName = DistributionPointName(GeneralNames(GeneralName(GeneralName.uniformResourceIdentifier, crlDistPoint)))
val crlIssuerGeneralNames = crlIssuer?.let {
GeneralNames(GeneralName(CordaX500Name.parse(crlIssuer).x500Name))
}
// The second argument is flag that allows you to define what reason of certificate revocation is served by this distribution point see [ReasonFlags].
// The idea is that you have different revocation per revocation reason. Since we won't go into such a granularity, we can skip that parameter.
// The third argument allows you to specify the name of the CRL issuer, it needs to be consistent with the crl (IssuingDistributionPoint) extension and the idp argument.
// If idp == true, set it, if idp == false, leave it null as done here.
val distPoint = DistributionPoint(distPointName, null, crlIssuerGeneralNames)
builder.addExtension(Extension.cRLDistributionPoints, false, CRLDistPoint(arrayOf(distPoint)))
}
}
}
private fun getX509Name(myLegalName: String, nearestCity: String, email: String): X500Name {
return X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.CN, myLegalName)
.addRDN(BCStyle.L, nearestCity)
.addRDN(BCStyle.E, email).build()
}
}
data class KeyPairAndCertificateChain(val keyPair: KeyPair, val certificateChain: Array<X509Certificate>)

View File

@ -0,0 +1,64 @@
package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigException
import net.corda.nodeapi.internal.crypto.CertificateType
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.File
import java.nio.file.Paths
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
class GeneratorParametersTest {
private val validConfigPath = File("generator.conf").absolutePath
private val invalidConfigPath = File(javaClass.getResource("/generator_fail.conf").toURI()).absolutePath
private val validArgs = arrayOf("--config-file", validConfigPath)
@Test
fun `should fail when config file is missing`() {
val message = assertFailsWith<IllegalStateException> {
parseCommandLine("--config-file", "not-existing-file")
}.message
Assertions.assertThat(message).contains("Config file ")
}
@Test
fun `should throw ShowHelpException when help option is passed on the command line`() {
assertFailsWith<ShowHelpException> {
parseCommandLine("-?")
}
}
@Test
fun `should fail when config is invalid`() {
assertFailsWith<ConfigException.Missing> {
parseParameters(parseCommandLine("--config-file", invalidConfigPath).configFile)
}
}
@Test
fun `should parse generator config correctly`() {
val parameters = parseCommandLineAndGetParameters()
assertEquals("127.0.0.1", parameters.hsmHost)
assertEquals(3001, parameters.hsmPort)
val certConfig = parameters.certConfig
assertEquals(1, certConfig.keySpecifier)
assertEquals("trustpass", parameters.trustStorePassword)
assertEquals(Paths.get("."), parameters.trustStoreDirectory)
assertFalse(certConfig.storeKeysExternal)
assertFalse(parameters.userConfigs.isEmpty())
val userConfig = parameters.userConfigs.first()
assertEquals("INTEGRATION_TEST", userConfig.username)
assertEquals(AuthMode.PASSWORD, userConfig.authMode)
assertEquals("INTEGRATION_TEST", userConfig.authToken)
assertEquals(3650, certConfig.validDays)
assertEquals(CertificateType.ROOT_CA, certConfig.certificateType)
assertEquals("NIST-P256", certConfig.keyCurve)
}
private fun parseCommandLineAndGetParameters(): GeneratorParameters {
return parseParameters(parseCommandLine(*validArgs).configFile)
}
}

View File

@ -0,0 +1,12 @@
hsmHost = 127.0.0.1
hsmPort = 3001
keyGroup = "DEV.DOORMAN"
keySpecifier = 1
userConfigs = [
{
username = "INTEGRATION_TEST"
authMode = PASSWORD
authToken = "INTEGRATION_TEST"
}
]

View File

@ -3,6 +3,7 @@ keyGroup = "DEV.DOORMAN"
keySpecifier = -1
authMode = PASSWORD
csrPrivateKeyPassword = ""
csrCertCrlDistPoint = "http://test.com/revoked.crl"
networkMapPrivateKeyPassword = ""
rootPrivateKeyPassword = ""
keyGroup = "DEV.DOORMAN"

View File

@ -3,6 +3,7 @@ keyGroup = "DEV.DOORMAN"
keySpecifier = -1
authMode = PASSWORD
csrPrivateKeyPassword = ""
csrCertCrlDistPoint = "http://test.com/revoked.crl"
networkMapPrivateKeyPassword = ""
rootPrivateKeyPassword = ""
keyGroup = "DEV.DOORMAN"

View File

@ -34,6 +34,7 @@ include 'perftestcordapp'
include 'network-management'
include 'network-management:capsule'
include 'network-management:capsule-hsm'
include 'network-management:capsule-hsm-cert-generator'
include 'tools:jmeter'
include 'tools:explorer'
include 'tools:explorer:capsule'