mirror of
https://github.com/corda/corda.git
synced 2025-02-12 13:45:48 +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