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:
Patrick Kuo 2017-12-11 10:06:29 +00:00 committed by GitHub
parent b1bac9e103
commit 8af7dc977f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 820 additions and 433 deletions

View File

@ -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 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 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
```

View File

@ -60,6 +60,9 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "net.corda:corda-node-api:$corda_dependency_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-node-driver:$corda_dependency_version"
testCompile "net.corda:corda-test-common:$corda_dependency_version" testCompile "net.corda:corda-test-common:$corda_dependency_version"
@ -104,4 +107,11 @@ dependencies {
// SQL connection pooling library // SQL connection pooling library
compile "com.zaxxer:HikariCP:2.5.1" 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'
} }

View File

@ -12,7 +12,7 @@ configurations {
task buildHsmJAR(type: FatCapsule, dependsOn: 'jar') { task buildHsmJAR(type: FatCapsule, dependsOn: 'jar') {
applicationClass 'com.r3.corda.networkmanage.hsm.MainKt' applicationClass 'com.r3.corda.networkmanage.hsm.MainKt'
archiveName "hsm-${version}-capsule.jar" archiveName "hsm-${version}.jar"
capsuleManifest { capsuleManifest {
applicationVersion = corda_dependency_version applicationVersion = corda_dependency_version
systemProperties['visualvm.display.name'] = 'HSM Signing Service' systemProperties['visualvm.display.name'] = 'HSM Signing Service'

View File

@ -12,7 +12,7 @@ configurations {
task buildDoormanJAR(type: FatCapsule, dependsOn: ':network-management:jar') { task buildDoormanJAR(type: FatCapsule, dependsOn: ':network-management:jar') {
applicationClass 'com.r3.corda.networkmanage.doorman.MainKt' applicationClass 'com.r3.corda.networkmanage.doorman.MainKt'
archiveName "doorman-${version}-capsule.jar" archiveName "doorman-${version}.jar"
capsuleManifest { capsuleManifest {
applicationVersion = corda_dependency_version applicationVersion = corda_dependency_version
systemProperties['visualvm.display.name'] = 'Doorman' systemProperties['visualvm.display.name'] = 'Doorman'

View 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
}

View 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
}

View File

@ -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
}

View File

@ -1,6 +1,7 @@
basedir = "." basedir = "."
host = localhost host = localhost
port = 0 port = 0
rootStorePath = ${basedir}"/certificates/rootstore.jks"
keystorePath = ${basedir}"/certificates/caKeystore.jks" keystorePath = ${basedir}"/certificates/caKeystore.jks"
keystorePassword = "password" keystorePassword = "password"
caPrivateKeyPassword = "password" caPrivateKeyPassword = "password"
@ -13,10 +14,22 @@ dataSourceProperties {
} }
h2port = 0 h2port = 0
jiraConfig{ # Comment out this section if running without doorman service
address = "https://doorman-jira-host.com/" doormanConfig{
projectCode = "TD" approveInterval = 10000
username = "username" approveAll = false
password = "password" jiraConfig{
doneTransitionCode = 41 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
}

View File

@ -16,6 +16,8 @@ import net.corda.core.internal.createDirectories
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort 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.services.network.NetworkMapClient
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper 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.common.internal.testNetworkParameters
import net.corda.testing.testNodeConfiguration import net.corda.testing.testNodeConfiguration
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
@ -161,15 +162,18 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to
return props return props
} }
fun startDoorman(intermediateCACertAndKey: CertificateAndKeyPair, rootCACert: X509CertificateHolder): DoormanServer { fun startDoorman(intermediateCACertAndKey: CertificateAndKeyPair, rootCACert: X509CertificateHolder): NetworkManagementServer {
val signer = LocalSigner(intermediateCACertAndKey.keyPair, val signer = LocalSigner(intermediateCACertAndKey.keyPair,
arrayOf(intermediateCACertAndKey.certificate.toX509Certificate(), rootCACert.toX509Certificate())) arrayOf(intermediateCACertAndKey.certificate.toX509Certificate(), rootCACert.toX509Certificate()))
//Start doorman server //Start doorman server
return startDoorman(signer) return startDoorman(signer)
} }
fun startDoorman(localSigner: LocalSigner? = null): DoormanServer { fun startDoorman(localSigner: LocalSigner? = null): NetworkManagementServer {
val database = configureDatabase(makeTestDataSourceProperties()) val database = configureDatabase(makeTestDataSourceProperties())
//Start doorman server //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
} }

View File

@ -7,7 +7,8 @@ import com.nhaarman.mockito_kotlin.whenever
import com.r3.corda.networkmanage.common.persistence.configureDatabase import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.buildCertPath
import com.r3.corda.networkmanage.common.utils.toX509Certificate 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.ApprovedCertificateRequestData
import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStorage import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStorage
import com.r3.corda.networkmanage.hsm.persistence.SignedCertificateRequestStorage 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.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.common.internal.testNetworkParameters
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.h2.tools.Server import org.h2.tools.Server
@ -94,44 +94,44 @@ class SigningServiceIntegrationTest {
fun `Signing service signs approved CSRs`() { fun `Signing service signs approved CSRs`() {
//Start doorman server //Start doorman server
val database = configureDatabase(makeTestDataSourceProperties()) val database = configureDatabase(makeTestDataSourceProperties())
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true, approveInterval = 2, signInterval = 30, networkMapParameters = testNetworkParameters(emptyList()))
// Start Corda network registration. NetworkManagementServer().use { server ->
val config = testNodeConfiguration( server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
baseDirectory = tempFolder.root.toPath(), // Start Corda network registration.
myLegalName = ALICE.name).also { val config = testNodeConfiguration(
val doormanHostAndPort = doorman.hostAndPort baseDirectory = tempFolder.root.toPath(),
whenever(it.compatibilityZoneURL).thenReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}")) myLegalName = ALICE.name).also {
} val doormanHostAndPort = server.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.
} }
}
config.rootCaCertFile.parent.createDirectories()
X509Utilities.saveCertificateAsPEMFile(rootCACert, config.rootCaCertFile)
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore() val signingServiceStorage = DBSignedCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties()))
verify(hsmSigner).sign(any())
doorman.close() 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`() { fun `DEMO - Create CSR and poll`() {
//Start doorman server //Start doorman server
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig()) 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) { NetworkManagementServer().use { server ->
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers") server.start(NetworkHostAndPort(HOST, 0), database, networkMapServiceParameter = null, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), updateNetworkParameters = null)
Server.createTcpServer(*h2ServerArgs).start() 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()
} }
}.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() }
}
} }
} }

