mirror of
https://github.com/corda/corda.git
synced 2025-02-07 11:30:22 +00:00
remove network services from corda repo (#875)
network services is in : https://github.com/corda/network-services
This commit is contained in:
parent
77ef131c0f
commit
9155000407
@ -1,374 +0,0 @@
|
||||
|
||||
# Building the binaries
|
||||
|
||||
## Network management server
|
||||
To build a fat jar containing all the doorman code you can simply invoke:
|
||||
```
|
||||
./gradlew network-management:capsule:buildDoormanJAR
|
||||
```
|
||||
|
||||
The built file will appear in:
|
||||
```
|
||||
network-management/capsule/build/libs/doorman-<version>.jar
|
||||
```
|
||||
## HSM signing server
|
||||
To build a fat jar containing all the HSM signing server code you can simply invoke:
|
||||
```
|
||||
./gradlew network-management:capsule-hsm:buildHsmJAR
|
||||
```
|
||||
|
||||
The built file will appear in:
|
||||
```
|
||||
network-management/capsule-hsm/build/libs/hsm-<version>.jar
|
||||
```
|
||||
|
||||
The binaries can also be obtained from artifactory after deployment in TeamCity.
|
||||
|
||||
To run the HSM signing server:
|
||||
|
||||
```
|
||||
cd network-management
|
||||
java -jar capsule-hsm/build/libs/hsm-<version>.jar --config-file [hsm-configuration-file]
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## HSM CRL Generator
|
||||
|
||||
To build a fat jar containing all the hsm CRL generator code you can simply invoke
|
||||
```
|
||||
./gradlew network-management:capsule-hsm-crl-generator:buildHsmCrlGeneratorJAR
|
||||
```
|
||||
|
||||
The built file will appear in
|
||||
```
|
||||
network-management/capsule-hsm-crl-generator/build/libs/hsm-crl-generator-<VERSION>.jar
|
||||
```
|
||||
|
||||
## Certificate Revocation Request Submission Tool
|
||||
|
||||
To build a fat jar containing all the CRR submission tool code you can simply invoke
|
||||
```
|
||||
./gradlew network-management:capsule-crr-submission:buildCrrSubmissionJAR
|
||||
```
|
||||
|
||||
The built file will appear in
|
||||
```
|
||||
network-management/capsule-crr-submission/build/libs/crr-submission-<VERSION>.jar
|
||||
```
|
||||
|
||||
|
||||
# Logs
|
||||
In order to set the desired logging level the system properties need to be used.
|
||||
Appropriate system properties can be set at the execution time.
|
||||
Example:
|
||||
```
|
||||
java -DdefaultLogLevel=TRACE -DconsoleLogLevel=TRACE -jar doorman-<version>.jar --config-file <config file>
|
||||
```
|
||||
|
||||
#Configuring network management service
|
||||
### Local signing
|
||||
|
||||
When `keystorePath` is provided in the config file, a signer will be created to handle all the signing periodically
|
||||
using the CA keys in the provided keystore.
|
||||
|
||||
The network management service can be started without a signer, the signing will be delegated to external process
|
||||
(e.g. HSM) connecting to the same database, the server will poll the database periodically for newly signed data and
|
||||
update the statuses accordingly.
|
||||
|
||||
Additional configuration needed for local signer:
|
||||
```
|
||||
#For local signing
|
||||
rootStorePath = ${basedir}"/certificates/rootstore.jks"
|
||||
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||
keystorePassword = "password"
|
||||
caPrivateKeyPassword = "password"
|
||||
```
|
||||
|
||||
## Doorman Service
|
||||
Doorman service can be started with the following options :
|
||||
|
||||
### JIRA
|
||||
|
||||
The doorman service can use JIRA to manage both the certificate signing request and the certificate revocation request approval work flows.
|
||||
This can be turned on by providing JIRA connection configuration in the config file.
|
||||
```
|
||||
doorman {
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
}
|
||||
.
|
||||
.
|
||||
.
|
||||
}
|
||||
```
|
||||
#### JIRA project configuration
|
||||
* The JIRA project should setup as "Business Project" with "Task" workflow.
|
||||
* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without
|
||||
these custom fields.
|
||||
|
||||
### Auto approval
|
||||
When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a
|
||||
test environment)
|
||||
|
||||
### Network map service
|
||||
Network map service can be enabled by providing the following config:
|
||||
```
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
```
|
||||
`cacheTimeout`(ms) determines how often the server should poll the database for a newly signed network map and also how often nodes should poll for a new network map (by including this value in the HTTP response header). **This is not how often changes to the network map are signed, which is a different process.**
|
||||
`signInterval`(ms) this is only relevant when local signer is enabled. The signer poll the database according to the `signInterval`, and create a new network map if the collection of node info hashes is different from the current network map.
|
||||
|
||||
## Example config file
|
||||
```
|
||||
basedir = "."
|
||||
address = "localhost:0"
|
||||
rootStorePath = ${basedir}"/certificates/rootstore.jks"
|
||||
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||
#keystorePassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#caPrivateKeyPassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#rootPrivateKeyPassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#rootKeystorePassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
|
||||
database {
|
||||
runMigration = true
|
||||
}
|
||||
|
||||
h2port = 0
|
||||
|
||||
# Comment out this section if running without doorman service
|
||||
doorman {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
}
|
||||
}
|
||||
|
||||
# Comment out this section if running without the revocation service
|
||||
revocation {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
crlUpdateInterval = 86400000
|
||||
crlEndpoint = "http://test.com/crl"
|
||||
crlCacheTimeout = 86400000
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "CRR"
|
||||
username = "username"
|
||||
password = "password"
|
||||
}
|
||||
}
|
||||
|
||||
# Comment out this section if running without network map service
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
```
|
||||
|
||||
# Running the network
|
||||
|
||||
### 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.
|
||||
The key stores can be created using `--mode` flag.
|
||||
```
|
||||
java -jar doorman-<version>.jar --config-file <config file> --mode ROOT_KEYGEN
|
||||
```
|
||||
and
|
||||
```
|
||||
java -jar doorman-<version>.jar --config-file <config file> --mode CA_KEYGEN
|
||||
```
|
||||
|
||||
A trust store containing the root certificate will be created in the location `distribute-nodes / network-root-truststore.jks`
|
||||
(relative to `rootStorePath`). The trust store's password can be set using command line argument `--trust-store-password`,
|
||||
or the doorman's keygen utility will ask for password input if trust store password is not provided using this flag.
|
||||
This trust store file is to be distributed to every node that wishes to register with the doorman. The node cannot
|
||||
register without it.
|
||||
|
||||
### 2. Start Doorman service for notary registration
|
||||
Start the network management server with the doorman service for initial bootstrapping. Network map service (`networkMap`)
|
||||
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 --config-file <config file>
|
||||
```
|
||||
|
||||
### 3. Create notary node and register with the doorman
|
||||
After the doorman service is started, start the notary node for registration.
|
||||
```
|
||||
java -jar corda.jar --initial-registration --network-root-truststore-password <trust store password>
|
||||
```
|
||||
By default it will expect trust store file received from the doorman to be in the location ``certificates/network-root-truststore.jks``.
|
||||
This can be overridden with the additional `--network-root-truststore` flag.
|
||||
|
||||
NOTE: This step applies to all nodes that wish to register with the doorman. You will have to configure ``compatibiityZoneURL`` and set ``devMode`` to false on each node.
|
||||
|
||||
### 4. Generate node info files for notary nodes
|
||||
Once notary nodes are registered, run the notary nodes with the `just-generate-node-info` flag.
|
||||
This will generate the node info files, which then should be referenced in the network parameters configuration.
|
||||
|
||||
### 5. Add notary identities to the network parameters
|
||||
The network parameters should contain reference to the notaries node info files.
|
||||
Example network parameters file:
|
||||
|
||||
notaries : [
|
||||
{
|
||||
notaryNodeInfoFile: "/Path/To/NodeInfo/File1"
|
||||
validating: true
|
||||
},
|
||||
{
|
||||
notaryNodeInfoFile: "/Path/To/NodeInfo/File2"
|
||||
validating: false
|
||||
}
|
||||
]
|
||||
minimumPlatformVersion = 1
|
||||
maxMessageSize = 10485760
|
||||
maxTransactionSize = 10485760
|
||||
eventHorizonDays = 30 # Duration in days
|
||||
|
||||
Save the parameters to `network-parameters.conf`
|
||||
|
||||
### 6. Load the initial network parameters
|
||||
A network parameters file is required to start the network map service for the first time. The initial network parameters
|
||||
file can be loaded using the `--set-network-parameters` flag. We can now restart the network management server with
|
||||
both doorman and network map service.
|
||||
```
|
||||
java -jar doorman-<version>.jar --config-file <config file> --set-network-parameters network-parameters.conf
|
||||
```
|
||||
|
||||
The server will terminate once this process is complete. Start it back up again with both the doorman and network map service.
|
||||
```
|
||||
java -jar doorman-<version>.jar --config-file <config file>
|
||||
```
|
||||
|
||||
### 7. Archive policy
|
||||
The ``node_info`` and ``network_map`` table are designed to retain all historical data for auditing purposes and will grow over time.
|
||||
**It is recommended to monitor the space usage and archive these tables according to the data retention policy.**
|
||||
|
||||
Run the following SQL script to archive the node info table (change the timestamp according to the archive policy):
|
||||
```
|
||||
delect from node_info where is_current = false and published_at < '2018-03-12'
|
||||
```
|
||||
|
||||
# Updating the network parameters
|
||||
The initial network parameters can be subsequently changed through an update process. However, these changes must first
|
||||
be advertised to the entire network to allow nodes time to agree to the changes. Every time the server needs to be shutdown
|
||||
and run with one of the following flags: `--set-network-parameters`, `--flag-day` or `--cancel-update`. For change to be
|
||||
advertised to the nodes new network map has to be signed (either by HSM or by local signer).
|
||||
|
||||
Typical update process is as follows:
|
||||
1. Start network map with initial network parameters.
|
||||
2. To advertise an update:
|
||||
* Stop network-management.
|
||||
* Run it with ``--set-network-parameters`` flag. The network parameters file must have `parametersUpdate` config block:
|
||||
```
|
||||
parametersUpdate {
|
||||
description = "Important update"
|
||||
updateDeadline = "2017-08-31T05:10:36.297Z" # ISO-8601 time, substitute it with update deadline
|
||||
}
|
||||
```
|
||||
Where `description` is a short description of the update that will be communicated to the nodes and `updateDeadline` is
|
||||
the time (in ISO-8601 format) by which all nodes in the network must decide that they have accepted the new parameters.
|
||||
|
||||
NOTE: Currently only backwards compatible changes to the network parameters can be made, i.e. notaries can't be removed,
|
||||
max transaction size can only increase, etc.
|
||||
|
||||
The process will exit, nothing will be sent to the nodes yet.
|
||||
* Start network-management as normal without any flags. This time, the nodes will be notified of the new parameters
|
||||
update next time they poll.
|
||||
3. Before the `updateDeadline` time, nodes will have to run the RPC command to accept new parameters.
|
||||
This will not activate the new network parameters on the nodes. It is possible to poll the network map database to check
|
||||
how many network participants have accepted the new network parameters - the information is stored in the `node-info.accepted_parameters_hash` column.
|
||||
4. When the flag day comes. Restart network-management with ``--flag-day`` flag. This will cause all nodes in the network
|
||||
to shutdown when they see that the network parameters have changed.
|
||||
The nodes that didn't accept the parameters will be removed from the network map. The ones that accepted, will need to be manually restarted.
|
||||
|
||||
It is possible to cancel the previously scheduled update. To do so simply run:
|
||||
```
|
||||
java -jar doorman-<version>.jar --cancel-update
|
||||
```
|
||||
|
||||
The network map will continue to advertise the cancelled update until the new network map is signed.
|
||||
|
||||
# Private Network Map
|
||||
The private network is a tactical solution to provide temporary privacy to the initial network map.
|
||||
|
||||
## Creating a private network
|
||||
To create a new private network, an entry has to be created in the ``private_network`` table manually.
|
||||
|
||||
Run the following SQL script to create a new private network:
|
||||
|
||||
```
|
||||
insert into private_network (id, name)
|
||||
values (NEWID(), 'Private Network Name')
|
||||
```
|
||||
|
||||
Then use the following SQL to retrieve the private network ID for the private network owner:
|
||||
```
|
||||
select id from private_network where name = 'Private Network Name'
|
||||
```
|
||||
|
||||
## Modify existing private network registration
|
||||
Since this is a tactical solution, any modification will require manual database changes.
|
||||
|
||||
**We should try to keep these changes to the minimal**
|
||||
|
||||
### Add nodes to a private network
|
||||
|
||||
```
|
||||
update certificate_signing_request
|
||||
set private_network = '<<private_network_id>>'
|
||||
where request_id in ('<<certificate_request_id>>', ...)
|
||||
```
|
||||
|
||||
or this SQL script to add all approved nodes to the private network map.
|
||||
|
||||
```
|
||||
update certificate_signing_request
|
||||
set private_network = '<<private_network_id>>'
|
||||
where status = 'APPROVED'
|
||||
```
|
||||
|
||||
**Important**
|
||||
If notary is to be used by private network participants add private network UUIDs to notary's ``node.conf`` using
|
||||
``extraNetworkMapKeys`` list.
|
||||
|
||||
### Move a node from its private network and into the global network map**
|
||||
|
||||
```
|
||||
update certificate_signing_request
|
||||
set private_network = null
|
||||
where request_id = '<<certificate_request_id>>'
|
||||
```
|
@ -1,124 +0,0 @@
|
||||
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
description 'Network management module encapsulating components such as Doorman, HSM Signing Service and Network Map'
|
||||
|
||||
apply plugin: 'us.kirchmeier.capsule'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-jpa'
|
||||
|
||||
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 {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
java {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/java')
|
||||
}
|
||||
resources {
|
||||
srcDir file('src/integration-test/resources')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: '*.jar')
|
||||
|
||||
compile project(':node-api')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
// Log4J: logging framework (with SLF4J bindings)
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
||||
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
||||
compile "org.apache.logging.log4j:log4j-web:${log4j_version}"
|
||||
|
||||
// Web stuff: for HTTP[S] servlets
|
||||
compile "org.eclipse.jetty:jetty-servlet:${jetty_version}"
|
||||
compile "org.eclipse.jetty:jetty-webapp:${jetty_version}"
|
||||
compile "javax.servlet:javax.servlet-api:3.1.0"
|
||||
|
||||
// Jersey for JAX-RS implementation for use in Jetty
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
compile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
||||
compile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
||||
|
||||
// JOpt: for command line flags.
|
||||
compile "net.sf.jopt-simple:jopt-simple:5.0.4"
|
||||
|
||||
// TypeSafe Config: for simple and human friendly config files.
|
||||
compile "com.typesafe:config:$typesafe_config_version"
|
||||
|
||||
// Hibernate audit plugin
|
||||
compile "org.hibernate:hibernate-envers:5.2.11.Final"
|
||||
|
||||
// Manifests: for reading stuff from the manifest file
|
||||
compile "com.jcabi:jcabi-manifests:1.1"
|
||||
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':node-driver')
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
testCompile "com.nhaarman:mockito-kotlin:1.5.0"
|
||||
testCompile "com.spotify:docker-client:8.9.1"
|
||||
|
||||
compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') {
|
||||
// The jira client includes jersey-core 1.5 which breaks everything.
|
||||
exclude module: 'jersey-core'
|
||||
}
|
||||
// Needed by jira rest client
|
||||
compile "com.atlassian.fugue:fugue:2.6.1"
|
||||
|
||||
// SQL connection pooling library
|
||||
compile "com.zaxxer:HikariCP:${hikari_version}"
|
||||
|
||||
// For H2 database support in persistence
|
||||
compile "com.h2database:h2:$h2_version"
|
||||
|
||||
//TODO remove once we can put driver jar into a predefined directory
|
||||
//JDBC driver can be passed to the Node at startup using setting the jarDirs property in the Node configuration file.
|
||||
compile 'com.microsoft.sqlserver:mssql-jdbc:6.2.1.jre8'
|
||||
|
||||
// Bouncy Castle for HSM signing
|
||||
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
|
||||
compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}"
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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 buildCrrSubmissionJAR(type: FatCapsule, dependsOn: 'jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.tools.crr.submission.MainKt'
|
||||
archiveName "crr-submission-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
systemProperties['visualvm.display.name'] = 'CRR Submission Tool'
|
||||
minJavaVersion = '1.8.0'
|
||||
jvmArgs = ['-XX:+UseG1GC']
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':network-management').configurations.runtime,
|
||||
project(':network-management').jar
|
||||
)
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildCrrSubmissionJAR
|
||||
publish buildCrrSubmissionJAR
|
||||
}
|
||||
|
||||
jar {
|
||||
classifier "ignore"
|
||||
}
|
||||
|
||||
publish {
|
||||
name 'crr-submission'
|
||||
disableDefaultJar = true
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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.certificate.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
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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 buildHsmCrlGeneratorJAR(type: FatCapsule, dependsOn: 'jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.hsm.generator.crl.MainKt'
|
||||
archiveName "hsm-crl-generator-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
systemProperties['visualvm.display.name'] = 'HSM CRL Generator'
|
||||
minJavaVersion = '1.8.0'
|
||||
jvmArgs = ['-XX:+UseG1GC']
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':network-management').configurations.runtime,
|
||||
project(':network-management').jar
|
||||
)
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildHsmCrlGeneratorJAR
|
||||
publish buildHsmCrlGeneratorJAR
|
||||
}
|
||||
|
||||
jar {
|
||||
classifier "ignore"
|
||||
}
|
||||
|
||||
publish {
|
||||
name 'hsm-crl-generator'
|
||||
disableDefaultJar = true
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'us.kirchmeier.capsule'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Doorman default'
|
||||
|
||||
version project(':network-management').version
|
||||
|
||||
configurations {
|
||||
runtimeArtifacts.extendsFrom runtime
|
||||
}
|
||||
|
||||
task buildHsmJAR(type: FatCapsule, dependsOn: 'jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.hsm.MainKt'
|
||||
archiveName "hsm-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
systemProperties['visualvm.display.name'] = 'HSM Signing Service'
|
||||
minJavaVersion = '1.8.0'
|
||||
jvmArgs = ['-XX:+UseG1GC']
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':network-management').configurations.runtime,
|
||||
project(':network-management').jar
|
||||
)
|
||||
tasks.withType(Jar) { task ->
|
||||
manifest {
|
||||
attributes('Signing-Service-Version': version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildHsmJAR
|
||||
publish buildHsmJAR
|
||||
}
|
||||
|
||||
jar {
|
||||
classifier "ignore"
|
||||
}
|
||||
|
||||
publish {
|
||||
name 'doorman-hsm'
|
||||
disableDefaultJar = true
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'us.kirchmeier.capsule'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Doorman default'
|
||||
|
||||
version project(':network-management').version
|
||||
|
||||
configurations {
|
||||
runtimeArtifacts.extendsFrom runtime
|
||||
}
|
||||
|
||||
task buildDoormanJAR(type: FatCapsule, dependsOn: ':network-management:jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.doorman.MainKt'
|
||||
archiveName "doorman-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
systemProperties['visualvm.display.name'] = 'Doorman'
|
||||
minJavaVersion = '1.8.0'
|
||||
jvmArgs = ['-XX:+UseG1GC']
|
||||
}
|
||||
applicationSource = files(
|
||||
project(':network-management').configurations.runtime,
|
||||
project(':network-management').jar
|
||||
)
|
||||
tasks.withType(Jar) { task ->
|
||||
manifest {
|
||||
attributes('Doorman-Version': version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildDoormanJAR
|
||||
publish buildDoormanJAR {
|
||||
classifier ""
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
classifier "ignore"
|
||||
}
|
||||
|
||||
publish {
|
||||
name 'doorman'
|
||||
disableDefaultJar = true
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
hsmHost = 127.0.0.1
|
||||
hsmPort = 3001
|
||||
trustStoreDirectory = "."
|
||||
trustStorePassword = "trustpass"
|
||||
|
||||
certConfig {
|
||||
subject = "CN=Corda Root, O=R3 HoldCo LLC, OU=Corda, L=New York, C=US"
|
||||
certificateType = ROOT_CA
|
||||
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"
|
||||
}
|
||||
]
|
@ -1,26 +0,0 @@
|
||||
basedir = "."
|
||||
host = localhost
|
||||
port = 0
|
||||
|
||||
# Database config
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
h2port = 0
|
||||
|
||||
# Doorman config
|
||||
# Comment out this section if running without doorman service
|
||||
doorman {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
}
|
||||
|
||||
# Network map config
|
||||
# Comment out this section if running without network map service
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
basedir = "."
|
||||
host = localhost
|
||||
port = 0
|
||||
|
||||
# Database config
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
h2port = 0
|
||||
|
||||
# Doorman config
|
||||
# Comment out this section if running without doorman service
|
||||
doorman {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
}
|
||||
|
||||
# Network map config
|
||||
# Comment out this section if running without network map service
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
basedir = "."
|
||||
host = localhost
|
||||
port = 0
|
||||
|
||||
#For local signing
|
||||
rootStorePath = ${basedir}"/certificates/rootstore.jks"
|
||||
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||
keystorePassword = "password"
|
||||
caPrivateKeyPassword = "password"
|
||||
|
||||
# Database config
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
h2port = 0
|
||||
|
||||
# Doorman config
|
||||
# Comment out this section if running without doorman service
|
||||
doorman {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
}
|
||||
|
||||
# Network map config
|
||||
# Comment out this section if running without network map service
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
hsmHost = 127.0.0.1
|
||||
hsmPort = 3001
|
||||
trustStoreFile = "./truststore.jks"
|
||||
trustStorePassword = "trustpass"
|
||||
|
||||
crl {
|
||||
keyGroup = "TEST.CORDACONNECT.ROOT"
|
||||
keySpecifier = 1
|
||||
validDays = 3650
|
||||
crlEndpoint = "http://test.com/crl"
|
||||
indirectIssuer = true
|
||||
filePath = "./bytes.crl"
|
||||
revocations = [
|
||||
{
|
||||
certificateSerialNumber = "12345"
|
||||
dateInMillis = 1526643707290
|
||||
reason = "KEY_COMPROMISE"
|
||||
},
|
||||
{
|
||||
certificateSerialNumber = "6789012"
|
||||
dateInMillis = 1526643712345
|
||||
reason = "KEY_COMPROMISE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
userConfigs = [
|
||||
{
|
||||
username = "INTEGRATION_TEST"
|
||||
authMode = PASSWORD
|
||||
authToken = "INTEGRATION_TEST"
|
||||
}
|
||||
]
|
@ -1,6 +0,0 @@
|
||||
privateKeyPass ="cordacadevkeypass"
|
||||
keyStorePass = "cordacadevpass"
|
||||
keyStoreFileName = "cordadevcakeys.jks"
|
||||
trustStorePass = "trustpass"
|
||||
trustStoreFileName = "cordatruststore.jks"
|
||||
directory = "./certificates"
|
@ -1,55 +0,0 @@
|
||||
basedir = "."
|
||||
address = "localhost:0"
|
||||
rootStorePath = ${basedir}"/certificates/rootstore.jks"
|
||||
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||
#keystorePassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#caPrivateKeyPassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#rootPrivateKeyPassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
#rootKeystorePassword = "password" #Optional if not specified, user will be prompted on the console.
|
||||
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
|
||||
database {
|
||||
runMigration = true
|
||||
}
|
||||
|
||||
h2port = 0
|
||||
|
||||
# Comment out this section if running without the doorman service
|
||||
doorman {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "CSR"
|
||||
username = "username"
|
||||
password = "password"
|
||||
}
|
||||
}
|
||||
|
||||
# Comment out this section if running without the revocation service
|
||||
revocation {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
crlUpdateInterval = 86400000
|
||||
crlEndpoint = "http://test.com/crl"
|
||||
crlCacheTimeout = 86400000
|
||||
jira {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "CRR"
|
||||
username = "username"
|
||||
password = "password"
|
||||
}
|
||||
}
|
||||
|
||||
# Comment out this section if running without network map service
|
||||
networkMap {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
basedir = "."
|
||||
device = "3001@192.168.0.1"
|
||||
keySpecifier = -1
|
||||
|
||||
doorman {
|
||||
crlDistributionPoint = "http://test.com/revoked.crl"
|
||||
crlServerSocketAddress = "test.com:2333"
|
||||
crlUpdatePeriod = 200000
|
||||
validDays = 3650
|
||||
mode = CSR
|
||||
rootKeyStoreFile = "dummyfile.jks"
|
||||
rootKeyStorePassword = "trustpass"
|
||||
keyGroup = "DEV.CORDACONNECT.OPS.CERT"
|
||||
authParameters {
|
||||
mode = PASSWORD
|
||||
password = "PASSWORD"
|
||||
threshold = 2
|
||||
}
|
||||
}
|
||||
|
||||
h2port = 0
|
||||
dataSourceProperties {
|
||||
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
basedir = "."
|
||||
device = "3001@192.168.0.1"
|
||||
keySpecifier = -1
|
||||
|
||||
networkMap {
|
||||
username = "TEST_USERNAME",
|
||||
keyGroup = "DEV.CORDACONNECT.OPS.NETMAP"
|
||||
authParameters {
|
||||
mode = KEY_FILE
|
||||
password = "PASSWORD"
|
||||
keyFilePath = "./Administrator.KEY"
|
||||
threshold = 2
|
||||
}
|
||||
}
|
||||
|
||||
h2port = 0
|
||||
dataSourceProperties {
|
||||
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
notaries : [{
|
||||
notaryNodeInfoFile: "/Path/To/NodeInfo/File1"
|
||||
validating: true
|
||||
}, {
|
||||
notaryNodeInfoFile: "/Path/To/NodeInfo/File2"
|
||||
validating: false
|
||||
}]
|
||||
minimumPlatformVersion = 1
|
||||
maxMessageSize = 10485760
|
||||
maxTransactionSize = 10485760
|
||||
eventHorizonDays = 30 # Duration in days
|
@ -1,79 +0,0 @@
|
||||
#Distributed Notary Registration Tool
|
||||
|
||||
The notary registration tool creates a CSR (Certificate Signing Request) with ``SERVICE_IDENTITY`` certificate role 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.
|
||||
|
||||
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.
|
||||
|
||||
keystorePath [Optional] Path of the generated keystore file, default to certificates/notaryidentitykeystore.jks
|
||||
```
|
||||
|
||||
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"
|
||||
|
||||
networkRootTrustStorePassword = "password"
|
||||
keyStorePassword = "password"
|
||||
trustStorePassword = "password"
|
||||
|
||||
```
|
||||
|
||||
##Running the registration tool
|
||||
|
||||
``java -jar registration-tool-<<version>>.jar --config-file <<config file path>>``
|
||||
|
||||
# Private key copy tool
|
||||
|
||||
The key copy tool copies the private key from the source keystore to the destination keystore, it's similar to the
|
||||
``importkeystore`` option in Java keytool with extra support for Corda's key algorithms.
|
||||
**This is useful for provisioning keystores for distributed notaries.**
|
||||
|
||||
### Usage
|
||||
|
||||
``java -jar registration-tool-<<version>>.jar --importkeystore [options]``
|
||||
|
||||
### Example
|
||||
|
||||
Copy ``distributed-notary-private-key`` from distributed notaries keystore to node's keystore.
|
||||
|
||||
```
|
||||
java -jar registration-tool-<<version>>.jar \
|
||||
--importkeystore \
|
||||
--srckeystore notaryKeystore.jks \
|
||||
--destkeystore node.jks \
|
||||
--srcstorepass notaryPassword \
|
||||
--deststorepass nodepassword \
|
||||
--srcalias distributed-notary-private-key
|
||||
```
|
||||
|
||||
``--help`` prints a list of all the available options.
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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.registration.MainKt'
|
||||
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')
|
||||
// TODO: remove this when ArgsParser is moved into corda.
|
||||
compile project(':network-management')
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import com.r3.corda.networkmanage.registration.ToolOption.KeyCopierOption
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
|
||||
/**
|
||||
* This utility will copy private key with [KeyCopierOption.sourceAlias] from provided source keystore and copy it to target
|
||||
* keystore with the same alias, or [KeyCopierOption.destinationAlias] if provided.
|
||||
*
|
||||
* This tool uses Corda's security provider which support EdDSA keys.
|
||||
*/
|
||||
fun KeyCopierOption.copyKeystore() {
|
||||
println("**************************************")
|
||||
println("* *")
|
||||
println("* Key copy tool *")
|
||||
println("* *")
|
||||
println("**************************************")
|
||||
println()
|
||||
println("This utility will copy private key with [srcalias] from provided source keystore and copy it to target keystore with the same alias, or [destalias] if provided.")
|
||||
println()
|
||||
|
||||
// Read private key and certificates from notary identity keystore.
|
||||
val srcKeystore = X509KeyStore.fromFile(sourceFile, sourcePassword ?: readPassword("Source key store password:"))
|
||||
val srcPrivateKey = srcKeystore.getPrivateKey(sourceAlias)
|
||||
val srcCertChain = srcKeystore.getCertificateChain(sourceAlias)
|
||||
|
||||
X509KeyStore.fromFile(destinationFile, destinationPassword ?: readPassword("Destination key store password:")).update {
|
||||
val keyAlias = destinationAlias ?: sourceAlias
|
||||
setPrivateKey(keyAlias, srcPrivateKey, srcCertChain)
|
||||
println("Added '$keyAlias' to keystore : $destinationFile")
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.ArgsParser
|
||||
import com.r3.corda.networkmanage.registration.ToolOption.KeyCopierOption
|
||||
import com.r3.corda.networkmanage.registration.ToolOption.RegistrationOption
|
||||
import joptsimple.OptionSet
|
||||
import joptsimple.OptionSpecBuilder
|
||||
import joptsimple.util.PathConverter
|
||||
import joptsimple.util.PathProperties
|
||||
import net.corda.core.crypto.Crypto
|
||||
import java.nio.file.Path
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Crypto.registerProviders() // Required to register Providers first thing on boot.
|
||||
val options = ToolArgsParser().parseOrExit(*args, printHelpOn = System.out)
|
||||
when (options) {
|
||||
is RegistrationOption -> options.runRegistration()
|
||||
is KeyCopierOption -> options.copyKeystore()
|
||||
}
|
||||
}
|
||||
|
||||
class ToolArgsParser : ArgsParser<ToolOption>() {
|
||||
private val importKeyStoreArg = optionParser.accepts("importkeystore")
|
||||
|
||||
private val configFileArg = optionParser
|
||||
.accepts("config-file", "Path to the registration config file")
|
||||
.availableUnless(importKeyStoreArg)
|
||||
.requiredUnless(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
// key copy tool args
|
||||
private val destKeystorePathArg = optionParser.accepts("destkeystore", "Path to the destination keystore which the private key should be copied to")
|
||||
.requireOnlyIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
|
||||
private val srcKeystorePathArg = optionParser.accepts("srckeystore", "Path to the source keystore containing the private key")
|
||||
.requireOnlyIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
|
||||
private val destPasswordArg = optionParser.accepts("deststorepass", "Source keystore password. Read in from the console if not specified.")
|
||||
.availableIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
|
||||
private val srcPasswordArg = optionParser.accepts("srcstorepass", "Destination keystore password. Read in from the console if not specified.")
|
||||
.availableIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
|
||||
private val destAliasArg = optionParser.accepts("destalias", "The alias under which the private key will be stored in the destination key store. If not provided then [srcalias] is used.")
|
||||
.availableIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
|
||||
private val srcAliasArg = optionParser.accepts("srcalias", "The alias under which the private key resides in the source key store")
|
||||
.requireOnlyIf(importKeyStoreArg)
|
||||
.withRequiredArg()
|
||||
|
||||
override fun parse(optionSet: OptionSet): ToolOption {
|
||||
val isCopyKey = optionSet.has(importKeyStoreArg)
|
||||
return if (isCopyKey) {
|
||||
val srcKeystorePath = optionSet.valueOf(srcKeystorePathArg)
|
||||
val targetKeystorePath = optionSet.valueOf(destKeystorePathArg)
|
||||
val srcPassword = optionSet.valueOf(srcPasswordArg)
|
||||
val destPassword = optionSet.valueOf(destPasswordArg)
|
||||
val srcAlias = optionSet.valueOf(srcAliasArg)
|
||||
val destAlias = optionSet.valueOf(destAliasArg)
|
||||
KeyCopierOption(srcKeystorePath, targetKeystorePath, srcPassword, destPassword, srcAlias, destAlias)
|
||||
} else {
|
||||
val configFile = optionSet.valueOf(configFileArg)
|
||||
RegistrationOption(configFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun OptionSpecBuilder.requireOnlyIf(option: OptionSpecBuilder): OptionSpecBuilder = requiredIf(option).availableIf(option)
|
||||
|
||||
sealed class ToolOption {
|
||||
data class RegistrationOption(val configFile: Path) : ToolOption()
|
||||
data class KeyCopierOption(val sourceFile: Path,
|
||||
val destinationFile: Path,
|
||||
val sourcePassword: String?,
|
||||
val destinationPassword: String?,
|
||||
val sourceAlias: String,
|
||||
val destinationAlias: String?) : ToolOption()
|
||||
}
|
||||
|
||||
fun readPassword(fmt: String): String {
|
||||
return if (System.console() != null) {
|
||||
String(System.console().readPassword(fmt))
|
||||
} else {
|
||||
print(fmt)
|
||||
readLine() ?: ""
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import com.r3.corda.networkmanage.registration.ToolOption.RegistrationOption
|
||||
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.core.internal.div
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
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
|
||||
|
||||
const val NOTARY_PRIVATE_KEY_ALIAS = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key"
|
||||
|
||||
fun RegistrationOption.runRegistration() {
|
||||
println("**********************************************************")
|
||||
println("* *")
|
||||
println("* Notary identity registration tool *")
|
||||
println("* *")
|
||||
println("**********************************************************")
|
||||
println()
|
||||
println("This tool will create a notary identity certificate signing request using information found in '$configFile'")
|
||||
println()
|
||||
|
||||
val config = ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
.resolve()
|
||||
.parseAs<NotaryRegistrationConfig>()
|
||||
|
||||
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:") }
|
||||
val parent = configFile.parent
|
||||
override val certificatesDirectory: Path = if (parent != null) parent / "certificates" else Paths.get("certificates")
|
||||
override val nodeKeystore: Path get() = config.keystorePath ?: certificatesDirectory/"notaryidentitykeystore.jks"
|
||||
override val crlCheckSoftFail: Boolean = config.crlCheckSoftFail
|
||||
}
|
||||
|
||||
NetworkRegistrationHelper(sslConfig,
|
||||
config.legalName,
|
||||
config.email,
|
||||
HTTPNetworkRegistrationService(config.compatibilityZoneURL),
|
||||
config.networkRootTrustStorePath,
|
||||
config.networkRootTrustStorePassword ?: readPassword("Network trust root password:"),
|
||||
NOTARY_PRIVATE_KEY_ALIAS,
|
||||
CertRole.SERVICE_IDENTITY).buildKeystore()
|
||||
}
|
||||
|
||||
data class NotaryRegistrationConfig(val legalName: CordaX500Name,
|
||||
val email: String,
|
||||
val compatibilityZoneURL: URL,
|
||||
val networkRootTrustStorePath: Path,
|
||||
val keyStorePassword: String?,
|
||||
val networkRootTrustStorePassword: String?,
|
||||
val trustStorePassword: String?,
|
||||
val keystorePath: Path?,
|
||||
val crlCheckSoftFail: Boolean,
|
||||
val crlDistributionPoint: URL? = null)
|
@ -1,15 +0,0 @@
|
||||
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"
|
@ -1,47 +0,0 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
class KeyCopyToolTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
private val tempDir: Path by lazy { tempFolder.root.toPath() }
|
||||
|
||||
@Test
|
||||
fun `key copy correctly`() {
|
||||
val keyCopyOption = ToolOption.KeyCopierOption(
|
||||
sourceFile = tempDir / "srcKeystore.jks",
|
||||
destinationFile = tempDir / "destKeystore.jks",
|
||||
sourcePassword = "srctestpass",
|
||||
destinationPassword = "desttestpass",
|
||||
sourceAlias = "TestKeyAlias",
|
||||
destinationAlias = null)
|
||||
|
||||
// Prepare source and destination keystores
|
||||
val keyPair = Crypto.generateKeyPair()
|
||||
val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test"), keyPair)
|
||||
|
||||
X509KeyStore.fromFile(keyCopyOption.sourceFile, keyCopyOption.sourcePassword!!, createNew = true).update {
|
||||
setPrivateKey(keyCopyOption.sourceAlias, keyPair.private, listOf(cert))
|
||||
}
|
||||
X509KeyStore.fromFile(keyCopyOption.destinationFile, keyCopyOption.destinationPassword!!, createNew = true)
|
||||
|
||||
// Copy private key from src keystore to dest keystore using the tool
|
||||
keyCopyOption.copyKeystore()
|
||||
|
||||
// Verify key copied correctly
|
||||
val destKeystore = X509KeyStore.fromFile(keyCopyOption.destinationFile, keyCopyOption.destinationPassword!!)
|
||||
assertEquals(keyPair.private, destKeystore.getPrivateKey(keyCopyOption.sourceAlias, keyCopyOption.destinationPassword!!))
|
||||
assertEquals(cert, destKeystore.getCertificate(keyCopyOption.sourceAlias))
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package com.r3.corda.networkmanage.registration
|
||||
|
||||
import joptsimple.OptionException
|
||||
import net.corda.core.internal.div
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class OptionParserTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
lateinit var tempDir: Path
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
tempDir = tempFolder.root.toPath()
|
||||
Files.createFile(tempDir / "test.file")
|
||||
Files.createFile(tempDir / "source.jks")
|
||||
Files.createFile(tempDir / "target.jks")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration args correctly`() {
|
||||
val options = ToolArgsParser().parseOrExit("--config-file", "${tempDir / "test.file"}") as ToolOption.RegistrationOption
|
||||
assertThat(options.configFile).isEqualTo(tempDir / "test.file")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `registration args should be unavailable in key copy mode`() {
|
||||
val keyCopyArgs = arrayOf(
|
||||
"--importkeystore",
|
||||
"--srckeystore", "${tempDir / "source.jks"}",
|
||||
"--srcstorepass", "password1",
|
||||
"--destkeystore", "${tempDir / "target.jks"}",
|
||||
"--deststorepass", "password2",
|
||||
"--srcalias", "testalias")
|
||||
assertThatThrownBy { ToolArgsParser().parseOrExit(*keyCopyArgs, "--config-file", "test.file", printHelpOn = null) }
|
||||
.isInstanceOf(OptionException::class.java)
|
||||
.hasMessageContaining("Option(s) [config-file] are unavailable given other options on the command line")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `key copy args should be unavailable in registration mode`() {
|
||||
assertThatThrownBy {
|
||||
ToolArgsParser().parseOrExit("--config-file", "${tempDir / "test.file"}", "--srckeystore", "${tempDir / "source.jks"}", printHelpOn = null)
|
||||
}.isInstanceOf(OptionException::class.java)
|
||||
.hasMessageContaining("Option(s) [srckeystore] are unavailable given other options on the command line")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all import keystore options`() {
|
||||
val keyCopyArgs = arrayOf(
|
||||
"--importkeystore",
|
||||
"--srckeystore", "${tempDir / "source.jks"}",
|
||||
"--srcstorepass", "password1",
|
||||
"--destkeystore", "${tempDir / "target.jks"}",
|
||||
"--deststorepass", "password2",
|
||||
"--srcalias", "testalias",
|
||||
"--destalias", "testalias2")
|
||||
assertThat(ToolArgsParser().parseOrExit(*keyCopyArgs)).isEqualTo(ToolOption.KeyCopierOption(
|
||||
sourceFile = tempDir / "source.jks",
|
||||
destinationFile = tempDir / "target.jks",
|
||||
sourcePassword = "password1",
|
||||
destinationPassword = "password2",
|
||||
sourceAlias = "testalias",
|
||||
destinationAlias = "testalias2"
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `minimum import keystore options`() {
|
||||
val keyCopyArgs = arrayOf(
|
||||
"--importkeystore",
|
||||
"--srckeystore", "${tempDir / "source.jks"}",
|
||||
"--destkeystore", "${tempDir / "target.jks"}",
|
||||
"--srcalias", "testalias")
|
||||
assertThat(ToolArgsParser().parseOrExit(*keyCopyArgs)).isEqualTo(ToolOption.KeyCopierOption(
|
||||
sourceFile = tempDir / "source.jks",
|
||||
destinationFile = tempDir / "target.jks",
|
||||
sourcePassword = null,
|
||||
destinationPassword = null,
|
||||
sourceAlias = "testalias",
|
||||
destinationAlias = null
|
||||
))
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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.nodeapi.internal.config.parseAs
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
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"
|
||||
keystorePath = "notaryidentitykeystore.jks"
|
||||
|
||||
networkRootTrustStorePassword = "password"
|
||||
keyStorePassword = "password"
|
||||
trustStorePassword = "password"
|
||||
crlCheckSoftFail = true
|
||||
""".trimIndent()
|
||||
|
||||
val config = ConfigFactory.parseString(testConfig, ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
.resolve()
|
||||
.parseAs<NotaryRegistrationConfig>()
|
||||
|
||||
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)
|
||||
assertEquals(Paths.get("notaryidentitykeystore.jks"), config.keystorePath)
|
||||
assertTrue(config.crlCheckSoftFail)
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage
|
||||
|
||||
import CryptoServerAPI.CryptoServerException
|
||||
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
|
||||
import com.r3.corda.networkmanage.hsm.authentication.createProvider
|
||||
import com.spotify.docker.client.DefaultDockerClient
|
||||
import com.spotify.docker.client.DockerClient
|
||||
import com.spotify.docker.client.messages.ContainerConfig
|
||||
import com.spotify.docker.client.messages.HostConfig
|
||||
import com.spotify.docker.client.messages.PortBinding
|
||||
import com.spotify.docker.client.messages.RegistryAuth
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.testing.core.freeLocalHostAndPort
|
||||
import org.junit.Assume.assumeFalse
|
||||
import org.junit.rules.ExternalResource
|
||||
|
||||
data class CryptoUserCredentials(val username: String, val password: String)
|
||||
|
||||
/**
|
||||
* HSM Simulator rule allowing to use the HSM Simulator within the integration tests. It is designed to be used mainly
|
||||
* on the TeamCity, but if the required setup is available it can be executed locally as well.
|
||||
* It will bind to the simulator to the local port
|
||||
* To use it locally, the following pre-requisites need to be met:
|
||||
* 1) Docker engine needs to be installed on the machine
|
||||
* 2) Environment variables (AZURE_CR_USER and AZURE_CR_PASS) are available and hold valid credentials to the corda.azurecr.io
|
||||
* repository
|
||||
* 3) HSM requires Unlimited Strength Jurisdiction extension to be installed on the machine connecting with the HSM box.
|
||||
* See http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
|
||||
*
|
||||
* Since the above setup is not a strong requirement for the integration tests to be executed it is intended that this
|
||||
* rule is used together with the assumption mechanism in tests.
|
||||
*/
|
||||
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() {
|
||||
|
||||
private companion object {
|
||||
val DEFAULT_SERVER_ADDRESS = "corda.azurecr.io"
|
||||
/*
|
||||
* Currently we have following images:
|
||||
* 1) corda.azurecr.io/network-management/hsm-simulator - having only one user configured:
|
||||
* - INTEGRATION_TEST (password: INTEGRATION_TEST) with the CXI_GROUP="*"
|
||||
* 2)corda.azurecr.io/network-management/hsm-simulator-with-groups - having following users configured:
|
||||
* - INTEGRATION_TEST (password: INTEGRATION_TEST) with the CXI_GROUP=*
|
||||
* - INTEGRATION_TEST_SUPER (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT
|
||||
* - INTEGRATION_TEST_ROOT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.ROOT
|
||||
* - INTEGRATION_TEST_OPS (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS
|
||||
* - INTEGRATION_TEST_SUPER_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.*
|
||||
* - INTEGRATION_TEST_ROOT_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.ROOT.*
|
||||
* - INTEGRATION_TEST_OPS_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.*
|
||||
* - INTEGRATION_TEST_OPS_CERT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.CERT
|
||||
* - INTEGRATION_TEST_OPS_NETMAP (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.NETMAP
|
||||
* - INTEGRATION_TEST_OPS_CERT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.CERT.*
|
||||
* - INTEGRATION_TEST_OPS_NETMAP (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.NETMAP.*
|
||||
*/
|
||||
val DEFAULT_IMAGE_REPO_TAG = "corda.azurecr.io/network-management/hsm-simulator-with-groups"
|
||||
val DEFAULT_IMAGE_VERSION = "latest"
|
||||
val DEFAULT_PULL_IMAGE = true
|
||||
|
||||
val HSM_SIMULATOR_PORT = "3001/tcp"
|
||||
val CONTAINER_KILL_TIMEOUT_SECONDS = 10
|
||||
|
||||
val CRYPTO_USER = System.getProperty("CRYPTO_USER", "INTEGRATION_TEST")
|
||||
val CRYPTO_PASSWORD = System.getProperty("CRYPTO_PASSWORD", "INTEGRATION_TEST")
|
||||
|
||||
val REGISTRY_USERNAME = System.getenv("AZURE_CR_USER")
|
||||
val REGISTRY_PASSWORD = System.getenv("AZURE_CR_PASS")
|
||||
|
||||
val log = loggerFor<HsmSimulator>()
|
||||
|
||||
private val HSM_STARTUP_SLEEP_INTERVAL_MS = 500L
|
||||
private val HSM_STARTUP_POLL_MAX_COUNT = 10;
|
||||
}
|
||||
|
||||
private val localHostAndPortBinding = freeLocalHostAndPort()
|
||||
private lateinit var docker: DockerClient
|
||||
private var containerId: String? = null
|
||||
|
||||
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()
|
||||
if (pullImage) {
|
||||
docker.pullHsmSimulatorImageFromRepository()
|
||||
}
|
||||
containerId = docker.createContainer()
|
||||
docker.startHsmSimulatorContainer()
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
docker.stopAndRemoveHsmSimulatorContainer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the port at which simulator is listening at.
|
||||
*/
|
||||
val port get(): Int = localHostAndPortBinding.port
|
||||
|
||||
/**
|
||||
* Retrieves the host IP address, which the simulator is listening at.
|
||||
*/
|
||||
val host get(): String = localHostAndPortBinding.host
|
||||
|
||||
/**
|
||||
* Retrieves the HSM user credentials. Those are supposed to be preconfigured on the HSM itself. Thus, when
|
||||
* tests are executed these credentials can be used to access HSM crypto user's functionality.
|
||||
* It is assumed that the docker image state has those configured already and they should match the ones returned.
|
||||
*/
|
||||
fun cryptoUserCredentials(): CryptoUserCredentials {
|
||||
return CryptoUserCredentials(CRYPTO_USER, CRYPTO_PASSWORD)
|
||||
}
|
||||
|
||||
private fun DockerClient.stopAndRemoveHsmSimulatorContainer() {
|
||||
if (containerId != null) {
|
||||
log.debug("Stopping container $containerId...")
|
||||
this.stopContainer(containerId, CONTAINER_KILL_TIMEOUT_SECONDS)
|
||||
log.debug("Removing container $containerId...")
|
||||
this.removeContainer(containerId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DockerClient.startHsmSimulatorContainer() {
|
||||
if (containerId != null) {
|
||||
log.debug("Starting container $containerId...")
|
||||
this.startContainer(containerId)
|
||||
pollAndWaitForHsmSimulator()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollAndWaitForHsmSimulator() {
|
||||
val config = CryptoServerProviderConfig(
|
||||
Device = "${localHostAndPortBinding.port}@${localHostAndPortBinding.host}",
|
||||
KeyGroup = "*",
|
||||
KeySpecifier = -1
|
||||
)
|
||||
var pollCount = HSM_STARTUP_POLL_MAX_COUNT
|
||||
while (pollCount > 0) {
|
||||
val provider = createProvider(config)
|
||||
try {
|
||||
provider.loginPassword(CRYPTO_USER, CRYPTO_PASSWORD)
|
||||
provider.cryptoServer.authState
|
||||
return
|
||||
} catch (e: CryptoServerException) {
|
||||
pollCount--
|
||||
Thread.sleep(HSM_STARTUP_SLEEP_INTERVAL_MS)
|
||||
} finally {
|
||||
provider.logoff()
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("Unable to obtain connection to initialised HSM Simulator")
|
||||
}
|
||||
|
||||
private fun getImageFullName() = "$imageRepoTag:$imageVersion"
|
||||
|
||||
private fun DockerClient.pullHsmSimulatorImageFromRepository(): DockerClient {
|
||||
this.pull(imageRepoTag,
|
||||
RegistryAuth.builder()
|
||||
.serverAddress(serverAddress)
|
||||
.username(registryUser)
|
||||
.password(registryPass)
|
||||
.build())
|
||||
return this
|
||||
}
|
||||
|
||||
private fun DockerClient.createContainer(): String? {
|
||||
val portBindings = mapOf(HSM_SIMULATOR_PORT to listOf(PortBinding.create(localHostAndPortBinding.host, localHostAndPortBinding.port.toString())))
|
||||
val hostConfig = HostConfig.builder().portBindings(portBindings).build()
|
||||
val containerConfig = ContainerConfig.builder()
|
||||
.hostConfig(hostConfig)
|
||||
.portSpecs()
|
||||
.image(getImageFullName())
|
||||
.exposedPorts(HSM_SIMULATOR_PORT)
|
||||
.build()
|
||||
val containerCreation = this.createContainer(containerConfig)
|
||||
return containerCreation.id()
|
||||
}
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common
|
||||
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.r3.corda.networkmanage.HsmSimulator
|
||||
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
|
||||
import com.r3.corda.networkmanage.hsm.authentication.InputReader
|
||||
import com.r3.corda.networkmanage.hsm.configuration.*
|
||||
import com.r3.corda.networkmanage.hsm.generator.UserAuthenticationParameters
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.CertificateConfiguration
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.GeneratorParameters
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.node.internal.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.makeTestDatabaseProperties
|
||||
import org.junit.Before
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import com.r3.corda.networkmanage.hsm.authentication.AuthMode as SigningServiceAuthMode
|
||||
import com.r3.corda.networkmanage.hsm.generator.AuthMode as GeneratorAuthMode
|
||||
|
||||
abstract class HsmBaseTest : IntegrationTest() {
|
||||
companion object {
|
||||
const val ROOT_CERT_KEY_GROUP = "TEST.CORDACONNECT.ROOT"
|
||||
const val NETWORK_MAP_CERT_KEY_GROUP = "TEST.CORDACONNECT.OPS.NETMAP"
|
||||
const val DOORMAN_CERT_KEY_GROUP = "TEST.CORDACONNECT.OPS.CERT"
|
||||
const val ROOT_CERT_SUBJECT = "CN=Corda Root CA, OU=Corda, O=R3 Ltd, L=London, C=GB"
|
||||
const val NETWORK_MAP_CERT_SUBJECT = "CN=Corda Network Map, OU=Corda, O=R3 Ltd, L=London, C=GB"
|
||||
const val DOORMAN_CERT_SUBJECT = "CN=Corda Doorman CA, OU=Corda, O=R3 Ltd, L=London, C=GB"
|
||||
const val TRUSTSTORE_PASSWORD: String = "trustpass"
|
||||
const val HSM_USERNAME = "INTEGRATION_TEST"
|
||||
const val HSM_PASSWORD = "INTEGRATION_TEST"
|
||||
const val HSM_USERNAME_SUPER = "INTEGRATION_TEST_SUPER"
|
||||
const val HSM_USERNAME_OPS = "INTEGRATION_TEST_OPS"
|
||||
const val HSM_USERNAME_ROOT = "INTEGRATION_TEST_ROOT"
|
||||
const val HSM_USERNAME_SUPER_ = "INTEGRATION_TEST_SUPER_"
|
||||
const val HSM_USERNAME_OPS_ = "INTEGRATION_TEST_OPS_"
|
||||
const val HSM_USERNAME_ROOT_ = "INTEGRATION_TEST_ROOT_"
|
||||
const val HSM_USERNAME_OPS_CERT = "INTEGRATION_TEST_OPS_CERT"
|
||||
const val HSM_USERNAME_OPS_NETMAP = "INTEGRATION_TEST_OPS_NETMAP"
|
||||
const val HSM_USERNAME_OPS_CERT_ = "INTEGRATION_TEST_OPS_CERT_"
|
||||
const val HSM_USERNAME_OPS_NETMAP_ = "INTEGRATION_TEST_OPS_NETMAP_"
|
||||
val HSM_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME)
|
||||
val HSM_SUPER_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_SUPER)
|
||||
val HSM_ROOT_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_ROOT)
|
||||
val HSM_OPS_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS)
|
||||
val HSM_SUPER__USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_SUPER_)
|
||||
val HSM_ROOT__USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_ROOT_)
|
||||
val HSM_OPS__USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS_)
|
||||
val HSM_OPS_CERT_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS_CERT)
|
||||
val HSM_OPS_NETMAP_USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS_NETMAP)
|
||||
val HSM_OPS_CERT__USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS_CERT_)
|
||||
val HSM_OPS_NETMAP__USER_CONFIGS = createHsmUserConfigs(HSM_USERNAME_OPS_NETMAP_)
|
||||
|
||||
private fun createHsmUserConfigs(username: String): List<UserAuthenticationParameters> {
|
||||
return listOf(UserAuthenticationParameters(
|
||||
username = username,
|
||||
authMode = GeneratorAuthMode.PASSWORD,
|
||||
authToken = "INTEGRATION_TEST",
|
||||
keyFilePassword = null))
|
||||
}
|
||||
|
||||
@ClassRule
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(DOORMAN_DB_NAME)
|
||||
}
|
||||
|
||||
protected lateinit var rootKeyStoreFile: Path
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val hsmSimulator: HsmSimulator = HsmSimulator()
|
||||
|
||||
private lateinit var dbName: String
|
||||
|
||||
@Before
|
||||
fun generateDbName() {
|
||||
rootKeyStoreFile = tempFolder.root.toPath() / "truststore.jks"
|
||||
dbName = random63BitValue().toString()
|
||||
}
|
||||
|
||||
private fun createGeneratorParameters(certConfig: CertificateConfiguration,
|
||||
userConfigs: List<UserAuthenticationParameters>): GeneratorParameters {
|
||||
return GeneratorParameters(
|
||||
hsmHost = hsmSimulator.host,
|
||||
hsmPort = hsmSimulator.port,
|
||||
trustStoreDirectory = rootKeyStoreFile.parent,
|
||||
trustStorePassword = TRUSTSTORE_PASSWORD,
|
||||
userConfigs = userConfigs,
|
||||
certConfig = certConfig
|
||||
)
|
||||
}
|
||||
|
||||
protected fun createGeneratorParameters(keyGroup: String,
|
||||
rootKeyGroup: String?,
|
||||
certificateType: CertificateType,
|
||||
subject: String,
|
||||
hsmUserConfigs: List<UserAuthenticationParameters> = HSM_USER_CONFIGS): GeneratorParameters {
|
||||
return createGeneratorParameters(CertificateConfiguration(
|
||||
keySpecifier = 1,
|
||||
keyGroup = keyGroup,
|
||||
storeKeysExternal = false,
|
||||
rootKeyGroup = rootKeyGroup,
|
||||
subject = subject,
|
||||
validDays = 3650,
|
||||
keyCurve = "NIST-P256",
|
||||
certificateType = certificateType,
|
||||
keyExport = 0,
|
||||
keyGenMechanism = 4,
|
||||
keyOverride = 0,
|
||||
crlIssuer = null,
|
||||
crlDistributionUrl = null
|
||||
), hsmUserConfigs)
|
||||
}
|
||||
|
||||
protected fun createHsmSigningServiceConfig(doormanCertConfig: DoormanCertificateConfig?,
|
||||
networkMapCertificateConfig: NetworkMapCertificateConfig?): SigningServiceConfig {
|
||||
return SigningServiceConfig(
|
||||
dataSourceProperties = mock(),
|
||||
device = "${hsmSimulator.port}@${hsmSimulator.host}",
|
||||
keySpecifier = 1,
|
||||
doorman = doormanCertConfig,
|
||||
networkMap = networkMapCertificateConfig
|
||||
)
|
||||
}
|
||||
|
||||
protected fun createDoormanCertificateConfig(): DoormanCertificateConfig {
|
||||
return DoormanCertificateConfig(
|
||||
rootKeyStoreFile = rootKeyStoreFile,
|
||||
keyGroup = DOORMAN_CERT_KEY_GROUP,
|
||||
validDays = 3650,
|
||||
rootKeyStorePassword = TRUSTSTORE_PASSWORD,
|
||||
crlDistributionPoint = URL("http://test.com/revoked.crl"),
|
||||
crlServerSocketAddress = NetworkHostAndPort("test.com", 4555),
|
||||
crlUpdatePeriod = 1000,
|
||||
mode = ManualMode.CSR,
|
||||
authParameters = AuthParametersConfig(
|
||||
mode = SigningServiceAuthMode.PASSWORD,
|
||||
threshold = 2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
protected fun createNetworkMapCertificateConfig(): NetworkMapCertificateConfig {
|
||||
return NetworkMapCertificateConfig(
|
||||
username = "INTEGRATION_TEST",
|
||||
keyGroup = NETWORK_MAP_CERT_KEY_GROUP,
|
||||
authParameters = AuthParametersConfig(
|
||||
mode = SigningServiceAuthMode.PASSWORD,
|
||||
password = "INTEGRATION_TEST",
|
||||
threshold = 2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
protected fun givenHsmUserAuthenticationInput(username: String = HSM_USERNAME,
|
||||
password: String = HSM_PASSWORD): InputReader {
|
||||
val inputReader = mock<InputReader>()
|
||||
whenever(inputReader.readLine()).thenReturn(username)
|
||||
whenever(inputReader.readPassword(any())).thenReturn(password)
|
||||
return inputReader
|
||||
}
|
||||
|
||||
fun makeTestDataSourceProperties(): Properties {
|
||||
return makeTestDataSourceProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases())
|
||||
}
|
||||
|
||||
fun makeTestDatabaseProperties(): DatabaseConfig {
|
||||
return makeTestDatabaseProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases())
|
||||
}
|
||||
|
||||
protected fun createProviderConfig(keyGroup: String): CryptoServerProviderConfig {
|
||||
return CryptoServerProviderConfig(
|
||||
Device = "${hsmSimulator.port}@${hsmSimulator.host}",
|
||||
KeySpecifier = 1,
|
||||
KeyGroup = keyGroup,
|
||||
StoreKeysExternal = false)
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.testing.database.DatabaseConstants
|
||||
import net.corda.testing.node.internal.databaseProviderDataSourceConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
|
||||
const val HOST = "localhost"
|
||||
|
||||
const val DOORMAN_DB_NAME = "doorman"
|
||||
|
||||
fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null): Config {
|
||||
val nodeName = nodeName ?: SecureHash.randomSHA256().toString()
|
||||
val h2InstanceName = if (postfix != null) nodeName + "_" + postfix else nodeName
|
||||
|
||||
return ConfigFactory.parseMap(mapOf(
|
||||
DatabaseConstants.DATA_SOURCE_CLASSNAME to "org.h2.jdbcx.JdbcDataSource",
|
||||
DatabaseConstants.DATA_SOURCE_URL to "jdbc:h2:mem:${h2InstanceName};DB_CLOSE_DELAY=-1",
|
||||
DatabaseConstants.DATA_SOURCE_USER to "sa",
|
||||
DatabaseConstants.DATA_SOURCE_PASSWORD to ""))
|
||||
}
|
||||
|
||||
fun generateEmptyCrls(tempFolder: TemporaryFolder, rootCertAndKeyPair: CertificateAndKeyPair, directEndpoint: URL, indirectEndpoint: URL): Pair<Path, Path> {
|
||||
val localSigner = LocalSigner(rootCertAndKeyPair)
|
||||
val directCrl = createSignedCrl(rootCertAndKeyPair.certificate, directEndpoint, 10.days, localSigner, emptyList(), false)
|
||||
val indirectCrl = createSignedCrl(rootCertAndKeyPair.certificate, indirectEndpoint, 10.days, localSigner, emptyList(), true)
|
||||
val directCrlFile = tempFolder.newFile()
|
||||
FileUtils.writeByteArrayToFile(directCrlFile, directCrl.encoded)
|
||||
val indirectCrlFile = tempFolder.newFile()
|
||||
FileUtils.writeByteArrayToFile(indirectCrlFile, indirectCrl.encoded)
|
||||
return Pair(directCrlFile.toPath(), indirectCrlFile.toPath())
|
||||
}
|
||||
|
||||
fun getCaCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/root")
|
||||
fun getEmptyCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/empty")
|
||||
fun getNodeCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/doorman")
|
||||
|
||||
//TODO add more dbs to test once doorman supports them
|
||||
fun configSupplierForSupportedDatabases(): (String?, String?) -> Config =
|
||||
when (System.getProperty("custom.databaseProvider", "")) {
|
||||
"integration-sql-server", "integration-azure-sql" -> ::databaseProviderDataSourceConfig
|
||||
else -> { _, _ -> ConfigFactory.empty() }
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME
|
||||
import com.r3.corda.networkmanage.common.configSupplierForSupportedDatabases
|
||||
import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.readObject
|
||||
import net.corda.core.messaging.ParametersUpdateInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import net.corda.testing.node.internal.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import java.net.URL
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
class NetworkParametersUpdateTest : IntegrationTest() {
|
||||
companion object {
|
||||
private val timeoutMillis = 5.seconds.toMillis()
|
||||
@ClassRule
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(),
|
||||
DUMMY_NOTARY_NAME.toDatabaseSchemaName(), DOORMAN_DB_NAME)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val serverAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
private lateinit var rootCaCert: X509Certificate
|
||||
private lateinit var doormanCa: CertificateAndKeyPair
|
||||
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||
//for normal integration tests (not against standalone db) to create a unique db name for each tests against H2
|
||||
private lateinit var dbNamePostfix: String
|
||||
|
||||
private var server: NetworkManagementServer? = null
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
dbNamePostfix = random63BitValue().toString()
|
||||
val (rootCa, doormanCa) = createDevIntermediateCaCertPath()
|
||||
rootCaCert = rootCa.certificate
|
||||
this.doormanCa = doormanCa
|
||||
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
server?.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network parameters update commnunicated to node`() {
|
||||
// Initialise the server with some network parameters
|
||||
val initialNetParams = NetworkParametersCmd.Set(
|
||||
notaries = emptyList(),
|
||||
minimumPlatformVersion = 1,
|
||||
maxMessageSize = 1_000_000,
|
||||
maxTransactionSize = 1_000_000,
|
||||
parametersUpdate = null,
|
||||
eventHorizonDays = 30
|
||||
)
|
||||
applyNetworkParametersAndStart(initialNetParams)
|
||||
|
||||
val compatibilityZone = CompatibilityZoneParams(
|
||||
URL("http://$serverAddress"),
|
||||
publishNotaries = {},
|
||||
rootCert = rootCaCert
|
||||
)
|
||||
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
notarySpecs = emptyList(),
|
||||
initialiseSerialization = false,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance"),
|
||||
notaryCustomOverrides = mapOf("devMode" to false)
|
||||
) {
|
||||
var (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)),
|
||||
startNode(providedName = BOB_NAME, customOverrides = mapOf("devMode" to false))
|
||||
).map { it.getOrThrow() as NodeHandleInternal }
|
||||
|
||||
// Make sure that stopping Bob doesn't remove him from the network map
|
||||
bob.stop()
|
||||
Thread.sleep(timeoutMillis * 2)
|
||||
assertThat(alice.rpc.networkMapSnapshot().map { it.legalIdentities[0].name }).contains(BOB_NAME)
|
||||
|
||||
val snapshot = alice.rpc.networkParametersFeed().snapshot
|
||||
val updates = alice.rpc.networkParametersFeed().updates.bufferUntilSubscribed()
|
||||
assertThat(snapshot).isNull()
|
||||
|
||||
val updateDeadline = Instant.now() + 10.seconds
|
||||
|
||||
applyNetworkParametersAndStart(initialNetParams.copy(
|
||||
maxTransactionSize = 1_000_001,
|
||||
parametersUpdate = ParametersUpdateConfig(
|
||||
description = "Very Important Update",
|
||||
updateDeadline = updateDeadline
|
||||
)
|
||||
))
|
||||
|
||||
updates.expectEvents(isStrict = true) {
|
||||
sequence(
|
||||
expect { update: ParametersUpdateInfo ->
|
||||
assertEquals(update.description, "Very Important Update")
|
||||
assertEquals(update.parameters.maxTransactionSize, 1_000_001)
|
||||
assertEquals(update.parameters.epoch, 2) // The epoch must increment automatically.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val paramUpdateInfo = alice.rpc.networkParametersFeed().snapshot!!
|
||||
alice.rpc.acceptNewNetworkParameters(paramUpdateInfo.hash)
|
||||
|
||||
// Make sure we've passed the deadline
|
||||
Thread.sleep(Math.max(updateDeadline.toEpochMilli() - System.currentTimeMillis(), 0))
|
||||
applyNetworkParametersAndStart(NetworkParametersCmd.FlagDay)
|
||||
|
||||
alice.stop()
|
||||
alice = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow() as NodeHandleInternal
|
||||
|
||||
// TODO It is also possible to check what version of parameters node runs by writing flow that reads that value from ServiceHub
|
||||
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
|
||||
.readObject<SignedNetworkParameters>().verified()
|
||||
assertEquals(networkParameters, paramUpdateInfo.parameters)
|
||||
assertThat(alice.rpc.networkParametersFeed().snapshot).isNull() // Check that NMS doesn't advertise updates anymore.
|
||||
// Check that Bob is no longer on the network as it didn't accept the new parameteres.
|
||||
assertThat(alice.rpc.networkMapSnapshot().map { it.legalIdentities[0].name }).doesNotContain(BOB_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
|
||||
val doormanConfig = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
|
||||
val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
|
||||
DatabaseConfig(runMigration = true), doormanConfig, null)
|
||||
server.start(
|
||||
serverAddress,
|
||||
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
|
||||
if (startNetworkMap) {
|
||||
NetworkMapStartParams(
|
||||
LocalSigner(networkMapCa),
|
||||
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
return server
|
||||
}
|
||||
|
||||
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
|
||||
server?.close()
|
||||
NetworkManagementServer(
|
||||
makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
|
||||
DatabaseConfig(runMigration = true),
|
||||
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
|
||||
null).use {
|
||||
it.netParamsUpdateHandler.processNetworkParameters(networkParametersCmd)
|
||||
}
|
||||
server = startServer(startNetworkMap = true)
|
||||
// Wait for server to process the parameters update and for the nodes to poll again
|
||||
Thread.sleep(timeoutMillis * 2)
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.*
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import net.corda.testing.node.internal.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.makeTestDatabaseProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.*
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.streams.toList
|
||||
|
||||
// This is the same test as the one in net.corda.node.utilities.registration but using the real doorman and with some
|
||||
// extra checks on the network map.
|
||||
class NodeRegistrationTest : IntegrationTest() {
|
||||
companion object {
|
||||
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
|
||||
private val aliceName = CordaX500Name("Alice", "London", "GB")
|
||||
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
|
||||
private val timeoutMillis = 5.seconds.toMillis()
|
||||
|
||||
@ClassRule
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(notaryName.organisation, aliceName.organisation, genevieveName.organisation,
|
||||
DOORMAN_DB_NAME)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val serverAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
private lateinit var rootCaCert: X509Certificate
|
||||
private lateinit var doormanCa: CertificateAndKeyPair
|
||||
private lateinit var networkMapCa: CertificateAndKeyPair
|
||||
//for normal integration tests (not against standalone db) to create a unique db name for each tests against H2
|
||||
private lateinit var dbNamePostfix: String
|
||||
|
||||
private var server: NetworkManagementServer? = null
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
|
||||
private lateinit var revocationConfig: CertificateRevocationConfig
|
||||
|
||||
private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
|
||||
return CertificateRevocationConfig(
|
||||
approveAll = true,
|
||||
jira = null,
|
||||
approveInterval = timeoutMillis,
|
||||
crlCacheTimeout = timeoutMillis,
|
||||
localSigning = CertificateRevocationConfig.LocalSigning(
|
||||
crlEndpoint = getNodeCrlEndpoint(serverAddress),
|
||||
crlUpdateInterval = timeoutMillis),
|
||||
emptyCrlPath = emptyCrlPath,
|
||||
caCrlPath = caCrlPath)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
dbNamePostfix = random63BitValue().toString()
|
||||
val (rootCa, doormanCa) = createDevIntermediateCaCertPath()
|
||||
rootCaCert = rootCa.certificate
|
||||
this.doormanCa = doormanCa
|
||||
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||
val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
|
||||
revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
server?.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register nodes with doorman and then they transact with each other`() {
|
||||
// Start the server without the network parameters config which won't start the network map. Just the doorman
|
||||
// registration process will start up, allowing us to register the notaries which will then be used in the network
|
||||
// parameters.
|
||||
server = startServer(startNetworkMap = false)
|
||||
val compatibilityZone = CompatibilityZoneParams(
|
||||
URL("http://$serverAddress"),
|
||||
publishNotaries = { notaryInfos ->
|
||||
val setNetParams = NetworkParametersCmd.Set(
|
||||
notaries = notaryInfos,
|
||||
minimumPlatformVersion = 1,
|
||||
maxMessageSize = 10485760,
|
||||
maxTransactionSize = 10485760,
|
||||
parametersUpdate = null,
|
||||
eventHorizonDays = 30
|
||||
)
|
||||
// Restart the server once we're able to generate the network parameters
|
||||
applyNetworkParametersAndStart(setNetParams)
|
||||
},
|
||||
rootCert = rootCaCert
|
||||
)
|
||||
internalDriver(
|
||||
portAllocation = portAllocation,
|
||||
compatibilityZone = compatibilityZone,
|
||||
initialiseSerialization = false,
|
||||
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance"),
|
||||
notaryCustomOverrides = mapOf("devMode" to false)
|
||||
) {
|
||||
val (alice, notary) = listOf(
|
||||
startNode(providedName = aliceName, customOverrides = mapOf("devMode" to false)),
|
||||
defaultNotaryNode
|
||||
).map { it.getOrThrow() as NodeHandleInternal }
|
||||
|
||||
alice.onlySeesFromNetworkMap(alice, notary)
|
||||
notary.onlySeesFromNetworkMap(alice, notary)
|
||||
|
||||
val genevieve = startNode(providedName = genevieveName, customOverrides = mapOf("devMode" to false)).getOrThrow() as NodeHandleInternal
|
||||
|
||||
// Wait for the nodes to poll again
|
||||
Thread.sleep(timeoutMillis * 2)
|
||||
|
||||
// Make sure the new node is visible to everyone
|
||||
alice.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||
notary.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||
genevieve.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||
|
||||
// Check the nodes can communicate among themselves (and the notary).
|
||||
val anonymous = false
|
||||
genevieve.rpc.startFlow(
|
||||
::CashIssueAndPaymentFlow,
|
||||
1000.DOLLARS,
|
||||
OpaqueBytes.of(12),
|
||||
alice.nodeInfo.singleIdentity(),
|
||||
anonymous,
|
||||
defaultNotaryIdentity
|
||||
).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeHandleInternal.onlySeesFromNetworkMap(vararg nodes: NodeHandle) {
|
||||
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
|
||||
if (nodeInfosDir.exists()) {
|
||||
assertThat(nodeInfosDir.list { it.toList() }).isEmpty()
|
||||
}
|
||||
assertThat(rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo })
|
||||
}
|
||||
|
||||
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
|
||||
val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
|
||||
makeTestDatabaseProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig)
|
||||
server.start(
|
||||
serverAddress,
|
||||
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
|
||||
if (startNetworkMap) {
|
||||
NetworkMapStartParams(
|
||||
LocalSigner(networkMapCa),
|
||||
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
return server
|
||||
}
|
||||
|
||||
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
|
||||
server?.close()
|
||||
NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
|
||||
makeTestDatabaseProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig).use {
|
||||
it.netParamsUpdateHandler.processNetworkParameters(networkParametersCmd)
|
||||
}
|
||||
server = startServer(startNetworkMap = true)
|
||||
// Wait for server to process the parameters update and for the nodes to poll again
|
||||
Thread.sleep(timeoutMillis * 2)
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||
import com.r3.corda.networkmanage.hsm.authentication.createProvider
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HsmAuthenticatorTest : HsmBaseTest() {
|
||||
|
||||
@Test
|
||||
fun `Authenticator executes the block once user is successfully authenticated`() {
|
||||
// given
|
||||
val userInput = givenHsmUserAuthenticationInput()
|
||||
val hsmSigningServiceConfig = createHsmSigningServiceConfig(createDoormanCertificateConfig(), null)
|
||||
val doormanCertificateConfig = hsmSigningServiceConfig.doorman!!
|
||||
val authenticator = Authenticator(provider = createProvider(
|
||||
doormanCertificateConfig.keyGroup,
|
||||
hsmSigningServiceConfig.keySpecifier,
|
||||
hsmSigningServiceConfig.device), inputReader = userInput)
|
||||
val executed = AtomicBoolean(false)
|
||||
|
||||
// when
|
||||
authenticator.connectAndAuthenticate { _, _ -> executed.set(true) }
|
||||
|
||||
// then
|
||||
assertTrue(executed.get())
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.hsm.authentication.InputReader
|
||||
import com.r3.corda.networkmanage.hsm.generator.AutoAuthenticator
|
||||
import com.r3.corda.networkmanage.hsm.generator.UserAuthenticationParameters
|
||||
import com.r3.corda.networkmanage.hsm.generator.crl.CrlConfig
|
||||
import com.r3.corda.networkmanage.hsm.generator.crl.GeneratorConfig
|
||||
import com.r3.corda.networkmanage.hsm.generator.crl.RevocationConfig
|
||||
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.run as runCertificateGeneration
|
||||
import com.r3.corda.networkmanage.hsm.generator.crl.run as runCrlGeneration
|
||||
|
||||
class HsmEmptyCrlGenerationTest : HsmBaseTest() {
|
||||
|
||||
private lateinit var inputReader: InputReader
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
inputReader = mock()
|
||||
whenever(inputReader.readLine()).thenReturn(hsmSimulator.cryptoUserCredentials().username)
|
||||
whenever(inputReader.readPassword(any())).thenReturn(hsmSimulator.cryptoUserCredentials().password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `An empty CRL is generated`() {
|
||||
// when root cert is created
|
||||
runCertificateGeneration(createGeneratorParameters(
|
||||
keyGroup = ROOT_CERT_KEY_GROUP,
|
||||
rootKeyGroup = null,
|
||||
certificateType = CertificateType.ROOT_CA,
|
||||
subject = ROOT_CERT_SUBJECT
|
||||
))
|
||||
|
||||
// then root cert is persisted in the HSM
|
||||
AutoAuthenticator(createProviderConfig(ROOT_CERT_KEY_GROUP), HSM_USER_CONFIGS).connectAndAuthenticate { provider ->
|
||||
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
|
||||
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
|
||||
assertEquals(rootCert.issuerX500Principal, rootCert.subjectX500Principal)
|
||||
}
|
||||
|
||||
val generatedFile = tempFolder.newFile()
|
||||
runCrlGeneration(createCrlGeneratorParameters(CrlConfig(
|
||||
crlEndpoint = URL("http://test.com/crl"),
|
||||
filePath = generatedFile.toPath(),
|
||||
keyGroup = ROOT_CERT_KEY_GROUP,
|
||||
keySpecifier = 1,
|
||||
validDays = 1000,
|
||||
indirectIssuer = true,
|
||||
revocations = emptyList()), HSM_ROOT_USER_CONFIGS))
|
||||
val crl = CertificateFactory.getInstance("X.509")
|
||||
.generateCRL(FileUtils.readFileToByteArray(generatedFile).inputStream()) as X509CRL
|
||||
assertNotNull(crl)
|
||||
assertEquals(ROOT_CERT_SUBJECT, crl.issuerDN.name)
|
||||
assertTrue { crl.revokedCertificates.isEmpty() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `A non-empty CRL is generated`() {
|
||||
// when root cert is created
|
||||
runCertificateGeneration(createGeneratorParameters(
|
||||
keyGroup = ROOT_CERT_KEY_GROUP,
|
||||
rootKeyGroup = null,
|
||||
certificateType = CertificateType.ROOT_CA,
|
||||
subject = ROOT_CERT_SUBJECT
|
||||
))
|
||||
|
||||
// then root cert is persisted in the HSM
|
||||
AutoAuthenticator(createProviderConfig(ROOT_CERT_KEY_GROUP), HSM_USER_CONFIGS).connectAndAuthenticate { provider ->
|
||||
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
|
||||
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
|
||||
assertEquals(rootCert.issuerX500Principal, rootCert.subjectX500Principal)
|
||||
}
|
||||
|
||||
val generatedFile = tempFolder.newFile()
|
||||
val revokedSerialNumber = "1234567890"
|
||||
runCrlGeneration(createCrlGeneratorParameters(CrlConfig(
|
||||
crlEndpoint = URL("http://test.com/crl"),
|
||||
filePath = generatedFile.toPath(),
|
||||
keyGroup = ROOT_CERT_KEY_GROUP,
|
||||
keySpecifier = 1,
|
||||
validDays = 1000,
|
||||
indirectIssuer = false,
|
||||
revocations = listOf(
|
||||
RevocationConfig(
|
||||
certificateSerialNumber = "1234567890",
|
||||
dateInMillis = 0,
|
||||
reason = "KEY_COMPROMISE"
|
||||
)
|
||||
)), HSM_ROOT_USER_CONFIGS))
|
||||
val crl = CertificateFactory.getInstance("X.509")
|
||||
.generateCRL(FileUtils.readFileToByteArray(generatedFile).inputStream()) as X509CRL
|
||||
assertNotNull(crl)
|
||||
assertEquals(ROOT_CERT_SUBJECT, crl.issuerDN.name)
|
||||
assertEquals(1, crl.revokedCertificates.size)
|
||||
val revoked = crl.revokedCertificates.first()
|
||||
assertEquals(revoked.serialNumber.toString(), revokedSerialNumber)
|
||||
}
|
||||
|
||||
private fun createCrlGeneratorParameters(crlConfg: CrlConfig,
|
||||
userConfigs: List<UserAuthenticationParameters>): GeneratorConfig {
|
||||
return GeneratorConfig(
|
||||
hsmHost = hsmSimulator.host,
|
||||
hsmPort = hsmSimulator.port,
|
||||
trustStoreFile = rootKeyStoreFile,
|
||||
trustStorePassword = TRUSTSTORE_PASSWORD,
|
||||
userConfigs = userConfigs,
|
||||
crl = crlConfg
|
||||
)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
|
||||
import com.r3.corda.networkmanage.hsm.authentication.InputReader
|
||||
import com.r3.corda.networkmanage.hsm.generator.AutoAuthenticator
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.run
|
||||
import com.r3.corda.networkmanage.hsm.utils.HsmX509Utilities
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
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.Before
|
||||
import org.junit.Test
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class HsmKeyGenerationTest : HsmBaseTest() {
|
||||
|
||||
private lateinit var inputReader: InputReader
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
inputReader = mock()
|
||||
whenever(inputReader.readLine()).thenReturn(hsmSimulator.cryptoUserCredentials().username)
|
||||
whenever(inputReader.readPassword(any())).thenReturn(hsmSimulator.cryptoUserCredentials().password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Root and network map certificates have different namespace`() {
|
||||
// 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
|
||||
))
|
||||
|
||||
// then root cert is persisted in the HSM
|
||||
AutoAuthenticator(createProviderConfig(ROOT_CERT_KEY_GROUP), HSM_USER_CONFIGS).connectAndAuthenticate { provider ->
|
||||
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
|
||||
val rootCert = keyStore.getCertificate(CORDA_ROOT_CA) as X509Certificate
|
||||
assertEquals(rootCert.issuerX500Principal, rootCert.subjectX500Principal)
|
||||
}
|
||||
|
||||
// then network map cert is persisted in the HSM
|
||||
|
||||
AutoAuthenticator(createProviderConfig(NETWORK_MAP_CERT_KEY_GROUP), HSM_USER_CONFIGS)
|
||||
.connectAndAuthenticate { provider ->
|
||||
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
|
||||
val networkMapCert = keyStore.getCertificate(CORDA_NETWORK_MAP) as X509Certificate
|
||||
assertNotNull(networkMapCert)
|
||||
assertEquals(CordaX500Name.parse(ROOT_CERT_SUBJECT).x500Principal, networkMapCert.issuerX500Principal)
|
||||
}
|
||||
|
||||
// then doorman cert is persisted in the HSM
|
||||
|
||||
AutoAuthenticator(createProviderConfig(DOORMAN_CERT_KEY_GROUP), HSM_USER_CONFIGS)
|
||||
.connectAndAuthenticate { provider ->
|
||||
val keyStore = HsmX509Utilities.getAndInitializeKeyStore(provider)
|
||||
val networkMapCert = keyStore.getCertificate(CORDA_INTERMEDIATE_CA) as X509Certificate
|
||||
assertNotNull(networkMapCert)
|
||||
assertEquals(CordaX500Name.parse(ROOT_CERT_SUBJECT).x500Principal, networkMapCert.issuerX500Principal)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||
import com.r3.corda.networkmanage.hsm.authentication.createProvider
|
||||
import com.r3.corda.networkmanage.hsm.generator.UserAuthenticationParameters
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.run
|
||||
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
|
||||
import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
|
||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name.Companion.parse
|
||||
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 org.junit.Test
|
||||
import java.security.GeneralSecurityException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class HsmPermissionTest : HsmBaseTest() {
|
||||
|
||||
/**
|
||||
* This test case scenario reflects the issue observed on 02.02.2018, when permissions user CXI_GROUP permissions
|
||||
* were wrongly configured on the PROD HSM box.
|
||||
*
|
||||
* Key groups are as follows:
|
||||
* "TEST.CORDACONNECT.ROOT"
|
||||
* "TEST.CORDACONNECT.OPS.NETMAP"
|
||||
* "TEST.CORDACONNECT.OPS.CERT"
|
||||
*
|
||||
* User CXI_GROUP configurations are as follows:
|
||||
* Root cert creator: TEST.CORDACONNECT.*
|
||||
* Doorman cert creator: TEST.CORDACONNECT.*
|
||||
* Networkmap cert creator: TEST.CORDACONNECT.*
|
||||
*
|
||||
* CSR signing user CXI_GROUP is as follows:
|
||||
* TEST.CORDACONNECT.OPS.CERT.*
|
||||
*/
|
||||
@Test
|
||||
fun `HSM signing service cannot sign CSR data when HSM user CXI_GROUP permissions are wrongly configured`() {
|
||||
// given certs created
|
||||
givenCertificatesCreated(HSM_SUPER__USER_CONFIGS, HSM_SUPER__USER_CONFIGS, HSM_SUPER__USER_CONFIGS)
|
||||
// given authenticated user
|
||||
val userInput = givenHsmUserAuthenticationInput(HSM_USERNAME_OPS_CERT_)
|
||||
|
||||
// given HSM CSR signer
|
||||
val hsmSigningServiceConfig = createHsmSigningServiceConfig(createDoormanCertificateConfig(), null)
|
||||
val signer = HsmCsrSigner(
|
||||
mock(),
|
||||
hsmSigningServiceConfig.doorman!!.loadRootKeyStore(),
|
||||
"",
|
||||
null,
|
||||
3650,
|
||||
Authenticator(
|
||||
provider = createProvider(
|
||||
hsmSigningServiceConfig.doorman!!.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)))
|
||||
|
||||
// then
|
||||
// The GeneralSecurityException is thrown by the JCE layer.
|
||||
// This exception is caused by the CryptoServerException with code B0680001 - permission denied.
|
||||
assertFailsWith(GeneralSecurityException::class) {
|
||||
signer.sign(listOf(toSign))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test case scenario reflects the fix for the issue observed on 02.02.2018, when permissions user CXI_GROUP permissions
|
||||
* were wrongly configured on the PROD HSM box.
|
||||
*
|
||||
* Key groups are as follows:
|
||||
* "TEST.CORDACONNECT.ROOT"
|
||||
* "TEST.CORDACONNECT.OPS.NETMAP"
|
||||
* "TEST.CORDACONNECT.OPS.CERT"
|
||||
*
|
||||
* User CXI_GROUP configurations are as follows:
|
||||
* Root cert creator: TEST.CORDACONNECT.*
|
||||
* Doorman cert creator: TEST.CORDACONNECT.*
|
||||
* Networkmap cert creator: TEST.CORDACONNECT.*
|
||||
*
|
||||
* CSR signing user CXI_GROUP is as follows:
|
||||
* TEST.CORDACONNECT.OPS.CERT
|
||||
*/
|
||||
@Test
|
||||
fun `HSM signing service signs CSR data when HSM user CXI_GROUP permissions are correctly configured`() {
|
||||
// given certs created
|
||||
givenCertificatesCreated(HSM_SUPER__USER_CONFIGS, HSM_SUPER__USER_CONFIGS, HSM_SUPER__USER_CONFIGS)
|
||||
// given authenticated user
|
||||
val userInput = givenHsmUserAuthenticationInput(HSM_USERNAME_OPS_CERT)
|
||||
|
||||
// given HSM CSR signer
|
||||
val hsmSigningServiceConfig = createHsmSigningServiceConfig(createDoormanCertificateConfig(), null)
|
||||
val signer = HsmCsrSigner(
|
||||
mock(),
|
||||
hsmSigningServiceConfig.doorman!!.loadRootKeyStore(),
|
||||
"trustpass",
|
||||
null,
|
||||
3650,
|
||||
Authenticator(
|
||||
provider = createProvider(
|
||||
hsmSigningServiceConfig.doorman!!.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)))
|
||||
|
||||
// when
|
||||
signer.sign(listOf(toSign))
|
||||
|
||||
// then
|
||||
assertNotNull(toSign.certPath)
|
||||
val certificates = toSign.certPath!!.certificates
|
||||
assertEquals(3, certificates.size)
|
||||
}
|
||||
|
||||
private fun givenCertificatesCreated(rootCertUserConfigs: List<UserAuthenticationParameters>,
|
||||
doormanCertUserConfigs: List<UserAuthenticationParameters>,
|
||||
netMapCertUserConfigs: List<UserAuthenticationParameters>) {
|
||||
// when root cert is created
|
||||
run(createGeneratorParameters(
|
||||
keyGroup = ROOT_CERT_KEY_GROUP,
|
||||
rootKeyGroup = null,
|
||||
certificateType = CertificateType.ROOT_CA,
|
||||
subject = ROOT_CERT_SUBJECT,
|
||||
hsmUserConfigs = rootCertUserConfigs))
|
||||
// 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,
|
||||
hsmUserConfigs = netMapCertUserConfigs
|
||||
))
|
||||
// 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,
|
||||
hsmUserConfigs = doormanCertUserConfigs
|
||||
))
|
||||
}
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
|
||||
import com.r3.corda.networkmanage.common.utils.initialiseSerialization
|
||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||
import com.r3.corda.networkmanage.hsm.authentication.createProvider
|
||||
import com.r3.corda.networkmanage.hsm.generator.certificate.run
|
||||
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
|
||||
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.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
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
loadOrCreateKeyStore(rootKeyStoreFile, TRUSTSTORE_PASSWORD)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HSM signing service can sign node CSR data`() {
|
||||
setupCertificates()
|
||||
|
||||
// given authenticated user
|
||||
val userInput = givenHsmUserAuthenticationInput()
|
||||
|
||||
// given HSM CSR signer
|
||||
val hsmSigningServiceConfig = createHsmSigningServiceConfig(createDoormanCertificateConfig(), null)
|
||||
val doormanCertificateConfig = hsmSigningServiceConfig.doorman!!
|
||||
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)))
|
||||
|
||||
// when
|
||||
signer.sign(listOf(toSign))
|
||||
|
||||
// then
|
||||
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(createDoormanCertificateConfig(), null)
|
||||
val doormanCertificateConfig = hsmSigningServiceConfig.doorman!!
|
||||
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`() {
|
||||
setupCertificates()
|
||||
|
||||
// given authenticated user
|
||||
val userInput = givenHsmUserAuthenticationInput()
|
||||
|
||||
// given HSM network map signer
|
||||
val hsmSigningServiceConfig = createHsmSigningServiceConfig(null, createNetworkMapCertificateConfig())
|
||||
val networkMapCertificateConfig = hsmSigningServiceConfig.networkMap!!
|
||||
val hsmDataSigner = HsmSigner(Authenticator(
|
||||
provider = createProvider(
|
||||
networkMapCertificateConfig.keyGroup,
|
||||
hsmSigningServiceConfig.keySpecifier,
|
||||
hsmSigningServiceConfig.device),
|
||||
inputReader = userInput),
|
||||
keyName = CORDA_NETWORK_MAP)
|
||||
|
||||
val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties())
|
||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
|
||||
// given network map parameters
|
||||
val networkMapParameters = testNetworkParameters(emptyList())
|
||||
val networkMapSigner = NetworkMapSigner(networkMapStorage, hsmDataSigner)
|
||||
|
||||
// when
|
||||
initialiseSerialization()
|
||||
networkMapStorage.saveNetworkParameters(networkMapParameters, hsmDataSigner.signBytes(networkMapParameters.serialize().bytes))
|
||||
networkMapSigner.signNetworkMaps()
|
||||
|
||||
// then
|
||||
val persistedNetworkMap = networkMapStorage.getNetworkMaps().publicNetworkMap!!.toSignedNetworkMap().verified()
|
||||
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
|
||||
))
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import com.r3.corda.networkmanage.common.*
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.doorman.CertificateRevocationConfig
|
||||
import com.r3.corda.networkmanage.doorman.DoormanConfig
|
||||
import com.r3.corda.networkmanage.doorman.NetworkManagementServer
|
||||
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
|
||||
import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStorage
|
||||
import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateSigningRequestStorage
|
||||
import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
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.NodeRegistrationHelper
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.persistence.PersistenceException
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
|
||||
class SigningServiceIntegrationTest : HsmBaseTest() {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val serverAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
private lateinit var timer: Timer
|
||||
private lateinit var rootCaCert: X509Certificate
|
||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
||||
private val timeoutMillis = 5.seconds.toMillis()
|
||||
|
||||
private lateinit var dbName: String
|
||||
|
||||
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null)
|
||||
private lateinit var revocationConfig: CertificateRevocationConfig
|
||||
|
||||
private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
|
||||
return CertificateRevocationConfig(
|
||||
approveAll = true,
|
||||
jira = null,
|
||||
approveInterval = timeoutMillis,
|
||||
crlCacheTimeout = timeoutMillis,
|
||||
localSigning = CertificateRevocationConfig.LocalSigning(
|
||||
crlEndpoint = getNodeCrlEndpoint(serverAddress),
|
||||
crlUpdateInterval = timeoutMillis),
|
||||
emptyCrlPath = emptyCrlPath,
|
||||
caCrlPath = caCrlPath)
|
||||
}
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
dbName = random63BitValue().toString()
|
||||
timer = Timer()
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
rootCaCert = rootCa.certificate
|
||||
this.intermediateCa = intermediateCa
|
||||
val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
|
||||
revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
|
||||
}
|
||||
|
||||
@After
|
||||
override fun tearDown() {
|
||||
super.tearDown()
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
private fun givenSignerSigningAllRequests(storage: SignedCertificateSigningRequestStorage): HsmCsrSigner {
|
||||
// Mock signing logic but keep certificate persistence
|
||||
return mock {
|
||||
on { sign(any()) }.then {
|
||||
val approvedRequests: List<ApprovedCertificateRequestData> = uncheckedCast(it.arguments[0])
|
||||
for (approvedRequest in approvedRequests) {
|
||||
JcaPKCS10CertificationRequest(approvedRequest.request).run {
|
||||
val nodeCa = createDevNodeCa(intermediateCa, CordaX500Name.parse(subject.toString()))
|
||||
approvedRequest.certPath = X509Utilities.buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCaCert)
|
||||
}
|
||||
}
|
||||
storage.store(approvedRequests, "TEST")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Signing service signs approved CSRs`() {
|
||||
//Start doorman server
|
||||
NetworkManagementServer(makeTestDataSourceProperties(), makeTestDatabaseProperties(), doormanConfig, revocationConfig).use { server ->
|
||||
server.start(
|
||||
hostAndPort = serverAddress,
|
||||
csrCertPathAndKey = null,
|
||||
startNetworkMap = null)
|
||||
val doormanHostAndPort = server.hostAndPort
|
||||
// Start Corda network registration.
|
||||
val config = createConfig().also {
|
||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||
doReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}")).whenever(it).compatibilityZoneURL
|
||||
}
|
||||
val signingServiceStorage = DBSignedCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties()))
|
||||
|
||||
val hsmSigner = givenSignerSigningAllRequests(signingServiceStorage)
|
||||
// Poll the database for approved requests
|
||||
timer.scheduleAtFixedRate(0, 1.seconds.toMillis()) {
|
||||
// The purpose of this tests is to validate the communication between this service and Doorman
|
||||
// by the means of data in the shared database.
|
||||
// Therefore the HSM interaction logic is mocked here.
|
||||
try {
|
||||
val approved = signingServiceStorage.getApprovedRequests()
|
||||
if (approved.isNotEmpty()) {
|
||||
hsmSigner.sign(approved)
|
||||
timer.cancel()
|
||||
}
|
||||
} catch (exception: PersistenceException) {
|
||||
// It may happen that Doorman DB is not created at the moment when the signing service polls it.
|
||||
// This is due to the fact that schema is initialized at the time first hibernate session is established.
|
||||
// Since Doorman does this at the time the first CSR arrives, which in turn happens after signing service
|
||||
// startup, the very first iteration of the signing service polling fails with
|
||||
// [org.hibernate.tool.schema.spi.SchemaManagementException] being thrown as the schema is missing.
|
||||
}
|
||||
}
|
||||
config.certificatesDirectory.createDirectories()
|
||||
val networkTrustStorePath = config.certificatesDirectory / "network-root-truststore.jks"
|
||||
val networkTrustStorePassword = "network-trust-password"
|
||||
val networkTrustStore = X509KeyStore.fromFile(networkTrustStorePath, networkTrustStorePassword, createNew = true)
|
||||
networkTrustStore.update {
|
||||
setCertificate(X509Utilities.CORDA_ROOT_CA, rootCaCert)
|
||||
}
|
||||
val trustStore = X509KeyStore.fromFile(config.trustStoreFile, config.trustStorePassword, createNew = true)
|
||||
val nodeKeyStore = X509KeyStore.fromFile(config.nodeKeystore, config.keyStorePassword, createNew = true)
|
||||
val sslKeyStore = X509KeyStore.fromFile(config.sslKeystore, config.keyStorePassword, createNew = true)
|
||||
config.also {
|
||||
doReturn(trustStore).whenever(it).loadTrustStore(any())
|
||||
doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any())
|
||||
doReturn(sslKeyStore).whenever(it).loadSslKeyStore(any())
|
||||
}
|
||||
val regConfig = NodeRegistrationOption(networkTrustStorePath, networkTrustStorePassword)
|
||||
NodeRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!), regConfig).buildKeystore()
|
||||
verify(hsmSigner).sign(any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createConfig(): NodeConfiguration {
|
||||
return rigorousMock<NodeConfiguration>().also {
|
||||
doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
|
||||
doReturn(it.baseDirectory / "certificates").whenever(it).certificatesDirectory
|
||||
doReturn(it.certificatesDirectory / "truststore.jks").whenever(it).trustStoreFile
|
||||
doReturn(it.certificatesDirectory / "nodekeystore.jks").whenever(it).nodeKeystore
|
||||
doReturn(it.certificatesDirectory / "sslkeystore.jks").whenever(it).sslKeystore
|
||||
doReturn("trustpass").whenever(it).trustStorePassword
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn("iTest@R3.com").whenever(it).emailAddress
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.configuration
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.ArgsParser
|
||||
import joptsimple.OptionSet
|
||||
import joptsimple.util.PathConverter
|
||||
import joptsimple.util.PathProperties
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Parses key generator command line options.
|
||||
*/
|
||||
class ConfigFilePathArgsParser : ArgsParser<Path>() {
|
||||
private val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.required()
|
||||
.describedAs("filepath")
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
|
||||
override fun parse(optionSet: OptionSet): Path {
|
||||
return optionSet.valueOf(configFileArg).toAbsolutePath()
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
|
||||
/**
|
||||
* This storage automatically approves all created requests.
|
||||
*/
|
||||
class ApproveAllCertificateRevocationRequestStorage(private val delegate: CertificateRevocationRequestStorage) : CertificateRevocationRequestStorage by delegate {
|
||||
|
||||
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
|
||||
val requestId = delegate.saveRevocationRequest(request)
|
||||
delegate.markRequestTicketCreated(requestId)
|
||||
approveRevocationRequest(requestId, DOORMAN_SIGNATURE)
|
||||
return requestId
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
|
||||
/**
|
||||
* This storage automatically approves all created requests.
|
||||
*/
|
||||
class ApproveAllCertificateSigningRequestStorage(private val delegate: CertificateSigningRequestStorage) : CertificateSigningRequestStorage by delegate {
|
||||
override fun saveRequest(request: PKCS10CertificationRequest): String {
|
||||
val requestId = delegate.saveRequest(request)
|
||||
delegate.markRequestTicketCreated(requestId)
|
||||
approveRequest(requestId, CertificateSigningRequestStorage.DOORMAN_SIGNATURE)
|
||||
return requestId
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import java.security.cert.X509CRL
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Interface for managing certificate revocation list persistence
|
||||
*/
|
||||
interface CertificateRevocationListStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman-Crl-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the latest certificate revocation list.
|
||||
*
|
||||
* @param crlIssuer CRL issuer CA type.
|
||||
* @return latest revocation list.
|
||||
*/
|
||||
fun getCertificateRevocationList(crlIssuer: CrlIssuer): X509CRL?
|
||||
|
||||
/**
|
||||
* Persists a new revocation list. Upon saving, statuses
|
||||
* of the approved revocation requests will automatically change to [RequestStatus.DONE].
|
||||
*
|
||||
* @param crl signed instance of the certificate revocation list. It will be serialized and stored as part of a
|
||||
* database entity.
|
||||
* @param crlIssuer CRL issuer CA type.
|
||||
* @param signedBy who signed this CRL.
|
||||
* @param revokedAt revocation time.
|
||||
*/
|
||||
fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant)
|
||||
}
|
||||
|
||||
/**
|
||||
* There are 2 CAs that issue CRLs (i.e. Root CA and Doorman CA).
|
||||
*/
|
||||
enum class CrlIssuer {
|
||||
ROOT, DOORMAN
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.CRLReason
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This data class is intended to be used internally certificate revocation request service.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class CertificateRevocationRequestData(val requestId: String, // This is a uniquely generated string
|
||||
val certificateSigningRequestId: String,
|
||||
val certificate: X509Certificate,
|
||||
val certificateSerialNumber: BigInteger,
|
||||
val modifiedAt: Instant,
|
||||
val legalName: CordaX500Name,
|
||||
val status: RequestStatus,
|
||||
val reason: CRLReason,
|
||||
val reporter: String) // Username of the reporter
|
||||
|
||||
/**
|
||||
* Interface for managing certificate revocation requests persistence
|
||||
*/
|
||||
interface CertificateRevocationRequestStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman-Crr-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new revocation request for the given [certificateSerialNumber].
|
||||
* The newly created revocation request has the [RequestStatus.NEW] status.
|
||||
* If the revocation request with the [certificateSerialNumber] already exists and has status
|
||||
* [RequestStatus.NEW], [RequestStatus.APPROVED] or [RequestStatus.REVOKED]
|
||||
* then nothing is persisted and the existing revocation request identifier is returned.
|
||||
*
|
||||
* @param request certificate revocation request to be stored.
|
||||
*
|
||||
* @return identifier of the newly created (or existing) revocation request.
|
||||
*/
|
||||
fun saveRevocationRequest(request: CertificateRevocationRequest): String
|
||||
|
||||
/**
|
||||
* Retrieves the revocation request with the given [requestId]
|
||||
*
|
||||
* @param requestId revocation request identifier
|
||||
*
|
||||
* @return CertificateRevocationRequest matching the specified identifier. Or null if it doesn't exist.
|
||||
*/
|
||||
fun getRevocationRequest(requestId: String): CertificateRevocationRequestData?
|
||||
|
||||
/**
|
||||
* Retrieves all the revocation requests with the specified revocation request status.
|
||||
*
|
||||
* @param revocationStatus revocation request status of the returned revocation requests.
|
||||
*
|
||||
* @return list of certificate revocation requests that match the revocation request status.
|
||||
*/
|
||||
fun getRevocationRequests(revocationStatus: RequestStatus): List<CertificateRevocationRequestData>
|
||||
|
||||
/**
|
||||
* Changes the revocation request status to [RequestStatus.APPROVED].
|
||||
*
|
||||
* @param requestId revocation request identifier
|
||||
* @param approvedBy who is approving it
|
||||
*/
|
||||
fun approveRevocationRequest(requestId: String, approvedBy: String)
|
||||
|
||||
/**
|
||||
* Changes the revocation request status to [RequestStatus.REJECTED].
|
||||
*
|
||||
* @param requestId revocation request identifier
|
||||
* @param rejectedBy who is rejecting it
|
||||
* @param reason description of the reason of this rejection.
|
||||
*/
|
||||
fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String?)
|
||||
|
||||
/**
|
||||
* Persist the fact that a ticket has been created for the given [requestId].
|
||||
*/
|
||||
fun markRequestTicketCreated(requestId: String)
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
|
||||
data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath)
|
||||
|
||||
data class CertificateSigningRequest(val requestId: String,
|
||||
val legalName: CordaX500Name?,
|
||||
val publicKeyHash: SecureHash,
|
||||
val status: RequestStatus,
|
||||
val request: PKCS10CertificationRequest,
|
||||
val remark: String?,
|
||||
val modifiedBy: String,
|
||||
val certData: CertificateData?)
|
||||
|
||||
/**
|
||||
* Provide certificate signing request storage for the certificate signing server.
|
||||
*/
|
||||
interface CertificateSigningRequestStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman-Csr-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist [PKCS10CertificationRequest] in storage for further approval if it's a valid request.
|
||||
* If not then it will be automatically rejected and not subject to any approval process.
|
||||
* In both cases a randomly generated request ID is returned.
|
||||
* @param request request to be stored
|
||||
*/
|
||||
fun saveRequest(request: PKCS10CertificationRequest): String
|
||||
|
||||
/**
|
||||
* Retrieve certificate singing request using [requestId].
|
||||
* @return certificate signing request or null if the request does not exist
|
||||
*/
|
||||
fun getRequest(requestId: String): CertificateSigningRequest?
|
||||
|
||||
/**
|
||||
* Retrieve list of certificate signing request based on the [RequestStatus].
|
||||
*/
|
||||
fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest>
|
||||
|
||||
/**
|
||||
* Persist the fact that a ticket has been created for the given [requestId].
|
||||
*/
|
||||
fun markRequestTicketCreated(requestId: String)
|
||||
|
||||
/**
|
||||
* Approve the given request if it has not already been approved. Otherwise do nothing.
|
||||
* @param requestId id of the certificate signing request
|
||||
* @param approvedBy authority (its identifier) approving this request.
|
||||
*/
|
||||
// TODO: Merge status changing methods.
|
||||
fun approveRequest(requestId: String, approvedBy: String)
|
||||
|
||||
/**
|
||||
* Reject the given request using the given reason.
|
||||
* @param requestId id of the certificate signing request
|
||||
* @param rejectedBy authority (its identifier) rejecting this request.
|
||||
* @param rejectReason brief description of the rejection reason
|
||||
*/
|
||||
fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?)
|
||||
|
||||
/**
|
||||
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.DONE].
|
||||
* @param requestId id of the certificate signing request
|
||||
* @param certPath chain of certificates starting with the one generated in response to the CSR up to the root.
|
||||
* @param signedBy authority (its identifier) signing this request.
|
||||
* @throws IllegalArgumentException if request is not found or not in Approved state.
|
||||
*/
|
||||
fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String)
|
||||
|
||||
/**
|
||||
* Retrieves the certificate path for the given public key hash if such exists and its certificate is considered to be valid.
|
||||
*
|
||||
* @param publicKey public key corresponding to the certificate being searched.
|
||||
* @return certificate path for the given public key hash or null if such certificate does not exist or is not valid.
|
||||
*/
|
||||
fun getValidCertificatePath(publicKey: PublicKey): CertPath?
|
||||
}
|
||||
|
||||
sealed class CertificateResponse {
|
||||
object NotReady : CertificateResponse()
|
||||
data class Ready(val certificatePath: CertPath) : CertificateResponse()
|
||||
data class Unauthorised(val message: String) : CertificateResponse()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class RequestStatus {
|
||||
/**
|
||||
* The request has been received, this is the initial state in which a request has been created.
|
||||
*/
|
||||
NEW,
|
||||
|
||||
/**
|
||||
* A ticket has been created but has not yet been approved nor rejected.
|
||||
*/
|
||||
TICKET_CREATED,
|
||||
|
||||
/**
|
||||
* The request has been approved, but not yet signed.
|
||||
*/
|
||||
APPROVED,
|
||||
|
||||
/**
|
||||
* The request has been rejected, this is a terminal state, once a request gets in this state it won't change anymore.
|
||||
*/
|
||||
REJECTED,
|
||||
|
||||
/**
|
||||
* The request has been successfully processed, this is a terminal state, once a request gets in this state it won't change anymore.
|
||||
*/
|
||||
DONE
|
||||
}
|
||||
|
||||
enum class CertificateStatus {
|
||||
VALID, SUSPENDED, REVOKED
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.nodeapi.internal.network.NetworkMapAndSigned
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Data access object interface for NetworkMap persistence layer
|
||||
*/
|
||||
// TODO This storage abstraction needs some thought. Some of the methods clearly don't make sense e.g. setParametersUpdateStatus.
|
||||
// TODO: We should avoid exposing entity objects.
|
||||
// The NetworkMapSignerTest uses a mock of this which means we need to provide methods for every trivial db operation.
|
||||
interface NetworkMapStorage {
|
||||
/**
|
||||
* Returns the network maps containing public network map and private network maps.
|
||||
*/
|
||||
fun getNetworkMaps(): NetworkMaps
|
||||
|
||||
/**
|
||||
* Persist the new network map for provided network ID, replacing any existing network map.
|
||||
* The map will be stored as public network map if [networkId] = null.
|
||||
*/
|
||||
fun saveNewNetworkMap(networkId: String? = null, networkMapAndSigned: NetworkMapAndSigned)
|
||||
|
||||
/**
|
||||
* Retrieves node info hashes for both public and private networks where [NodeInfoEntity.isCurrent] is true and the certificate status is [CertificateStatus.VALID],
|
||||
* and that were published less than eventHorizon ago.
|
||||
* Nodes should have declared that they are using correct set of parameters.
|
||||
*/
|
||||
fun getNodeInfoHashes(): NodeInfoHashes
|
||||
|
||||
/**
|
||||
* Retrieve the signed with certificate network parameters by their hash. The hash is that of the underlying
|
||||
* [NetworkParameters] object and not the [SignedNetworkParameters] object that's returned.
|
||||
* @return signed network parameters corresponding to the given hash or null if it does not exist (parameters don't exist or they haven't been signed yet)
|
||||
*/
|
||||
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
|
||||
|
||||
/**
|
||||
* Persists given network parameters with signature if provided.
|
||||
* @return The newly inserted [NetworkParametersEntity]
|
||||
*/
|
||||
fun saveNetworkParameters(networkParameters: NetworkParameters, signature: DigitalSignatureWithCert?): NetworkParametersEntity
|
||||
|
||||
/**
|
||||
* Save new parameters update information with corresponding network parameters. Only one parameters update entity
|
||||
* can be NEW or FLAG_DAY at any time - if one exists it will be cancelled.
|
||||
*/
|
||||
fun saveNewParametersUpdate(networkParameters: NetworkParameters, description: String, updateDeadline: Instant)
|
||||
|
||||
/**
|
||||
* Retrieves the latest (i.e. most recently inserted) network parameters entity
|
||||
* Note that they may not have been signed up yet.
|
||||
* @return latest network parameters entity
|
||||
*/
|
||||
fun getLatestNetworkParameters(): NetworkParametersEntity?
|
||||
|
||||
/** Returns the single new or flag day parameters update, or null if there isn't one. */
|
||||
fun getCurrentParametersUpdate(): ParametersUpdateEntity?
|
||||
|
||||
fun setParametersUpdateStatus(update: ParametersUpdateEntity, newStatus: UpdateStatus)
|
||||
|
||||
/**
|
||||
* Perform the switch of parameters on the flagDay.
|
||||
* 1. Change status of ParametersUpdateEntity to [UpdateStatus.APPLIED]
|
||||
* 2. Mark all the node infos that didn't accept the update as not current (so they won't be advertised in the network map)
|
||||
*/
|
||||
fun switchFlagDay(update: ParametersUpdateEntity)
|
||||
}
|
||||
|
||||
data class NetworkMaps(val publicNetworkMap: NetworkMapEntity?, val privateNetworkMap: Map<String, NetworkMapEntity>) {
|
||||
val allNodeInfoHashes: Set<SecureHash> = privateNetworkMap.flatMap { it.value.networkMap.nodeInfoHashes }.toSet() + (publicNetworkMap?.networkMap?.nodeInfoHashes ?: emptySet())
|
||||
}
|
||||
|
||||
data class NodeInfoHashes(val publicNodeInfoHashes: List<SecureHash>, val privateNodeInfoHashes: Map<String, List<SecureHash>>)
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
|
||||
/**
|
||||
* Data access object interface for NetworkMap/NodeInfo persistence layer
|
||||
*/
|
||||
interface NodeInfoStorage {
|
||||
/**
|
||||
* Retrieve node certificate path using the node public key hash.
|
||||
* @return [CertPath] or null if the public key is not registered with the Doorman.
|
||||
*/
|
||||
fun getCertificatePath(publicKeyHash: SecureHash): CertPath?
|
||||
|
||||
/**
|
||||
* Retrieve node info together with its signature using nodeInfo's hash
|
||||
* @return [NodeInfo] or null if the node info is not registered.
|
||||
*/
|
||||
fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo?
|
||||
|
||||
/**
|
||||
* Returns the parameters update the node has accepted or null if couldn't find node info with given hash or
|
||||
* there is no information on accepted parameters update stored for this entity.
|
||||
*/
|
||||
fun getAcceptedParametersUpdate(nodeInfoHash: SecureHash): ParametersUpdateEntity?
|
||||
|
||||
/**
|
||||
* The [nodeInfoAndSigned] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
|
||||
* If republishing of the same nodeInfo happens, then we will record the time it was republished in the database.
|
||||
* Based on that information we can remove unresponsive nodes from network (event horizon is the parameter that tells how
|
||||
* long node can be down before it gets removed). If the nodes becomes active again, it will enter back to the network map
|
||||
* after republishing its [NodeInfo].
|
||||
* @param nodeInfoAndSigned signed node info data to be stored
|
||||
* @return hash for the newly created node info entry
|
||||
*/
|
||||
fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash
|
||||
|
||||
/**
|
||||
* Store information about latest accepted [NetworkParameters] hash.
|
||||
* @param publicKey Public key that accepted network parameters. This public key should belong to [NodeInfo]
|
||||
* @param acceptedParametersHash Hash of latest accepted network parameters.
|
||||
*/
|
||||
fun ackNodeInfoParametersUpdate(publicKey: PublicKey, acceptedParametersHash: SecureHash)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.*
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import org.hibernate.Session
|
||||
import org.hibernate.query.Query
|
||||
import java.util.*
|
||||
import javax.persistence.LockModeType
|
||||
import javax.persistence.criteria.CriteriaBuilder
|
||||
import javax.persistence.criteria.Path
|
||||
import javax.persistence.criteria.Predicate
|
||||
|
||||
inline fun <reified T> Session.fromQuery(query: String): Query<T> {
|
||||
return createQuery("from ${T::class.java.name} $query", T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T> DatabaseTransaction.uniqueEntityWhere(predicate: (CriteriaBuilder, Path<T>) -> Predicate): T? {
|
||||
return entitiesWhere(predicate).setMaxResults(1).uniqueResult()
|
||||
}
|
||||
|
||||
inline fun <reified T> DatabaseTransaction.entitiesWhere(predicate: (CriteriaBuilder, Path<T>) -> Predicate): Query<T> {
|
||||
val builder = session.criteriaBuilder
|
||||
val criteriaQuery = builder.createQuery(T::class.java)
|
||||
val query = criteriaQuery.from(T::class.java).run {
|
||||
criteriaQuery.where(predicate(builder, this))
|
||||
}
|
||||
return session.createQuery(query).setLockMode(LockModeType.PESSIMISTIC_WRITE)
|
||||
}
|
||||
|
||||
inline fun <reified T> DatabaseTransaction.deleteEntity(predicate: (CriteriaBuilder, Path<T>) -> Predicate): Int {
|
||||
val builder = session.criteriaBuilder
|
||||
val criteriaDelete = builder.createCriteriaDelete(T::class.java)
|
||||
val delete = criteriaDelete.from(T::class.java).run {
|
||||
criteriaDelete.where(predicate(builder, this))
|
||||
}
|
||||
return session.createQuery(delete).executeUpdate()
|
||||
}
|
||||
|
||||
fun configureDatabase(dataSourceProperties: Properties,
|
||||
databaseConfig: DatabaseConfig = DatabaseConfig()): CordaPersistence {
|
||||
val config = HikariConfig(dataSourceProperties)
|
||||
val dataSource = HikariDataSource(config)
|
||||
|
||||
val schemas = setOf(NetworkManagementSchemaServices.SchemaV1)
|
||||
SchemaMigration(schemas, dataSource, true, databaseConfig).nodeStartup()
|
||||
|
||||
return CordaPersistence(dataSource, databaseConfig, schemas, config.dataSourceProperties.getProperty("url", ""), emptyList())
|
||||
}
|
||||
|
||||
sealed class NetworkManagementSchemaServices {
|
||||
object SchemaV1 : MappedSchema(schemaFamily = NetworkManagementSchemaServices::class.java, version = 1,
|
||||
mappedTypes = listOf(
|
||||
CertificateSigningRequestEntity::class.java,
|
||||
CertificateDataEntity::class.java,
|
||||
CertificateRevocationRequestEntity::class.java,
|
||||
PrivateNetworkEntity::class.java,
|
||||
CertificateRevocationListEntity::class.java,
|
||||
NodeInfoEntity::class.java,
|
||||
NetworkParametersEntity::class.java,
|
||||
NetworkMapEntity::class.java,
|
||||
ParametersUpdateEntity::class.java)) {
|
||||
override val migrationResource = "network-manager.changelog-master"
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationListEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationRequestEntity
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.X509CRL
|
||||
import java.time.Instant
|
||||
|
||||
class PersistentCertificateRevocationListStorage(private val database: CordaPersistence) : CertificateRevocationListStorage {
|
||||
override fun getCertificateRevocationList(crlIssuer: CrlIssuer): X509CRL? {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(CertificateRevocationListEntity::class.java).run {
|
||||
from(CertificateRevocationListEntity::class.java).run {
|
||||
orderBy(builder.desc(get<String>(CertificateRevocationListEntity::id.name)))
|
||||
where(builder.equal(get<CrlIssuer>(CertificateRevocationListEntity::crlIssuer.name), crlIssuer))
|
||||
}
|
||||
}
|
||||
// We just want the last signed entry
|
||||
session.createQuery(query).setMaxResults(1).uniqueResult()?.crl
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant) {
|
||||
database.transaction {
|
||||
crl.revokedCertificates?.forEach {
|
||||
revokeCertificate(it.serialNumber, revokedAt, this)
|
||||
}
|
||||
session.save(CertificateRevocationListEntity(
|
||||
crl = crl,
|
||||
crlIssuer = crlIssuer,
|
||||
signedBy = signedBy,
|
||||
modifiedAt = Instant.now()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun revokeCertificate(certificateSerialNumber: BigInteger, time: Instant, transaction: DatabaseTransaction) {
|
||||
val revocation = transaction.uniqueEntityWhere<CertificateRevocationRequestEntity> { builder, path ->
|
||||
builder.and(
|
||||
builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber),
|
||||
builder.notEqual(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), RequestStatus.REJECTED)
|
||||
)
|
||||
}
|
||||
revocation ?: throw IllegalStateException("The certificate revocation request for $certificateSerialNumber does not exist")
|
||||
check(revocation.status in arrayOf(RequestStatus.APPROVED, RequestStatus.DONE)) {
|
||||
"The certificate revocation request for $certificateSerialNumber has unexpected status of ${revocation.status}"
|
||||
}
|
||||
val session = transaction.session
|
||||
val certificateData = session.merge(revocation.certificateData.copy(certificateStatus = CertificateStatus.REVOKED)) as CertificateDataEntity
|
||||
session.merge(revocation.copy(
|
||||
status = RequestStatus.DONE,
|
||||
modifiedAt = time,
|
||||
certificateData = certificateData
|
||||
))
|
||||
}
|
||||
}
|
@ -1,237 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationRequestEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.CRLReason
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
class PersistentCertificateRevocationRequestStorage(private val database: CordaPersistence) : CertificateRevocationRequestStorage {
|
||||
private companion object {
|
||||
val ALLOWED_REASONS = arrayOf(
|
||||
CRLReason.KEY_COMPROMISE,
|
||||
CRLReason.AFFILIATION_CHANGED,
|
||||
CRLReason.CA_COMPROMISE,
|
||||
CRLReason.CESSATION_OF_OPERATION,
|
||||
CRLReason.PRIVILEGE_WITHDRAWN,
|
||||
CRLReason.SUPERSEDED,
|
||||
CRLReason.UNSPECIFIED
|
||||
)
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
// Get matching CSR
|
||||
validate(request)
|
||||
val csr = requireNotNull(retrieveCsr(request.certificateSerialNumber, request.csrRequestId, request.legalName)) {
|
||||
"No CSR matching the given criteria was found"
|
||||
}
|
||||
// Check if there is an entry for the given certificate serial number
|
||||
val revocation = uniqueEntityWhere<CertificateRevocationRequestEntity> { builder, path ->
|
||||
val serialNumberEqual = builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), request.certificateSerialNumber)
|
||||
val statusNotEqualRejected = builder.notEqual(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), RequestStatus.REJECTED)
|
||||
builder.and(serialNumberEqual, statusNotEqualRejected)
|
||||
}
|
||||
if (revocation != null) {
|
||||
revocation.requestId
|
||||
} else {
|
||||
val certificateData = csr.certificateData!!
|
||||
requireNotNull(certificateData) { "The certificate with the given serial number cannot be found." }
|
||||
val requestId = SecureHash.randomSHA256().toString()
|
||||
session.save(CertificateRevocationRequestEntity(
|
||||
certificateSerialNumber = certificateData.certificateSerialNumber,
|
||||
revocationReason = request.reason,
|
||||
requestId = requestId,
|
||||
modifiedBy = request.reporter,
|
||||
certificateData = certificateData,
|
||||
reporter = request.reporter,
|
||||
legalName = certificateData.legalName
|
||||
))
|
||||
requestId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(request: CertificateRevocationRequest) {
|
||||
require(request.reason in ALLOWED_REASONS) { "The given revocation reason is not allowed." }
|
||||
}
|
||||
|
||||
private fun DatabaseTransaction.retrieveCsr(certificateSerialNumber: BigInteger?, csrRequestId: String?, legalName: CordaX500Name?): CertificateSigningRequestEntity? {
|
||||
val csr = if (csrRequestId != null) {
|
||||
uniqueEntityWhere<CertificateSigningRequestEntity> { builder, path ->
|
||||
builder.and(
|
||||
builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), csrRequestId),
|
||||
builder.notEqual(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED),
|
||||
builder.equal(path
|
||||
.get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
||||
.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
|
||||
)
|
||||
}
|
||||
} else if (legalName != null) {
|
||||
uniqueEntityWhere<CertificateSigningRequestEntity> { builder, path ->
|
||||
builder.and(
|
||||
builder.equal(path.get<String>(CertificateSigningRequestEntity::legalName.name), legalName.toString()),
|
||||
builder.notEqual(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED),
|
||||
builder.equal(path
|
||||
.get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
||||
.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val certificateData = uniqueEntityWhere<CertificateDataEntity> { builder, path ->
|
||||
builder.and(
|
||||
builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID),
|
||||
builder.equal(path.get<String>(CertificateDataEntity::certificateSerialNumber.name), certificateSerialNumber)
|
||||
)
|
||||
}
|
||||
certificateData?.certificateSigningRequest
|
||||
}
|
||||
return csr?.let {
|
||||
validateCsrConsistency(certificateSerialNumber, csrRequestId, legalName, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateCsrConsistency(certificateSerialNumber: BigInteger?,
|
||||
csrRequestId: String?,
|
||||
legalName: CordaX500Name?,
|
||||
result: CertificateSigningRequestEntity): CertificateSigningRequestEntity {
|
||||
val certData = result.certificateData!!
|
||||
require(legalName == null || result.legalName == legalName) {
|
||||
"The legal name does not match."
|
||||
}
|
||||
require(csrRequestId == null || result.requestId == csrRequestId) {
|
||||
"The CSR request ID does not match."
|
||||
}
|
||||
require(certificateSerialNumber == null || certData.certificateSerialNumber == certificateSerialNumber) {
|
||||
"The certificate serial number does not match."
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getRevocationRequest(requestId: String): CertificateRevocationRequestData? {
|
||||
return database.transaction {
|
||||
getRevocationRequestEntity(requestId)?.toCertificateRevocationRequestData()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRevocationRequests(revocationStatus: RequestStatus): List<CertificateRevocationRequestData> {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(CertificateRevocationRequestEntity::class.java).run {
|
||||
from(CertificateRevocationRequestEntity::class.java).run {
|
||||
where(builder.equal(get<RequestStatus>(CertificateRevocationRequestEntity::status.name), revocationStatus))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).resultList.map { it.toCertificateRevocationRequestData() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun approveRevocationRequest(requestId: String, approvedBy: String) {
|
||||
database.transaction {
|
||||
val revocation = getRevocationRequestEntity(requestId)
|
||||
if (revocation == null) {
|
||||
throw NoSuchElementException("Error while approving! Certificate revocation id=$id does not exist")
|
||||
} else {
|
||||
when (revocation.status) {
|
||||
RequestStatus.TICKET_CREATED -> {
|
||||
session.merge(revocation.copy(
|
||||
status = RequestStatus.APPROVED,
|
||||
modifiedAt = Instant.now(),
|
||||
modifiedBy = approvedBy
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as APPROVED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as APPROVED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String?) {
|
||||
database.transaction {
|
||||
val revocation = getRevocationRequestEntity(requestId)
|
||||
if (revocation == null) {
|
||||
throw NoSuchElementException("Error while rejecting! Certificate revocation id=$id does not exist")
|
||||
} else {
|
||||
when (revocation.status) {
|
||||
RequestStatus.TICKET_CREATED -> {
|
||||
session.merge(revocation.copy(
|
||||
status = RequestStatus.REJECTED,
|
||||
modifiedAt = Instant.now(),
|
||||
modifiedBy = rejectedBy,
|
||||
remark = reason
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as REJECTED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as REJECTED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markRequestTicketCreated(requestId: String) {
|
||||
database.transaction {
|
||||
val revocation = getRevocationRequestEntity(requestId)
|
||||
if (revocation == null) {
|
||||
throw NoSuchElementException("Error while marking the request as ticket created! Certificate revocation id=$id does not exist")
|
||||
} else {
|
||||
when (revocation.status) {
|
||||
RequestStatus.NEW -> {
|
||||
session.merge(revocation.copy(
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.TICKET_CREATED
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as TICKED_CREATED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as TICKED_CREATED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRevocationRequestEntity(requestId: String, status: RequestStatus? = null): CertificateRevocationRequestEntity? {
|
||||
return database.transaction {
|
||||
uniqueEntityWhere { builder, path ->
|
||||
val idEqual = builder.equal(path.get<String>(CertificateRevocationRequestEntity::requestId.name), requestId)
|
||||
if (status == null) {
|
||||
idEqual
|
||||
} else {
|
||||
builder.and(idEqual, builder.equal(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CertificateRevocationRequestEntity.toCertificateRevocationRequestData(): CertificateRevocationRequestData {
|
||||
return CertificateRevocationRequestData(
|
||||
requestId,
|
||||
certificateData.certificateSigningRequest.requestId,
|
||||
certificateData.certPath.certificates.first() as X509Certificate,
|
||||
certificateSerialNumber,
|
||||
modifiedAt,
|
||||
legalName,
|
||||
status,
|
||||
revocationReason,
|
||||
reporter
|
||||
)
|
||||
}
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
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 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.core.internal.hash
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
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 java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
import javax.persistence.LockModeType
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
/**
|
||||
* Database implementation of the [CertificateSigningRequestStorage] interface.
|
||||
*/
|
||||
class PersistentCertificateSigningRequestStorage(private val database: CordaPersistence) : CertificateSigningRequestStorage {
|
||||
companion object {
|
||||
// TODO: make this configurable?
|
||||
private val allowedCertRoles = setOf(CertRole.NODE_CA, CertRole.SERVICE_IDENTITY)
|
||||
}
|
||||
|
||||
override fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = requireNotNull(uniqueEntityWhere<CertificateSigningRequestEntity> { builder, path ->
|
||||
val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId)
|
||||
val statusEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::status.name), RequestStatus.APPROVED)
|
||||
builder.and(requestIdEq, statusEq)
|
||||
}) { "Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId" }
|
||||
val certificateSigningRequest = request.copy(
|
||||
modifiedBy = signedBy,
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.DONE)
|
||||
session.merge(certificateSigningRequest)
|
||||
val certificateDataEntity = CertificateDataEntity(
|
||||
certificateStatus = CertificateStatus.VALID,
|
||||
certPath = certPath,
|
||||
certificateSigningRequest = certificateSigningRequest,
|
||||
certificateSerialNumber = certPath.x509Certificates.first().serialNumber)
|
||||
session.persist(certificateDataEntity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveRequest(request: PKCS10CertificationRequest): String {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val (legalName, exception) = try {
|
||||
val legalName = parseLegalName(request)
|
||||
// Return existing request ID, if request already exists for the same request.
|
||||
validateRequest(legalName, request)?.let { return@transaction it }
|
||||
Pair(legalName, null)
|
||||
} catch (e: RequestValidationException) {
|
||||
Pair(e.legalName, e)
|
||||
}
|
||||
|
||||
val requestEntity = CertificateSigningRequestEntity(
|
||||
requestId = SecureHash.randomSHA256().toString(),
|
||||
legalName = legalName,
|
||||
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hash,
|
||||
request = request,
|
||||
remark = exception?.rejectMessage,
|
||||
modifiedBy = CertificateSigningRequestStorage.DOORMAN_SIGNATURE,
|
||||
status = if (exception == null) RequestStatus.NEW else RequestStatus.REJECTED
|
||||
)
|
||||
session.save(requestEntity) as String
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseTransaction.findRequest(requestId: String,
|
||||
requestStatus: RequestStatus? = null): CertificateSigningRequestEntity? {
|
||||
return uniqueEntityWhere { builder, path ->
|
||||
val idClause = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId)
|
||||
if (requestStatus == null) {
|
||||
idClause
|
||||
} else {
|
||||
val statusClause = builder.equal(path.get<String>(CertificateSigningRequestEntity::status.name), requestStatus)
|
||||
builder.and(idClause, statusClause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markRequestTicketCreated(requestId: String) {
|
||||
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = requireNotNull(findRequest(requestId, RequestStatus.NEW)) { "Error when creating request ticket with id: $requestId. Request does not exist or its status is not NEW." }
|
||||
val update = request.copy(
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.TICKET_CREATED)
|
||||
session.merge(update)
|
||||
}
|
||||
}
|
||||
|
||||
override fun approveRequest(requestId: String, approvedBy: String) {
|
||||
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
findRequest(requestId, RequestStatus.TICKET_CREATED)?.let {
|
||||
val update = it.copy(
|
||||
modifiedBy = approvedBy,
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.APPROVED)
|
||||
session.merge(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) {
|
||||
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = requireNotNull(findRequest(requestId)) { "Error when rejecting request with id: $requestId. Request does not exist." }
|
||||
val update = request.copy(
|
||||
modifiedBy = rejectedBy,
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.REJECTED,
|
||||
remark = rejectReason
|
||||
)
|
||||
session.merge(update)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getValidCertificatePath(publicKey: PublicKey): CertPath? {
|
||||
return database.transaction {
|
||||
session.createQuery(
|
||||
"select a.certificateData.certPath from ${CertificateSigningRequestEntity::class.java.name} a " +
|
||||
"where a.publicKeyHash = :publicKeyHash and a.status = 'DONE' and a.certificateData.certificateStatus = 'VALID'", CertPath::class.java)
|
||||
.setParameter("publicKeyHash", publicKey.hash.toString())
|
||||
.uniqueResult()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequest(requestId: String): CertificateSigningRequest? {
|
||||
return database.transaction {
|
||||
findRequest(requestId)?.toCertificateSigningRequest()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest> {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(CertificateSigningRequestEntity::class.java).run {
|
||||
from(CertificateSigningRequestEntity::class.java).run {
|
||||
where(builder.equal(get<RequestStatus>(CertificateSigningRequestEntity::status.name), requestStatus))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).resultList.map { it.toCertificateSigningRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLegalName(request: PKCS10CertificationRequest): CordaX500Name {
|
||||
return try {
|
||||
CordaX500Name.build(X500Principal(request.subject.encoded))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw RequestValidationException(null, request.subject.toString(), rejectMessage = "Name validation failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate certificate signing request, returns request ID if same request already exists.
|
||||
*/
|
||||
private fun DatabaseTransaction.validateRequest(legalName: CordaX500Name, request: PKCS10CertificationRequest): String? {
|
||||
// Check if the same request exists and returns the request id.
|
||||
val existingRequestByPubKeyHash = nonRejectedRequest(CertificateSigningRequestEntity::publicKeyHash.name, toSupportedPublicKey(request.subjectPublicKeyInfo).hash)
|
||||
|
||||
existingRequestByPubKeyHash?.let {
|
||||
// Compare subject, attribute.
|
||||
// We cannot compare the request directly because it contains nonce.
|
||||
if (certNotRevoked(it) && it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) {
|
||||
return it.requestId
|
||||
} else {
|
||||
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
|
||||
throw RequestValidationException(legalName, rejectMessage = "Duplicate public key")
|
||||
}
|
||||
}
|
||||
// Check if requested role is valid.
|
||||
if (request.getCertRole() !in allowedCertRoles)
|
||||
throw RequestValidationException(legalName, rejectMessage = "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 existingRequestByLegalName = nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName)
|
||||
existingRequestByLegalName?.let {
|
||||
if (certNotRevoked(it)) {
|
||||
throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun certNotRevoked(request: CertificateSigningRequestEntity): Boolean {
|
||||
return request.status != RequestStatus.DONE || request.certificateData?.certificateStatus != CertificateStatus.REVOKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve "non-rejected" request which matches provided column and value predicate.
|
||||
*/
|
||||
private fun <T : Any> DatabaseTransaction.nonRejectedRequest(columnName: String, value: T): CertificateSigningRequestEntity? {
|
||||
val query = session.criteriaBuilder.run {
|
||||
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
|
||||
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
||||
val valueQuery = equal(get<T>(columnName), value)
|
||||
val statusQuery = notEqual(get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.REJECTED)
|
||||
criteriaQuery.where(and(valueQuery, statusQuery))
|
||||
}
|
||||
}
|
||||
return session.createQuery(query).setLockMode(LockModeType.PESSIMISTIC_WRITE).setMaxResults(1).uniqueResult()
|
||||
}
|
||||
|
||||
private data class RequestValidationException(val legalName: CordaX500Name?, val subjectName: String = legalName.toString(), val rejectMessage: String) : Exception("Validation failed for $subjectName. $rejectMessage.")
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.*
|
||||
import com.r3.corda.networkmanage.common.utils.logger
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.NetworkMapAndSigned
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Database implementation of the [NetworkMapStorage] interface
|
||||
*/
|
||||
class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage {
|
||||
companion object {
|
||||
// Used internally to identify global network map in database table.
|
||||
private const val PUBLIC_NETWORK_ID = "PUBLIC_NETWORK"
|
||||
}
|
||||
|
||||
override fun getNetworkMaps(): NetworkMaps {
|
||||
return database.transaction {
|
||||
val networkMapEntities = session.createQuery("from ${NetworkMapEntity::class.java.name}", NetworkMapEntity::class.java)
|
||||
.resultList
|
||||
.associateBy { it.id }
|
||||
NetworkMaps(networkMapEntities[PUBLIC_NETWORK_ID], networkMapEntities.filterKeys { it != PUBLIC_NETWORK_ID })
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveNewNetworkMap(networkId: String?, networkMapAndSigned: NetworkMapAndSigned) {
|
||||
val (networkMap, signedNetworkMap) = networkMapAndSigned
|
||||
database.transaction {
|
||||
val networkParametersEntity = checkNotNull(getNetworkParametersEntity(networkMap.networkParameterHash)) {
|
||||
"Network parameters ${networkMap.networkParameterHash} must be first persisted"
|
||||
}
|
||||
check(networkParametersEntity.isSigned) {
|
||||
"Network parameters ${networkMap.networkParameterHash} are not signed"
|
||||
}
|
||||
session.merge(NetworkMapEntity(
|
||||
id = networkId ?: PUBLIC_NETWORK_ID,
|
||||
networkMap = networkMap,
|
||||
signature = signedNetworkMap.sig.bytes,
|
||||
certificate = signedNetworkMap.sig.by,
|
||||
networkParameters = networkParametersEntity
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? {
|
||||
return database.transaction {
|
||||
getNetworkParametersEntity(hash)?.let {
|
||||
if (it.isSigned) it.toSignedNetworkParameters() else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNodeInfoHashes(): NodeInfoHashes {
|
||||
return database.transaction {
|
||||
val currentParameters = getNetworkMaps().publicNetworkMap?.networkParameters?.networkParameters
|
||||
val builder = session.criteriaBuilder
|
||||
// TODO Convert this query to JPQL so it's more readable.
|
||||
val query = builder.createTupleQuery().run {
|
||||
from(NodeInfoEntity::class.java).run {
|
||||
val certStatusExpression = get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name)
|
||||
.get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
||||
.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name)
|
||||
// TODO When revoking certs, all node-infos that point to it must be made non-current. Then this check
|
||||
// isn't needed.
|
||||
val certStatusEq = builder.equal(certStatusExpression, CertificateStatus.VALID)
|
||||
val isCurrentNodeInfo = builder.isTrue(get<Boolean>(NodeInfoEntity::isCurrent.name))
|
||||
// We enable eventHorizon only if minimum platform version is greater than 3, nodes on previous versions
|
||||
// don't republish their node infos on regular intervals so they shouldn't be evicted from network after eventHorizon.
|
||||
val eventHorizonAgo = if (currentParameters != null && currentParameters.minimumPlatformVersion >= 4) {
|
||||
builder.greaterThanOrEqualTo(get<Instant>(NodeInfoEntity::publishedAt.name),
|
||||
Instant.now().minus(currentParameters.eventHorizon))
|
||||
} else {
|
||||
builder.and() // This expression is always true. It's needed when eventHorizon isn't enabled.
|
||||
}
|
||||
val networkIdSelector = get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name)
|
||||
.get<PrivateNetworkEntity>(CertificateSigningRequestEntity::privateNetwork.name)
|
||||
.get<String>(PrivateNetworkEntity::networkId.name)
|
||||
multiselect(networkIdSelector, get<String>(NodeInfoEntity::nodeInfoHash.name))
|
||||
.where(builder.and(certStatusEq, isCurrentNodeInfo, eventHorizonAgo))
|
||||
}
|
||||
}
|
||||
val allNodeInfos = session.createQuery(query).resultList.groupBy { it[0]?.toString() ?: PUBLIC_NETWORK_ID }.mapValues { it.value.map { SecureHash.parse(it.get(1, String::class.java)) } }
|
||||
NodeInfoHashes(allNodeInfos[PUBLIC_NETWORK_ID] ?: emptyList(), allNodeInfos.filterKeys { it != PUBLIC_NETWORK_ID })
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveNetworkParameters(networkParameters: NetworkParameters, signature: DigitalSignatureWithCert?): NetworkParametersEntity {
|
||||
val serialized = networkParameters.serialize()
|
||||
signature?.verify(serialized)
|
||||
val hash = serialized.hash
|
||||
return database.transaction {
|
||||
val entity = getNetworkParametersEntity(hash)
|
||||
val newNetworkParamsEntity = entity?.copy(
|
||||
signature = signature?.bytes,
|
||||
certificate = signature?.by
|
||||
) ?: NetworkParametersEntity(
|
||||
networkParameters = networkParameters,
|
||||
hash = hash.toString(),
|
||||
signature = signature?.bytes,
|
||||
certificate = signature?.by
|
||||
)
|
||||
session.merge(newNetworkParamsEntity) as NetworkParametersEntity
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLatestNetworkParameters(): NetworkParametersEntity? {
|
||||
return database.transaction {
|
||||
val query = session.criteriaBuilder.run {
|
||||
createQuery(NetworkParametersEntity::class.java).run {
|
||||
from(NetworkParametersEntity::class.java).run {
|
||||
orderBy(desc(get<String>(NetworkParametersEntity::created.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
// We just want the last entry
|
||||
session.createQuery(query).setMaxResults(1).uniqueResult()
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveNewParametersUpdate(networkParameters: NetworkParameters, description: String, updateDeadline: Instant) {
|
||||
database.transaction {
|
||||
val existingUpdate = getCurrentParametersUpdate()
|
||||
if (existingUpdate != null) {
|
||||
logger.info("Cancelling existing update: $existingUpdate")
|
||||
session.merge(existingUpdate.copy(status = UpdateStatus.CANCELLED))
|
||||
}
|
||||
val netParamsEntity = saveNetworkParameters(networkParameters, null)
|
||||
session.save(ParametersUpdateEntity(
|
||||
networkParameters = netParamsEntity,
|
||||
description = description,
|
||||
updateDeadline = updateDeadline
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentParametersUpdate(): ParametersUpdateEntity? {
|
||||
return database.transaction {
|
||||
val newParamsUpdates = session.fromQuery<ParametersUpdateEntity>("u where u.status in :statuses")
|
||||
.setParameterList("statuses", listOf(UpdateStatus.NEW, UpdateStatus.FLAG_DAY))
|
||||
.resultList
|
||||
when (newParamsUpdates.size) {
|
||||
0 -> null
|
||||
1 -> newParamsUpdates[0]
|
||||
else -> throw IllegalStateException("More than one update found: $newParamsUpdates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setParametersUpdateStatus(update: ParametersUpdateEntity, newStatus: UpdateStatus) {
|
||||
require(newStatus != UpdateStatus.NEW)
|
||||
database.transaction {
|
||||
session.merge(update.copy(status = newStatus))
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchFlagDay(update: ParametersUpdateEntity) {
|
||||
database.transaction {
|
||||
setParametersUpdateStatus(update, UpdateStatus.APPLIED)
|
||||
session.createQuery("update ${NodeInfoEntity::class.java.name} n set n.isCurrent = false " +
|
||||
"where (n.acceptedParametersUpdate != :acceptedParamUp or n.acceptedParametersUpdate is null) and n.isCurrent = true")
|
||||
.setParameter("acceptedParamUp", update).executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseTransaction.getNetworkParametersEntity(hash: SecureHash): NetworkParametersEntity? {
|
||||
return uniqueEntityWhere { builder, path ->
|
||||
builder.equal(path.get<String>(NetworkParametersEntity::hash.name), hash.toString())
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus
|
||||
import com.r3.corda.networkmanage.common.utils.logger
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.CertRole.NODE_CA
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Database implementation of the [NetworkMapStorage] interface
|
||||
*/
|
||||
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
|
||||
override fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash {
|
||||
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
|
||||
val nodeInfoHash = signedNodeInfo.raw.hash
|
||||
|
||||
// Extract identities issued by the intermediate CAs (doorman).
|
||||
val registeredIdentities = nodeInfo.legalIdentitiesAndCerts.map { it.certPath.x509Certificates.single { CertRole.extract(it) in setOf(CertRole.SERVICE_IDENTITY, NODE_CA) } }
|
||||
|
||||
database.transaction {
|
||||
// Record fact of republishing of the node info, it's treated as a heartbeat from the node.
|
||||
val rowsUpdated = session.createQuery("update ${NodeInfoEntity::class.java.name} n set publishedAt = :now " +
|
||||
"where n.nodeInfoHash = :nodeInfoHash and n.isCurrent = true")
|
||||
.setParameter("now", Instant.now())
|
||||
.setParameter("nodeInfoHash", nodeInfoHash.toString())
|
||||
.executeUpdate()
|
||||
if (rowsUpdated != 0) {
|
||||
logger.debug { "Republish of $nodeInfo" }
|
||||
return@transaction nodeInfoHash
|
||||
}
|
||||
// TODO Move these checks out of data access layer
|
||||
// For each identity known by the doorman, validate against it's CSR.
|
||||
val requests = registeredIdentities.map {
|
||||
val request = requireNotNull(getSignedRequestByPublicHash(it.publicKey.hash)) {
|
||||
"Node-info not registered with us"
|
||||
}
|
||||
request.certificateData?.certificateStatus.let {
|
||||
require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" }
|
||||
}
|
||||
CertRole.extract(it) to request
|
||||
}
|
||||
|
||||
// Ensure only 1 NodeCA identity.
|
||||
// TODO: make this support multiple node identities.
|
||||
val (_, request) = requireNotNull(requests.singleOrNull { it.first == CertRole.NODE_CA }) { "Require exactly 1 Node CA identity in the node-info." }
|
||||
|
||||
val existingNodeInfos = session.fromQuery<NodeInfoEntity>(
|
||||
"n where n.certificateSigningRequest = :csr and n.isCurrent = true order by n.publishedAt desc")
|
||||
.setParameter("csr", request)
|
||||
.resultList
|
||||
|
||||
// Update any [NodeInfoEntity] instance for this CSR as not current.
|
||||
existingNodeInfos.forEach { session.merge(it.copy(isCurrent = false)) }
|
||||
|
||||
session.saveOrUpdate(NodeInfoEntity(
|
||||
nodeInfoHash = nodeInfoHash.toString(),
|
||||
publicKeyHash = nodeInfo.legalIdentities[0].owningKey.hash,
|
||||
certificateSigningRequest = request,
|
||||
signedNodeInfo = signedNodeInfo,
|
||||
isCurrent = true,
|
||||
acceptedParametersUpdate = existingNodeInfos.firstOrNull()?.acceptedParametersUpdate
|
||||
))
|
||||
}
|
||||
|
||||
return nodeInfoHash
|
||||
}
|
||||
|
||||
override fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo? {
|
||||
return database.transaction {
|
||||
session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.signedNodeInfo
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAcceptedParametersUpdate(nodeInfoHash: SecureHash): ParametersUpdateEntity? {
|
||||
return database.transaction {
|
||||
session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.acceptedParametersUpdate
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
|
||||
return database.transaction {
|
||||
val request = getSignedRequestByPublicHash(publicKeyHash)
|
||||
request?.let { it.certificateData!!.certPath }
|
||||
}
|
||||
}
|
||||
|
||||
override fun ackNodeInfoParametersUpdate(publicKey: PublicKey, acceptedParametersHash: SecureHash) {
|
||||
return database.transaction {
|
||||
val nodeInfoEntity = session.fromQuery<NodeInfoEntity>(
|
||||
"n where n.publicKeyHash = :publicKeyHash and isCurrent = true")
|
||||
.setParameter("publicKeyHash", publicKey.hash)
|
||||
.singleResult
|
||||
val parametersUpdateEntity = session.fromQuery<ParametersUpdateEntity>(
|
||||
"u where u.networkParameters.hash = :acceptedParametersHash").
|
||||
setParameter("acceptedParametersHash", acceptedParametersHash.toString())
|
||||
.singleResult
|
||||
require(parametersUpdateEntity.status in listOf(UpdateStatus.NEW, UpdateStatus.FLAG_DAY)) {
|
||||
"$parametersUpdateEntity can no longer be accepted as it's ${parametersUpdateEntity.status}"
|
||||
}
|
||||
session.merge(nodeInfoEntity.copy(acceptedParametersUpdate = parametersUpdateEntity))
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseTransaction.getSignedRequestByPublicHash(publicKeyHash: SecureHash): CertificateSigningRequestEntity? {
|
||||
return uniqueEntityWhere { builder, path ->
|
||||
val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash)
|
||||
val statusEq = builder.equal(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.DONE)
|
||||
builder.and(publicKeyEq, statusEq)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.sequence
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.persistence.AttributeConverter
|
||||
|
||||
class PKCS10CertificationRequestConverter : AttributeConverter<PKCS10CertificationRequest, ByteArray> {
|
||||
override fun convertToEntityAttribute(dbData: ByteArray?): PKCS10CertificationRequest? = dbData?.let(::PKCS10CertificationRequest)
|
||||
override fun convertToDatabaseColumn(attribute: PKCS10CertificationRequest?): ByteArray? = attribute?.encoded
|
||||
}
|
||||
|
||||
class CertPathConverter : AttributeConverter<CertPath, ByteArray> {
|
||||
override fun convertToEntityAttribute(dbData: ByteArray?): CertPath? = dbData?.let(::buildCertPath)
|
||||
override fun convertToDatabaseColumn(attribute: CertPath?): ByteArray? = attribute?.encoded
|
||||
}
|
||||
|
||||
class X509CertificateConverter : AttributeConverter<X509Certificate, ByteArray> {
|
||||
override fun convertToEntityAttribute(dbData: ByteArray?): X509Certificate? {
|
||||
return dbData?.let { X509CertificateFactory().generateCertificate(it.inputStream()) }
|
||||
}
|
||||
override fun convertToDatabaseColumn(attribute: X509Certificate?): ByteArray? = attribute?.encoded
|
||||
}
|
||||
|
||||
class X509CRLConverter : AttributeConverter<X509CRL, ByteArray> {
|
||||
override fun convertToEntityAttribute(dbData: ByteArray?): X509CRL? {
|
||||
return dbData?.let { X509CertificateFactory().delegate.generateCRL(it.inputStream()) as X509CRL }
|
||||
}
|
||||
override fun convertToDatabaseColumn(attribute: X509CRL?): ByteArray? = attribute?.encoded
|
||||
}
|
||||
|
||||
class NetworkParametersConverter : CordaSerializationConverter<NetworkParameters>(NetworkParameters::class.java)
|
||||
|
||||
class NetworkMapConverter : CordaSerializationConverter<NetworkMap>(NetworkMap::class.java)
|
||||
|
||||
class SignedNodeInfoConverter : CordaSerializationConverter<SignedNodeInfo>(SignedNodeInfo::class.java)
|
||||
|
||||
class CordaX500NameAttributeConverter : ToStringConverter<CordaX500Name>(CordaX500Name.Companion::parse)
|
||||
|
||||
class SecureHashAttributeConverter : ToStringConverter<SecureHash>(SecureHash.Companion::parse)
|
||||
|
||||
abstract class CordaSerializationConverter<T : Any>(private val clazz: Class<T>) : AttributeConverter<T, ByteArray> {
|
||||
override fun convertToEntityAttribute(dbData: ByteArray?): T? {
|
||||
return dbData?.let {
|
||||
val serializationFactory = SerializationFactory.defaultFactory
|
||||
serializationFactory.deserialize(it.sequence(), clazz, serializationFactory.defaultContext)
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertToDatabaseColumn(attribute: T?): ByteArray? = attribute?.serialize()?.bytes
|
||||
}
|
||||
|
||||
abstract class ToStringConverter<T : Any>(private val parser: (String) -> T) : AttributeConverter<T, String> {
|
||||
override fun convertToDatabaseColumn(attribute: T?): String? = attribute?.toString()
|
||||
override fun convertToEntityAttribute(dbData: String?): T? = dbData?.let { parser(it) }
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import java.io.Serializable
|
||||
import java.security.cert.X509CRL
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "cert_revocation_list")
|
||||
class CertificateRevocationListEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "issuer", length = 16, nullable = false, columnDefinition = "NVARCHAR(16)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val crlIssuer: CrlIssuer,
|
||||
|
||||
@Lob
|
||||
@Column(name = "crl_bytes", nullable = false)
|
||||
@Convert(converter = X509CRLConverter::class)
|
||||
val crl: X509CRL,
|
||||
|
||||
@Column(name = "signed_by", length = 64, nullable = false)
|
||||
val signedBy: String,
|
||||
|
||||
@Column(name = "modified_at", nullable = false)
|
||||
val modifiedAt: Instant = Instant.now()
|
||||
) : Serializable
|
@ -1,61 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import org.hibernate.envers.Audited
|
||||
import java.io.Serializable
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.CRLReason
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "cert_revocation_request")
|
||||
data class CertificateRevocationRequestEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "request_id", length = 64, nullable = false, unique = true)
|
||||
val requestId: String,
|
||||
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "cert_data", nullable = false)
|
||||
val certificateData: CertificateDataEntity,
|
||||
|
||||
@Column(name = "cert_serial_number", nullable = false, columnDefinition = "NUMERIC(28)")
|
||||
val certificateSerialNumber: BigInteger,
|
||||
|
||||
@Column(name = "legal_name", length = 256, nullable = false)
|
||||
@Convert(converter = CordaX500NameAttributeConverter::class)
|
||||
val legalName: CordaX500Name,
|
||||
|
||||
// Setting [columnDefinition] is a work around for a hibernate problem when using SQL database.
|
||||
// TODO: Remove this when we find out the cause of the problem.
|
||||
@Audited
|
||||
@Column(name = "status", length = 16, nullable = false, columnDefinition = "NVARCHAR(16)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val status: RequestStatus = RequestStatus.NEW,
|
||||
|
||||
@Column(name = "reporter", nullable = false, length = 64)
|
||||
val reporter: String,
|
||||
|
||||
@Audited
|
||||
@Column(name = "modified_by", nullable = false, length = 64)
|
||||
val modifiedBy: String,
|
||||
|
||||
@Audited
|
||||
@Column(name = "modified_at", nullable = false)
|
||||
val modifiedAt: Instant = Instant.now(),
|
||||
|
||||
// Setting [columnDefinition] is a work around for a hibernate problem when using SQL database.
|
||||
// TODO: Remove this when we find out the cause of the problem.
|
||||
@Audited
|
||||
@Column(name = "revocation_reason", length = 32, nullable = false, columnDefinition = "NVARCHAR(32)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val revocationReason: CRLReason,
|
||||
|
||||
@Audited
|
||||
@Column(name = "remark", length = 256)
|
||||
val remark: String? = null
|
||||
) : Serializable
|
@ -1,130 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateData
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.hibernate.envers.Audited
|
||||
import java.io.Serializable
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "cert_signing_request", indexes = arrayOf(Index(name = "IDX__CSR__PKH", columnList = "public_key_hash")))
|
||||
data class CertificateSigningRequestEntity(
|
||||
@Id
|
||||
@Column(name = "request_id", length = 64, nullable = false)
|
||||
val requestId: String,
|
||||
|
||||
// TODO: Store X500Name with a proper schema.
|
||||
@Column(name = "legal_name", length = 256)
|
||||
@Convert(converter = CordaX500NameAttributeConverter::class)
|
||||
val legalName: CordaX500Name?,
|
||||
|
||||
@Column(name = "public_key_hash", length = 64, nullable = false)
|
||||
@Convert(converter = SecureHashAttributeConverter::class)
|
||||
val publicKeyHash: SecureHash,
|
||||
|
||||
// Setting [columnDefinition] is a work around for a hibernate problem when using SQL database.
|
||||
// TODO: Remove this when we find out the cause of the problem.
|
||||
@Audited
|
||||
@Column(name = "status", length = 16, nullable = false, columnDefinition = "NVARCHAR(16)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val status: RequestStatus = RequestStatus.NEW,
|
||||
|
||||
@Audited
|
||||
@Column(name = "modified_by", length = 64, nullable = false)
|
||||
val modifiedBy: String,
|
||||
|
||||
// TODO: Use audit framework instead.
|
||||
@Column(name = "modified_at", nullable = false)
|
||||
val modifiedAt: Instant = Instant.now(),
|
||||
|
||||
@Audited
|
||||
@Column(name = "remark", length = 256)
|
||||
val remark: String? = null,
|
||||
|
||||
@OneToOne(fetch = FetchType.EAGER, mappedBy = "certificateSigningRequest")
|
||||
val certificateData: CertificateDataEntity? = null,
|
||||
|
||||
@Lob
|
||||
@Column(name = "request_bytes", nullable = false)
|
||||
@Convert(converter = PKCS10CertificationRequestConverter::class)
|
||||
val request: PKCS10CertificationRequest,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "private_network", foreignKey = ForeignKey(name = "FK__CSR__PN"))
|
||||
val privateNetwork: PrivateNetworkEntity? = null
|
||||
) : Serializable {
|
||||
fun toCertificateSigningRequest(): CertificateSigningRequest {
|
||||
return CertificateSigningRequest(
|
||||
requestId = requestId,
|
||||
legalName = legalName,
|
||||
publicKeyHash = publicKeyHash,
|
||||
status = status,
|
||||
request = request,
|
||||
remark = remark,
|
||||
modifiedBy = modifiedBy,
|
||||
certData = certificateData?.toCertificateData()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "cert_data")
|
||||
data class CertificateDataEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
val id: Long? = null,
|
||||
|
||||
// Setting [columnDefinition] is a work around for a hibernate problem when using SQL database.
|
||||
// TODO: Remove this when we find out the cause of the problem.
|
||||
@Column(name = "cert_status", length = 16, nullable = false, columnDefinition = "NVARCHAR(16)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val certificateStatus: CertificateStatus,
|
||||
|
||||
@Lob
|
||||
@Column(name = "cert_path_bytes", nullable = false)
|
||||
@Convert(converter = CertPathConverter::class)
|
||||
val certPath: CertPath,
|
||||
|
||||
@OneToOne(fetch = FetchType.EAGER, optional = false)
|
||||
@JoinColumn(name = "cert_signing_request", foreignKey = ForeignKey(name = "FK__CD__CSR"), nullable = false)
|
||||
val certificateSigningRequest: CertificateSigningRequestEntity,
|
||||
|
||||
@Column(name = "cert_serial_number", unique = true, nullable = false, columnDefinition = "NUMERIC(28)")
|
||||
val certificateSerialNumber: BigInteger
|
||||
) : Serializable {
|
||||
fun toCertificateData(): CertificateData = CertificateData(certificateStatus, certPath)
|
||||
|
||||
val legalName: CordaX500Name get() {
|
||||
return CordaX500Name.build(certPath.x509Certificates[0].subjectX500Principal)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "private_network")
|
||||
data class PrivateNetworkEntity(
|
||||
@Id
|
||||
@Column(name = "id", length = 64)
|
||||
val networkId: String,
|
||||
|
||||
@Column(name = "name", length = 255, nullable = false)
|
||||
val networkName: String
|
||||
) : Serializable
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkMap
|
||||
import org.hibernate.envers.Audited
|
||||
import org.hibernate.envers.RelationTargetAuditMode
|
||||
import java.io.Serializable
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Audited
|
||||
@Table(name = "network_map")
|
||||
class NetworkMapEntity(
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
val id: String,
|
||||
|
||||
@Lob
|
||||
@Column(name = "serialized_network_map", nullable = false)
|
||||
@Convert(converter = NetworkMapConverter::class)
|
||||
val networkMap: NetworkMap,
|
||||
|
||||
@Lob
|
||||
@Column(name = "signature", nullable = false)
|
||||
val signature: ByteArray,
|
||||
|
||||
@Lob
|
||||
@Column(name = "cert", nullable = false)
|
||||
@Convert(converter = X509CertificateConverter::class)
|
||||
val certificate: X509Certificate,
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "network_parameters", nullable = false)
|
||||
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
|
||||
val networkParameters: NetworkParametersEntity,
|
||||
|
||||
@Column(nullable = false)
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : Serializable {
|
||||
fun toSignedNetworkMap(): SignedNetworkMap {
|
||||
return SignedNetworkMap(networkMap.serialize(), DigitalSignatureWithCert(certificate, signature))
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
import java.io.Serializable
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "network_parameters", indexes = arrayOf(Index(name = "IDX_NP_HASH", columnList = "hash")))
|
||||
class NetworkParametersEntity(
|
||||
@Id
|
||||
@Column(name = "hash", length = 64, nullable = false)
|
||||
// AttributeConverters can't be used on @Id attributes, otherwise this would be SecureHash
|
||||
val hash: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val created: Instant = Instant.now(),
|
||||
|
||||
@Lob
|
||||
@Column(name = "parameters_bytes", nullable = false)
|
||||
@Convert(converter = NetworkParametersConverter::class)
|
||||
val networkParameters: NetworkParameters,
|
||||
|
||||
// Both of the fields below are nullable, because of the way we sign network map data. NetworkParameters can be
|
||||
// inserted into database without signature. Then signing service will sign them.
|
||||
@Lob
|
||||
@Column(name = "signature")
|
||||
val signature: ByteArray?,
|
||||
|
||||
@Lob
|
||||
@Column(name = "cert")
|
||||
@Convert(converter = X509CertificateConverter::class)
|
||||
val certificate: X509Certificate?
|
||||
) : Serializable {
|
||||
val isSigned: Boolean get() = certificate != null && signature != null
|
||||
|
||||
fun toSignedNetworkParameters(): SignedNetworkParameters {
|
||||
val cert = certificate ?: throw IllegalStateException("Network parameters entity is not signed: $hash")
|
||||
val sign = signature ?: throw IllegalStateException("Network parameters entity is not signed: $hash")
|
||||
return SignedNetworkParameters(networkParameters.serialize(), DigitalSignatureWithCert(cert, sign))
|
||||
}
|
||||
|
||||
fun copy(parametersHash: String = this.hash,
|
||||
created: Instant = this.created,
|
||||
networkParameters: NetworkParameters = this.networkParameters,
|
||||
signature: ByteArray? = this.signature,
|
||||
certificate: X509Certificate? = this.certificate
|
||||
): NetworkParametersEntity {
|
||||
return NetworkParametersEntity(
|
||||
hash = parametersHash,
|
||||
created = created,
|
||||
networkParameters = networkParameters,
|
||||
signature = signature,
|
||||
certificate = certificate
|
||||
)
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import org.hibernate.annotations.UpdateTimestamp
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_info")
|
||||
data class NodeInfoEntity(
|
||||
// Hash of serialized [NodeInfo] without signatures.
|
||||
@Id
|
||||
@Column(name = "node_info_hash", length = 64)
|
||||
// AttributeConverters can't be used on @Id attributes, otherwise this would be SecureHash
|
||||
val nodeInfoHash: String,
|
||||
|
||||
@Column(name = "public_key_hash", length = 64)
|
||||
@Convert(converter = SecureHashAttributeConverter::class)
|
||||
val publicKeyHash: SecureHash,
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cert_signing_request", nullable = false)
|
||||
val certificateSigningRequest: CertificateSigningRequestEntity,
|
||||
|
||||
@Lob
|
||||
@Column(name = "signed_node_info_bytes", nullable = false)
|
||||
@Convert(converter = SignedNodeInfoConverter::class)
|
||||
val signedNodeInfo: SignedNodeInfo,
|
||||
|
||||
@Column(name = "is_current", nullable = false)
|
||||
val isCurrent: Boolean,
|
||||
|
||||
@Column(name = "published_at", nullable = false)
|
||||
@UpdateTimestamp
|
||||
val publishedAt: Instant = Instant.now(),
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "accepted_params_update")
|
||||
val acceptedParametersUpdate: ParametersUpdateEntity?
|
||||
) : Serializable
|
@ -1,49 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.persistence.entity
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.nodeapi.internal.network.ParametersUpdate
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "parameters_update")
|
||||
data class ParametersUpdateEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
val id: Long? = null,
|
||||
|
||||
// TODO use @MapsId to get rid of additional sequence
|
||||
@ManyToOne(fetch = FetchType.EAGER, optional = false)
|
||||
@JoinColumn(name = "network_parameters", foreignKey = ForeignKey(name = "FK__param_up__net_param"))
|
||||
val networkParameters: NetworkParametersEntity,
|
||||
|
||||
@Column(name = "description", nullable = false)
|
||||
val description: String,
|
||||
|
||||
@Column(name = "update_deadline", nullable = false)
|
||||
val updateDeadline: Instant,
|
||||
|
||||
@Column(name = "status", length = 16, nullable = false, columnDefinition = "NVARCHAR(16)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
val status: UpdateStatus = UpdateStatus.NEW
|
||||
) : Serializable {
|
||||
fun toParametersUpdate(): ParametersUpdate {
|
||||
return ParametersUpdate(SecureHash.parse(networkParameters.hash), description, updateDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
enum class UpdateStatus {
|
||||
/** A newly created update. */
|
||||
NEW,
|
||||
/**
|
||||
* An update that has passed its deadline and flagged to be made active on the next signing event. At most only one
|
||||
* update with status either NEW or FLAG_DAY can exist.
|
||||
*/
|
||||
FLAG_DAY,
|
||||
/** Any previously flagged update that has been activated into the network map. */
|
||||
APPLIED,
|
||||
/** A new or flag day update that has been cancelled. */
|
||||
CANCELLED
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.common.utils.Revocation
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
import java.net.URL
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class CertificateRevocationListSigner(
|
||||
private val revocationListStorage: CertificateRevocationListStorage,
|
||||
private val issuerCertificate: X509Certificate,
|
||||
private val updateInterval: Duration,
|
||||
private val endpoint: URL,
|
||||
private val signer: Signer) {
|
||||
private companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds, signs and persists a new certificate revocation list.
|
||||
* It happens only if there are new entries to be added to the current list.
|
||||
*
|
||||
* @param newCRRs list of approved certificate revocation requests. An approved certificate revocation request
|
||||
* is a request that has status [RequestStatus.APPROVED] - i.e. it has not yet been included in any CRL.
|
||||
* @param existingCRRs list of revoked certificate revocation requests. A revoked certificate revocation request
|
||||
* is a request that has status [RequestStatus.DONE] - i.e. it has already been included in another CRL.
|
||||
* @param signedBy who signs this CRL.
|
||||
*
|
||||
* @return A new signed CRL containing both revoked and approved certificate revocation requests.
|
||||
*/
|
||||
fun createSignedCRL(newCRRs: List<CertificateRevocationRequestData>,
|
||||
existingCRRs: List<CertificateRevocationRequestData>,
|
||||
signedBy: String): X509CRL {
|
||||
require(existingCRRs.all { it.status == RequestStatus.DONE }) { "All existing CRRs need to be in the ${RequestStatus.DONE} state" }
|
||||
require(newCRRs.all { it.status == RequestStatus.APPROVED }) { "All newly included CRRs need to be in the ${RequestStatus.APPROVED} state" }
|
||||
logger.info("Signing a new Certificate Revocation List...")
|
||||
logger.debug("Retrieving approved Certificate Revocation Requests...")
|
||||
val revocationTime = Instant.now()
|
||||
val approvedWithTimestamp = newCRRs.map { it.copy(modifiedAt = revocationTime) }
|
||||
logger.trace { "Approved Certificate Revocation Requests to be included in the new Certificate Revocation List: $approvedWithTimestamp" }
|
||||
logger.debug("Retrieving revoked Certificate Revocation Requests...")
|
||||
logger.trace { "Revoked Certificate Revocation Requests to be included in the new Certificate Revocation List: $existingCRRs" }
|
||||
val revocations = (existingCRRs + approvedWithTimestamp).map {
|
||||
Revocation(it.certificateSerialNumber, Date(it.modifiedAt.toEpochMilli()), it.reason)
|
||||
}
|
||||
val crl = createSignedCrl(issuerCertificate, endpoint, updateInterval, signer, revocations)
|
||||
logger.debug { "Created a new Certificate Revocation List $crl" }
|
||||
revocationListStorage.saveCertificateRevocationList(crl, CrlIssuer.DOORMAN, signedBy, revocationTime)
|
||||
logger.info("A new Certificate Revocation List has been persisted.")
|
||||
return crl
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus.FLAG_DAY
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus.NEW
|
||||
import com.r3.corda.networkmanage.common.utils.join
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkMapAndSigned
|
||||
import net.corda.nodeapi.internal.network.ParametersUpdate
|
||||
|
||||
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
|
||||
private companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the network map and latest network parameters if they haven't been signed yet.
|
||||
*/
|
||||
fun signNetworkMaps() {
|
||||
val (publicNetworkMap, privateNetworkMaps) = networkMapStorage.getNetworkMaps()
|
||||
val (publicNodeInfoHashes, privateNodeInfoHashes) = networkMapStorage.getNodeInfoHashes()
|
||||
|
||||
val (networkParameterHash, parametersUpdate) = maybeUpdateNetworkParameters(publicNetworkMap?.networkMap?.networkParameterHash)
|
||||
logger.debug { "Current network parameters: $networkParameterHash" }
|
||||
|
||||
// Process public network map.
|
||||
maybeSignNetworkMap(publicNetworkMap, publicNodeInfoHashes, parametersUpdate, networkParameterHash)
|
||||
|
||||
// Process each private network map.
|
||||
privateNetworkMaps.join(privateNodeInfoHashes).forEach { networkId, (currentNetworkMap, nodeInfoHashes) ->
|
||||
maybeSignNetworkMap(currentNetworkMap, nodeInfoHashes, parametersUpdate, networkParameterHash, networkId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeSignNetworkMap(currentNetworkMap: NetworkMapEntity?, nodeInfoHashes: List<SecureHash>?, parametersUpdate: ParametersUpdate?, networkParameterHash: SecureHash, networkId: String? = null) {
|
||||
val printableNetworkId = networkId ?: "Public Network"
|
||||
if (currentNetworkMap == null) {
|
||||
logger.info("There is currently no network map for network '$printableNetworkId'")
|
||||
} else {
|
||||
logger.debug { "Current network map for network '$printableNetworkId': ${currentNetworkMap.networkMap}" }
|
||||
}
|
||||
|
||||
logger.debug { "Retrieved node info hashes for network '$printableNetworkId' :\n${nodeInfoHashes?.joinToString("\n")}" }
|
||||
|
||||
val newNetworkMap = NetworkMap(nodeInfoHashes ?: emptyList(), networkParameterHash, parametersUpdate)
|
||||
logger.debug { "Potential new network map for network '$printableNetworkId': $newNetworkMap" }
|
||||
|
||||
if (currentNetworkMap?.networkMap != newNetworkMap) {
|
||||
val newNetworkMapAndSigned = NetworkMapAndSigned(newNetworkMap) { signer.signBytes(it.bytes) }
|
||||
networkMapStorage.saveNewNetworkMap(networkId, newNetworkMapAndSigned)
|
||||
logger.info("Signed new network map for network '$printableNetworkId' : $newNetworkMap")
|
||||
} else {
|
||||
logger.debug("Current network map for network '$printableNetworkId' is up-to-date")
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeUpdateNetworkParameters(currentNetworkParametersHash: SecureHash?): Pair<SecureHash, ParametersUpdate?> {
|
||||
val latestNetworkParameters = requireNotNull(networkMapStorage.getLatestNetworkParameters()) { "No network parameters present" }
|
||||
logger.debug { "Retrieved latest network parameters: ${latestNetworkParameters.networkParameters}" }
|
||||
|
||||
val parametersUpdate = networkMapStorage.getCurrentParametersUpdate()
|
||||
logger.debug { "Retrieved parameters update: $parametersUpdate" }
|
||||
check(parametersUpdate == null || parametersUpdate.networkParameters.hash == latestNetworkParameters.hash) {
|
||||
"The latest network parameters are not the scheduled updated ones"
|
||||
}
|
||||
|
||||
// We persist signed parameters only if they were not persisted before (they are not in currentSignedNetworkMap as
|
||||
// normal parameters or as an update)
|
||||
if (!latestNetworkParameters.isSigned) {
|
||||
signAndPersistNetworkParameters(latestNetworkParameters.networkParameters)
|
||||
} else {
|
||||
logger.debug { "No need to sign any network parameters as they're up-to-date" }
|
||||
}
|
||||
|
||||
val parametersToNetworkMap = if (parametersUpdate?.status == FLAG_DAY || currentNetworkParametersHash == null) {
|
||||
parametersUpdate?.let { networkMapStorage.switchFlagDay(it) }
|
||||
SecureHash.parse(latestNetworkParameters.hash)
|
||||
} else {
|
||||
currentNetworkParametersHash
|
||||
}
|
||||
|
||||
return Pair(parametersToNetworkMap, parametersUpdate?.let { if (it.status == NEW) it.toParametersUpdate() else null })
|
||||
}
|
||||
|
||||
fun signAndPersistNetworkParameters(networkParameters: NetworkParameters) {
|
||||
networkMapStorage.saveNetworkParameters(networkParameters, signer.signObject(networkParameters).sig)
|
||||
logger.info("Signed network parameters: $networkParameters")
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.signer
|
||||
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.serialization.serialize
|
||||
|
||||
/**
|
||||
* An interface for arbitrary data signing functionality.
|
||||
*/
|
||||
interface Signer {
|
||||
/**
|
||||
* Signs given bytes. The signing key selection strategy is left to the implementing class.
|
||||
* @return [DigitalSignatureWithCert] that encapsulates the signature and the certificate path used in the signing process.
|
||||
* @throws [AuthenticationException] if fails authentication
|
||||
*/
|
||||
fun signBytes(data: ByteArray): DigitalSignatureWithCert
|
||||
|
||||
fun <T : Any> signObject(obj: T): SignedDataWithCert<T> {
|
||||
val serialised = obj.serialize()
|
||||
return SignedDataWithCert(serialised, signBytes(serialised.bytes))
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationException : Exception()
|
@ -1,10 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.sockets
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.cert.X509CRL
|
||||
import java.time.Instant
|
||||
|
||||
@CordaSerializable
|
||||
data class CertificateRevocationListSubmission(val list: X509CRL,
|
||||
val signer: String,
|
||||
val revocationTime: Instant)
|
@ -1,28 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.sockets
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.cert.X509CRL
|
||||
|
||||
@CordaSerializable
|
||||
interface CrrSocketMessage
|
||||
|
||||
/**
|
||||
* CRL retrieval message type
|
||||
*/
|
||||
class CrlRetrievalMessage : CrrSocketMessage
|
||||
|
||||
/**
|
||||
* CRL response message type
|
||||
*/
|
||||
data class CrlResponseMessage(val crl: X509CRL?) : CrrSocketMessage
|
||||
|
||||
/**
|
||||
* CRL submission message type
|
||||
*/
|
||||
class CrlSubmissionMessage : CrrSocketMessage
|
||||
|
||||
/**
|
||||
* By status CRRs retrieval message type
|
||||
*/
|
||||
data class CrrsByStatusMessage(val status: RequestStatus) : CrrSocketMessage
|
@ -1,29 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.serialization.internal.CordaSerializationMagic
|
||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.amqpMagic
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class AMQPNetworkServicesSerializationScheme (
|
||||
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
|
||||
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>
|
||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
|
||||
|
||||
constructor() : this(emptySet(), ConcurrentHashMap())
|
||||
|
||||
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) =
|
||||
(magic == amqpMagic && target == SerializationContext.UseCase.P2P)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import joptsimple.OptionException
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.OptionSet
|
||||
import java.io.PrintStream
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// TODO: This class could be useful for the rest of the codebase.
|
||||
abstract class ArgsParser<out T : Any> {
|
||||
protected val optionParser = OptionParser()
|
||||
private val helpOption = optionParser.acceptsAll(listOf("h", "help"), "show help").forHelp()
|
||||
|
||||
/**
|
||||
If [printHelpOn] output stream is not null, this method will print help message and exit process
|
||||
when encountered any error during args parsing, or when help flag is set.
|
||||
*/
|
||||
fun parseOrExit(vararg args: String, printHelpOn: PrintStream? = System.out): T {
|
||||
val optionSet = try {
|
||||
optionParser.parse(*args)
|
||||
} catch (e: OptionException) {
|
||||
printHelpOn?.let {
|
||||
it.println(e.message ?: "Unable to parse arguments.")
|
||||
optionParser.printHelpOn(it)
|
||||
exitProcess(1)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
if (optionSet.has(helpOption)) {
|
||||
printHelpOn?.let(optionParser::printHelpOn)
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
return try {
|
||||
parse(optionSet)
|
||||
} catch (e: RuntimeException) {
|
||||
// We handle errors from the parsing of the command line arguments as a runtime
|
||||
// exception because the joptsimple library is overly helpful and doesn't expose
|
||||
// parsing / conversion exceptions in a way that makes reporting the message out
|
||||
// to the user possible. Thus, that library is re-throwing those exceptions as simple
|
||||
// runtime exceptions with a modified cause to preserve the error location
|
||||
printHelpOn?.let {
|
||||
it.println("ERROR: ${e.message ?: "Unable to parse arguments."}")
|
||||
exitProcess(2)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun parse(optionSet: OptionSet): T
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import com.r3.corda.networkmanage.common.signer.Signer
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.cert.X509v2CRLBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigInteger
|
||||
import java.net.URL
|
||||
import java.security.cert.CRLReason
|
||||
import java.security.cert.X509CRL
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
fun createSignedCrl(issuerCertificate: X509Certificate,
|
||||
endpointUrl: URL,
|
||||
nextUpdateInterval: Duration,
|
||||
signer: Signer,
|
||||
includeInCrl: List<Revocation>,
|
||||
indirectIssuingPoint: Boolean = false): X509CRL {
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.subjectX500Principal.encoded), Date())
|
||||
builder.addExtension(Extension.authorityKeyIdentifier, false, extensionUtils.createAuthorityKeyIdentifier(issuerCertificate))
|
||||
val issuingDistributionPointName = GeneralName(GeneralName.uniformResourceIdentifier, endpointUrl.toString())
|
||||
val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), indirectIssuingPoint, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistributionPoint)
|
||||
builder.setNextUpdate(Date(Instant.now().toEpochMilli() + nextUpdateInterval.toMillis()))
|
||||
includeInCrl.forEach {
|
||||
builder.addCRLEntry(it.certificateSerialNumber, it.date, it.reason.ordinal)
|
||||
}
|
||||
val crlHolder = builder.build(CrlContentSigner(signer))
|
||||
return JcaX509CRLConverter().setProvider(BouncyCastleProvider()).getCRL(crlHolder)
|
||||
}
|
||||
|
||||
data class Revocation(val certificateSerialNumber: BigInteger, val date: Date, val reason: CRLReason)
|
||||
|
||||
private class CrlContentSigner(private val signer: Signer) : ContentSigner {
|
||||
|
||||
private val outputStream = ByteArrayOutputStream()
|
||||
|
||||
override fun getAlgorithmIdentifier(): AlgorithmIdentifier = X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME.signatureOID
|
||||
override fun getOutputStream(): OutputStream = outputStream
|
||||
override fun getSignature(): ByteArray = signer.signBytes(outputStream.toByteArray()).bytes
|
||||
}
|
||||
|
||||
enum class SupportedCrlReasons {
|
||||
UNSPECIFIED,
|
||||
KEY_COMPROMISE,
|
||||
CA_COMPROMISE,
|
||||
AFFILIATION_CHANGED,
|
||||
SUPERSEDED,
|
||||
CESSATION_OF_OPERATION,
|
||||
PRIVILEGE_WITHDRAWN
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
inline fun <reified T : Any> InputStream.readObject(): T {
|
||||
DataInputStream(this).run {
|
||||
val messageSize = this.readInt()
|
||||
val messageBytes = ByteArray(messageSize)
|
||||
this.read(messageBytes)
|
||||
return messageBytes.deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
fun OutputStream.writeObject(message: Any) {
|
||||
DataOutputStream(this).run {
|
||||
val messageBytes = message.serialize().bytes
|
||||
this.writeInt(messageBytes.size)
|
||||
this.write(messageBytes)
|
||||
this.flush()
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.CordaOID
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
|
||||
import net.corda.serialization.internal.SerializationFactoryImpl
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
const val CORDA_NETWORK_MAP = "cordanetworkmap"
|
||||
const val NETWORK_ROOT_TRUSTSTORE_FILENAME = "network-root-truststore.jks"
|
||||
|
||||
val logger: Logger = LoggerFactory.getLogger("com.r3.corda.networkmanage.common.utils")
|
||||
|
||||
data class CertPathAndKey(val certPath: List<X509Certificate>, val key: PrivateKey) {
|
||||
fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> parseConfig(file: Path): T {
|
||||
val config = ConfigFactory.parseFile(file.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)).resolve()
|
||||
logger.info(config.root().render(ConfigRenderOptions.defaults()))
|
||||
return config.parseAs(UnknownConfigKeysPolicy.IGNORE::handle)
|
||||
}
|
||||
|
||||
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
|
||||
|
||||
fun X509KeyStore.getCertPathAndKey(alias: String, privateKeyPassword: String): CertPathAndKey {
|
||||
return CertPathAndKey(getCertificateChain(alias), getPrivateKey(alias, privateKeyPassword))
|
||||
}
|
||||
|
||||
fun initialiseSerialization() {
|
||||
val context = AMQP_P2P_CONTEXT
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPNetworkServicesSerializationScheme())
|
||||
},
|
||||
context)
|
||||
}
|
||||
|
||||
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 = firstAttributeValue(BCStyle.E).toString()
|
||||
|
||||
fun <K, V, U> Map<K, V>.join(otherMap: Map<K, U>): Map<K, Pair<V?, U?>> = (keys + otherMap.keys).map { it to Pair(get(it), otherMap[it]) }.toMap()
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.dev
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.nodeapi.internal.*
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Holds configuration necessary for generating DEV key store and trust store.
|
||||
*/
|
||||
data class GeneratorConfiguration(val privateKeyPass: String = DEV_CA_PRIVATE_KEY_PASS,
|
||||
val keyStorePass: String = DEV_CA_KEY_STORE_PASS,
|
||||
val keyStoreFileName: String = DEV_CA_KEY_STORE_FILE,
|
||||
val trustStorePass: String = DEV_CA_TRUST_STORE_PASS,
|
||||
val trustStoreFileName: String = DEV_CA_TRUST_STORE_FILE,
|
||||
val directory: Path = DEFAULT_DIRECTORY) {
|
||||
companion object {
|
||||
val DEFAULT_DIRECTORY = File("./certificates").toPath()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a configuration file, which contains all the configuration - i.e. for the key store generator.
|
||||
*/
|
||||
fun parseParameters(configFile: Path?): GeneratorConfiguration {
|
||||
return if (configFile == null) {
|
||||
GeneratorConfiguration()
|
||||
} else {
|
||||
ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))
|
||||
.resolve()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.dev
|
||||
|
||||
import com.r3.corda.networkmanage.common.configuration.ConfigFilePathArgsParser
|
||||
import com.r3.corda.networkmanage.doorman.CORDA_X500_BASE
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import java.io.File
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private val logger = LogManager.getLogger("com.r3.corda.networkmanage.dev.Main")
|
||||
|
||||
/**
|
||||
* This is an internal utility method used to generate a DEV certificate store file containing both root and doorman keys/certificates.
|
||||
* Additionally, it generates a trust file containing the root certificate.
|
||||
*
|
||||
* Note: It can be quickly executed in IntelliJ by right-click on the main method. It will generate the keystore and the trustore
|
||||
* with settings expected by node running in the dev mode. The files will be generated in the root project directory.
|
||||
* Look for the 'certificates' directory.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
run(parseParameters(ConfigFilePathArgsParser().parseOrExit(*args)))
|
||||
}
|
||||
|
||||
fun run(configuration: GeneratorConfiguration) {
|
||||
configuration.run {
|
||||
val keyStoreFile = File("$directory/$keyStoreFileName").toPath()
|
||||
keyStoreFile.parent.createDirectories()
|
||||
val keyStore = X509KeyStore.fromFile(keyStoreFile, keyStorePass, createNew = true)
|
||||
|
||||
checkCertificateNotInKeyStore(X509Utilities.CORDA_ROOT_CA, keyStore) { exitProcess(1) }
|
||||
checkCertificateNotInKeyStore(X509Utilities.CORDA_INTERMEDIATE_CA, keyStore) { exitProcess(1) }
|
||||
|
||||
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCert = X509Utilities.createSelfSignedCACertificate(
|
||||
X500Principal("CN=Corda Root CA,$CORDA_X500_BASE"),
|
||||
rootKeyPair)
|
||||
keyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_ROOT_CA, rootKeyPair.private, listOf(rootCert), privateKeyPass)
|
||||
}
|
||||
logger.info("Root CA keypair and certificate stored in ${keyStoreFile.toAbsolutePath()}.")
|
||||
logger.info(rootCert)
|
||||
|
||||
val trustStorePath = directory / trustStoreFileName
|
||||
X509KeyStore.fromFile(trustStorePath, trustStorePass, createNew = true).update {
|
||||
setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
|
||||
}
|
||||
logger.info("Trust store for distribution to nodes created in $trustStorePath")
|
||||
|
||||
val doormanKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val cert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
rootCert,
|
||||
rootKeyPair,
|
||||
X500Principal("CN=Corda Doorman CA,$CORDA_X500_BASE"),
|
||||
doormanKeyPair.public
|
||||
)
|
||||
keyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_INTERMEDIATE_CA, doormanKeyPair.private, listOf(cert, rootCert), privateKeyPass)
|
||||
}
|
||||
logger.info("Doorman CA keypair and certificate stored in ${keyStoreFile.toAbsolutePath()}.")
|
||||
logger.info(cert)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkCertificateNotInKeyStore(certAlias: String, keyStore: X509KeyStore, onFail: () -> Unit) {
|
||||
if (certAlias in keyStore) {
|
||||
logger.info("$certAlias already exists in keystore, process will now terminate.")
|
||||
logger.info(keyStore.getCertificate(certAlias))
|
||||
onFail.invoke()
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.api.JiraRestClient
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
|
||||
class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
|
||||
fun createCertificateRevocationRequestTicket(revocationRequest: CertificateRevocationRequestData) {
|
||||
val ticketDescription = "Legal name: ${revocationRequest.legalName}\n" +
|
||||
"Certificate serial number: ${revocationRequest.certificateSerialNumber}\n" +
|
||||
"Revocation reason: ${revocationRequest.reason.name}\n" +
|
||||
"Reporter: ${revocationRequest.reporter}\n" +
|
||||
"Original CSR request ID: ${revocationRequest.certificateSigningRequestId}"
|
||||
|
||||
val subject = CordaX500Name.build(revocationRequest.certificate.subjectX500Principal)
|
||||
val ticketSummary = if (subject.organisationUnit != null) {
|
||||
"${subject.organisationUnit}, ${subject.organisation}"
|
||||
} else {
|
||||
subject.organisation
|
||||
}
|
||||
|
||||
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
|
||||
.setProjectKey(projectCode)
|
||||
.setDescription(ticketDescription)
|
||||
.setSummary(ticketSummary)
|
||||
.setFieldValue(requestIdField.id, revocationRequest.requestId)
|
||||
|
||||
val attachment = Pair("${revocationRequest.certificateSerialNumber}.cer", revocationRequest.certificate.encoded.inputStream())
|
||||
createJiraTicket(revocationRequest.requestId, issue.build(), attachment)
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.api.JiraRestClient
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.r3.corda.networkmanage.common.utils.getCertRole
|
||||
import com.r3.corda.networkmanage.common.utils.getEmail
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import java.io.StringWriter
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
class CsrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
|
||||
fun createCertificateSigningRequestTicket(requestData: CertificationRequestData) {
|
||||
val (requestId, signingRequest) = requestData
|
||||
|
||||
// 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 ticketSummary = if (subject.organisationUnit != null) {
|
||||
"${subject.organisationUnit}, ${subject.organisation}"
|
||||
} else {
|
||||
subject.organisation
|
||||
}
|
||||
|
||||
val data = mapOf(
|
||||
"Requested Role Type" to signingRequest.getCertRole().name,
|
||||
"Common Name" to subject.commonName,
|
||||
"Organisation" to subject.organisation,
|
||||
"Organisation Unit" to subject.organisationUnit,
|
||||
"State" to subject.state,
|
||||
"Nearest City" to subject.locality,
|
||||
"Country" to subject.country,
|
||||
"Email" to signingRequest.getEmail(),
|
||||
"X500 Name" to subject.toString())
|
||||
|
||||
val requestPemString = StringWriter().apply {
|
||||
JcaPEMWriter(this).use {
|
||||
it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded))
|
||||
}
|
||||
}.toString()
|
||||
|
||||
val ticketDescription = data.filter { it.value != null }.map { "${it.key}: ${it.value}" }.joinToString("\n") + "\n\n{code}$requestPemString{code}"
|
||||
|
||||
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
|
||||
.setProjectKey(projectCode)
|
||||
.setDescription(ticketDescription)
|
||||
.setSummary(ticketSummary)
|
||||
.setFieldValue(requestIdField.id, requestId)
|
||||
|
||||
createJiraTicket(requestId, issue.build())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Parse PKCS10 request.
|
||||
data class CertificationRequestData(val requestId: String, val rawRequest: PKCS10CertificationRequest)
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.google.common.primitives.Booleans
|
||||
import com.r3.corda.networkmanage.common.utils.ArgsParser
|
||||
import joptsimple.OptionException
|
||||
import joptsimple.OptionSet
|
||||
import joptsimple.util.EnumConverter
|
||||
import joptsimple.util.PathConverter
|
||||
import joptsimple.util.PathProperties
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.utilities.days
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
|
||||
class DoormanArgsParser : ArgsParser<DoormanCmdLineOptions>() {
|
||||
private val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
.required()
|
||||
private val modeArg = optionParser
|
||||
.accepts("mode", "Set the mode of this application")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(object : EnumConverter<Mode>(Mode::class.java) {})
|
||||
.defaultsTo(Mode.DOORMAN)
|
||||
private val setNetworkParametersArg = optionParser
|
||||
.accepts("set-network-parameters", "Set the network parameters using the given file. This can be for either the initial set or to schedule an update.")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
|
||||
private val flagDayArg = optionParser.accepts("flag-day", "Roll over the scheduled network parameters to be the current.")
|
||||
private val cancelUpdateArg = optionParser.accepts("cancel-update", "Cancel the scheduled update of the network parameters.")
|
||||
private val trustStorePasswordArg = optionParser
|
||||
.accepts("trust-store-password", "Password for the generated network root trust store. Only applicable when operating in ${Mode.ROOT_KEYGEN} mode.")
|
||||
.withRequiredArg()
|
||||
|
||||
override fun parse(optionSet: OptionSet): DoormanCmdLineOptions {
|
||||
val configFile = try {
|
||||
optionSet.valueOf(configFileArg)
|
||||
} catch (e: OptionException) {
|
||||
throw RuntimeException("Specified config file doesn't exist").apply {
|
||||
initCause(e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
val mode = try {
|
||||
optionSet.valueOf(modeArg)
|
||||
} catch (e: OptionException) {
|
||||
throw RuntimeException(
|
||||
"Unknown mode specified, valid options are [${Mode.values().joinToString(", ")}]"
|
||||
).apply {
|
||||
initCause(e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
val setNetworkParametersFile = optionSet.valueOf(setNetworkParametersArg)
|
||||
val flagDay = optionSet.has(flagDayArg)
|
||||
val cancelUpdate = optionSet.has(cancelUpdateArg)
|
||||
require(Booleans.countTrue(setNetworkParametersFile != null, flagDay, cancelUpdate) <= 1) {
|
||||
"Only one of $setNetworkParametersArg, $flagDay and $cancelUpdate can be specified"
|
||||
}
|
||||
val trustStorePassword = optionSet.valueOf(trustStorePasswordArg)
|
||||
return DoormanCmdLineOptions(configFile, mode, trustStorePassword, setNetworkParametersFile, flagDay, cancelUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
data class DoormanCmdLineOptions(val configFile: Path,
|
||||
val mode: Mode,
|
||||
val trustStorePassword: String?,
|
||||
val setNetworkParametersFile: Path?,
|
||||
val flagDay: Boolean,
|
||||
val cancelUpdate: Boolean
|
||||
) {
|
||||
init {
|
||||
// Make sure trust store password is only specified in root keygen mode.
|
||||
if (mode != Mode.ROOT_KEYGEN) {
|
||||
require(trustStorePassword == null) { "Trust store password should not be specified in this mode." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NetworkParametersCmd {
|
||||
/**
|
||||
* This is the same as [NetworkParametersConfig] but instead of using [NotaryConfig] it uses the simpler to test with
|
||||
* [NotaryInfo].
|
||||
*/
|
||||
data class Set(val minimumPlatformVersion: Int,
|
||||
val notaries: List<NotaryInfo>,
|
||||
val maxMessageSize: Int,
|
||||
val maxTransactionSize: Int,
|
||||
val eventHorizonDays: Int,
|
||||
val parametersUpdate: ParametersUpdateConfig?
|
||||
) : NetworkParametersCmd() {
|
||||
companion object {
|
||||
fun fromConfig(config: NetworkParametersConfig): Set {
|
||||
return Set(
|
||||
config.minimumPlatformVersion,
|
||||
config.notaries.map { it.toNotaryInfo() },
|
||||
config.maxMessageSize,
|
||||
config.maxTransactionSize,
|
||||
config.eventHorizonDays,
|
||||
config.parametersUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkCompatibility(currentNetParams: NetworkParameters) {
|
||||
// TODO Comment it out when maxMessageSize is properly wired
|
||||
// require(previousParameters.maxMessageSize <= newParameters.maxMessageSize) { "maxMessageSize can only increase" }
|
||||
require(maxTransactionSize >= currentNetParams.maxTransactionSize) { "maxTransactionSize can only increase" }
|
||||
val removedNames = currentNetParams.notaries.map { it.identity.name } - notaries.map { it.identity.name }
|
||||
require(removedNames.isEmpty()) { "notaries cannot be removed: $removedNames" }
|
||||
val removedKeys = currentNetParams.notaries.map { it.identity.owningKey } - notaries.map { it.identity.owningKey }
|
||||
require(removedKeys.isEmpty()) { "notaries cannot be removed: $removedKeys" }
|
||||
}
|
||||
|
||||
fun toNetworkParameters(modifiedTime: Instant, epoch: Int): NetworkParameters {
|
||||
return NetworkParameters(
|
||||
minimumPlatformVersion,
|
||||
notaries,
|
||||
maxMessageSize,
|
||||
maxTransactionSize,
|
||||
modifiedTime,
|
||||
epoch,
|
||||
// TODO: Tudor, Michal - pass the actual network parameters where we figure out how
|
||||
emptyMap(),
|
||||
eventHorizonDays.days
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object FlagDay : NetworkParametersCmd()
|
||||
|
||||
object CancelUpdate : NetworkParametersCmd()
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.google.common.primitives.Booleans
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.config.OldConfig
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
data class NetworkManagementServerConfig( // TODO: Move local signing to signing server.
|
||||
val address: NetworkHostAndPort,
|
||||
val dataSourceProperties: Properties,
|
||||
@OldConfig("databaseConfig")
|
||||
val database: DatabaseConfig = DatabaseConfig(),
|
||||
|
||||
@OldConfig("doormanConfig")
|
||||
val doorman: DoormanConfig?,
|
||||
@OldConfig("networkMapConfig")
|
||||
val networkMap: NetworkMapConfig?,
|
||||
val revocation: CertificateRevocationConfig?,
|
||||
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val keystorePath: Path? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val rootStorePath: Path? = null,
|
||||
val keystorePassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val caPrivateKeyPassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val rootKeystorePassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val rootPrivateKeyPassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val caCrlPath: Path? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val caCrlUrl: URL? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val emptyCrlPath: Path? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val emptyCrlUrl: URL? = null
|
||||
) {
|
||||
companion object {
|
||||
// TODO: Do we really need these defaults?
|
||||
val DEFAULT_APPROVE_INTERVAL = 5.seconds
|
||||
val DEFAULT_SIGN_INTERVAL = 5.seconds
|
||||
}
|
||||
}
|
||||
|
||||
data class DoormanConfig(val approveAll: Boolean = false,
|
||||
@OldConfig("jiraConfig")
|
||||
val jira: JiraConfig? = null,
|
||||
val crlEndpoint: URL? = null,
|
||||
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
|
||||
init {
|
||||
require(Booleans.countTrue(approveAll, jira != null) == 1) {
|
||||
"Either 'approveAll' or 'jira' config settings need to be specified but not both"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CertificateRevocationConfig(val approveAll: Boolean = false,
|
||||
val jira: JiraConfig? = null,
|
||||
val localSigning: LocalSigning?,
|
||||
val crlCacheTimeout: Long,
|
||||
val caCrlPath: Path,
|
||||
val emptyCrlPath: Path,
|
||||
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
|
||||
init {
|
||||
require(Booleans.countTrue(approveAll, jira != null) == 1) {
|
||||
"Either 'approveAll' or 'jira' config settings need to be specified but not both"
|
||||
}
|
||||
}
|
||||
|
||||
data class LocalSigning(val crlUpdateInterval: Long,
|
||||
val crlEndpoint: URL)
|
||||
}
|
||||
|
||||
data class NetworkMapConfig(val cacheTimeout: Long,
|
||||
// TODO: Move signing to signing server.
|
||||
val signInterval: Long = NetworkManagementServerConfig.DEFAULT_SIGN_INTERVAL.toMillis())
|
||||
|
||||
enum class Mode {
|
||||
// TODO CA_KEYGEN now also generates the network map cert, so it should be renamed.
|
||||
DOORMAN,
|
||||
CA_KEYGEN,
|
||||
ROOT_KEYGEN
|
||||
}
|
||||
|
||||
data class JiraConfig(
|
||||
val address: String,
|
||||
val projectCode: String,
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
@ -1,142 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.api.IssueRestClient
|
||||
import com.atlassian.jira.rest.client.api.JiraRestClient
|
||||
import com.atlassian.jira.rest.client.api.domain.Comment
|
||||
import com.atlassian.jira.rest.client.api.domain.Field
|
||||
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.IssueInput
|
||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||
import com.r3.corda.networkmanage.doorman.JiraConstant.DONE_TRANSITION_KEY
|
||||
import com.r3.corda.networkmanage.doorman.JiraConstant.START_TRANSITION_KEY
|
||||
import com.r3.corda.networkmanage.doorman.JiraConstant.STOP_TRANSITION_KEY
|
||||
import net.corda.core.internal.ConcurrentBox
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import org.slf4j.Logger
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* The Jira client class manages concurrent access to the [JiraRestClient] to ensure atomic read and write for some operations to prevent race condition.
|
||||
*/
|
||||
abstract class JiraClient(restClient: JiraRestClient, protected val projectCode: String) {
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val restClientLock = ConcurrentBox(restClient)
|
||||
|
||||
// The JIRA project must have a Request ID and reject reason field, and the Task issue type.
|
||||
protected val requestIdField: Field = requireNotNull(restClient.metadataClient.fields.claim().find { it.name == "Request ID" }) { "Request ID field not found in JIRA '$projectCode'" }
|
||||
protected val taskIssueType: IssueType = requireNotNull(restClient.metadataClient.issueTypes.claim().find { it.name == "Task" }) { "Task issue type field not found in JIRA '$projectCode'" }
|
||||
private val rejectReasonField: Field = requireNotNull(restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" }) { "Reject Reason field not found in JIRA '$projectCode'" }
|
||||
|
||||
private val transitions = mutableMapOf<String, Int>()
|
||||
|
||||
fun getApprovedRequests(): List<ApprovedRequest> {
|
||||
return restClientLock.concurrent {
|
||||
val issues = searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||
issues.mapNotNull { issue ->
|
||||
val requestId = requireNotNull(issue.getField(requestIdField.id)?.value?.toString()) { "Error processing request '${issue.key}' : RequestId cannot be null." }
|
||||
// Issue retrieved via search doesn't contain change logs.
|
||||
val fullIssue = issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
|
||||
val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
|
||||
ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getRejectedRequests(): List<RejectedRequest> {
|
||||
return restClientLock.concurrent {
|
||||
val issues = searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues
|
||||
issues.mapNotNull { issue ->
|
||||
val requestId = requireNotNull(issue.getField(requestIdField.id)?.value?.toString()) { "Error processing request '${issue.key}' : RequestId cannot be null." }
|
||||
val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString()
|
||||
// Issue retrieved via search doesn't contain comments.
|
||||
val fullIssue = issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
|
||||
val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } }
|
||||
RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRejectedRequest(requestId: String) {
|
||||
restClientLock.exclusive {
|
||||
val issue = requireNotNull(getIssueById(requestId)) { "Issue with the `request ID` = $requestId does not exist." }
|
||||
// Move status to in progress.
|
||||
issueClient.transition(issue, TransitionInput(getTransitionId(START_TRANSITION_KEY, issue))).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
|
||||
// Move status to stopped.
|
||||
issueClient.transition(issue, TransitionInput(getTransitionId(STOP_TRANSITION_KEY, issue))).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
|
||||
issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JiraRestClient.getIssueById(requestId: String): Issue? {
|
||||
// Jira only support ~ (contains) search for custom textfield.
|
||||
return searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
|
||||
}
|
||||
|
||||
private fun getTransitionId(transitionKey: String, issue: Issue): Int {
|
||||
return transitions.computeIfAbsent(transitionKey, { key ->
|
||||
restClientLock.concurrent {
|
||||
issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == key }.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected fun createJiraTicket(requestId: String, issue: IssueInput, attachment: Pair<String, InputStream>? = null) {
|
||||
restClientLock.exclusive {
|
||||
if (getIssueById(requestId) != null) {
|
||||
logger.warn("There is already a ticket corresponding to request Id $requestId, not creating a new one.")
|
||||
return
|
||||
}
|
||||
// This will block until the issue is created.
|
||||
issueClient.createIssue(issue).fail { logger.error("Exception when creating JIRA issue for request: '$requestId'.", it) }.claim()
|
||||
attachment?.let { addAttachment(requestId, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun transitRequestStatusToDone(requestId: String, attachment: Pair<String, InputStream>? = null) {
|
||||
restClientLock.exclusive {
|
||||
val issue = requireNotNull(getIssueById(requestId)) { "Cannot find the JIRA ticket `request ID` = $requestId" }
|
||||
issueClient.transition(issue, TransitionInput(getTransitionId(DONE_TRANSITION_KEY, issue))).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
|
||||
attachment?.let { addAttachment(requestId, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun JiraRestClient.addAttachment(requestId: String, attachment: Pair<String, InputStream>) {
|
||||
val createdIssue = checkNotNull(getIssueById(requestId)) { "Missing the JIRA ticket for the request ID: $requestId" }
|
||||
issueClient.addAttachment(createdIssue.attachmentsUri, attachment.second, attachment.first)
|
||||
.fail { logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
|
||||
}
|
||||
}
|
||||
|
||||
data class ApprovedRequest(val requestId: String, val approvedBy: String)
|
||||
|
||||
data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?)
|
||||
|
||||
inline fun <T : Any> Iterable<T>.forEachWithExceptionLogging(logger: Logger, action: (T) -> Unit) {
|
||||
for (element in this) {
|
||||
try {
|
||||
action(element)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error while processing an element: $element", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object JiraConstant{
|
||||
const val DONE_TRANSITION_KEY = "Done"
|
||||
const val START_TRANSITION_KEY = "Start Progress"
|
||||
const val STOP_TRANSITION_KEY = "Stop Progress"
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import com.r3.corda.networkmanage.common.utils.*
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private val logger = LoggerFactory.getLogger("com.r3.corda.networkmanage.doorman")
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Crypto.registerProviders() // Required to register Providers first thing on boot.
|
||||
if (Manifests.exists("Doorman-Version")) {
|
||||
println("Version: ${Manifests.read("Doorman-Version")}")
|
||||
}
|
||||
|
||||
initialiseSerialization()
|
||||
val cmdLineOptions = DoormanArgsParser().parseOrExit(*args, printHelpOn = System.err)
|
||||
|
||||
val config = parseConfig<NetworkManagementServerConfig>(cmdLineOptions.configFile)
|
||||
|
||||
logger.info("Running in ${cmdLineOptions.mode} mode")
|
||||
when (cmdLineOptions.mode) {
|
||||
Mode.ROOT_KEYGEN -> {
|
||||
val emptyCrlPath = requireNotNull(config.emptyCrlPath) { "emptyCrlPath needs to be specified" }
|
||||
val emptyCrlUrl = requireNotNull(config.emptyCrlUrl) { "emptyCrlUrl needs to be specified" }
|
||||
val caCrlPath = requireNotNull(config.caCrlPath) { "caCrlPath needs to be specified" }
|
||||
val caCrlUrl = requireNotNull(config.caCrlUrl) { "caCrlUrl needs to be specified" }
|
||||
val rootCertificateAndKeyPair = rootKeyGenMode(cmdLineOptions, config)
|
||||
createEmptyCrls(rootCertificateAndKeyPair, emptyCrlPath, emptyCrlUrl, caCrlPath, caCrlUrl)
|
||||
}
|
||||
Mode.CA_KEYGEN -> caKeyGenMode(config)
|
||||
Mode.DOORMAN -> doormanMode(cmdLineOptions, config)
|
||||
}
|
||||
}
|
||||
|
||||
data class NetworkMapStartParams(val signer: LocalSigner?, val config: NetworkMapConfig)
|
||||
|
||||
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
||||
|
||||
private fun processKeyStore(config: NetworkManagementServerConfig): Pair<CertPathAndKey, LocalSigner>? {
|
||||
if (config.keystorePath == null) return null
|
||||
if (!config.keystorePath.exists()) {
|
||||
println("Could not find keystore: ${config.keystorePath}. You need to create it first or point to the correct location. Please consult the documentation.")
|
||||
exitProcess(0)
|
||||
}
|
||||
// Get password from console if not in config.
|
||||
val keyStorePassword = config.keystorePassword ?: readPassword("Key store password: ")
|
||||
val privateKeyPassword = config.caPrivateKeyPassword ?: readPassword("Private key password: ")
|
||||
val keyStore = X509KeyStore.fromFile(config.keystorePath, keyStorePassword)
|
||||
val csrCertPathAndKey = keyStore.getCertPathAndKey(X509Utilities.CORDA_INTERMEDIATE_CA, privateKeyPassword)
|
||||
val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(CORDA_NETWORK_MAP, privateKeyPassword))
|
||||
return Pair(csrCertPathAndKey, networkMapSigner)
|
||||
}
|
||||
|
||||
private fun rootKeyGenMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig): CertificateAndKeyPair {
|
||||
return generateRootKeyPair(
|
||||
requireNotNull(config.rootStorePath) { "The 'rootStorePath' parameter must be specified when generating keys!" },
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
cmdLineOptions.trustStorePassword
|
||||
)
|
||||
}
|
||||
|
||||
private fun caKeyGenMode(config: NetworkManagementServerConfig) {
|
||||
generateSigningKeyPairs(
|
||||
requireNotNull(config.keystorePath) { "The 'keystorePath' parameter must be specified when generating keys!" },
|
||||
requireNotNull(config.rootStorePath) { "The 'rootStorePath' parameter must be specified when generating keys!" },
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
config.keystorePassword,
|
||||
config.caPrivateKeyPassword,
|
||||
config.caCrlUrl
|
||||
)
|
||||
}
|
||||
|
||||
private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) {
|
||||
val networkManagementServer = NetworkManagementServer(config.dataSourceProperties, config.database, config.doorman, config.revocation)
|
||||
|
||||
val networkParametersCmd = when {
|
||||
cmdLineOptions.setNetworkParametersFile != null ->
|
||||
networkManagementServer.netParamsUpdateHandler.loadParametersFromFile(cmdLineOptions.setNetworkParametersFile)
|
||||
cmdLineOptions.flagDay -> NetworkParametersCmd.FlagDay
|
||||
cmdLineOptions.cancelUpdate -> NetworkParametersCmd.CancelUpdate
|
||||
else -> null
|
||||
}
|
||||
if (networkParametersCmd == null) {
|
||||
// TODO: move signing to signing server.
|
||||
val csrAndNetworkMap = processKeyStore(config)
|
||||
if (csrAndNetworkMap != null) {
|
||||
logger.info("Starting network management services with local signing")
|
||||
}
|
||||
|
||||
val networkMapStartParams = config.networkMap?.let {
|
||||
NetworkMapStartParams(csrAndNetworkMap?.second, it)
|
||||
}
|
||||
|
||||
networkManagementServer.start(
|
||||
config.address,
|
||||
csrAndNetworkMap?.first,
|
||||
networkMapStartParams)
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") {
|
||||
override fun run() {
|
||||
networkManagementServer.close()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
networkManagementServer.use {
|
||||
it.netParamsUpdateHandler.processNetworkParameters(networkParametersCmd)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
|
||||
import com.r3.corda.networkmanage.common.persistence.*
|
||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.doorman.signer.*
|
||||
import com.r3.corda.networkmanage.doorman.webservice.*
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
databaseConfig: DatabaseConfig,
|
||||
private val doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
|
||||
private val revocationConfig: CertificateRevocationConfig?) : Closeable {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val closeActions = mutableListOf<() -> Unit>()
|
||||
private val database = configureDatabase(dataSourceProperties, databaseConfig).also { closeActions += it::close }
|
||||
private val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
private val nodeInfoStorage = PersistentNodeInfoStorage(database)
|
||||
private val csrStorage = PersistentCertificateSigningRequestStorage(database).let {
|
||||
if (doormanConfig?.approveAll ?: false) {
|
||||
ApproveAllCertificateSigningRequestStorage(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
val netParamsUpdateHandler = ParametersUpdateHandler(csrStorage, networkMapStorage)
|
||||
|
||||
lateinit var hostAndPort: NetworkHostAndPort
|
||||
|
||||
override fun close() {
|
||||
logger.info("Closing server...")
|
||||
for (closeAction in closeActions.reversed()) {
|
||||
try {
|
||||
closeAction()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Disregarding exception thrown during close", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNetworkMapService(config: NetworkMapConfig, signer: LocalSigner?): NetworkMapWebService {
|
||||
logger.info("Starting Network Map server.")
|
||||
val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
|
||||
val latestParameters = networkMapStorage.getLatestNetworkParameters()?.networkParameters ?: throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
|
||||
logger.info("Starting network map service with latest network parameters: $latestParameters")
|
||||
localNetworkMapSigner?.signAndPersistNetworkParameters(latestParameters)
|
||||
|
||||
if (localNetworkMapSigner != null) {
|
||||
logger.info("Starting background worker for signing the network map using the local key store")
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
scheduledExecutor.scheduleAtFixedRate({
|
||||
try {
|
||||
localNetworkMapSigner.signNetworkMaps()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
logger.error("Unable to sign network map", e)
|
||||
}
|
||||
}, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
|
||||
closeActions += scheduledExecutor::shutdown
|
||||
}
|
||||
return NetworkMapWebService(nodeInfoStorage, networkMapStorage, PersistentCertificateSigningRequestStorage(database), config)
|
||||
}
|
||||
|
||||
private fun getDoormanService(config: DoormanConfig,
|
||||
csrCertPathAndKey: CertPathAndKey?,
|
||||
serverStatus: NetworkManagementServerStatus): RegistrationWebService {
|
||||
logger.info("Starting Doorman server.")
|
||||
|
||||
val jiraConfig = config.jira
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode)
|
||||
JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint))
|
||||
} else {
|
||||
DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint)
|
||||
}
|
||||
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
val approvalThread = Runnable {
|
||||
try {
|
||||
serverStatus.lastRequestCheckTime = Instant.now()
|
||||
// Process Jira approved tickets.
|
||||
requestProcessor.processRequests()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
logger.error("Error encountered when approving request.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
|
||||
closeActions += scheduledExecutor::shutdown
|
||||
|
||||
return RegistrationWebService(requestProcessor, Duration.ofMillis(config.approveInterval))
|
||||
}
|
||||
|
||||
private fun getRevocationServices(config: CertificateRevocationConfig,
|
||||
csrCertPathAndKeyPair: CertPathAndKey?): Pair<CertificateRevocationRequestWebService, CertificateRevocationListWebService> {
|
||||
logger.info("Starting Revocation server.")
|
||||
|
||||
val crrStorage = PersistentCertificateRevocationRequestStorage(database).let {
|
||||
if (config.approveAll) {
|
||||
ApproveAllCertificateRevocationRequestStorage(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
val crlStorage = PersistentCertificateRevocationListStorage(database)
|
||||
val crlHandler = csrCertPathAndKeyPair?.let {
|
||||
LocalCrlHandler(crrStorage,
|
||||
crlStorage,
|
||||
CertificateAndKeyPair(it.certPath[0], it.toKeyPair()),
|
||||
Duration.ofMillis(config.localSigning!!.crlUpdateInterval),
|
||||
config.localSigning.crlEndpoint)
|
||||
}
|
||||
|
||||
val jiraConfig = config.jira
|
||||
val crrHandler = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = CrrJiraClient(jiraWebAPI, jiraConfig.projectCode)
|
||||
JiraCrrHandler(jiraClient, crrStorage, crlHandler)
|
||||
} else {
|
||||
DefaultCrrHandler(crrStorage, crlHandler)
|
||||
}
|
||||
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
val approvalThread = Runnable {
|
||||
try {
|
||||
// Process Jira approved tickets.
|
||||
crrHandler.processRequests()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
logger.error("Error encountered when approving request.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
|
||||
closeActions += scheduledExecutor::shutdown
|
||||
// TODO start socket server
|
||||
return Pair(
|
||||
CertificateRevocationRequestWebService(crrHandler),
|
||||
CertificateRevocationListWebService(
|
||||
crlStorage,
|
||||
FileUtils.readFileToByteArray(config.caCrlPath.toFile()),
|
||||
FileUtils.readFileToByteArray(config.emptyCrlPath.toFile()),
|
||||
Duration.ofMillis(config.crlCacheTimeout)))
|
||||
}
|
||||
|
||||
fun start(hostAndPort: NetworkHostAndPort,
|
||||
csrCertPathAndKey: CertPathAndKey?,
|
||||
startNetworkMap: NetworkMapStartParams?
|
||||
) {
|
||||
val services = mutableListOf<Any>()
|
||||
val serverStatus = NetworkManagementServerStatus()
|
||||
|
||||
startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) }
|
||||
doormanConfig?.let { services += getDoormanService(it, csrCertPathAndKey, serverStatus) }
|
||||
revocationConfig?.let {
|
||||
val revocationServices = getRevocationServices(it, csrCertPathAndKey)
|
||||
services += revocationServices.first
|
||||
services += revocationServices.second
|
||||
}
|
||||
|
||||
require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
|
||||
|
||||
// TODO: use mbean to expose audit data?
|
||||
services += MonitoringWebService(serverStatus)
|
||||
|
||||
val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
|
||||
webServer.start()
|
||||
|
||||
closeActions += webServer::close
|
||||
this.hostAndPort = webServer.hostAndPort
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
|
||||
import com.r3.corda.networkmanage.common.utils.NETWORK_ROOT_TRUSTSTORE_FILENAME
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
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.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificate
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// TODO The cert subjects need to be configurable
|
||||
const val CORDA_X500_BASE = "O=R3 HoldCo LLC,OU=Corda,L=New York,C=US"
|
||||
|
||||
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
|
||||
internal fun readPassword(fmt: String): String {
|
||||
return if (System.console() != null) {
|
||||
String(System.console().readPassword(fmt))
|
||||
} else {
|
||||
print(fmt)
|
||||
readLine() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
// Keygen utilities.
|
||||
fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, networkRootTrustPass: String?): CertificateAndKeyPair {
|
||||
println("Generating Root CA keypair and certificate.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
|
||||
// Ensure folder exists.
|
||||
rootStoreFile.parent.createDirectories()
|
||||
val rootStore = X509KeyStore.fromFile(rootStoreFile, rootKeystorePassword, createNew = true)
|
||||
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
|
||||
|
||||
if (CORDA_ROOT_CA in rootStore) {
|
||||
println("$CORDA_ROOT_CA already exists in keystore, process will now terminate.")
|
||||
println(rootStore.getCertificate(CORDA_ROOT_CA))
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val selfSignKey = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
// TODO Make the cert subject configurable
|
||||
val rootCert = createSelfSignedCACertificate(
|
||||
X500Principal("CN=Corda Root CA,$CORDA_X500_BASE"),
|
||||
selfSignKey)
|
||||
rootStore.update {
|
||||
setPrivateKey(CORDA_ROOT_CA, selfSignKey.private, listOf(rootCert), rootPrivateKeyPassword)
|
||||
}
|
||||
|
||||
val trustStorePath = (rootStoreFile.parent / "distribute-nodes").createDirectories() / NETWORK_ROOT_TRUSTSTORE_FILENAME
|
||||
|
||||
val networkRootTrustPassword = networkRootTrustPass ?: readPassword("Network Root Trust Store Password: ")
|
||||
|
||||
X509KeyStore.fromFile(trustStorePath, networkRootTrustPassword, createNew = true).update {
|
||||
setCertificate(CORDA_ROOT_CA, rootCert)
|
||||
}
|
||||
|
||||
println("Trust store for distribution to nodes created in $trustStorePath")
|
||||
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
|
||||
println(rootCert)
|
||||
return CertificateAndKeyPair(rootCert, selfSignKey)
|
||||
}
|
||||
|
||||
fun createEmptyCrls(rootCertificateAndKeyPair: CertificateAndKeyPair, emptyCrlPath: Path, emptyCrlUrl: URL, caCrlPath: Path, caCrlUrl: URL) {
|
||||
val rootCert = rootCertificateAndKeyPair.certificate
|
||||
val rootKey = rootCertificateAndKeyPair.keyPair.private
|
||||
val emptyCrl = createSignedCrl(rootCert, emptyCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList(), true)
|
||||
FileUtils.writeByteArrayToFile(emptyCrlPath.toFile(), emptyCrl.encoded)
|
||||
val caCrl = createSignedCrl(rootCert, caCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList())
|
||||
FileUtils.writeByteArrayToFile(caCrlPath.toFile(), caCrl.encoded)
|
||||
println("Empty CRL: $emptyCrl")
|
||||
println("CA CRL: $caCrl")
|
||||
println("Root signed empty and CA CRL files created in $emptyCrlPath and $caCrlPath respectively")
|
||||
}
|
||||
|
||||
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?, caCrlUrl: URL?) {
|
||||
println("Generating intermediate and network map key pairs and certificates using root key store $rootStoreFile.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root key store password: ")
|
||||
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root private key password: ")
|
||||
val rootKeyStore = X509KeyStore.fromFile(rootStoreFile, rootKeystorePassword)
|
||||
|
||||
val rootKeyPairAndCert = rootKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA, rootPrivateKeyPassword)
|
||||
|
||||
val keyStorePassword = keystorePass ?: readPassword("Key store Password: ")
|
||||
val privateKeyPassword = caPrivateKeyPass ?: readPassword("Private key Password: ")
|
||||
// Ensure folder exists.
|
||||
keystoreFile.parent.createDirectories()
|
||||
val keyStore = X509KeyStore.fromFile(keystoreFile, keyStorePassword, createNew = true)
|
||||
|
||||
fun storeCertIfAbsent(alias: String, certificateType: CertificateType, subject: X500Principal, signatureScheme: SignatureScheme) {
|
||||
if (alias in keyStore) {
|
||||
println("$alias already exists in keystore:")
|
||||
println(keyStore.getCertificate(alias))
|
||||
return
|
||||
}
|
||||
|
||||
val keyPair = Crypto.generateKeyPair(signatureScheme)
|
||||
val cert = createCertificate(
|
||||
certificateType,
|
||||
rootKeyPairAndCert.certificate,
|
||||
rootKeyPairAndCert.keyPair,
|
||||
subject,
|
||||
keyPair.public,
|
||||
crlDistPoint = caCrlUrl?.toString()
|
||||
)
|
||||
|
||||
keyStore.update {
|
||||
setPrivateKey(alias, keyPair.private, listOf(cert, rootKeyPairAndCert.certificate), privateKeyPassword)
|
||||
}
|
||||
|
||||
println("$certificateType key pair and certificate stored in $keystoreFile.")
|
||||
println(cert)
|
||||
}
|
||||
|
||||
storeCertIfAbsent(
|
||||
CORDA_INTERMEDIATE_CA,
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
X500Principal("CN=Corda Doorman CA,$CORDA_X500_BASE"),
|
||||
DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
|
||||
storeCertIfAbsent(
|
||||
CORDA_NETWORK_MAP,
|
||||
CertificateType.NETWORK_MAP,
|
||||
X500Principal("CN=Corda Network Map,$CORDA_X500_BASE"),
|
||||
Crypto.EDDSA_ED25519_SHA512)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||
import org.eclipse.jetty.servlet.ServletHolder
|
||||
import org.glassfish.jersey.server.ResourceConfig
|
||||
import org.glassfish.jersey.servlet.ServletContainer
|
||||
import java.io.Closeable
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/**
|
||||
* NetworkManagementWebServer runs on Jetty server and provides service via http.
|
||||
*/
|
||||
class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||
handler = HandlerCollection().apply {
|
||||
addHandler(buildServletContextHandler())
|
||||
}
|
||||
}
|
||||
|
||||
val hostAndPort: NetworkHostAndPort
|
||||
get() = server.connectors.mapNotNull { it as? ServerConnector }
|
||||
.map { NetworkHostAndPort(it.host, it.localPort) }
|
||||
.first()
|
||||
|
||||
override fun close() {
|
||||
logger.info("Shutting down network management web services...")
|
||||
server.stop()
|
||||
server.join()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
logger.info("Starting network management web services...")
|
||||
server.start()
|
||||
logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
|
||||
println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
|
||||
}
|
||||
|
||||
private fun buildServletContextHandler(): ServletContextHandler {
|
||||
return ServletContextHandler().apply {
|
||||
contextPath = "/"
|
||||
val resourceConfig = ResourceConfig().apply {
|
||||
// Add your API provider classes (annotated for JAX-RS) here
|
||||
webServices.forEach { register(it) }
|
||||
}
|
||||
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
|
||||
addServlet(jerseyServlet, "/*")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.internal.readObject
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Data class representing a [NotaryInfo] which can be easily parsed by a typesafe [ConfigFactory].
|
||||
* @property notaryNodeInfoFile path to the node info file of the notary node.
|
||||
* @property validating whether the notary is validating
|
||||
*/
|
||||
data class NotaryConfig(private val notaryNodeInfoFile: Path,
|
||||
private val validating: Boolean) {
|
||||
val nodeInfo by lazy { toNodeInfo() }
|
||||
fun toNotaryInfo(): NotaryInfo {
|
||||
// It is always the last identity (in the list of identities) that corresponds to the notary identity.
|
||||
// In case of a single notary, the list has only one element. In case of distributed notaries the list has
|
||||
// two items and the second one corresponds to the notary identity.
|
||||
return NotaryInfo(nodeInfo.legalIdentities.last(), validating)
|
||||
}
|
||||
|
||||
private fun toNodeInfo(): NodeInfo = notaryNodeInfoFile.readObject<SignedNodeInfo>().verified()
|
||||
}
|
||||
|
||||
data class ParametersUpdateConfig(val description: String, val updateDeadline: Instant) {
|
||||
init {
|
||||
val now = Instant.now()
|
||||
require(updateDeadline == now || updateDeadline.isAfter(now)) { "The updateDeadline time must be in the future." }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class containing the fields from [NetworkParameters] which can be read at start-up time from doorman.
|
||||
* It is a proper subset of [NetworkParameters] except for the [notaries] field which is replaced by a list of
|
||||
* [NotaryConfig] which is parsable.
|
||||
*/
|
||||
data class NetworkParametersConfig(val minimumPlatformVersion: Int,
|
||||
val notaries: List<NotaryConfig>,
|
||||
val maxMessageSize: Int,
|
||||
val maxTransactionSize: Int,
|
||||
val eventHorizonDays: Int,
|
||||
val parametersUpdate: ParametersUpdateConfig?)
|
@ -1,123 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
|
||||
class ParametersUpdateHandler(val csrStorage: CertificateSigningRequestStorage, val networkMapStorage: NetworkMapStorage) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
fun loadParametersFromFile(file: Path): NetworkParametersCmd.Set {
|
||||
val netParamsConfig = ConfigFactory.parseFile(file.toFile(), ConfigParseOptions.defaults())
|
||||
.parseAs<NetworkParametersConfig>()
|
||||
checkNotaryCertificates(netParamsConfig.notaries.map { it.nodeInfo })
|
||||
return NetworkParametersCmd.Set.fromConfig(netParamsConfig)
|
||||
}
|
||||
|
||||
fun processNetworkParameters(networkParametersCmd: NetworkParametersCmd) {
|
||||
when (networkParametersCmd) {
|
||||
is NetworkParametersCmd.Set -> handleSetNetworkParameters(networkParametersCmd)
|
||||
NetworkParametersCmd.FlagDay -> handleFlagDay()
|
||||
NetworkParametersCmd.CancelUpdate -> handleCancelUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkNotaryCertificates(notaryNodeInfos: List<NodeInfo>) {
|
||||
notaryNodeInfos.forEach { notaryInfo ->
|
||||
val cert = notaryInfo.legalIdentitiesAndCerts.last().certPath.x509Certificates.find {
|
||||
val certRole = CertRole.extract(it)
|
||||
certRole == CertRole.SERVICE_IDENTITY || certRole == CertRole.NODE_CA
|
||||
}
|
||||
cert ?: throw IllegalArgumentException("The notary certificate path does not contain SERVICE_IDENTITY or NODE_CA role in it")
|
||||
csrStorage.getValidCertificatePath(cert.publicKey)
|
||||
?: throw IllegalArgumentException("Notary with node info: $notaryInfo is not registered with the doorman")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetNetworkParameters(setNetParams: NetworkParametersCmd.Set) {
|
||||
logger.info("maxMessageSize is not currently wired in the nodes")
|
||||
val activeMap = networkMapStorage.getNetworkMaps().publicNetworkMap
|
||||
val activeNetParams = activeMap?.networkParameters?.networkParameters
|
||||
// Setting initial parameters case.
|
||||
if (activeNetParams == null) {
|
||||
require(setNetParams.parametersUpdate == null) {
|
||||
"'parametersUpdate' specified in network parameters file but there are no network parameters to update"
|
||||
}
|
||||
val initialNetParams = setNetParams.toNetworkParameters(modifiedTime = Instant.now(), epoch = 1)
|
||||
logger.info("Saving initial network parameters to be signed:\n$initialNetParams")
|
||||
networkMapStorage.saveNetworkParameters(initialNetParams, null)
|
||||
println("Saved initial network parameters to be signed:\n$initialNetParams")
|
||||
} else { // An update.
|
||||
val parametersUpdate = requireNotNull(setNetParams.parametersUpdate) {
|
||||
"'parametersUpdate' not specified in network parameters file but there is already an active set of network parameters"
|
||||
}
|
||||
setNetParams.checkCompatibility(activeNetParams)
|
||||
val latestNetParams = checkNotNull(networkMapStorage.getLatestNetworkParameters()?.networkParameters) {
|
||||
"Something has gone wrong! We have an active set of network parameters ($activeNetParams) but apparently no latest network parameters!"
|
||||
}
|
||||
// It's not necessary that latestNetParams is the current active network parameters. It can be the network
|
||||
// parameters from a previous update attempt which hasn't activated yet. We still take the epoch value for this
|
||||
// new set from latestNetParams to make sure the advertised update attempts have incrementing epochs.
|
||||
// This has the implication that *active* network parameters may have gaps in their epochs.
|
||||
val newNetParams = setNetParams.toNetworkParameters(modifiedTime = Instant.now(), epoch = latestNetParams.epoch + 1)
|
||||
val activeUpdate = activeMap.networkMap.parametersUpdate
|
||||
if (sameNetworkParameters(latestNetParams, newNetParams) && activeUpdate != null) {
|
||||
// TODO We don't have cancel event propagation on client side.
|
||||
throw IllegalArgumentException("New network parameters are the same as the latest ones and there is an update scheduled: $activeUpdate\n" +
|
||||
"If you want to just change updateDeadline or update description - cancel old update first.")
|
||||
}
|
||||
logger.info("Enabling update to network parameters:\n$newNetParams\n$parametersUpdate")
|
||||
networkMapStorage.saveNewParametersUpdate(newNetParams, parametersUpdate.description, parametersUpdate.updateDeadline)
|
||||
|
||||
logger.info("Update enabled")
|
||||
println("Enabled update to network parameters:\n$newNetParams\n$parametersUpdate")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sameNetworkParameters(params1: NetworkParameters, params2: NetworkParameters): Boolean {
|
||||
return params1.copy(epoch = 1, modifiedTime = Instant.MAX) == params2.copy(epoch = 1, modifiedTime = Instant.MAX)
|
||||
}
|
||||
|
||||
private fun handleFlagDay() {
|
||||
val parametersUpdate = checkNotNull(networkMapStorage.getCurrentParametersUpdate()) {
|
||||
"No network parameters updates are scheduled"
|
||||
}
|
||||
check(Instant.now() >= parametersUpdate.updateDeadline) {
|
||||
"Update deadline of ${parametersUpdate.updateDeadline} hasn't passed yet"
|
||||
}
|
||||
val latestNetParamsEntity = networkMapStorage.getLatestNetworkParameters()
|
||||
check(parametersUpdate.networkParameters.hash == networkMapStorage.getLatestNetworkParameters()?.hash) {
|
||||
"The latest network parameters is not the scheduled one:\n${latestNetParamsEntity?.networkParameters}\n${parametersUpdate.toParametersUpdate()}"
|
||||
}
|
||||
val activeNetParams = networkMapStorage.getNetworkMaps().publicNetworkMap?.networkParameters
|
||||
check(parametersUpdate.networkParameters.isSigned) {
|
||||
"Parameters we are trying to switch to haven't been signed yet"
|
||||
}
|
||||
logger.info("""Flag day has occurred, however the new network parameters won't be active until the new network map is signed.
|
||||
From: ${activeNetParams?.networkParameters}
|
||||
To: ${parametersUpdate.networkParameters.networkParameters}""")
|
||||
networkMapStorage.setParametersUpdateStatus(parametersUpdate, UpdateStatus.FLAG_DAY)
|
||||
}
|
||||
|
||||
private fun handleCancelUpdate() {
|
||||
val parametersUpdate = checkNotNull(networkMapStorage.getCurrentParametersUpdate()) {
|
||||
"No network parameters updates are scheduled"
|
||||
}
|
||||
logger.info("""Cancelling parameters update: ${parametersUpdate.toParametersUpdate()}.
|
||||
However, the network map will continue to advertise this update until the new one is signed.""")
|
||||
networkMapStorage.setParametersUpdateStatus(parametersUpdate, UpdateStatus.CANCELLED)
|
||||
println("Done with cancel update")
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.common.signer.CertificateRevocationListSigner
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
|
||||
class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorage,
|
||||
private val crlStorage: CertificateRevocationListStorage,
|
||||
issuerCertAndKey: CertificateAndKeyPair,
|
||||
crlUpdateInterval: Duration,
|
||||
crlEndpoint: URL) {
|
||||
private companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private val crlSigner = CertificateRevocationListSigner(
|
||||
crlStorage,
|
||||
issuerCertAndKey.certificate,
|
||||
crlUpdateInterval,
|
||||
crlEndpoint,
|
||||
LocalSigner(issuerCertAndKey))
|
||||
|
||||
fun signCrl() {
|
||||
if (crlStorage.getCertificateRevocationList(CrlIssuer.DOORMAN) == null) {
|
||||
val crl = crlSigner.createSignedCRL(emptyList(), emptyList(), DOORMAN_SIGNATURE)
|
||||
logger.info("Saving a new empty CRL: $crl")
|
||||
return
|
||||
}
|
||||
logger.info("Executing CRL signing...")
|
||||
val approvedRequests = crrStorage.getRevocationRequests(RequestStatus.APPROVED)
|
||||
logger.debug("Approved certificate revocation requests retrieved: $approvedRequests")
|
||||
if (approvedRequests.isEmpty()) {
|
||||
// Nothing to add to the current CRL
|
||||
logger.debug("There are no APPROVED certificate revocation requests. Aborting CRL signing.")
|
||||
return
|
||||
}
|
||||
val currentRequests = crrStorage.getRevocationRequests(RequestStatus.DONE)
|
||||
logger.debug("Existing certificate revocation requests retrieved: $currentRequests")
|
||||
val crl = crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
|
||||
logger.info("New CRL signed: $crl")
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
|
||||
interface CrrHandler {
|
||||
fun processRequests()
|
||||
fun saveRevocationRequest(request: CertificateRevocationRequest): String
|
||||
}
|
||||
|
||||
class DefaultCrrHandler(private val crrStorage: CertificateRevocationRequestStorage,
|
||||
private val localCrlHandler: LocalCrlHandler?) : CrrHandler {
|
||||
override fun saveRevocationRequest(request: CertificateRevocationRequest): String = crrStorage.saveRevocationRequest(request)
|
||||
override fun processRequests() {
|
||||
localCrlHandler?.signCrl()
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage.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.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
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.net.URL
|
||||
import java.security.cert.CertPath
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
interface CsrHandler {
|
||||
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
||||
fun processRequests()
|
||||
fun getResponse(requestId: String): CertificateResponse
|
||||
}
|
||||
|
||||
class DefaultCsrHandler(private val storage: CertificateSigningRequestStorage,
|
||||
private val csrCertPathAndKey: CertPathAndKey?,
|
||||
private val crlDistributionPoint: URL? = null) : CsrHandler {
|
||||
|
||||
override fun processRequests() {
|
||||
if (csrCertPathAndKey == null) return
|
||||
storage.getRequests(RequestStatus.APPROVED).forEach {
|
||||
val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
|
||||
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
|
||||
// we use DOORMAN_SIGNATURE as the signer.
|
||||
storage.putCertificatePath(it.requestId, nodeCertPath, DOORMAN_SIGNATURE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
|
||||
|
||||
override fun getResponse(requestId: String): CertificateResponse {
|
||||
val response = storage.getRequest(requestId)
|
||||
return when (response?.status) {
|
||||
RequestStatus.NEW, RequestStatus.APPROVED, RequestStatus.TICKET_CREATED, null -> CertificateResponse.NotReady
|
||||
RequestStatus.REJECTED -> CertificateResponse.Unauthorised(response.remark ?: "Unknown reason")
|
||||
RequestStatus.DONE -> CertificateResponse.Ready(requireNotNull(response.certData?.certPath) { "Certificate should not be null." })
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest,
|
||||
csrCertPathAndKey: CertPathAndKey): CertPath {
|
||||
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints,
|
||||
// sub certs' directory name must be within client CA's name's subtree,
|
||||
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
|
||||
// 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(
|
||||
certRole.certificateType,
|
||||
csrCertPathAndKey.certPath[0],
|
||||
csrCertPathAndKey.toKeyPair(),
|
||||
X500Principal(request.subject.encoded),
|
||||
request.publicKey,
|
||||
nameConstraints = nameConstraints,
|
||||
crlDistPoint = crlDistributionPoint?.toString())
|
||||
return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.doorman.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
|
||||
class JiraCrrHandler(private val jiraClient: CrrJiraClient,
|
||||
private val crrStorage: CertificateRevocationRequestStorage,
|
||||
private val localCrlHandler: LocalCrlHandler?) : CrrHandler {
|
||||
private companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
|
||||
try {
|
||||
val requestId = crrStorage.saveRevocationRequest(request)
|
||||
val requestData = crrStorage.getRevocationRequest(requestId)
|
||||
requestData ?: throw IllegalStateException("Request $requestId does not exist.")
|
||||
jiraClient.createCertificateRevocationRequestTicket(requestData)
|
||||
crrStorage.markRequestTicketCreated(requestId)
|
||||
return requestId
|
||||
} catch (e: Exception) {
|
||||
logger.error("There was an error while creating JIRA tickets", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun processRequests() {
|
||||
createTickets()
|
||||
val (approvedRequests, rejectedRequests) = updateRequestStatuses()
|
||||
updateJiraTickets(approvedRequests, rejectedRequests)
|
||||
localCrlHandler?.signCrl()
|
||||
}
|
||||
|
||||
private fun updateRequestStatuses(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
|
||||
// Update local request statuses.
|
||||
val approvedRequest = jiraClient.getApprovedRequests()
|
||||
approvedRequest.forEachWithExceptionLogging(logger) { (id, approvedBy) -> crrStorage.approveRevocationRequest(id, approvedBy) }
|
||||
val rejectedRequest = jiraClient.getRejectedRequests()
|
||||
rejectedRequest.forEachWithExceptionLogging(logger) { (id, rejectedBy, reason) -> crrStorage.rejectRevocationRequest(id, rejectedBy, reason) }
|
||||
return Pair(approvedRequest, rejectedRequest)
|
||||
}
|
||||
|
||||
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||
// Reconfirm request status and update jira status
|
||||
logger.debug("Updating JIRA tickets: `approved` = $approvedRequest, `rejected` = $rejectedRequest")
|
||||
approvedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.DONE }
|
||||
.forEachWithExceptionLogging(logger) { jiraClient.transitRequestStatusToDone(it.requestId) }
|
||||
rejectedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.REJECTED }
|
||||
.forEachWithExceptionLogging(logger) { jiraClient.updateRejectedRequest(it.requestId) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Jira tickets for all request in [RequestStatus.NEW] state.
|
||||
*
|
||||
* Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
|
||||
* they might be left in the [RequestStatus.NEW] state if Jira is down.
|
||||
*/
|
||||
private fun createTickets() {
|
||||
crrStorage.getRevocationRequests(RequestStatus.NEW).forEachWithExceptionLogging(logger) {
|
||||
createTicket(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTicket(revocationRequest: CertificateRevocationRequestData) {
|
||||
jiraClient.createCertificateRevocationRequestTicket(revocationRequest)
|
||||
crrStorage.markRequestTicketCreated(revocationRequest.requestId)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.doorman.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
|
||||
class JiraCsrHandler(private val jiraClient: CsrJiraClient, private val storage: CertificateSigningRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
|
||||
private companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
|
||||
val requestId = delegate.saveRequest(rawRequest)
|
||||
// Make sure request has been accepted.
|
||||
try {
|
||||
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
|
||||
jiraClient.createCertificateSigningRequestTicket(CertificationRequestData(requestId, rawRequest))
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("There was an error while creating Jira tickets", e)
|
||||
} finally {
|
||||
return requestId
|
||||
}
|
||||
}
|
||||
|
||||
override fun processRequests() {
|
||||
createTickets()
|
||||
val (approvedRequests, rejectedRequests) = updateRequestStatus()
|
||||
delegate.processRequests()
|
||||
updateJiraTickets(approvedRequests, rejectedRequests)
|
||||
}
|
||||
|
||||
private fun updateRequestStatus(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
|
||||
// Update local request statuses.
|
||||
val approvedRequest = jiraClient.getApprovedRequests()
|
||||
approvedRequest.forEachWithExceptionLogging(logger) { (id, approvedBy) ->
|
||||
storage.approveRequest(id, approvedBy)
|
||||
}
|
||||
val rejectedRequest = jiraClient.getRejectedRequests()
|
||||
rejectedRequest.forEachWithExceptionLogging(logger) { (id, rejectedBy, reason) ->
|
||||
storage.rejectRequest(id, rejectedBy, reason)
|
||||
}
|
||||
return Pair(approvedRequest, rejectedRequest)
|
||||
}
|
||||
|
||||
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||
// Reconfirm request status and update jira status
|
||||
approvedRequest.mapNotNull { storage.getRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.DONE && it.certData != null }
|
||||
.forEachWithExceptionLogging(logger) {
|
||||
val attachment = it.certData?.certPath?.certificates?.firstOrNull()?.let {
|
||||
Pair("${X509Utilities.CORDA_CLIENT_CA}.cer", it.encoded.inputStream())
|
||||
}
|
||||
jiraClient.transitRequestStatusToDone(it.requestId, attachment)
|
||||
}
|
||||
rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.REJECTED }
|
||||
.forEachWithExceptionLogging(logger) {
|
||||
jiraClient.updateRejectedRequest(it.requestId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Jira tickets for all request in [RequestStatus.NEW] state.
|
||||
*
|
||||
* Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
|
||||
* they might be left in the [RequestStatus.NEW] state if Jira is down.
|
||||
*/
|
||||
private fun createTickets() {
|
||||
storage.getRequests(RequestStatus.NEW).forEachWithExceptionLogging(logger) {
|
||||
createTicket(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTicket(signingRequest: CertificateSigningRequest) {
|
||||
jiraClient.createCertificateSigningRequestTicket(CertificationRequestData(signingRequest.requestId, signingRequest.request))
|
||||
storage.markRequestTicketCreated(signingRequest.requestId)
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.signer.Signer
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* This local signer is intended to be used in testing environment where hardware signing module is not available.
|
||||
*/
|
||||
class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer {
|
||||
constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate)
|
||||
|
||||
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||
return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data))
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman.sockets
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors.shutdownAndAwaitTermination
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.common.sockets.*
|
||||
import com.r3.corda.networkmanage.common.utils.readObject
|
||||
import com.r3.corda.networkmanage.common.utils.writeObject
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.net.ServerSocket
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CertificateRevocationSocketServer(val port: Int,
|
||||
private val crlStorage: CertificateRevocationListStorage,
|
||||
private val crrStorage: CertificateRevocationRequestStorage) : AutoCloseable {
|
||||
private companion object {
|
||||
private val logger = contextLogger()
|
||||
|
||||
private val RECONNECT_INTERVAL = 10.seconds.toMillis()
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
@Volatile
|
||||
private var isRunning = false
|
||||
|
||||
fun start() {
|
||||
check(!isRunning) { "The server has already been started." }
|
||||
isRunning = true
|
||||
executor.submit {
|
||||
while (isRunning) {
|
||||
try {
|
||||
listen()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Socket execution error.", e)
|
||||
if (isRunning) {
|
||||
logger.info("Server socket will be recreated in $RECONNECT_INTERVAL milliseconds")
|
||||
Thread.sleep(RECONNECT_INTERVAL)
|
||||
logger.info("Recreating server socket...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
isRunning = false
|
||||
shutdownAndAwaitTermination(executor, RECONNECT_INTERVAL, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
private fun listen() {
|
||||
ServerSocket(port).use {
|
||||
logger.info("Server socket is running.")
|
||||
while (isRunning) {
|
||||
logger.debug("Waiting for socket data...")
|
||||
val acceptedSocket = it.accept()
|
||||
acceptedSocket.use {
|
||||
val message = it.getInputStream().readObject<CrrSocketMessage>()
|
||||
logger.debug("Received message type is $message.")
|
||||
acceptedSocket.getOutputStream().let {
|
||||
when (message) {
|
||||
is CrlRetrievalMessage -> {
|
||||
val crl = crlStorage.getCertificateRevocationList(CrlIssuer.DOORMAN)
|
||||
it.writeObject(CrlResponseMessage(crl))
|
||||
logger.debug("Sending the current certificate revocation list.")
|
||||
}
|
||||
is CrrsByStatusMessage -> {
|
||||
it.writeObject(crrStorage.getRevocationRequests(message.status))
|
||||
logger.debug("Sending ${message.status.name} certificate revocation requests.")
|
||||
}
|
||||
is CrlSubmissionMessage -> {
|
||||
val crlSubmission = acceptedSocket.getInputStream().readObject<CertificateRevocationListSubmission>()
|
||||
crlStorage.saveCertificateRevocationList(crlSubmission.list, CrlIssuer.DOORMAN, crlSubmission.signer, crlSubmission.revocationTime)
|
||||
}
|
||||
else -> logger.warn("Unknown message type $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationListWebService.Companion.CRL_PATH
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.ok
|
||||
import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(CRL_PATH)
|
||||
class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage,
|
||||
private val caCrlBytes: ByteArray,
|
||||
private val emptyCrlBytes: ByteArray,
|
||||
cacheTimeout: Duration) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
const val CRL_PATH = "certificate-revocation-list"
|
||||
const val CRL_DATA_TYPE = "application/pkcs7-crl"
|
||||
const val DOORMAN = "doorman"
|
||||
const val ROOT = "root"
|
||||
const val EMPTY = "empty"
|
||||
}
|
||||
|
||||
private val crlCache: LoadingCache<CrlIssuer, ByteArray> = Caffeine.newBuilder()
|
||||
.expireAfterWrite(cacheTimeout.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.build({ key ->
|
||||
revocationListStorage.getCertificateRevocationList(key)?.encoded
|
||||
})
|
||||
|
||||
@GET
|
||||
@Path(DOORMAN)
|
||||
@Produces(CRL_DATA_TYPE)
|
||||
fun getDoormanRevocationList(): Response {
|
||||
return getCrlResponse(CrlIssuer.DOORMAN)
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(ROOT)
|
||||
@Produces(CRL_DATA_TYPE)
|
||||
fun getRootRevocationList(): Response {
|
||||
return ok(caCrlBytes).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(EMPTY)
|
||||
@Produces(CRL_DATA_TYPE)
|
||||
fun getEmptyRevocationList(): Response {
|
||||
return ok(emptyCrlBytes).build()
|
||||
}
|
||||
|
||||
private fun getCrlResponse(issuer: CrlIssuer): Response {
|
||||
return try {
|
||||
val crlBytes = crlCache.get(issuer)
|
||||
if (crlBytes != null) {
|
||||
ok(crlBytes).build()
|
||||
} else {
|
||||
status(Response.Status.NOT_FOUND).build()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error when retrieving the ${issuer.name} crl.", e)
|
||||
status(Response.Status.INTERNAL_SERVER_ERROR).build()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.r3.corda.networkmanage.doorman.signer.CrrHandler
|
||||
import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationRequestWebService.Companion.CRR_PATH
|
||||
import net.corda.core.internal.readObject
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
import java.io.InputStream
|
||||
import javax.ws.rs.Consumes
|
||||
import javax.ws.rs.POST
|
||||
import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.ok
|
||||
import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(CRR_PATH)
|
||||
class CertificateRevocationRequestWebService(private val crrHandler: CrrHandler) {
|
||||
|
||||
companion object {
|
||||
const val CRR_PATH = "certificate-revocation-request"
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
fun submitRequest(input: InputStream): Response {
|
||||
return try {
|
||||
val request = input.readObject<CertificateRevocationRequest>()
|
||||
val requestId = crrHandler.saveRevocationRequest(request)
|
||||
ok(requestId)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Unable to process the revocation request.", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> status(Response.Status.BAD_REQUEST).entity(e.message)
|
||||
else -> throw e
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.r3.corda.networkmanage.doorman.NetworkManagementServerStatus
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
@Path("/status")
|
||||
class MonitoringWebService(private val serverStatus: NetworkManagementServerStatus) {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun status(): Response {
|
||||
return Response.ok(ObjectMapper().writeValueAsString(serverStatus)).build()
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMaps
|
||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity
|
||||
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.readObject
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.validateCertPath
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import java.io.InputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.Context
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.ok
|
||||
import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(NETWORK_MAP_PATH)
|
||||
class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
private val networkMapStorage: NetworkMapStorage,
|
||||
private val certificateSigningRequestStorage: CertificateSigningRequestStorage,
|
||||
private val config: NetworkMapConfig) {
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
const val NETWORK_MAP_PATH = "network-map"
|
||||
}
|
||||
|
||||
private val networkMapCache: LoadingCache<Boolean, NetworkMaps> = Caffeine.newBuilder()
|
||||
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
|
||||
.build {
|
||||
logger.info("Re-publishing network map")
|
||||
networkMapStorage.getNetworkMaps()
|
||||
}
|
||||
|
||||
private val nodeInfoCache: LoadingCache<SecureHash, SignedNodeInfo> = Caffeine.newBuilder()
|
||||
// TODO: Define cache retention policy.
|
||||
.softValues()
|
||||
.build(nodeInfoStorage::getNodeInfo)
|
||||
|
||||
private val networkMaps: NetworkMaps?
|
||||
get() {
|
||||
if (networkMapCache[true]?.publicNetworkMap == null) {
|
||||
// Refresh cache every time the public network map is NULL.
|
||||
networkMapCache.refresh(true)
|
||||
}
|
||||
return networkMapCache[true]
|
||||
}
|
||||
|
||||
private val currentNodeInfoHashes: Set<SecureHash> get() = networkMaps?.allNodeInfoHashes ?: emptySet()
|
||||
private val currentNetworkParameters: NetworkParameters? get() = networkMaps?.publicNetworkMap?.networkParameters?.networkParameters
|
||||
|
||||
@POST
|
||||
@Path("publish")
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun registerNode(input: InputStream): Response {
|
||||
val signedNodeInfo = input.readObject<SignedNodeInfo>()
|
||||
var nodeInfo: NodeInfo? = null
|
||||
return try {
|
||||
// Store the NodeInfo
|
||||
val nodeInfoAndSigned = NodeInfoAndSigned(signedNodeInfo)
|
||||
nodeInfo = nodeInfoAndSigned.nodeInfo
|
||||
logger.debug { "Publishing node-info: $nodeInfo" }
|
||||
verifyNodeInfo(nodeInfo)
|
||||
nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
|
||||
ok()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Unable to process node-info: $nodeInfo", e)
|
||||
when (e) {
|
||||
is NetworkMapNotInitialisedException -> status(Response.Status.SERVICE_UNAVAILABLE).entity(e.message)
|
||||
is RequestException -> status(Response.Status.BAD_REQUEST).entity(e.message)
|
||||
is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message)
|
||||
// Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
|
||||
else -> throw e
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("ack-parameters")
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun ackNetworkParameters(input: InputStream): Response {
|
||||
return try {
|
||||
val signedParametersHash = input.readObject<SignedData<SecureHash>>()
|
||||
val hash = signedParametersHash.verified()
|
||||
requireNotNull(networkMapStorage.getSignedNetworkParameters(hash)) { "No network parameters with hash $hash" }
|
||||
logger.debug { "Received ack-parameters with $hash from ${signedParametersHash.sig.by}" }
|
||||
nodeInfoStorage.ackNodeInfoParametersUpdate(signedParametersHash.sig.by, hash)
|
||||
ok()
|
||||
} catch (e: SignatureException) {
|
||||
status(Response.Status.FORBIDDEN).entity(e.message)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun getNetworkMap(): Response = createNetworkMapResponse(networkMaps?.publicNetworkMap)
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun getNetworkMap(@PathParam("id") privateNetworkID: String?): Response = createNetworkMapResponse(networkMaps?.privateNetworkMap?.get(privateNetworkID))
|
||||
|
||||
private fun createNetworkMapResponse(networkMap: NetworkMapEntity?) = createResponse(networkMap?.toSignedNetworkMap(), addCacheTimeout = true, timestamp = networkMap?.timestamp)
|
||||
|
||||
@GET
|
||||
@Path("node-info/{nodeInfoHash}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response {
|
||||
// Only serve node info if its in the current network map, otherwise return 404.
|
||||
logger.trace { "Processing node info request for hash: '$nodeInfoHash'" }
|
||||
val signedNodeInfo = if (SecureHash.parse(nodeInfoHash) in currentNodeInfoHashes) {
|
||||
nodeInfoCache.get(SecureHash.parse(nodeInfoHash))
|
||||
} else {
|
||||
logger.trace { "Requested node info is not current, returning null." }
|
||||
null
|
||||
}
|
||||
logger.trace { "Node Info: ${signedNodeInfo?.verified()}" }
|
||||
return createResponse(signedNodeInfo)
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("network-parameters/{hash}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun getNetworkParameters(@PathParam("hash") hash: String): Response {
|
||||
val signedNetParams = networkMapStorage.getSignedNetworkParameters(SecureHash.parse(hash))
|
||||
logger.trace { "Precessed network parameter request for hash: '$hash'" }
|
||||
logger.trace { "Network parameter : ${signedNetParams?.verified()}" }
|
||||
return createResponse(signedNetParams)
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("my-ip")
|
||||
fun myIp(@Context request: HttpServletRequest): Response {
|
||||
val ip = request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}"
|
||||
logger.trace { "Processed IP request from client, IP: '$ip'" }
|
||||
return ok(ip).build()
|
||||
}
|
||||
|
||||
private fun verifyNodeInfo(nodeInfo: NodeInfo) {
|
||||
checkCertificates(nodeInfo)
|
||||
checkCompositeKeys(nodeInfo)
|
||||
val minimumPlatformVersion = currentNetworkParameters?.minimumPlatformVersion
|
||||
?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
|
||||
if (nodeInfo.platformVersion < minimumPlatformVersion) {
|
||||
throw RequestException("Minimum platform version is $minimumPlatformVersion")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkCompositeKeys(nodeInfo: NodeInfo) {
|
||||
val compositeKeyIdentities = nodeInfo.legalIdentities.filter { it.owningKey is CompositeKey }
|
||||
if (compositeKeyIdentities.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val parameters = checkNotNull(currentNetworkParameters) { "Network parameters not available." }
|
||||
val notaryIdentities = parameters.notaries.map { it.identity }
|
||||
if (!notaryIdentities.containsAll(compositeKeyIdentities)) {
|
||||
throw RequestException("A composite key needs to belong to a notary.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createResponse(payload: Any?, addCacheTimeout: Boolean = false, timestamp: Instant? = null): Response {
|
||||
return if (payload != null) {
|
||||
val ok = Response.ok(payload.serialize().bytes)
|
||||
if (addCacheTimeout) {
|
||||
ok.header("Cache-Control", "max-age=${Duration.ofMillis(config.cacheTimeout).seconds}")
|
||||
}
|
||||
timestamp?.let {
|
||||
ok.header("Last-Modified", DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneId.of("GMT")).format(it))
|
||||
}
|
||||
ok
|
||||
} else {
|
||||
status(Response.Status.NOT_FOUND)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun checkCertificates(nodeInfo: NodeInfo) {
|
||||
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts.first().certPath.x509Certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
|
||||
nodeCaCert ?: throw RequestException("The node certificate path does not contain the node CA certificate type in it.")
|
||||
val nodeCertPath = certificateSigningRequestStorage.getValidCertificatePath(nodeCaCert.publicKey)
|
||||
nodeCertPath ?: throw RequestException("Node certificate is either no longer valid or was never registered.")
|
||||
val rootCert = nodeCertPath.certificates.last().x509
|
||||
try {
|
||||
nodeInfo.legalIdentitiesAndCerts.forEach {
|
||||
validateCertPath(rootCert, it.certPath)
|
||||
}
|
||||
} catch (e: CertPathValidatorException) {
|
||||
throw RequestException("Invalid certificate path.")
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkMapNotInitialisedException(message: String?) : Exception(message)
|
||||
class RequestException(message: String) : Exception(message)
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.isSignatureValid
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.time.Duration
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.Context
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.*
|
||||
import javax.ws.rs.core.Response.Status.UNAUTHORIZED
|
||||
|
||||
/**
|
||||
* Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results.
|
||||
*/
|
||||
@Path("certificate")
|
||||
class RegistrationWebService(private val csrHandler: CsrHandler, private val clientPollInterval: Duration) {
|
||||
@Context lateinit var request: HttpServletRequest
|
||||
/**
|
||||
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval.
|
||||
* Server returns HTTP 200 response with random generated request Id after request has been persisted.
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
fun submitRequest(input: InputStream): Response {
|
||||
val csr = JcaPKCS10CertificationRequest(input.readFully())
|
||||
if (!csr.isSignatureValid()) {
|
||||
return status(Response.Status.BAD_REQUEST).entity("Invalid CSR signature").build()
|
||||
}
|
||||
val requestId = csrHandler.saveRequest(csr)
|
||||
return ok(requestId).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Certificate signing request from storage using the [requestId] and create a signed certificate if request has been approved.
|
||||
* Returns HTTP 200 with DER encoded signed certificates if request has been approved else HTTP 204 No content
|
||||
*/
|
||||
@GET
|
||||
@Path("{var}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun retrieveCert(@PathParam("var") requestId: String): Response {
|
||||
val response = csrHandler.getResponse(requestId)
|
||||
return when (response) {
|
||||
is CertificateResponse.Ready -> {
|
||||
// Write certificate chain to a zip stream and extract the bit array output.
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zip ->
|
||||
// Client certificate must come first and root certificate should come last.
|
||||
val certificates = ArrayList(response.certificatePath.certificates)
|
||||
listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach {
|
||||
zip.putNextEntry(ZipEntry("${it.first}.cer"))
|
||||
zip.write(it.second.encoded)
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
ok(baos.toByteArray())
|
||||
.type("application/zip")
|
||||
.header("Content-Disposition", "attachment; filename=\"certificates.zip\"")
|
||||
}
|
||||
is CertificateResponse.NotReady -> noContent().header("Cache-Control", "max-age=${clientPollInterval.seconds}")
|
||||
is CertificateResponse.Unauthorised -> status(UNAUTHORIZED).entity(response.message)
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.common.utils.initialiseSerialization
|
||||
import com.r3.corda.networkmanage.common.utils.parseConfig
|
||||
import com.r3.corda.networkmanage.hsm.configuration.ManualMode
|
||||
import com.r3.corda.networkmanage.hsm.configuration.SigningServiceArgsParser
|
||||
import com.r3.corda.networkmanage.hsm.configuration.SigningServiceConfig
|
||||
import com.r3.corda.networkmanage.hsm.processor.CrrProcessor
|
||||
import com.r3.corda.networkmanage.hsm.processor.CsrProcessor
|
||||
import com.r3.corda.networkmanage.hsm.processor.NetworkMapProcessor
|
||||
import net.corda.core.internal.exists
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.Security
|
||||
import javax.crypto.Cipher
|
||||
|
||||
private val logger = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main")
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
if (Manifests.exists("Signing-Service-Version")) {
|
||||
println("Signing Service Version: ${Manifests.read("Signing-Service-Version")}")
|
||||
}
|
||||
|
||||
val cmdLineOptions = SigningServiceArgsParser().parseOrExit(*args)
|
||||
|
||||
val config = if (cmdLineOptions.configFile.exists()) {
|
||||
parseConfig<SigningServiceConfig>(cmdLineOptions.configFile)
|
||||
} else {
|
||||
println("Please provide existing HSM config file using --config-file option")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate
|
||||
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
|
||||
require(Cipher.getMaxAllowedKeyLength("AES") >= 256) {
|
||||
"Unlimited Strength Jurisdiction Policy Files must be installed, see http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html"
|
||||
}
|
||||
|
||||
// Ensure the BouncyCastle provider is installed
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
initialiseSerialization()
|
||||
// Create DB connection.
|
||||
val persistence = configureDatabase(config.dataSourceProperties, config.database)
|
||||
if (config.networkMap != null) {
|
||||
NetworkMapProcessor(config.networkMap, config.device, config.keySpecifier, persistence).run()
|
||||
} else if (config.doorman != null) {
|
||||
when (config.doorman.mode) {
|
||||
ManualMode.CSR -> CsrProcessor(config.doorman, config.device, config.keySpecifier, persistence).showMenu()
|
||||
ManualMode.CRL -> CrrProcessor(config.doorman, config.device, config.keySpecifier).showMenu()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm.authentication
|
||||
|
||||
/**
|
||||
* Supported authentication modes
|
||||
*/
|
||||
enum class AuthMode {
|
||||
PASSWORD, CARD_READER, KEY_FILE
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm.authentication
|
||||
|
||||
import CryptoServerJCE.CryptoServerProvider
|
||||
import com.r3.corda.networkmanage.common.signer.AuthenticationException
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Path
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
/**
|
||||
* Performs user authentication against the HSM
|
||||
*/
|
||||
class Authenticator(private val mode: AuthMode = AuthMode.PASSWORD,
|
||||
private val autoUsername: String? = null,
|
||||
private val authKeyFilePath: Path? = null,
|
||||
private val authKeyFilePass: String? = null,
|
||||
private val authStrengthThreshold: Int = 2,
|
||||
inputReader: InputReader = ConsoleInputReader(),
|
||||
private val provider: CryptoServerProvider) : InputReader by inputReader {
|
||||
|
||||
/**
|
||||
* Interactively (using console) authenticates a user against the HSM. Once authentication is successful the
|
||||
* [block] is executed.
|
||||
* @param block to be executed once the authentication process succeeds. The block should take 3 parameters:
|
||||
* 1) [CryptoServerProvider] instance of the certificate provider
|
||||
* 2) List of strings that corresponds to user names authenticated against the HSM.
|
||||
*/
|
||||
fun <T : Any> connectAndAuthenticate(block: (CryptoServerProvider, List<String>) -> T): T {
|
||||
return try {
|
||||
val authenticated = mutableListOf<String>()
|
||||
loop@ while (true) {
|
||||
val user = if (autoUsername.isNullOrEmpty()) {
|
||||
print("Enter User Name (or Q to quit): ")
|
||||
val input = readLine()
|
||||
if (input != null && "q" == input.toLowerCase()) {
|
||||
authenticated.clear()
|
||||
break
|
||||
}
|
||||
input
|
||||
} else {
|
||||
println("Authenticating using preconfigured user name: $autoUsername")
|
||||
autoUsername
|
||||
}
|
||||
when (mode) {
|
||||
AuthMode.CARD_READER -> {
|
||||
println("Authenticating using card reader")
|
||||
println("Accessing the certificate key group data...")
|
||||
provider.loginSign(user, ":cs2:cyb:USB0", null)
|
||||
}
|
||||
AuthMode.KEY_FILE -> {
|
||||
println("Authenticating using preconfigured key file $authKeyFilePath")
|
||||
val password = if (authKeyFilePass == null) {
|
||||
val input = readPassword("Enter key file password (or Q to quit): ")
|
||||
if ("q" == input.toLowerCase().trim()) {
|
||||
authenticated.clear()
|
||||
break@loop
|
||||
} else {
|
||||
input
|
||||
}
|
||||
} else {
|
||||
authKeyFilePass
|
||||
}
|
||||
println("Accessing the certificate key group data...")
|
||||
provider.loginSign(user, authKeyFilePath.toString(), password)
|
||||
}
|
||||
AuthMode.PASSWORD -> {
|
||||
println("Authenticating using password")
|
||||
val password = readPassword("Enter password (or Q to quit): ")
|
||||
if ("q" == password.toLowerCase()) {
|
||||
authenticated.clear()
|
||||
break@loop
|
||||
}
|
||||
println("Accessing the certificate key group data...")
|
||||
provider.loginPassword(user, password)
|
||||
}
|
||||
}
|
||||
authenticated.add(user!!)
|
||||
val auth = provider.cryptoServer.authState
|
||||
if ((auth and 0x0000000F) >= authStrengthThreshold) {
|
||||
println("Authentication sufficient")
|
||||
break
|
||||
} else {
|
||||
println("Need more permissions. Add extra login")
|
||||
}
|
||||
}
|
||||
if (!authenticated.isEmpty()) {
|
||||
block(provider, authenticated)
|
||||
} else {
|
||||
throw AuthenticationException()
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
provider.logoff()
|
||||
} catch (throwable: Throwable) {
|
||||
println("WARNING Exception while logging off")
|
||||
throwable.printStackTrace(System.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Configuration class for [CryptoServerProvider]
|
||||
*/
|
||||
data class CryptoServerProviderConfig(
|
||||
val Device: String = "3001@127.0.0.1",
|
||||
val ConnectionTimeout: Int = 30000,
|
||||
val Timeout: Int = 60000,
|
||||
val EndSessionOnShutdown: Int = 1,
|
||||
val KeepSessionAlive: Int = 0,
|
||||
val KeyGroup: String = "*",
|
||||
val KeySpecifier: Int = -1,
|
||||
val StoreKeysExternal: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates an instance of [CryptoServerProvider] that corresponds to the HSM.
|
||||
*
|
||||
* @param keyGroup HSM key group.
|
||||
* @param keySpecifier HSM key specifier.
|
||||
* @param device HSM device address.
|
||||
*
|
||||
* @return preconfigured instance of [CryptoServerProvider]
|
||||
*/
|
||||
fun createProvider(keyGroup: String, keySpecifier: Int, device: String): CryptoServerProvider {
|
||||
val config = CryptoServerProviderConfig(
|
||||
Device = device,
|
||||
KeyGroup = keyGroup,
|
||||
KeySpecifier = keySpecifier
|
||||
)
|
||||
return createProvider(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [CryptoServerProvider] configured accordingly to the passed configuration.
|
||||
*
|
||||
* @param config crypto server provider configuration.
|
||||
*
|
||||
* @return preconfigured instance of [CryptoServerProvider]
|
||||
*/
|
||||
fun createProvider(config: CryptoServerProviderConfig): CryptoServerProvider {
|
||||
val cfgBuffer = ByteArrayOutputStream()
|
||||
val writer = cfgBuffer.writer(Charsets.UTF_8)
|
||||
for (property in CryptoServerProviderConfig::class.memberProperties) {
|
||||
writer.write("${property.name} = ${property.get(config)}\n")
|
||||
}
|
||||
writer.close()
|
||||
val cfg = cfgBuffer.toByteArray().inputStream()
|
||||
return CryptoServerProvider(cfg)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package com.r3.corda.networkmanage.hsm.authentication
|
||||
|
||||
/**
|
||||
* User input reader interface
|
||||
*/
|
||||
interface InputReader {
|
||||
/**
|
||||
* Reads a single line from user's input.
|
||||
*/
|
||||
fun readLine(): String?
|
||||
|
||||
/**
|
||||
* Reads a single line from user's input. The characters from the input are masked while being entered.
|
||||
* @param format message string displayed before user's input
|
||||
*/
|
||||
fun readPassword(format: String): String
|
||||
}
|
||||
|
||||
class ConsoleInputReader : InputReader {
|
||||
private val console = System.console()
|
||||
|
||||
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
|
||||
override fun readPassword(format: String): String {
|
||||
return if (console != null) {
|
||||
String(console.readPassword(format))
|
||||
} else {
|
||||
print(format)
|
||||
requireNotNull(kotlin.io.readLine()) { "Password required" }
|
||||
}
|
||||
}
|
||||
|
||||
/** Read console line */
|
||||
override fun readLine(): String? {
|
||||
return if (console == null) {
|
||||
kotlin.io.readLine()
|
||||
} else {
|
||||
console.readLine()
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user