mirror of
https://github.com/corda/corda.git
synced 2024-12-26 16:11:12 +00:00
Doorman refactoring and improve UX (#160)
* * change corda dependencies to 3.0-NETWORKMAP_SNAPSHOT * packages move fix * address PR issues * * refactorings * cleaned up network management server start up code. * renamed a few classes * segregate doorman and network map config and startup code. * make `config-file` optional, default to ./networkManagement.conf * readme.md and UX changes * added dependency on rpc for the serilization env * move init serilization env to main method to avoid interfering with test * move cert path check to the storage, and remove redundant checks in NodeInfoWebService. * minor fix * some refactoring * fix broken test and added steps to start the network * address PR issues * write root cert to pem file * address PR issues fix bugs in doorman where it try to transit jira ticket to done multiple times * address PR issue * approve request no longer throws exception when approve again, it will simply ignore, test is no longer relevant
This commit is contained in:
parent
b1bac9e103
commit
8af7dc977f
@ -1,8 +1,172 @@
|
||||
The Doorman source code is located under `network-management/src`
|
||||
|
||||
# Building the binaries
|
||||
|
||||
## Network management server
|
||||
To build a fat jar containing all the doorman code you can simply invoke
|
||||
.. sourcecode:: bash
|
||||
./gradlew network-management:buildDoormanJAR
|
||||
```
|
||||
./gradlew network-management:capsule:buildDoormanJAR
|
||||
```
|
||||
|
||||
The built file will appear in
|
||||
``network-management/build/libs/doorman-<VERSION>-capsule.jar``
|
||||
```
|
||||
network-management/capsule/build/libs/doorman-<VERSION>.jar
|
||||
```
|
||||
## HSM signing server
|
||||
To build a fat jar containing all the HSM signer 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
|
||||
|
||||
#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 the certificate signing request approval workflow. This can be turned on by providing JIRA connection configuration in the config file.
|
||||
```
|
||||
doormanConfig {
|
||||
jiraConfig {
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
.
|
||||
.
|
||||
.
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
```
|
||||
networkMapConfig {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
```
|
||||
`cacheTimeout`(ms) indicates how often the network map should poll the database for a newly signed network map. This is also added to the HTTP response header to set the node's network map update frequency.
|
||||
`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 = "."
|
||||
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
|
||||
doormanConfig {
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jiraConfig {
|
||||
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
|
||||
networkMapConfig {
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
```
|
||||
|
||||
# Running the network
|
||||
|
||||
### 1. Create keystore for local signer
|
||||
|
||||
If local signer is enabled, the server will look for keystores in the certificate folder on start up.
|
||||
The keystores can be created using `--mode` flag.
|
||||
```
|
||||
java -jar doorman-<version>.jar --mode ROOT_KEYGEN
|
||||
```
|
||||
and
|
||||
```
|
||||
java -jar doorman-<version>.jar --mode CA_KEYGEN
|
||||
```
|
||||
|
||||
A root certificate `pem` file will also be created, this will be distributed to the client via a "out-of-band" process.
|
||||
Note: We will be distributing a trust store instead of the pem file in future updates.
|
||||
|
||||
### 2. Start Doorman service for notary registration
|
||||
Start the network management server with the doorman service for initial bootstrapping. Network map service 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
|
||||
```
|
||||
|
||||
### 3. Create notary node and register with the doorman
|
||||
After the doorman service is started, copy the `rootcert.pem` file to the notaries' certificates folder and start the `initial-registration` process.
|
||||
|
||||
### 4. Add notary identities to the network parameter
|
||||
The network parameter should contain the name and public key of the newly created notaries.
|
||||
Example network parameter file:
|
||||
|
||||
notaries : [{
|
||||
name: "O=Notary A, L=Port Louis, C=MU, OU=Org Unit, CN=Service Name"
|
||||
key: "GfHq2tTVk9z4eXgyWmExBB3JfHpeuYrk9jUc4zaVVSXpnW8FdCUNDhw6GRGN"
|
||||
validating: true
|
||||
}, {
|
||||
name: "O=Notary B, L=Bali, C=ID, OU=Org Unit, CN=Service Name"
|
||||
key: "GfHq2tTVk9z4eXgyEshv6vtBDjp7n76QZH5hk6VXLhk3vRTAmKcP9F9tRfPj"
|
||||
validating: false
|
||||
}]
|
||||
minimumPlatformVersion = 1
|
||||
maxMessageSize = 100
|
||||
maxTransactionSize = 100
|
||||
|
||||
Save the parameters to `parameter.conf`
|
||||
|
||||
### 5. Load initial network parameters file for network map service
|
||||
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 `--update-network-parameter` flag.
|
||||
We can now restart the network management server with both doorman and network map service.
|
||||
```
|
||||
java -jar doorman-<version>.jar --update-network-parameter parameter.conf
|
||||
```
|
||||
|
@ -60,6 +60,9 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
compile "net.corda:corda-node-api:$corda_dependency_version"
|
||||
|
||||
// TODO remove this when AMQP P2P serialization context is supported.
|
||||
compile "net.corda:corda-rpc:$corda_dependency_version"
|
||||
testCompile "net.corda:corda-node-driver:$corda_dependency_version"
|
||||
testCompile "net.corda:corda-test-common:$corda_dependency_version"
|
||||
|
||||
@ -104,4 +107,11 @@ dependencies {
|
||||
|
||||
// SQL connection pooling library
|
||||
compile "com.zaxxer:HikariCP:2.5.1"
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ configurations {
|
||||
|
||||
task buildHsmJAR(type: FatCapsule, dependsOn: 'jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.hsm.MainKt'
|
||||
archiveName "hsm-${version}-capsule.jar"
|
||||
archiveName "hsm-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_dependency_version
|
||||
systemProperties['visualvm.display.name'] = 'HSM Signing Service'
|
||||
|
@ -12,7 +12,7 @@ configurations {
|
||||
|
||||
task buildDoormanJAR(type: FatCapsule, dependsOn: ':network-management:jar') {
|
||||
applicationClass 'com.r3.corda.networkmanage.doorman.MainKt'
|
||||
archiveName "doorman-${version}-capsule.jar"
|
||||
archiveName "doorman-${version}.jar"
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_dependency_version
|
||||
systemProperties['visualvm.display.name'] = 'Doorman'
|
||||
|
26
network-management/config/network-management-base.conf
Normal file
26
network-management/config/network-management-base.conf
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
||||
doormanConfig{
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
}
|
||||
|
||||
# Network map config
|
||||
# Comment out this section if running without network map service
|
||||
networkMapConfig{
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
33
network-management/config/network-management-jira.conf
Normal file
33
network-management/config/network-management-jira.conf
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
doormanConfig{
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jiraConfig{
|
||||
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
|
||||
networkMapConfig{
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
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
|
||||
doormanConfig{
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
}
|
||||
|
||||
# Network map config
|
||||
# Comment out this section if running without network map service
|
||||
networkMapConfig{
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
basedir = "."
|
||||
host = localhost
|
||||
port = 0
|
||||
rootStorePath = ${basedir}"/certificates/rootstore.jks"
|
||||
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||
keystorePassword = "password"
|
||||
caPrivateKeyPassword = "password"
|
||||
@ -13,10 +14,22 @@ dataSourceProperties {
|
||||
}
|
||||
h2port = 0
|
||||
|
||||
jiraConfig{
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
# Comment out this section if running without doorman service
|
||||
doormanConfig{
|
||||
approveInterval = 10000
|
||||
approveAll = false
|
||||
jiraConfig{
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
}
|
||||
|
||||
# Comment out this section if running without network map service
|
||||
networkMapConfig{
|
||||
cacheTimeout = 600000
|
||||
signInterval = 10000
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.network.NetworkMapClient
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
||||
@ -25,7 +27,6 @@ import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.testNodeConfiguration
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
@ -161,15 +162,18 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to
|
||||
return props
|
||||
}
|
||||
|
||||
fun startDoorman(intermediateCACertAndKey: CertificateAndKeyPair, rootCACert: X509CertificateHolder): DoormanServer {
|
||||
fun startDoorman(intermediateCACertAndKey: CertificateAndKeyPair, rootCACert: X509CertificateHolder): NetworkManagementServer {
|
||||
val signer = LocalSigner(intermediateCACertAndKey.keyPair,
|
||||
arrayOf(intermediateCACertAndKey.certificate.toX509Certificate(), rootCACert.toX509Certificate()))
|
||||
//Start doorman server
|
||||
return startDoorman(signer)
|
||||
}
|
||||
|
||||
fun startDoorman(localSigner: LocalSigner? = null): DoormanServer {
|
||||
fun startDoorman(localSigner: LocalSigner? = null): NetworkManagementServer {
|
||||
val database = configureDatabase(makeTestDataSourceProperties())
|
||||
//Start doorman server
|
||||
return startDoorman(NetworkHostAndPort("localhost", 0), database, true, testNetworkParameters(emptyList()), localSigner, 2, 30, null)
|
||||
val server = NetworkManagementServer()
|
||||
server.start(NetworkHostAndPort("localhost", 0), database, localSigner, testNetworkParameters(emptyList()), NetworkMapConfig(1.minutes.toMillis(), 1.minutes.toMillis()), DoormanConfig(true, null, 3.seconds.toMillis()))
|
||||
|
||||
return server
|
||||
}
|
@ -7,7 +7,8 @@ import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.toX509Certificate
|
||||
import com.r3.corda.networkmanage.doorman.startDoorman
|
||||
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.SignedCertificateRequestStorage
|
||||
@ -24,7 +25,6 @@ import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import org.h2.tools.Server
|
||||
@ -94,44 +94,44 @@ class SigningServiceIntegrationTest {
|
||||
fun `Signing service signs approved CSRs`() {
|
||||
//Start doorman server
|
||||
val database = configureDatabase(makeTestDataSourceProperties())
|
||||
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 30, networkMapParameters = testNetworkParameters(emptyList()))
|
||||
|
||||
// Start Corda network registration.
|
||||
val config = testNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath(),
|
||||
myLegalName = ALICE.name).also {
|
||||
val doormanHostAndPort = doorman.hostAndPort
|
||||
whenever(it.compatibilityZoneURL).thenReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}"))
|
||||
}
|
||||
|
||||
val signingServiceStorage = DBSignedCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties()))
|
||||
|
||||
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.
|
||||
NetworkManagementServer().use { server ->
|
||||
server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
|
||||
// Start Corda network registration.
|
||||
val config = testNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath(),
|
||||
myLegalName = ALICE.name).also {
|
||||
val doormanHostAndPort = server.hostAndPort
|
||||
whenever(it.compatibilityZoneURL).thenReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}"))
|
||||
}
|
||||
}
|
||||
config.rootCaCertFile.parent.createDirectories()
|
||||
X509Utilities.saveCertificateAsPEMFile(rootCACert, config.rootCaCertFile)
|
||||
|
||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
||||
verify(hsmSigner).sign(any())
|
||||
doorman.close()
|
||||
val signingServiceStorage = DBSignedCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties()))
|
||||
|
||||
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.rootCaCertFile.parent.createDirectories()
|
||||
X509Utilities.saveCertificateAsPEMFile(rootCACert, config.rootCaCertFile)
|
||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
||||
verify(hsmSigner).sign(any())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -147,31 +147,31 @@ class SigningServiceIntegrationTest {
|
||||
fun `DEMO - Create CSR and poll`() {
|
||||
//Start doorman server
|
||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig())
|
||||
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 10, networkMapParameters = testNetworkParameters(emptyList()))
|
||||
|
||||
thread(start = true, isDaemon = true) {
|
||||
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")
|
||||
Server.createTcpServer(*h2ServerArgs).start()
|
||||
}
|
||||
|
||||
// Start Corda network registration.
|
||||
(1..3).map {
|
||||
thread(start = true) {
|
||||
|
||||
val config = testNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath(),
|
||||
myLegalName = when (it) {
|
||||
1 -> ALICE.name
|
||||
2 -> BOB.name
|
||||
3 -> CHARLIE.name
|
||||
else -> throw IllegalArgumentException("Unrecognised option")
|
||||
}).also {
|
||||
whenever(it.compatibilityZoneURL).thenReturn(URL("http://$HOST:${doorman.hostAndPort.port}"))
|
||||
}
|
||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
||||
NetworkManagementServer().use { server ->
|
||||
server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
|
||||
thread(start = true, isDaemon = true) {
|
||||
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")
|
||||
Server.createTcpServer(*h2ServerArgs).start()
|
||||
}
|
||||
}.map { it.join() }
|
||||
doorman.close()
|
||||
|
||||
// Start Corda network registration.
|
||||
(1..3).map {
|
||||
thread(start = true) {
|
||||
val config = testNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath(),
|
||||
myLegalName = when (it) {
|
||||
1 -> ALICE.name
|
||||
2 -> BOB.name
|
||||
3 -> CHARLIE.name
|
||||
else -> throw IllegalArgumentException("Unrecognised option")
|
||||
}).also {
|
||||
whenever(it.compatibilityZoneURL).thenReturn(URL("http://$HOST:${server.hostAndPort.port}"))
|
||||
}
|
||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
||||
}
|
||||
}.map { it.join() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.node.NodeInfo
|
||||
|
@ -84,13 +84,13 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
||||
|
||||
override fun approveRequest(requestId: String, approvedBy: String) {
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = findRequest(requestId, RequestStatus.TICKET_CREATED)
|
||||
request ?: throw IllegalArgumentException("Error when approving request with id: $requestId. Request does not exist or its status is not TICKET_CREATED.")
|
||||
val update = request.copy(
|
||||
modifiedBy = listOf(approvedBy),
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.APPROVED)
|
||||
session.merge(update)
|
||||
findRequest(requestId, RequestStatus.TICKET_CREATED)?.let {
|
||||
val update = it.copy(
|
||||
modifiedBy = listOf(approvedBy),
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.APPROVED)
|
||||
session.merge(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,16 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntit
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.hashString
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Database implementation of the [NetworkMapStorage] interface
|
||||
@ -19,13 +21,19 @@ import java.security.cert.CertPath
|
||||
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
|
||||
override fun putNodeInfo(signedNodeInfo: SignedData<NodeInfo>): SecureHash = database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val nodeInfo = signedNodeInfo.verified()
|
||||
val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hashString()
|
||||
val request = singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
|
||||
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash)
|
||||
val certStatusValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
|
||||
builder.and(certPublicKeyHashEq, certStatusValid)
|
||||
val orgName = nodeInfo.legalIdentities.first().name.organisation
|
||||
// TODO: use cert extension to identify NodeCA cert when Ross's work is in.
|
||||
val nodeCACert = nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.map { it as X509Certificate }
|
||||
.find { CordaX500Name.build(it.issuerX500Principal).organisation != orgName && CordaX500Name.build(it.subjectX500Principal).organisation == orgName }
|
||||
|
||||
val request = nodeCACert?.let {
|
||||
singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
|
||||
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString())
|
||||
val certStatusValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
|
||||
builder.and(certPublicKeyHashEq, certStatusValid)
|
||||
}
|
||||
}
|
||||
request ?: throw IllegalArgumentException("CSR data missing for provided node info: $nodeInfo")
|
||||
request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.")
|
||||
/*
|
||||
* Delete any previous [NodeInfoEntity] instance for this CSR
|
||||
* Possibly it should be moved at the network signing process at the network signing process
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.r3.corda.networkmanage.common.utils
|
||||
|
||||
import com.google.common.base.CaseFormat
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import joptsimple.ArgumentAcceptingOptionSpec
|
||||
@ -33,10 +34,10 @@ fun Array<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Un
|
||||
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
|
||||
val optionSpec = it.value
|
||||
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet)
|
||||
}.filterValues { it != null })
|
||||
}.mapKeys { it.key.toCamelcase() }.filterValues { it != null })
|
||||
}
|
||||
|
||||
class ShowHelpException(val parser: OptionParser) : Exception()
|
||||
class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception()
|
||||
|
||||
fun X509CertificateHolder.toX509Certificate(): X509Certificate = X509CertificateFactory().generateCertificate(encoded.inputStream())
|
||||
|
||||
@ -44,4 +45,10 @@ fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateF
|
||||
|
||||
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
|
||||
|
||||
fun DigitalSignature.WithKey.withCert(cert: X509Certificate): DigitalSignatureWithCert = DigitalSignatureWithCert(cert, bytes)
|
||||
fun DigitalSignature.WithKey.withCert(cert: X509Certificate): DigitalSignatureWithCert = DigitalSignatureWithCert(cert, bytes)
|
||||
|
||||
private fun String.toCamelcase(): String {
|
||||
return if (contains('_') || contains('-')) {
|
||||
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this.replace("-", "_"))
|
||||
} else this
|
||||
}
|
@ -1,113 +1,100 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.ShowHelpException
|
||||
import com.typesafe.config.Config
|
||||
import com.r3.corda.networkmanage.common.utils.toConfigWithOptions
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.util.EnumConverter
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.isRegularFile
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
data class DoormanParameters(// TODO Create a localSigning sub-config and put that there
|
||||
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?,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val dataSourceProperties: Properties,
|
||||
val approveAll: Boolean = false,
|
||||
val databaseProperties: Properties? = null,
|
||||
val jiraConfig: JiraConfig? = null,
|
||||
// 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,
|
||||
// TODO Change these to Duration in the future
|
||||
val approveInterval: Long = DEFAULT_APPROVE_INTERVAL,
|
||||
val signInterval: Long = DEFAULT_SIGN_INTERVAL
|
||||
data class NetworkManagementServerParameters(// TODO: Move local signing to signing server.
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val dataSourceProperties: Properties,
|
||||
val databaseProperties: Properties? = null,
|
||||
val mode: Mode,
|
||||
|
||||
val doormanConfig: DoormanConfig?,
|
||||
val networkMapConfig: NetworkMapConfig?,
|
||||
|
||||
val updateNetworkParameters: Path?,
|
||||
|
||||
// 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?
|
||||
|
||||
) {
|
||||
enum class Mode {
|
||||
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
|
||||
}
|
||||
|
||||
data class JiraConfig(
|
||||
val address: String,
|
||||
val projectCode: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val doneTransitionCode: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
val DEFAULT_APPROVE_INTERVAL = 5L // seconds
|
||||
val DEFAULT_SIGN_INTERVAL = 5L // seconds
|
||||
// TODO: Do we really need these defaults?
|
||||
val DEFAULT_APPROVE_INTERVAL = 5.seconds
|
||||
val DEFAULT_SIGN_INTERVAL = 5.seconds
|
||||
}
|
||||
}
|
||||
|
||||
data class CommandLineOptions(val configFile: Path,
|
||||
val updateNetworkParametersFile: Path?,
|
||||
val mode: DoormanParameters.Mode) {
|
||||
init {
|
||||
check(configFile.isRegularFile()) { "Config file $configFile does not exist" }
|
||||
if (updateNetworkParametersFile != null) {
|
||||
check(updateNetworkParametersFile.isRegularFile()) { "Update network parameters file $updateNetworkParametersFile does not exist" }
|
||||
if (updateNetworkParameters != null) {
|
||||
check(updateNetworkParameters.isRegularFile()) { "Update network parameters file $updateNetworkParameters does not exist" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DoormanConfig(val approveAll: Boolean = false,
|
||||
val jiraConfig: JiraConfig? = null,
|
||||
val approveInterval: Long = NetworkManagementServerParameters.DEFAULT_APPROVE_INTERVAL.toMillis())
|
||||
|
||||
data class NetworkMapConfig(val cacheTimeout: Long,
|
||||
// TODO: Move signing to signing server.
|
||||
val signInterval: Long = NetworkManagementServerParameters.DEFAULT_SIGN_INTERVAL.toMillis())
|
||||
|
||||
enum class Mode {
|
||||
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
|
||||
}
|
||||
|
||||
data class JiraConfig(
|
||||
val address: String,
|
||||
val projectCode: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val doneTransitionCode: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the doorman command line options.
|
||||
*/
|
||||
fun parseCommandLine(vararg args: String): CommandLineOptions {
|
||||
val optionParser = OptionParser()
|
||||
val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.describedAs("filepath")
|
||||
val updateNetworkParametersArg = optionParser
|
||||
.accepts("update-network-parameters", "Update network parameters filepath. Currently only network parameters initialisation is supported.")
|
||||
.withRequiredArg()
|
||||
.describedAs("The new network map")
|
||||
.describedAs("filepath")
|
||||
val modeArg = optionParser
|
||||
.accepts("mode", "Set the mode of this application")
|
||||
.withRequiredArg()
|
||||
.withValuesConvertedBy(object : EnumConverter<DoormanParameters.Mode>(DoormanParameters.Mode::class.java) {})
|
||||
.defaultsTo(DoormanParameters.Mode.DOORMAN)
|
||||
val helpOption = optionParser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp()
|
||||
|
||||
val optionSet = optionParser.parse(*args)
|
||||
// Print help and exit on help option or if there are missing options.
|
||||
if (optionSet.has(helpOption) || !optionSet.has(configFileArg)) {
|
||||
throw ShowHelpException(optionParser)
|
||||
fun parseParameters(vararg args: String): NetworkManagementServerParameters {
|
||||
val argConfig = args.toConfigWithOptions {
|
||||
accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.describedAs("filepath")
|
||||
accepts("update-network-parameters", "Update network parameters filepath. Currently only network parameters initialisation is supported.")
|
||||
.withRequiredArg()
|
||||
.describedAs("The new network map")
|
||||
.describedAs("filepath")
|
||||
accepts("mode", "Set the mode of this application")
|
||||
.withRequiredArg()
|
||||
.defaultsTo(Mode.DOORMAN.name)
|
||||
}
|
||||
|
||||
val configFile = Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath()
|
||||
val updateNetworkParametersOptionValue = optionSet.valueOf(updateNetworkParametersArg)
|
||||
val updateNetworkParameters = updateNetworkParametersOptionValue?.let {
|
||||
Paths.get(it).toAbsolutePath()
|
||||
val configFile = if (argConfig.hasPath("configFile")) {
|
||||
Paths.get(argConfig.getString("configFile"))
|
||||
} else {
|
||||
Paths.get(".") / "network-management.conf"
|
||||
}
|
||||
check(configFile.isRegularFile()) { "Config file $configFile does not exist" }
|
||||
|
||||
return CommandLineOptions(configFile, updateNetworkParameters, optionSet.valueOf(modeArg))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a configuration file, which contains all the configuration except the initial values for the network
|
||||
* parameters.
|
||||
*/
|
||||
fun parseParameters(configFile: Path, overrides: Config = ConfigFactory.empty()): DoormanParameters {
|
||||
val config = ConfigFactory
|
||||
.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))
|
||||
return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)))
|
||||
.resolve()
|
||||
return overrides
|
||||
.withFallback(config)
|
||||
.parseAs()
|
||||
}
|
||||
|
||||
|
@ -5,88 +5,158 @@ import com.r3.corda.networkmanage.common.persistence.*
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.ShowHelpException
|
||||
import com.r3.corda.networkmanage.doorman.DoormanServer.Companion.logger
|
||||
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
|
||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
||||
import net.corda.client.rpc.internal.KryoClientSerializationScheme
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.NetworkParameters
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
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
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* DoormanServer runs on Jetty server and provides certificate signing service via http.
|
||||
* The server will require keystorePath, keystore password and key password via command line input.
|
||||
* The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
|
||||
*/
|
||||
// TODO: Move this class to its own file.
|
||||
class DoormanServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
|
||||
class NetworkManagementServer : Closeable {
|
||||
private val doOnClose = mutableListOf<() -> Unit>()
|
||||
lateinit var hostAndPort: NetworkHostAndPort
|
||||
|
||||
override fun close() = doOnClose.forEach { it() }
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<DoormanServer>()
|
||||
val serverStatus = DoormanServerStatus()
|
||||
private val logger = loggerFor<NetworkManagementServer>()
|
||||
}
|
||||
|
||||
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||
handler = HandlerCollection().apply {
|
||||
addHandler(buildServletContextHandler())
|
||||
}
|
||||
}
|
||||
private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService {
|
||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
val nodeInfoStorage = PersistentNodeInfoStorage(database)
|
||||
|
||||
val hostAndPort: NetworkHostAndPort
|
||||
get() = server.connectors.mapNotNull { it as? ServerConnector }
|
||||
.map { NetworkHostAndPort(it.host, it.localPort) }
|
||||
.first()
|
||||
|
||||
override fun close() {
|
||||
logger.info("Shutting down Doorman Web Services...")
|
||||
server.stop()
|
||||
server.join()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
logger.info("Starting Doorman Web Services...")
|
||||
server.start()
|
||||
logger.info("Doorman Web Services started on $hostAndPort")
|
||||
}
|
||||
|
||||
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) }
|
||||
updateNetworkParameters?.let {
|
||||
// Persisting new network parameters
|
||||
val currentNetworkParameters = networkMapStorage.getCurrentNetworkParameters()
|
||||
if (currentNetworkParameters == null) {
|
||||
networkMapStorage.saveNetworkParameters(it)
|
||||
} else {
|
||||
throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.")
|
||||
}
|
||||
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
|
||||
addServlet(jerseyServlet, "/*")
|
||||
}
|
||||
|
||||
// This call will fail if parameter is null in DB.
|
||||
try {
|
||||
val latestParameter = networkMapStorage.getLatestNetworkParameters()
|
||||
logger.info("Starting network map service with network parameters : $latestParameter")
|
||||
} catch (e: NoSuchElementException) {
|
||||
logger.error("No network parameter found, please upload new network parameter before starting network map service. The server will now exit.")
|
||||
exitProcess(-1)
|
||||
}
|
||||
|
||||
val networkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null
|
||||
|
||||
// Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed).
|
||||
if (networkMapSigner != null) {
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
val signingThread = Runnable {
|
||||
try {
|
||||
networkMapSigner.signNetworkMap()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
logger.error("Error encountered when processing node info changes.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(signingThread, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
|
||||
doOnClose += { scheduledExecutor.shutdown() }
|
||||
}
|
||||
|
||||
return NodeInfoWebService(nodeInfoStorage, networkMapStorage, config)
|
||||
}
|
||||
|
||||
|
||||
private fun getDoormanService(config: DoormanConfig, database: CordaPersistence, signer: LocalSigner?, serverStatus: NetworkManagementServerStatus): RegistrationWebService {
|
||||
logger.info("Starting Doorman server.")
|
||||
val requestService = if (config.approveAll) {
|
||||
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
|
||||
ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
|
||||
} else {
|
||||
PersistentCertificateRequestStorage(database)
|
||||
}
|
||||
|
||||
val jiraConfig = config.jiraConfig
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
|
||||
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer))
|
||||
} else {
|
||||
DefaultCsrHandler(requestService, signer)
|
||||
}
|
||||
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
val approvalThread = Runnable {
|
||||
try {
|
||||
serverStatus.lastRequestCheckTime = Instant.now()
|
||||
// Create tickets for requests which don't have one yet.
|
||||
requestProcessor.createTickets()
|
||||
// Process Jira approved tickets.
|
||||
requestProcessor.processApprovedRequests()
|
||||
} 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)
|
||||
doOnClose += { scheduledExecutor.shutdown() }
|
||||
|
||||
return RegistrationWebService(requestProcessor)
|
||||
}
|
||||
|
||||
fun start(hostAndPort: NetworkHostAndPort,
|
||||
database: CordaPersistence,
|
||||
signer: LocalSigner? = null,
|
||||
updateNetworkParameters: NetworkParameters?,
|
||||
networkMapServiceParameter: NetworkMapConfig?,
|
||||
doormanServiceParameter: DoormanConfig?) {
|
||||
|
||||
val services = mutableListOf<Any>()
|
||||
val serverStatus = NetworkManagementServerStatus()
|
||||
|
||||
// TODO: move signing to signing server.
|
||||
networkMapServiceParameter?.let { services += getNetworkMapService(it, database, signer, updateNetworkParameters) }
|
||||
doormanServiceParameter?.let { services += getDoormanService(it, database, signer, serverStatus) }
|
||||
|
||||
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()
|
||||
|
||||
doOnClose += { webServer.close() }
|
||||
this.hostAndPort = webServer.hostAndPort
|
||||
}
|
||||
}
|
||||
|
||||
data class DoormanServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
||||
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
||||
|
||||
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
|
||||
internal fun readPassword(fmt: String): String {
|
||||
@ -121,6 +191,9 @@ fun generateRootKeyPair(rootStorePath: Path, rootKeystorePass: String?, rootPriv
|
||||
rootStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert))
|
||||
rootStore.save(rootStorePath, rootKeystorePassword)
|
||||
|
||||
// TODO: remove this once we create truststore for nodes.
|
||||
X509Utilities.saveCertificateAsPEMFile(selfSignCert, rootStorePath.parent / "rootcert.pem")
|
||||
|
||||
println("Root CA keypair and certificate stored in ${rootStorePath.toAbsolutePath()}.")
|
||||
println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey)
|
||||
}
|
||||
@ -157,85 +230,8 @@ fun generateCAKeyPair(keystorePath: Path, rootStorePath: Path, rootKeystorePass:
|
||||
println(loadKeyStore(keystorePath, keystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey)
|
||||
}
|
||||
|
||||
// TODO: Move this method to DoormanServer.
|
||||
fun startDoorman(hostAndPort: NetworkHostAndPort,
|
||||
database: CordaPersistence,
|
||||
approveAll: Boolean,
|
||||
networkMapParameters: NetworkParameters?,
|
||||
signer: LocalSigner? = null,
|
||||
approveInterval: Long,
|
||||
signInterval: Long,
|
||||
jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer {
|
||||
|
||||
logger.info("Starting Doorman server.")
|
||||
|
||||
val requestService = if (approveAll) {
|
||||
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
|
||||
ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
|
||||
} else {
|
||||
PersistentCertificateRequestStorage(database)
|
||||
}
|
||||
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
|
||||
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer))
|
||||
} else {
|
||||
DefaultCsrHandler(requestService, signer)
|
||||
}
|
||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||
val nodeInfoStorage = PersistentNodeInfoStorage(database)
|
||||
|
||||
if (networkMapParameters != null) {
|
||||
// Persisting new network parameters
|
||||
val currentNetworkParameters = networkMapStorage.getCurrentNetworkParameters()
|
||||
if (currentNetworkParameters == null) {
|
||||
networkMapStorage.saveNetworkParameters(networkMapParameters)
|
||||
} else {
|
||||
throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.")
|
||||
}
|
||||
}
|
||||
|
||||
val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(nodeInfoStorage, networkMapStorage))
|
||||
doorman.start()
|
||||
|
||||
val networkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null
|
||||
|
||||
// Thread process approved request periodically.
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(2)
|
||||
val approvalThread = Runnable {
|
||||
try {
|
||||
DoormanServer.serverStatus.lastRequestCheckTime = Instant.now()
|
||||
// Create tickets for requests which don't have one yet.
|
||||
requestProcessor.createTickets()
|
||||
// Process Jira approved tickets.
|
||||
requestProcessor.processApprovedRequests()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
DoormanServer.logger.error("Error encountered when approving request.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, approveInterval, approveInterval, TimeUnit.SECONDS)
|
||||
// Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed).
|
||||
if (networkMapSigner != null) {
|
||||
val signingThread = Runnable {
|
||||
try {
|
||||
networkMapSigner.signNetworkMap()
|
||||
} catch (e: Exception) {
|
||||
// Log the error and carry on.
|
||||
DoormanServer.logger.error("Error encountered when processing node info changes.", e)
|
||||
}
|
||||
}
|
||||
scheduledExecutor.scheduleAtFixedRate(signingThread, signInterval, signInterval, TimeUnit.SECONDS)
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||
scheduledExecutor.shutdown()
|
||||
doorman.close()
|
||||
})
|
||||
return doorman
|
||||
}
|
||||
|
||||
private fun buildLocalSigner(parameters: DoormanParameters): LocalSigner? {
|
||||
private fun buildLocalSigner(parameters: NetworkManagementServerParameters): LocalSigner? {
|
||||
return parameters.keystorePath?.let {
|
||||
// Get password from console if not in config.
|
||||
val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ")
|
||||
@ -261,33 +257,55 @@ private class ApproveAllCertificateRequestStorage(private val delegate: Certific
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
try {
|
||||
val commandLineOptions = parseCommandLine(*args)
|
||||
val mode = commandLineOptions.mode
|
||||
parseParameters(commandLineOptions.configFile).run {
|
||||
parseParameters(*args).run {
|
||||
println("Starting in $mode mode")
|
||||
when (mode) {
|
||||
DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair(
|
||||
Mode.ROOT_KEYGEN -> generateRootKeyPair(
|
||||
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
||||
rootKeystorePassword,
|
||||
rootPrivateKeyPassword)
|
||||
DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair(
|
||||
Mode.CA_KEYGEN -> generateCAKeyPair(
|
||||
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
|
||||
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
|
||||
rootKeystorePassword,
|
||||
rootPrivateKeyPassword,
|
||||
keystorePassword,
|
||||
caPrivateKeyPassword)
|
||||
DoormanParameters.Mode.DOORMAN -> {
|
||||
Mode.DOORMAN -> {
|
||||
initialiseSerialization()
|
||||
val database = configureDatabase(dataSourceProperties)
|
||||
// TODO: move signing to signing server.
|
||||
val signer = buildLocalSigner(this)
|
||||
val networkParameters = commandLineOptions.updateNetworkParametersFile?.let {
|
||||
|
||||
if (signer != null) {
|
||||
println("Starting network management services with local signer.")
|
||||
}
|
||||
|
||||
val networkManagementServer = NetworkManagementServer()
|
||||
val networkParameter = updateNetworkParameters?.let {
|
||||
println("Parsing network parameter from '${it.fileName}'...")
|
||||
parseNetworkParametersFrom(it)
|
||||
}
|
||||
startDoorman(NetworkHostAndPort(host, port), database, approveAll, networkParameters, signer, approveInterval, signInterval, jiraConfig)
|
||||
networkManagementServer.start(NetworkHostAndPort(host, port), database, signer, networkParameter, networkMapConfig, doormanConfig)
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||
networkManagementServer.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: ShowHelpException) {
|
||||
e.errorMessage?.let(::println)
|
||||
e.parser.printHelpOn(System.out)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialiseSerialization() {
|
||||
val context = KRYO_P2P_CONTEXT
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
},
|
||||
context)
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
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 = loggerFor<NetworkManagementServer>()
|
||||
}
|
||||
|
||||
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, "/*")
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat
|
||||
.forEach { processRequest(it.requestId, it.request) }
|
||||
}
|
||||
|
||||
override fun createTickets() { }
|
||||
override fun createTickets() {}
|
||||
|
||||
private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
|
||||
if (signer != null) {
|
||||
@ -68,10 +68,18 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
|
||||
}
|
||||
|
||||
override fun processApprovedRequests() {
|
||||
jiraClient.getApprovedRequests().forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
|
||||
val approvedRequest = jiraClient.getApprovedRequests()
|
||||
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
|
||||
delegate.processApprovedRequests()
|
||||
val signedRequests = storage.getRequests(RequestStatus.SIGNED).mapNotNull {
|
||||
it.certData?.certPath?.let { certs -> it.requestId to certs }
|
||||
|
||||
val signedRequests = approvedRequest.mapNotNull { (id, _) ->
|
||||
val request = storage.getRequest(id)
|
||||
|
||||
if (request != null && request.status == RequestStatus.SIGNED) {
|
||||
request.certData?.certPath?.let { certs -> id to certs }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
jiraClient.updateSignedRequests(signedRequests)
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
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
|
||||
|
||||
class MonitoringWebService(private val serverStatus: NetworkManagementServerStatus) {
|
||||
@GET
|
||||
@Path("status")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun status(): Response {
|
||||
return Response.ok(ObjectMapper().writeValueAsString(serverStatus)).build()
|
||||
}
|
||||
}
|
@ -1,18 +1,23 @@
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import com.google.common.cache.LoadingCache
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
|
||||
import com.r3.corda.networkmanage.common.utils.hashString
|
||||
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.SignedNetworkMap
|
||||
import java.io.InputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.Context
|
||||
@ -23,54 +28,45 @@ import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(NETWORK_MAP_PATH)
|
||||
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
private val networkMapStorage: NetworkMapStorage) {
|
||||
private val networkMapStorage: NetworkMapStorage,
|
||||
private val config: NetworkMapConfig) {
|
||||
companion object {
|
||||
const val NETWORK_MAP_PATH = "network-map"
|
||||
}
|
||||
|
||||
private val networkMapCache: LoadingCache<Boolean, SignedNetworkMap?> = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
|
||||
.build(CacheLoader.from { _ ->
|
||||
networkMapStorage.getCurrentNetworkMap()
|
||||
})
|
||||
|
||||
@POST
|
||||
@Path("publish")
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun registerNode(input: InputStream): Response {
|
||||
val registrationData = input.readBytes().deserialize<SignedData<NodeInfo>>()
|
||||
|
||||
val nodeInfo = registrationData.verified()
|
||||
|
||||
val certPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.first().publicKey.hashString()))
|
||||
return if (certPath != null) {
|
||||
try {
|
||||
val nodeCAPubKey = certPath.certificates.first().publicKey
|
||||
// Validate node public key
|
||||
nodeInfo.legalIdentitiesAndCerts.forEach {
|
||||
require(it.certPath.certificates.any { it.publicKey == nodeCAPubKey })
|
||||
}
|
||||
val digitalSignature = registrationData.sig
|
||||
require(Crypto.doVerify(nodeCAPubKey, digitalSignature.bytes, registrationData.raw.bytes))
|
||||
// Store the NodeInfo
|
||||
nodeInfoStorage.putNodeInfo(registrationData)
|
||||
ok()
|
||||
} catch (e: Exception) {
|
||||
// Catch exceptions thrown by signature verification.
|
||||
when (e) {
|
||||
is IllegalArgumentException, 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
|
||||
}
|
||||
return try {
|
||||
// Store the NodeInfo
|
||||
nodeInfoStorage.putNodeInfo(registrationData)
|
||||
ok()
|
||||
} catch (e: Exception) {
|
||||
// Catch exceptions thrown by signature verification.
|
||||
when (e) {
|
||||
is IllegalArgumentException, 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
|
||||
}
|
||||
} else {
|
||||
status(Response.Status.BAD_REQUEST).entity("Unknown node info, this public key is not registered or approved by Corda Doorman.")
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
fun getNetworkMap(): Response {
|
||||
// TODO: Cache the response?
|
||||
val currentNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||
val currentNetworkMap = networkMapCache.get(true)
|
||||
return if (currentNetworkMap != null) {
|
||||
ok(currentNetworkMap.serialize().bytes).build()
|
||||
Response.ok(currentNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${Duration.ofMillis(config.cacheTimeout).seconds}")
|
||||
} else {
|
||||
status(Response.Status.NOT_FOUND).build()
|
||||
}
|
||||
status(Response.Status.NOT_FOUND)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -78,10 +74,10 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||
fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response {
|
||||
val nodeInfo = nodeInfoStorage.getNodeInfo(SecureHash.parse(nodeInfoHash))
|
||||
return if (nodeInfo != null) {
|
||||
ok(nodeInfo.serialize().bytes).build()
|
||||
ok(nodeInfo.serialize().bytes)
|
||||
} else {
|
||||
status(Response.Status.NOT_FOUND).build()
|
||||
}
|
||||
status(Response.Status.NOT_FOUND)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -1,13 +1,11 @@
|
||||
package com.r3.corda.networkmanage.doorman.webservice
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||
import com.r3.corda.networkmanage.doorman.DoormanServerStatus
|
||||
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
||||
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 org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
@ -24,7 +22,7 @@ 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 serverStatus: DoormanServerStatus) {
|
||||
class RegistrationWebService(private val csrHandler: CsrHandler) {
|
||||
@Context lateinit var request: HttpServletRequest
|
||||
/**
|
||||
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval.
|
||||
@ -69,11 +67,4 @@ class RegistrationWebService(private val csrHandler: CsrHandler, private val ser
|
||||
is CertificateResponse.Unauthorised -> status(UNAUTHORIZED).entity(response.message)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("status")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun status(): Response {
|
||||
return ok(ObjectMapper().writeValueAsString(serverStatus)).build()
|
||||
}
|
||||
}
|
64
network-management/src/main/resources/log4j2.xml
Normal file
64
network-management/src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Properties>
|
||||
<Property name="log-path">logs</Property>
|
||||
<Property name="log-name">node-${hostName}</Property>
|
||||
<Property name="archive">${sys:log-path}/archive</Property>
|
||||
<Property name="consoleLogLevel">error</Property>
|
||||
<Property name="defaultLogLevel">info</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%highlight{%level{length=1} %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n}{INFO=white,WARN=red,FATAL=bright red}" />
|
||||
</Console>
|
||||
|
||||
<!-- Required for printBasicInfo -->
|
||||
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n" />
|
||||
</Console>
|
||||
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${sys:log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg %X%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="10">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="${sys:defaultLogLevel}">
|
||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Root>
|
||||
<Logger name="BasicInfo" additivity="false">
|
||||
<AppenderRef ref="Console-Appender-Println"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Logger>
|
||||
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -67,26 +67,6 @@ class DBCertificateRequestStorageTest : TestBase() {
|
||||
assertTrue(storage.getRequests(RequestStatus.NEW).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approve request ignores subsequent approvals`() {
|
||||
// Given
|
||||
val (request, _) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(request)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
storage.approveRequest(requestId, "ApproverA")
|
||||
|
||||
var thrown: Exception? = null
|
||||
// When subsequent approval is performed
|
||||
try {
|
||||
storage.approveRequest(requestId, "ApproverB")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
thrown = e
|
||||
}
|
||||
// Then check request has not been approved
|
||||
assertNotNull(thrown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sign request`() {
|
||||
val (csr, _) = createRequest("LegalName")
|
||||
|
@ -5,6 +5,7 @@ import com.typesafe.config.ConfigException
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@ -16,16 +17,16 @@ class DoormanParametersTest {
|
||||
|
||||
@Test
|
||||
fun `should fail when initial network parameters file is missing`() {
|
||||
val message = assertFailsWith<IllegalStateException> {
|
||||
parseCommandLine("--config-file", validConfigPath, "--update-network-parameters", "not-here")
|
||||
}.message
|
||||
val message = assertFailsWith<InvocationTargetException> {
|
||||
parseParameters("--config-file", validConfigPath, "--update-network-parameters", "not-here")
|
||||
}.targetException.message
|
||||
assertThat(message).contains("Update network parameters file ")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when config file is missing`() {
|
||||
val message = assertFailsWith<IllegalStateException> {
|
||||
parseCommandLine("--config-file", "not-existing-file")
|
||||
parseParameters("--config-file", "not-existing-file")
|
||||
}.message
|
||||
assertThat(message).contains("Config file ")
|
||||
}
|
||||
@ -33,28 +34,24 @@ class DoormanParametersTest {
|
||||
@Test
|
||||
fun `should throw ShowHelpException when help option is passed on the command line`() {
|
||||
assertFailsWith<ShowHelpException> {
|
||||
parseCommandLine("-?")
|
||||
parseParameters("-?")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when config missing`() {
|
||||
assertFailsWith<ConfigException.Missing> {
|
||||
parseParameters(parseCommandLine("--config-file", invalidConfigPath).configFile)
|
||||
parseParameters("--config-file", invalidConfigPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse jira config correctly`() {
|
||||
val parameter = parseCommandLineAndGetParameters()
|
||||
val parameter = parseParameters(*validArgs).doormanConfig!!
|
||||
assertEquals("https://doorman-jira-host.com/", parameter.jiraConfig?.address)
|
||||
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
||||
assertEquals("username", parameter.jiraConfig?.username)
|
||||
assertEquals("password", parameter.jiraConfig?.password)
|
||||
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
|
||||
}
|
||||
|
||||
private fun parseCommandLineAndGetParameters(): DoormanParameters {
|
||||
return parseParameters(parseCommandLine(*validArgs).configFile)
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.NetworkMap
|
||||
import net.corda.nodeapi.internal.SignedNetworkMap
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
@ -44,6 +45,7 @@ class NodeInfoWebServiceTest {
|
||||
private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
|
||||
|
||||
private val testNetwotkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis())
|
||||
@Test
|
||||
fun `submit nodeInfo`() {
|
||||
// Create node info.
|
||||
@ -59,41 +61,12 @@ class NodeInfoWebServiceTest {
|
||||
on { getCertificatePath(any()) }.thenReturn(certPath)
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use {
|
||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use {
|
||||
it.start()
|
||||
val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish")
|
||||
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
|
||||
// Post node info and signature to doorman
|
||||
// Post node info and signature to doorman, this should pass without any exception.
|
||||
doPost(registerURL, nodeInfoAndSignature)
|
||||
verify(nodeInfoStorage, times(1)).getCertificatePath(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submit nodeInfo with invalid signature`() {
|
||||
// Create node info.
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public)
|
||||
val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
|
||||
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
|
||||
|
||||
// Create digital signature.
|
||||
val attackerKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val digitalSignature = DigitalSignature.WithKey(attackerKeyPair.public, Crypto.doSign(attackerKeyPair.private, nodeInfo.serialize().bytes))
|
||||
|
||||
val nodeInfoStorage: NodeInfoStorage = mock {
|
||||
on { getCertificatePath(any()) }.thenReturn(certPath)
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use {
|
||||
it.start()
|
||||
val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish")
|
||||
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
|
||||
// Post node info and signature to doorman
|
||||
assertFailsWith(IOException::class) {
|
||||
doPost(registerURL, nodeInfoAndSignature)
|
||||
}
|
||||
verify(nodeInfoStorage, times(1)).getCertificatePath(any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +77,7 @@ class NodeInfoWebServiceTest {
|
||||
val networkMapStorage: NetworkMapStorage = mock {
|
||||
on { getCurrentNetworkMap() }.thenReturn(SignedNetworkMap(serializedNetworkMap, intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert)))
|
||||
}
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage)).use {
|
||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetwotkMapConfig)).use {
|
||||
it.start()
|
||||
val conn = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}").openConnection() as HttpURLConnection
|
||||
val signedNetworkMap = conn.inputStream.readBytes().deserialize<SignedNetworkMap>()
|
||||
@ -127,7 +100,7 @@ class NodeInfoWebServiceTest {
|
||||
on { getNodeInfo(nodeInfoHash) }.thenReturn(SignedData(serializedNodeInfo, keyPair.sign(serializedNodeInfo)))
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use {
|
||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use {
|
||||
it.start()
|
||||
val nodeInfoURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/node-info/$nodeInfoHash")
|
||||
val conn = nodeInfoURL.openConnection()
|
||||
|
@ -42,16 +42,16 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 Ltd", country = "GB"), rootCAKey)
|
||||
private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
|
||||
private lateinit var doormanServer: DoormanServer
|
||||
private lateinit var webServer: NetworkManagementWebServer
|
||||
|
||||
private fun startSigningServer(csrHandler: CsrHandler) {
|
||||
doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus()))
|
||||
doormanServer.start()
|
||||
webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler))
|
||||
webServer.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun close() {
|
||||
doormanServer.close()
|
||||
webServer.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -169,7 +169,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
}
|
||||
|
||||
private fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||
val conn = URL("http://${doormanServer.hostAndPort}/certificate").openConnection() as HttpURLConnection
|
||||
val conn = URL("http://${webServer.hostAndPort}/certificate").openConnection() as HttpURLConnection
|
||||
conn.doOutput = true
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
||||
@ -178,7 +178,7 @@ class RegistrationWebServiceTest : TestBase() {
|
||||
}
|
||||
|
||||
private fun pollForResponse(id: String): PollResponse {
|
||||
val url = URL("http://${doormanServer.hostAndPort}/certificate/$id")
|
||||
val url = URL("http://${webServer.hostAndPort}/certificate/$id")
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user