View File

@ -1,6 +1,5 @@
package com.r3.corda.networkmanage.common.persistence package com.r3.corda.networkmanage.common.persistence
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData import net.corda.core.crypto.SignedData
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo

View File

@ -84,13 +84,13 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
override fun approveRequest(requestId: String, approvedBy: String) { override fun approveRequest(requestId: String, approvedBy: String) {
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = findRequest(requestId, RequestStatus.TICKET_CREATED) findRequest(requestId, RequestStatus.TICKET_CREATED)?.let {
request ?: throw IllegalArgumentException("Error when approving request with id: $requestId. Request does not exist or its status is not TICKET_CREATED.") val update = it.copy(
val update = request.copy( modifiedBy = listOf(approvedBy),
modifiedBy = listOf(approvedBy), modifiedAt = Instant.now(),
modifiedAt = Instant.now(), status = RequestStatus.APPROVED)
status = RequestStatus.APPROVED) session.merge(update)
session.merge(update) }
} }
} }

View File

@ -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.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import com.r3.corda.networkmanage.common.utils.buildCertPath 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.SecureHash
import net.corda.core.crypto.SignedData 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.node.NodeInfo
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.X509Certificate
/** /**
* Database implementation of the [NetworkMapStorage] interface * Database implementation of the [NetworkMapStorage] interface
@ -19,13 +21,19 @@ import java.security.cert.CertPath
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
override fun putNodeInfo(signedNodeInfo: SignedData<NodeInfo>): SecureHash = database.transaction(TransactionIsolationLevel.SERIALIZABLE) { override fun putNodeInfo(signedNodeInfo: SignedData<NodeInfo>): SecureHash = database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val nodeInfo = signedNodeInfo.verified() val nodeInfo = signedNodeInfo.verified()
val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hashString() val orgName = nodeInfo.legalIdentities.first().name.organisation
val request = singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> // TODO: use cert extension to identify NodeCA cert when Ross's work is in.
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash) val nodeCACert = nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.map { it as X509Certificate }
val certStatusValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) .find { CordaX500Name.build(it.issuerX500Principal).organisation != orgName && CordaX500Name.build(it.subjectX500Principal).organisation == orgName }
builder.and(certPublicKeyHashEq, certStatusValid)
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 * Delete any previous [NodeInfoEntity] instance for this CSR
* Possibly it should be moved at the network signing process at the network signing process * Possibly it should be moved at the network signing process at the network signing process

View File

@ -1,5 +1,6 @@
package com.r3.corda.networkmanage.common.utils package com.r3.corda.networkmanage.common.utils
import com.google.common.base.CaseFormat
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.ArgumentAcceptingOptionSpec
@ -33,10 +34,10 @@ fun Array<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Un
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues { return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
val optionSpec = it.value val optionSpec = it.value
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet) 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()) 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 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
}

View File

@ -1,113 +1,100 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.common.utils.toConfigWithOptions
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser import net.corda.core.internal.div
import joptsimple.util.EnumConverter
import net.corda.core.internal.isRegularFile import net.corda.core.internal.isRegularFile
import net.corda.core.utilities.seconds
import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.parseAs
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
data class DoormanParameters(// TODO Create a localSigning sub-config and put that there data class NetworkManagementServerParameters(// TODO: Move local signing to signing server.
val keystorePassword: String?, val host: String,
// TODO Should be part of a localSigning sub-config val port: Int,
val caPrivateKeyPassword: String?, val dataSourceProperties: Properties,
// TODO Should be part of a localSigning sub-config val databaseProperties: Properties? = null,
val rootKeystorePassword: String?, val mode: Mode,
// TODO Should be part of a localSigning sub-config
val rootPrivateKeyPassword: String?, val doormanConfig: DoormanConfig?,
val host: String, val networkMapConfig: NetworkMapConfig?,
val port: Int,
val dataSourceProperties: Properties, val updateNetworkParameters: Path?,
val approveAll: Boolean = false,
val databaseProperties: Properties? = null, // TODO Should be part of a localSigning sub-config
val jiraConfig: JiraConfig? = null, val keystorePath: Path? = null,
// TODO Should be part of a localSigning sub-config // TODO Should be part of a localSigning sub-config
val keystorePath: Path? = null, val rootStorePath: Path? = null,
// TODO Should be part of a localSigning sub-config val keystorePassword: String?,
val rootStorePath: Path? = null, // TODO Should be part of a localSigning sub-config
// TODO Change these to Duration in the future val caPrivateKeyPassword: String?,
val approveInterval: Long = DEFAULT_APPROVE_INTERVAL, // TODO Should be part of a localSigning sub-config
val signInterval: Long = DEFAULT_SIGN_INTERVAL 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 { companion object {
val DEFAULT_APPROVE_INTERVAL = 5L // seconds // TODO: Do we really need these defaults?
val DEFAULT_SIGN_INTERVAL = 5L // seconds 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 { init {
check(configFile.isRegularFile()) { "Config file $configFile does not exist" } if (updateNetworkParameters != null) {
if (updateNetworkParametersFile != null) { check(updateNetworkParameters.isRegularFile()) { "Update network parameters file $updateNetworkParameters does not exist" }
check(updateNetworkParametersFile.isRegularFile()) { "Update network parameters file $updateNetworkParametersFile 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. * Parses the doorman command line options.
*/ */
fun parseCommandLine(vararg args: String): CommandLineOptions { fun parseParameters(vararg args: String): NetworkManagementServerParameters {
val optionParser = OptionParser() val argConfig = args.toConfigWithOptions {
val configFileArg = optionParser accepts("config-file", "The path to the config file")
.accepts("config-file", "The path to the config file") .withRequiredArg()
.withRequiredArg() .describedAs("filepath")
.describedAs("filepath") accepts("update-network-parameters", "Update network parameters filepath. Currently only network parameters initialisation is supported.")
val updateNetworkParametersArg = optionParser .withRequiredArg()
.accepts("update-network-parameters", "Update network parameters filepath. Currently only network parameters initialisation is supported.") .describedAs("The new network map")
.withRequiredArg() .describedAs("filepath")
.describedAs("The new network map") accepts("mode", "Set the mode of this application")
.describedAs("filepath") .withRequiredArg()
val modeArg = optionParser .defaultsTo(Mode.DOORMAN.name)
.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)
} }
val configFile = Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath() val configFile = if (argConfig.hasPath("configFile")) {
val updateNetworkParametersOptionValue = optionSet.valueOf(updateNetworkParametersArg) Paths.get(argConfig.getString("configFile"))
val updateNetworkParameters = updateNetworkParametersOptionValue?.let { } else {
Paths.get(it).toAbsolutePath() Paths.get(".") / "network-management.conf"
} }
check(configFile.isRegularFile()) { "Config file $configFile does not exist" }
return CommandLineOptions(configFile, updateNetworkParameters, optionSet.valueOf(modeArg)) return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)))
}
/**
* 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))
.resolve() .resolve()
return overrides
.withFallback(config)
.parseAs() .parseAs()
} }

View File

@ -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.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.ShowHelpException 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.DefaultCsrHandler
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
import com.r3.corda.networkmanage.doorman.signer.LocalSigner 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.NodeInfoWebService
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService 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.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories 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.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.NetworkParameters import net.corda.nodeapi.internal.NetworkParameters
import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.persistence.CordaPersistence 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.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.io.Closeable
import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** class NetworkManagementServer : Closeable {
* DoormanServer runs on Jetty server and provides certificate signing service via http. private val doOnClose = mutableListOf<() -> Unit>()
* The server will require keystorePath, keystore password and key password via command line input. lateinit var hostAndPort: NetworkHostAndPort
* The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
*/ override fun close() = doOnClose.forEach { it() }
// TODO: Move this class to its own file.
class DoormanServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
companion object { companion object {
val logger = loggerFor<DoormanServer>() private val logger = loggerFor<NetworkManagementServer>()
val serverStatus = DoormanServerStatus()
} }
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService {
handler = HandlerCollection().apply { val networkMapStorage = PersistentNetworkMapStorage(database)
addHandler(buildServletContextHandler()) val nodeInfoStorage = PersistentNodeInfoStorage(database)
}
}
val hostAndPort: NetworkHostAndPort updateNetworkParameters?.let {
get() = server.connectors.mapNotNull { it as? ServerConnector } // Persisting new network parameters
.map { NetworkHostAndPort(it.host, it.localPort) } val currentNetworkParameters = networkMapStorage.getCurrentNetworkParameters()
.first() if (currentNetworkParameters == null) {
networkMapStorage.saveNetworkParameters(it)
override fun close() { } else {
logger.info("Shutting down Doorman Web Services...") throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.")
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) }
} }
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). */ /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
internal fun readPassword(fmt: String): String { 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.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert))
rootStore.save(rootStorePath, rootKeystorePassword) 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("Root CA keypair and certificate stored in ${rootStorePath.toAbsolutePath()}.")
println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey) 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) 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.") private fun buildLocalSigner(parameters: NetworkManagementServerParameters): LocalSigner? {
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? {
return parameters.keystorePath?.let { return parameters.keystorePath?.let {
// Get password from console if not in config. // Get password from console if not in config.
val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ")
@ -261,33 +257,55 @@ private class ApproveAllCertificateRequestStorage(private val delegate: Certific
fun main(args: Array<String>) { fun main(args: Array<String>) {
try { try {
val commandLineOptions = parseCommandLine(*args) parseParameters(*args).run {
val mode = commandLineOptions.mode
parseParameters(commandLineOptions.configFile).run {
println("Starting in $mode mode") println("Starting in $mode mode")
when (mode) { when (mode) {
DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair( Mode.ROOT_KEYGEN -> generateRootKeyPair(
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"), rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword, rootKeystorePassword,
rootPrivateKeyPassword) rootPrivateKeyPassword)
DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair( Mode.CA_KEYGEN -> generateCAKeyPair(
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"), keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"), rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword, rootKeystorePassword,
rootPrivateKeyPassword, rootPrivateKeyPassword,
keystorePassword, keystorePassword,
caPrivateKeyPassword) caPrivateKeyPassword)
DoormanParameters.Mode.DOORMAN -> { Mode.DOORMAN -> {
initialiseSerialization()
val database = configureDatabase(dataSourceProperties) val database = configureDatabase(dataSourceProperties)
// TODO: move signing to signing server.
val signer = buildLocalSigner(this) 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) 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) { } catch (e: ShowHelpException) {
e.errorMessage?.let(::println)
e.parser.printHelpOn(System.out) e.parser.printHelpOn(System.out)
} }
} }
private fun initialiseSerialization() {
val context = KRYO_P2P_CONTEXT
nodeSerializationEnv = SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme())
},
context)
}

View File

@ -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, "/*")
}
}
}

View File

@ -22,7 +22,7 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat
.forEach { processRequest(it.requestId, it.request) } .forEach { processRequest(it.requestId, it.request) }
} }
override fun createTickets() { } override fun createTickets() {}
private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
if (signer != null) { if (signer != null) {
@ -68,10 +68,18 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
} }
override fun processApprovedRequests() { 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() 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() }.toMap()
jiraClient.updateSignedRequests(signedRequests) jiraClient.updateSignedRequests(signedRequests)
} }

View File

@ -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()
}
}

View File

@ -1,18 +1,23 @@
package com.r3.corda.networkmanage.doorman.webservice 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.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage 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 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.SecureHash
import net.corda.core.crypto.SignedData import net.corda.core.crypto.SignedData
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNetworkMap
import java.io.InputStream import java.io.InputStream
import java.security.InvalidKeyException import java.security.InvalidKeyException
import java.security.SignatureException import java.security.SignatureException
import java.time.Duration
import java.util.concurrent.TimeUnit
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.ws.rs.* import javax.ws.rs.*
import javax.ws.rs.core.Context import javax.ws.rs.core.Context
@ -23,54 +28,45 @@ import javax.ws.rs.core.Response.status
@Path(NETWORK_MAP_PATH) @Path(NETWORK_MAP_PATH)
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapStorage: NetworkMapStorage) { private val networkMapStorage: NetworkMapStorage,
private val config: NetworkMapConfig) {
companion object { companion object {
const val NETWORK_MAP_PATH = "network-map" 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 @POST
@Path("publish") @Path("publish")
@Consumes(MediaType.APPLICATION_OCTET_STREAM) @Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun registerNode(input: InputStream): Response { fun registerNode(input: InputStream): Response {
val registrationData = input.readBytes().deserialize<SignedData<NodeInfo>>() val registrationData = input.readBytes().deserialize<SignedData<NodeInfo>>()
return try {
val nodeInfo = registrationData.verified() // Store the NodeInfo
nodeInfoStorage.putNodeInfo(registrationData)
val certPath = nodeInfoStorage.getCertificatePath(SecureHash.parse(nodeInfo.legalIdentitiesAndCerts.first().certPath.certificates.first().publicKey.hashString())) ok()
return if (certPath != null) { } catch (e: Exception) {
try { // Catch exceptions thrown by signature verification.
val nodeCAPubKey = certPath.certificates.first().publicKey when (e) {
// Validate node public key is IllegalArgumentException, is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message)
nodeInfo.legalIdentitiesAndCerts.forEach { // Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
require(it.certPath.certificates.any { it.publicKey == nodeCAPubKey }) else -> throw e
}
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
}
} }
} else {
status(Response.Status.BAD_REQUEST).entity("Unknown node info, this public key is not registered or approved by Corda Doorman.")
}.build() }.build()
} }
@GET @GET
fun getNetworkMap(): Response { fun getNetworkMap(): Response {
// TODO: Cache the response? val currentNetworkMap = networkMapCache.get(true)
val currentNetworkMap = networkMapStorage.getCurrentNetworkMap()
return if (currentNetworkMap != null) { 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 { } else {
status(Response.Status.NOT_FOUND).build() status(Response.Status.NOT_FOUND)
} }.build()
} }
@GET @GET
@ -78,10 +74,10 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response { fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response {
val nodeInfo = nodeInfoStorage.getNodeInfo(SecureHash.parse(nodeInfoHash)) val nodeInfo = nodeInfoStorage.getNodeInfo(SecureHash.parse(nodeInfoHash))
return if (nodeInfo != null) { return if (nodeInfo != null) {
ok(nodeInfo.serialize().bytes).build() ok(nodeInfo.serialize().bytes)
} else { } else {
status(Response.Status.NOT_FOUND).build() status(Response.Status.NOT_FOUND)
} }.build()
} }
@GET @GET

View File

@ -1,13 +1,11 @@
package com.r3.corda.networkmanage.doorman.webservice package com.r3.corda.networkmanage.doorman.webservice
import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.doorman.DoormanServerStatus
import com.r3.corda.networkmanage.doorman.signer.CsrHandler 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_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_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.X509Utilities.CORDA_ROOT_CA
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.codehaus.jackson.map.ObjectMapper
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipEntry 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. * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results.
*/ */
@Path("certificate") @Path("certificate")
class RegistrationWebService(private val csrHandler: CsrHandler, private val serverStatus: DoormanServerStatus) { class RegistrationWebService(private val csrHandler: CsrHandler) {
@Context lateinit var request: HttpServletRequest @Context lateinit var request: HttpServletRequest
/** /**
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval. * 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) is CertificateResponse.Unauthorised -> status(UNAUTHORIZED).entity(response.message)
}.build() }.build()
} }
@GET
@Path("status")
@Produces(MediaType.APPLICATION_JSON)
fun status(): Response {
return ok(ObjectMapper().writeValueAsString(serverStatus)).build()
}
} }

View 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>

View File

@ -67,26 +67,6 @@ class DBCertificateRequestStorageTest : TestBase() {
assertTrue(storage.getRequests(RequestStatus.NEW).isEmpty()) 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 @Test
fun `sign request`() { fun `sign request`() {
val (csr, _) = createRequest("LegalName") val (csr, _) = createRequest("LegalName")

View File

@ -5,6 +5,7 @@ import com.typesafe.config.ConfigException
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.lang.reflect.InvocationTargetException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
@ -16,16 +17,16 @@ class DoormanParametersTest {
@Test @Test
fun `should fail when initial network parameters file is missing`() { fun `should fail when initial network parameters file is missing`() {
val message = assertFailsWith<IllegalStateException> { val message = assertFailsWith<InvocationTargetException> {
parseCommandLine("--config-file", validConfigPath, "--update-network-parameters", "not-here") parseParameters("--config-file", validConfigPath, "--update-network-parameters", "not-here")
}.message }.targetException.message
assertThat(message).contains("Update network parameters file ") assertThat(message).contains("Update network parameters file ")
} }
@Test @Test
fun `should fail when config file is missing`() { fun `should fail when config file is missing`() {
val message = assertFailsWith<IllegalStateException> { val message = assertFailsWith<IllegalStateException> {
parseCommandLine("--config-file", "not-existing-file") parseParameters("--config-file", "not-existing-file")
}.message }.message
assertThat(message).contains("Config file ") assertThat(message).contains("Config file ")
} }
@ -33,28 +34,24 @@ class DoormanParametersTest {
@Test @Test
fun `should throw ShowHelpException when help option is passed on the command line`() { fun `should throw ShowHelpException when help option is passed on the command line`() {
assertFailsWith<ShowHelpException> { assertFailsWith<ShowHelpException> {
parseCommandLine("-?") parseParameters("-?")
} }
} }
@Test @Test
fun `should fail when config missing`() { fun `should fail when config missing`() {
assertFailsWith<ConfigException.Missing> { assertFailsWith<ConfigException.Missing> {
parseParameters(parseCommandLine("--config-file", invalidConfigPath).configFile) parseParameters("--config-file", invalidConfigPath)
} }
} }
@Test @Test
fun `should parse jira config correctly`() { fun `should parse jira config correctly`() {
val parameter = parseCommandLineAndGetParameters() val parameter = parseParameters(*validArgs).doormanConfig!!
assertEquals("https://doorman-jira-host.com/", parameter.jiraConfig?.address) assertEquals("https://doorman-jira-host.com/", parameter.jiraConfig?.address)
assertEquals("TD", parameter.jiraConfig?.projectCode) assertEquals("TD", parameter.jiraConfig?.projectCode)
assertEquals("username", parameter.jiraConfig?.username) assertEquals("username", parameter.jiraConfig?.username)
assertEquals("password", parameter.jiraConfig?.password) assertEquals("password", parameter.jiraConfig?.password)
assertEquals(41, parameter.jiraConfig?.doneTransitionCode) assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
} }
private fun parseCommandLineAndGetParameters(): DoormanParameters {
return parseParameters(parseCommandLine(*validArgs).configFile)
}
} }

View File

@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.NetworkMap import net.corda.nodeapi.internal.NetworkMap
import net.corda.nodeapi.internal.SignedNetworkMap import net.corda.nodeapi.internal.SignedNetworkMap
import net.corda.nodeapi.internal.crypto.CertificateType 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 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 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 @Test
fun `submit nodeInfo`() { fun `submit nodeInfo`() {
// Create node info. // Create node info.
@ -59,41 +61,12 @@ class NodeInfoWebServiceTest {
on { getCertificatePath(any()) }.thenReturn(certPath) on { getCertificatePath(any()) }.thenReturn(certPath)
} }
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock())).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetwotkMapConfig)).use {
it.start() it.start()
val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish") val registerURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/publish")
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes 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) 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 { val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentNetworkMap() }.thenReturn(SignedNetworkMap(serializedNetworkMap, intermediateCAKey.sign(serializedNetworkMap).withCert(intermediateCACert.cert))) 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() it.start()
val conn = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}").openConnection() as HttpURLConnection val conn = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}").openConnection() as HttpURLConnection
val signedNetworkMap = conn.inputStream.readBytes().deserialize<SignedNetworkMap>() val signedNetworkMap = conn.inputStream.readBytes().deserialize<SignedNetworkMap>()
@ -127,7 +100,7 @@ class NodeInfoWebServiceTest {
on { getNodeInfo(nodeInfoHash) }.thenReturn(SignedData(serializedNodeInfo, keyPair.sign(serializedNodeInfo))) 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() it.start()
val nodeInfoURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/node-info/$nodeInfoHash") val nodeInfoURL = URL("http://${it.hostAndPort}/${NodeInfoWebService.NETWORK_MAP_PATH}/node-info/$nodeInfoHash")
val conn = nodeInfoURL.openConnection() val conn = nodeInfoURL.openConnection()

View File

@ -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 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 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 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) { private fun startSigningServer(csrHandler: CsrHandler) {
doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus())) webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler))
doormanServer.start() webServer.start()
} }
@After @After
fun close() { fun close() {
doormanServer.close() webServer.close()
} }
@Test @Test
@ -169,7 +169,7 @@ class RegistrationWebServiceTest : TestBase() {
} }
private fun submitRequest(request: PKCS10CertificationRequest): String { 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.doOutput = true
conn.requestMethod = "POST" conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM) conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
@ -178,7 +178,7 @@ class RegistrationWebServiceTest : TestBase() {
} }
private fun pollForResponse(id: String): PollResponse { 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 val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "GET" conn.requestMethod = "GET"