Merge branch 'master' of https://github.com/corda/enterprise into christians_perftestflows

This commit is contained in:
Christian Sailer 2017-10-19 09:54:06 +01:00
commit f579393d88
34 changed files with 1146 additions and 607 deletions

View File

@ -85,13 +85,12 @@ object NodeInfoSchemaV1 : MappedSchema(
@Table(name = "node_info_party_cert") @Table(name = "node_info_party_cert")
data class DBPartyAndCertificate( data class DBPartyAndCertificate(
@Id @Id
@Column(name = "owning_key", length = 65535, nullable = false)
val owningKey: String,
//@Id // TODO Do we assume that names are unique? Note: We can't have it as Id, because our toString on X500 is inconsistent.
@Column(name = "party_name", nullable = false) @Column(name = "party_name", nullable = false)
val name: String, val name: String,
@Column(name = "owning_key", length = 65535, nullable = false)
val owningKey: String,
@Column(name = "party_cert_binary") @Column(name = "party_cert_binary")
@Lob @Lob
val partyCertBinary: ByteArray, val partyCertBinary: ByteArray,
@ -102,10 +101,10 @@ object NodeInfoSchemaV1 : MappedSchema(
private val persistentNodeInfos: Set<PersistentNodeInfo> = emptySet() private val persistentNodeInfos: Set<PersistentNodeInfo> = emptySet()
) { ) {
constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false) constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false)
: this(partyAndCert.party.owningKey.toBase58String(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) : this(partyAndCert.party.name.toString(), partyAndCert.party.owningKey.toBase58String(), partyAndCert.serialize().bytes, isMain)
fun toLegalIdentityAndCert(): PartyAndCertificate { fun toLegalIdentityAndCert(): PartyAndCertificate {
return partyCertBinary.deserialize<PartyAndCertificate>() return partyCertBinary.deserialize()
} }
} }
} }

View File

@ -1,7 +1,7 @@
ext { ext {
// We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are // We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are
// align with the corresponding Corda release. // align with the corresponding Corda release.
corda_dependency_version = '0.16-20170913.101300-6' corda_dependency_version = '1.0.0'
} }
version "$corda_dependency_version" version "$corda_dependency_version"

View File

@ -1,16 +1,13 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.google.common.net.HostAndPort
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.signer.DefaultCsrHandler import com.r3.corda.doorman.signer.Signer
import com.r3.corda.doorman.signer.LocalSigner
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.cert import net.corda.core.internal.cert
import net.corda.core.utilities.subject import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.utilities.* import net.corda.node.utilities.*
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
@ -29,10 +26,9 @@ class DoormanIntegrationTest {
val tempFolder = TemporaryFolder() val tempFolder = TemporaryFolder()
@Test @Test
fun `Network Registration With Doorman`() { fun `initial registration`() {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", organisation = "R3 Ltd", val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootCAKey)
locality = "London", country = "GB").x500Name, rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey,
CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", organisation = "R3 Ltd"), intermediateCAKey.public) CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", organisation = "R3 Ltd"), intermediateCAKey.public)
@ -41,16 +37,17 @@ class DoormanIntegrationTest {
// Identity service not needed doorman, corda persistence is not very generic. // Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException() throw UnsupportedOperationException()
}) })
val signer = Signer(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()))
//Start doorman server //Start doorman server
val storage = ApprovingAllCertificateRequestStorage(database) val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, signer, null)
val doorman = DoormanServer(HostAndPort.fromParts("localhost", 0), DefaultCsrHandler(storage, LocalSigner(storage, CertificateAndKeyPair(intermediateCACert, intermediateCAKey), rootCACert.toX509Certificate())))
doorman.start()
// Start Corda network registration. // Start Corda network registration.
val config = testNodeConfiguration( val config = testNodeConfiguration(
baseDirectory = tempFolder.root.toPath(), baseDirectory = tempFolder.root.toPath(),
myLegalName = ALICE.name).also { myLegalName = ALICE.name).also {
whenever(it.certificateSigningService).thenReturn(URL("http://localhost:${doorman.hostAndPort.port}")) whenever(it.certificateSigningService).thenReturn(URL("http://localhost:${doorman.hostAndPort.port}"))
whenever(it.emailAddress).thenReturn("iTest@R3.com")
} }
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore() NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore()
@ -62,24 +59,23 @@ class DoormanIntegrationTest {
loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply { loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply {
assert(containsAlias(X509Utilities.CORDA_CLIENT_CA)) assert(containsAlias(X509Utilities.CORDA_CLIENT_CA))
assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subject) assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal)
assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList()) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList())
} }
loadKeyStore(config.sslKeystore, config.keyStorePassword).apply { loadKeyStore(config.sslKeystore, config.keyStorePassword).apply {
assert(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) assert(containsAlias(X509Utilities.CORDA_CLIENT_TLS))
assertEquals(ALICE.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name, getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subject) assertEquals(ALICE.name.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal)
assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(2).toList()) assertEquals(listOf(intermediateCACert.cert, rootCACert.cert), getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(2).toList())
} }
loadKeyStore(config.trustStoreFile, config.trustStorePassword).apply { loadKeyStore(config.trustStoreFile, config.trustStorePassword).apply {
assert(containsAlias(X509Utilities.CORDA_ROOT_CA)) assert(containsAlias(X509Utilities.CORDA_ROOT_CA))
assertEquals(rootCACert.cert.subject, getX509Certificate(X509Utilities.CORDA_ROOT_CA).subject) assertEquals(rootCACert.cert.subjectX500Principal, getX509Certificate(X509Utilities.CORDA_ROOT_CA).subjectX500Principal)
} }
doorman.close() doorman.close()
} }
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties() val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")

View File

@ -18,12 +18,12 @@ data class DoormanParameters(val basedir: Path,
val port: Int, val port: Int,
val dataSourceProperties: Properties, val dataSourceProperties: Properties,
val mode: Mode, val mode: Mode,
val approveAll: Boolean = false,
val databaseProperties: Properties? = null, val databaseProperties: Properties? = null,
val jiraConfig: JiraConfig? = null, val jiraConfig: JiraConfig? = null,
val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks", val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks",
val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks" val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks"
) { ) {
enum class Mode { enum class Mode {
DOORMAN, CA_KEYGEN, ROOT_KEYGEN DOORMAN, CA_KEYGEN, ROOT_KEYGEN
} }
@ -57,6 +57,7 @@ fun parseParameters(vararg args: String): DoormanParameters {
} else { } else {
Paths.get(argConfig.getString("basedir")) / "node.conf" Paths.get(argConfig.getString("basedir")) / "node.conf"
} }
val config = argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))).resolve() return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)))
return config.parseAs<DoormanParameters>() .resolve()
.parseAs()
} }

View File

@ -0,0 +1,74 @@
package com.r3.corda.doorman
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.internal.country
import net.corda.core.internal.locality
import net.corda.core.internal.organisation
import net.corda.core.utilities.loggerFor
import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.security.cert.CertPath
class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) {
companion object {
val logger = loggerFor<JiraClient>()
}
// The JIRA project must have a Request ID field and the Task issue type.
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Make sure request has been accepted.
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded))
}
val organisation = signingRequest.subject.organisation
val nearestCity = signingRequest.subject.locality
val country = signingRequest.subject.country
val email = signingRequest.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}")
.setSummary(organisation)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
fun getApprovedRequests(): List<Pair<String, String>> {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
return issues.map { issue ->
issue.getField(requestIdField.id)?.value?.toString().let {
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
val approvedBy = issue.assignee?.displayName ?: "Unknown"
Pair(requestId, approvedBy)
}
}
}
fun updateSignedRequests(signedRequests: Map<String, CertPath>) {
// Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first()
// Jira only support ~ (contains) search for custom textfield.
val issue = restClient.searchClient.searchJql("'Request ID' ~ $id").claim().issues.firstOrNull()
if (issue != null) {
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
}
}
}
}

View File

@ -1,22 +1,24 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.google.common.net.HostAndPort
import com.r3.corda.doorman.DoormanServer.Companion.logger import com.r3.corda.doorman.DoormanServer.Companion.logger
import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.signer.* import com.r3.corda.doorman.persistence.PersistenceNodeInfoStorage
import com.r3.corda.doorman.signer.DefaultCsrHandler
import com.r3.corda.doorman.signer.JiraCsrHandler
import com.r3.corda.doorman.signer.Signer
import com.r3.corda.doorman.webservice.NodeInfoWebService
import com.r3.corda.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.utilities.* import net.corda.node.utilities.*
import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA import org.bouncycastle.pkcs.PKCS10CertificationRequest
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
import net.corda.node.utilities.X509Utilities.createCertificate
import org.bouncycastle.asn1.x500.X500Name
import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection import org.eclipse.jetty.server.handler.HandlerCollection
@ -25,37 +27,34 @@ import org.eclipse.jetty.servlet.ServletHolder
import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.lang.Thread.sleep
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.nio.file.Path
import java.time.Instant import java.time.Instant
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
* DoormanServer runs on Jetty server and provide certificate signing service via http. * 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 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] * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
*/ */
class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandler) : Closeable { // TODO: Move this class to its own file.
val serverStatus = DoormanServerStatus() class DoormanServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
companion object { companion object {
val logger = loggerFor<DoormanServer>() val logger = loggerFor<DoormanServer>()
val serverStatus = DoormanServerStatus()
} }
private val server: Server = Server(InetSocketAddress(webServerAddr.host, webServerAddr.port)).apply { private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
handler = HandlerCollection().apply { handler = HandlerCollection().apply {
addHandler(buildServletContextHandler()) addHandler(buildServletContextHandler())
} }
} }
val hostAndPort: HostAndPort val hostAndPort: NetworkHostAndPort
get() = server.connectors get() = server.connectors.mapNotNull { it as? ServerConnector }
.map { it as? ServerConnector } .map { NetworkHostAndPort(it.host, it.localPort) }
.filterNotNull()
.map { HostAndPort.fromParts(it.host, it.localPort) }
.first() .first()
override fun close() { override fun close() {
@ -68,22 +67,6 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle
logger.info("Starting Doorman Web Services...") logger.info("Starting Doorman Web Services...")
server.start() server.start()
logger.info("Doorman Web Services started on $hostAndPort") logger.info("Doorman Web Services started on $hostAndPort")
serverStatus.serverStartTime = Instant.now()
// Thread approving request periodically.
thread(name = "Request Approval Thread") {
while (true) {
try {
sleep(10.seconds.toMillis())
// TODO: Handle rejected request?
serverStatus.lastRequestCheckTime = Instant.now()
csrHandler.sign()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when approving request.", e)
}
}
}
} }
private fun buildServletContextHandler(): ServletContextHandler { private fun buildServletContextHandler(): ServletContextHandler {
@ -91,18 +74,15 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle
contextPath = "/" contextPath = "/"
val resourceConfig = ResourceConfig().apply { val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here // Add your API provider classes (annotated for JAX-RS) here
register(DoormanWebService(csrHandler, serverStatus)) webServices.forEach { register(it) }
}
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
initOrder = 0 // Initialise at server start
} }
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
addServlet(jerseyServlet, "/api/*") addServlet(jerseyServlet, "/api/*")
} }
} }
} }
data class DoormanServerStatus(var serverStartTime: Instant? = null, data class DoormanServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
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 {
@ -110,111 +90,134 @@ internal fun readPassword(fmt: String): String {
String(System.console().readPassword(fmt)) String(System.console().readPassword(fmt))
} else { } else {
print(fmt) print(fmt)
readLine()!! readLine() ?: ""
} }
} }
private fun DoormanParameters.generateRootKeyPair() { // Keygen utilities.
if (rootStorePath == null) { // TODO: Move keygen methods to Utilities.kt
throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!") fun generateRootKeyPair(rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?) {
}
println("Generating Root CA keypair and certificate.") println("Generating Root CA keypair and certificate.")
// Get password from console if not in config. // Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
// Ensure folder exists. // Ensure folder exists.
rootStorePath.parent.createDirectories() rootStorePath.parent.createDirectories()
val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword) val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword)
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
if (rootStore.containsAlias(CORDA_ROOT_CA)) { if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey
println("Key $CORDA_ROOT_CA already exists in keystore, process will now terminate.") println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
println(oldKey) println(oldKey)
exitProcess(1) exitProcess(1)
} }
val selfSignKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val selfSignKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Root CA, O=R3, OU=Corda, L=London, C=GB"), selfSignKey) val selfSignCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB", organisationUnit = "Corda", state = null), selfSignKey)
rootStore.addOrReplaceKey(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)
println("Root CA keypair and certificate stored in $rootStorePath.") println("Root CA keypair and certificate stored in $rootStorePath.")
println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey) println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey)
} }
private fun DoormanParameters.generateCAKeyPair() { fun generateCAKeyPair(keystorePath: Path, rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
if (keystorePath == null) {
throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!")
}
if (rootStorePath == null) {
throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!")
}
println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.") println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.")
// Get password from console if not in config. // Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword) val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword)
val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA, rootPrivateKeyPassword) val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, rootPrivateKeyPassword)
val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") val keystorePassword = keystorePass ?: readPassword("Keystore Password: ")
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") val caPrivateKeyPassword = caPrivateKeyPass ?: readPassword("CA Private Key Password: ")
// Ensure folder exists. // Ensure folder exists.
keystorePath.parent.createDirectories() keystorePath.parent.createDirectories()
val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword)
if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA)) { if (keyStore.containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) {
val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey
println("Key $CORDA_INTERMEDIATE_CA already exists in keystore, process will now terminate.") println("Key ${X509Utilities.CORDA_INTERMEDIATE_CA} already exists in keystore, process will now terminate.")
println(oldKey) println(oldKey)
exitProcess(1) exitProcess(1)
} }
val intermediateKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCert = createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, X500Name("CN=Corda Intermediate CA, O=R3, OU=Corda, L=London, C=GB"), intermediateKey.public) val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair,
keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA, intermediateKey.private, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", organisationUnit = "Corda", locality = "London", country = "GB", state = null), intermediateKey.public)
keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateKey.private,
caPrivateKeyPassword.toCharArray(), arrayOf(intermediateCert, rootKeyAndCert.certificate)) caPrivateKeyPassword.toCharArray(), arrayOf(intermediateCert, rootKeyAndCert.certificate))
keyStore.save(keystorePath, keystorePassword) keyStore.save(keystorePath, keystorePassword)
println("Intermediate CA keypair and certificate stored in $keystorePath.") println("Intermediate CA keypair and certificate stored in $keystorePath.")
println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey) println(loadKeyStore(keystorePath, keystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey)
} }
private fun DoormanParameters.startDoorman(isLocalSigning: Boolean = false) { // TODO: Move this method to DoormanServer.
fun startDoorman(hostAndPort: NetworkHostAndPort,
database: CordaPersistence,
approveAll: Boolean,
signer: Signer? = null,
jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer {
logger.info("Starting Doorman server.") logger.info("Starting Doorman server.")
// Create DB connection.
val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, createIdentityService = { val requestService = if (approveAll) {
// Identity service not needed doorman, corda persistence is not very generic. logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
throw UnsupportedOperationException() ApproveAllCertificateRequestStorage(DBCertificateRequestStorage(database))
})
val csrHandler = if (jiraConfig == null) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.")
val storage = ApprovingAllCertificateRequestStorage(database)
DefaultCsrHandler(storage, buildLocalSigner(storage, this))
} else { } else {
val storage = DBCertificateRequestStorage(database) DBCertificateRequestStorage(database)
val signer = if (isLocalSigning) {
buildLocalSigner(storage, this)
} else {
ExternalSigner()
}
val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
JiraCsrHandler(jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode, storage, signer)
} }
val doorman = DoormanServer(HostAndPort.fromParts(host, port), csrHandler)
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 doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(PersistenceNodeInfoStorage(database)))
doorman.start() doorman.start()
// Thread process approved request periodically.
thread(name = "Approved Request Process Thread") {
while (true) {
try {
Thread.sleep(10.seconds.toMillis())
DoormanServer.serverStatus.lastRequestCheckTime = Instant.now()
requestProcessor.processApprovedRequests()
} catch (e: Exception) {
// Log the error and carry on.
DoormanServer.logger.error("Error encountered when approving request.", e)
}
}
}
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })
return doorman
} }
private fun buildLocalSigner(storage: CertificationRequestStorage, parameters: DoormanParameters): Signer { private fun buildLocalSigner(parameters: DoormanParameters): Signer? {
checkNotNull(parameters.keystorePath) {"The keystorePath parameter must be specified when using local signing!"} 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: ")
val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
val keystore = loadOrCreateKeyStore(parameters.keystorePath!!, keystorePassword) val keystore = loadOrCreateKeyStore(parameters.keystorePath, keystorePassword)
val rootCACert = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last() val caKeyPair = keystore.getKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword)
val caCertAndKey = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword) val caCertPath = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA)
return LocalSigner(storage, caCertAndKey, rootCACert) Signer(caKeyPair, caCertPath)
}
}
/**
* This storage automatically approves all created requests.
*/
private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate {
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(rawRequest)
approveRequest(requestId)
return requestId
}
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
@ -222,9 +225,22 @@ fun main(args: Array<String>) {
// TODO : Remove config overrides and solely use config file after testnet is finalized. // TODO : Remove config overrides and solely use config file after testnet is finalized.
parseParameters(*args).run { parseParameters(*args).run {
when (mode) { when (mode) {
DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair(
DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair() rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
DoormanParameters.Mode.DOORMAN -> startDoorman(keystorePath != null) rootKeystorePassword,
rootPrivateKeyPassword)
DoormanParameters.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 -> {
val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, { throw UnsupportedOperationException() })
val signer = buildLocalSigner(this)
startDoorman(NetworkHostAndPort(host, port), database, approveAll, signer, jiraConfig)
}
} }
} }
} catch (e: ShowHelpException) { } catch (e: ShowHelpException) {

View File

@ -4,8 +4,10 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser import joptsimple.OptionParser
import net.corda.core.crypto.sha256
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
@ -44,4 +46,9 @@ fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilitie
fun buildCertPath(vararg certificates: Certificate): CertPath { fun buildCertPath(vararg certificates: Certificate): CertPath {
return CertificateFactory.getInstance("X509").generateCertPath(certificates.asList()) return CertificateFactory.getInstance("X509").generateCertPath(certificates.asList())
} }
fun buildCertPath(certPathBytes: ByteArray): CertPath = CertificateFactory.getInstance("X509").generateCertPath(certPathBytes.inputStream())
// TODO: replace this with Crypto.hash when its available.
fun PublicKey.hash() = encoded.sha256().toString()

View File

@ -1,14 +0,0 @@
package com.r3.corda.doorman.persistence
import net.corda.node.utilities.CordaPersistence
/**
* This storage automatically approves all created requests.
*/
class ApprovingAllCertificateRequestStorage(database: CordaPersistence) : DBCertificateRequestStorage(database) {
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = super.saveRequest(certificationData)
approveRequest(requestId)
return requestId
}
}

View File

@ -2,71 +2,127 @@ package com.r3.corda.doorman.persistence
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.CertPath import java.security.cert.CertPath
import java.time.Instant
import javax.persistence.*
/** /**
* Provide certificate signing request storage for the certificate signing server. * Provide certificate signing request storage for the certificate signing server.
*/ */
interface CertificationRequestStorage { interface CertificationRequestStorage {
companion object { companion object {
val DOORMAN_SIGNATURE = listOf("Doorman") val DOORMAN_SIGNATURE = "Doorman"
} }
/** /**
* Persist [certificationData] in storage for further approval if it's a valid request. If not then it will be automatically * Persist [PKCS10CertificationRequest] in storage for further approval if it's a valid request. If not then it will be automatically
* rejected and not subject to any approval process. In both cases a randomly generated request ID is returned. * rejected and not subject to any approval process. In both cases a randomly generated request ID is returned.
*/ */
fun saveRequest(certificationData: CertificationRequestData): String fun saveRequest(rawRequest: PKCS10CertificationRequest): String
/** /**
* Retrieve certificate singing request and Host/IP information using [requestId]. * Retrieve certificate singing request using [requestId].
*/ */
fun getRequest(requestId: String): CertificationRequestData? fun getRequest(requestId: String): CertificateSigningRequest?
/** /**
* Return the response for a previously saved request with ID [requestId]. * Retrieve list of certificate singing request base on the [RequestStatus].
*/ */
fun getResponse(requestId: String): CertificateResponse fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest>
/** /**
* Approve the given request if it has not already been approved. Otherwise do nothing. * Approve the given request if it has not already been approved. Otherwise do nothing.
* *
* @return True if the request has been approved and false otherwise. * @return True if the request has been approved and false otherwise.
*/ */
fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE.first()): Boolean // TODO: Merge status changing methods.
fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE): Boolean
/**
* Signs the certificate signing request by assigning the given certificate.
*
* @return True if the request has been signed and false otherwise.
*/
fun signCertificate(requestId: String, signedBy: List<String> = DOORMAN_SIGNATURE, generateCertificate: CertificationRequestData.() -> CertPath): Boolean
/** /**
* Reject the given request using the given reason. * Reject the given request using the given reason.
*/ */
fun rejectRequest(requestId: String, rejectedBy: String = DOORMAN_SIGNATURE.first(), rejectReason: String) fun rejectRequest(requestId: String, rejectedBy: String = DOORMAN_SIGNATURE, rejectReason: String)
/** /**
* Retrieve list of request IDs waiting for approval. * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request statue to [RequestStatus.Signed].
*
* @throws IllegalArgumentException if request is not found or not in Approved state.
*/ */
fun getNewRequestIds(): List<String> fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String> = listOf(DOORMAN_SIGNATURE))
/**
* Retrieve list of approved request IDs.
*/
fun getApprovedRequestIds(): List<String>
/**
* Retrieve list of signed request IDs.
*/
fun getSignedRequestIds(): List<String>
} }
data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest) @Entity
@Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
// TODO: Use Hibernate Envers to audit the table instead of individual "changed_by"/"changed_at" columns.
class CertificateSigningRequest(
@Id
@Column(name = "request_id", length = 64)
var requestId: String = "",
// TODO: Store X500Name with a proper schema.
@Column(name = "legal_name", length = 256)
var legalName: String = "",
@Lob
@Column
var request: ByteArray = ByteArray(0),
@Column(name = "created_at")
var createdAt: Instant = Instant.now(),
@Column(name = "approved_at")
var approvedAt: Instant = Instant.now(),
@Column(name = "approved_by", length = 64)
var approvedBy: String? = null,
@Column
@Enumerated(EnumType.STRING)
var status: RequestStatus = RequestStatus.New,
@Column(name = "signed_by", length = 512)
@ElementCollection(targetClass = String::class, fetch = FetchType.EAGER)
var signedBy: List<String>? = null,
@Column(name = "signed_at")
var signedAt: Instant? = Instant.now(),
@Column(name = "rejected_by", length = 64)
var rejectedBy: String? = null,
@Column(name = "rejected_at")
var rejectedAt: Instant? = Instant.now(),
@Column(name = "reject_reason", length = 256, nullable = true)
var rejectReason: String? = null,
// TODO: The certificate data can have its own table.
@Embedded
var certificateData: CertificateData? = null
)
@Embeddable
class CertificateData(
@Column(name = "public_key_hash", length = 64, nullable = true)
var publicKeyHash: String? = null,
@Lob
@Column(nullable = true)
var certificatePath: ByteArray? = null,
@Column(name = "certificate_status", nullable = true)
var certificateStatus: CertificateStatus? = null
)
enum class CertificateStatus {
VALID, SUSPENDED, REVOKED
}
enum class RequestStatus {
New, Approved, Rejected, Signed
}
sealed class CertificateResponse { sealed class CertificateResponse {
object NotReady : CertificateResponse() object NotReady : CertificateResponse()
class Ready(val certificatePath: CertPath) : CertificateResponse() data class Ready(val certificatePath: CertPath) : CertificateResponse()
class Unauthorised(val message: String) : CertificateResponse() data class Unauthorised(val message: String) : CertificateResponse()
} }

View File

@ -1,91 +1,53 @@
package com.r3.corda.doorman.persistence package com.r3.corda.doorman.persistence
import com.r3.corda.doorman.hash
import com.r3.corda.doorman.persistence.RequestStatus.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.x500Name
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.DatabaseTransaction
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.io.ByteArrayInputStream import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.CertificateFactory
import java.time.Instant import java.time.Instant
import javax.persistence.*
import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.Path import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate import javax.persistence.criteria.Predicate
// TODO Relax the uniqueness requirement to be on the entire X.500 subject rather than just the legal name class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage {
open class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String>) {
@Entity return database.transaction {
@Table(name = "certificate_signing_request") val request = singleRequestWhere { builder, path ->
class CertificateSigningRequest( val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
@Id val statusEq = builder.equal(path.get<String>(CertificateSigningRequest::status.name), Approved)
@Column(name = "request_id", length = 64) builder.and(requestIdEq, statusEq)
var requestId: String = "", }
require(request != null) { "Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId" }
@Column(name = "host_name", length = 100) val publicKeyHash = certificates.certificates.first().publicKey.hash()
var hostName: String = "", request!!.certificateData = CertificateData(publicKeyHash, certificates.encoded, CertificateStatus.VALID)
request.status = Signed
@Column(name = "ip_address", length = 15) request.signedBy = signedBy
var ipAddress: String = "", request.signedAt = Instant.now()
session.save(request)
@Column(name = "legal_name", length = 256) }
var legalName: String = "",
@Lob
@Column
var request: ByteArray = ByteArray(0),
@Column(name = "created_at")
var createdAt: Instant = Instant.now(),
@Column(name = "approved_at")
var approvedAt: Instant = Instant.now(),
@Column(name = "approved_by", length = 64)
var approvedBy: String? = null,
@Column
@Enumerated(EnumType.STRING)
var status: Status = Status.New,
@Column(name = "signed_by", length = 512)
@ElementCollection(targetClass = String::class, fetch = FetchType.EAGER)
var signedBy: List<String>? = null,
@Column(name = "signed_at")
var signedAt: Instant? = Instant.now(),
@Column(name = "rejected_by", length = 64)
var rejectedBy: String? = null,
@Column(name = "rejected_at")
var rejectedAt: Instant? = Instant.now(),
@Lob
@Column(nullable = true)
var certificatePath: ByteArray? = null,
@Column(name = "reject_reason", length = 256, nullable = true)
var rejectReason: String? = null
)
enum class Status {
New, Approved, Rejected, Signed
} }
override fun saveRequest(certificationData: CertificationRequestData): String { override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
val request = JcaPKCS10CertificationRequest(rawRequest)
val requestId = SecureHash.randomSHA256().toString() val requestId = SecureHash.randomSHA256().toString()
database.transaction { database.transaction {
// TODO ensure public key not duplicated.
val (legalName, rejectReason) = try { val (legalName, rejectReason) = try {
// This will fail with IllegalArgumentException if subject name is malformed. // This will fail with IllegalArgumentException if subject name is malformed.
val legalName = CordaX500Name.build(certificationData.request.subject).copy(commonName = null) val legalName = CordaX500Name.parse(request.subject.toString()).copy(commonName = null)
// Checks database for duplicate name. // Checks database for duplicate name.
val query = session.criteriaBuilder.run { val query = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequest::class.java) val criteriaQuery = createQuery(CertificateSigningRequest::class.java)
criteriaQuery.from(CertificateSigningRequest::class.java).run { criteriaQuery.from(CertificateSigningRequest::class.java).run {
val nameEq = equal(get<String>(CertificateSigningRequest::legalName.name), legalName.toString()) val nameEq = equal(get<String>(CertificateSigningRequest::legalName.name), legalName.toString())
val statusNewOrApproved = get<String>(CertificateSigningRequest::status.name).`in`(Status.Approved, Status.New) val statusNewOrApproved = get<String>(CertificateSigningRequest::status.name).`in`(Approved, New)
criteriaQuery.where(and(nameEq, statusNewOrApproved)) criteriaQuery.where(and(nameEq, statusNewOrApproved))
} }
} }
@ -96,50 +58,30 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) :
Pair(legalName.x500Name, null) Pair(legalName.x500Name, null)
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Pair(certificationData.request.subject, "Name validation failed with exception : ${e.message}") Pair(request.subject, "Name validation failed with exception : ${e.message}")
} }
val request = CertificateSigningRequest( session.save(CertificateSigningRequest(
requestId = requestId, requestId = requestId,
hostName = certificationData.hostName,
ipAddress = certificationData.ipAddress,
legalName = legalName.toString(), legalName = legalName.toString(),
request = certificationData.request.encoded, request = request.encoded,
rejectReason = rejectReason, rejectReason = rejectReason,
status = if (rejectReason == null) Status.New else Status.Rejected status = if (rejectReason == null) New else Rejected
) ))
session.save(request)
} }
return requestId return requestId
} }
override fun getResponse(requestId: String): CertificateResponse {
return database.transaction {
val response = singleRequestWhere { builder, path ->
builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
}
if (response == null) {
CertificateResponse.NotReady
} else {
when (response.status) {
Status.New, Status.Approved -> CertificateResponse.NotReady
Status.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason")
Status.Signed -> CertificateResponse.Ready(buildCertPath(response.certificatePath))
}
}
}
}
override fun approveRequest(requestId: String, approvedBy: String): Boolean { override fun approveRequest(requestId: String, approvedBy: String): Boolean {
var approved = false var approved = false
database.transaction { database.transaction {
val request = singleRequestWhere { builder, path -> val request = singleRequestWhere { builder, path ->
builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId), builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.New)) builder.equal(path.get<String>(CertificateSigningRequest::status.name), New))
} }
if (request != null) { if (request != null) {
request.approvedAt = Instant.now() request.approvedAt = Instant.now()
request.approvedBy = approvedBy request.approvedBy = approvedBy
request.status = Status.Approved request.status = Approved
session.save(request) session.save(request)
approved = true approved = true
} }
@ -147,26 +89,6 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) :
return approved return approved
} }
override fun signCertificate(requestId: String, signedBy: List<String>, generateCertificate: CertificationRequestData.() -> CertPath): Boolean {
var signed = false
database.transaction {
val request = singleRequestWhere { builder, path ->
builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.Approved))
}
if (request != null) {
val now = Instant.now()
request.certificatePath = request.toRequestData().generateCertificate().encoded
request.status = Status.Signed
request.signedAt = now
request.signedBy = signedBy
session.save(request)
signed = true
}
}
return signed
}
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) {
database.transaction { database.transaction {
val request = singleRequestWhere { builder, path -> val request = singleRequestWhere { builder, path ->
@ -174,7 +96,7 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) :
} }
if (request != null) { if (request != null) {
request.rejectReason = rejectReason request.rejectReason = rejectReason
request.status = Status.Rejected request.status = Rejected
request.rejectedBy = rejectedBy request.rejectedBy = rejectedBy
request.rejectedAt = Instant.now() request.rejectedAt = Instant.now()
session.save(request) session.save(request)
@ -182,51 +104,32 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) :
} }
} }
override fun getRequest(requestId: String): CertificationRequestData? { override fun getRequest(requestId: String): CertificateSigningRequest? {
return database.transaction { return database.transaction {
singleRequestWhere { builder, path -> singleRequestWhere { builder, path ->
builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId) builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
} }
}?.toRequestData() }
} }
override fun getApprovedRequestIds(): List<String> { override fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest> {
return getRequestIdsByStatus(Status.Approved)
}
override fun getNewRequestIds(): List<String> {
return getRequestIdsByStatus(Status.New)
}
override fun getSignedRequestIds(): List<String> {
return getRequestIdsByStatus(Status.Signed)
}
private fun getRequestIdsByStatus(status: Status): List<String> {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val builder = session.criteriaBuilder
val query = builder.createQuery(String::class.java).run { val query = builder.createQuery(CertificateSigningRequest::class.java).run {
from(CertificateSigningRequest::class.java).run { from(CertificateSigningRequest::class.java).run {
select(get(CertificateSigningRequest::requestId.name)) where(builder.equal(get<RequestStatus>(CertificateSigningRequest::status.name), requestStatus))
where(builder.equal(get<Status>(CertificateSigningRequest::status.name), status))
} }
} }
session.createQuery(query).resultList session.createQuery(query).resultList
} }
} }
private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? { private fun DatabaseTransaction.singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? {
return database.transaction { val builder = session.criteriaBuilder
val builder = session.criteriaBuilder val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java)
val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java) val query = criteriaQuery.from(CertificateSigningRequest::class.java).run {
val query = criteriaQuery.from(CertificateSigningRequest::class.java).run { criteriaQuery.where(predicate(builder, this))
criteriaQuery.where(predicate(builder, this))
}
session.createQuery(query).uniqueResultOptional().orElse(null)
} }
return session.createQuery(query).uniqueResultOptional().orElse(null)
} }
private fun CertificateSigningRequest.toRequestData() = CertificationRequestData(hostName, ipAddress, PKCS10CertificationRequest(request))
private fun buildCertPath(certPathBytes: ByteArray?) = CertificateFactory.getInstance("X509").generateCertPath(ByteArrayInputStream(certPathBytes))
} }

View File

@ -10,12 +10,17 @@ class DoormanSchemaService : SchemaService {
object DoormanServices object DoormanServices
object DoormanServicesV1 : MappedSchema(schemaFamily = DoormanServices.javaClass, version = 1, object DoormanServicesV1 : MappedSchema(schemaFamily = DoormanServices.javaClass, version = 1,
mappedTypes = listOf(DBCertificateRequestStorage.CertificateSigningRequest::class.java)) mappedTypes = listOf(CertificateSigningRequest::class.java, NodeInfoEntity::class.java, PublicKeyNodeInfoLink::class.java))
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) override var schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions()))
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(DoormanServicesV1) override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(DoormanServicesV1)
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException() override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException()
override fun registerCustomSchemas(customSchemas: Set<MappedSchema>) {
schemaOptions = schemaOptions.plus(customSchemas.map { mappedSchema ->
Pair(mappedSchema, SchemaService.SchemaOptions())
})
}
} }

View File

@ -0,0 +1,53 @@
package com.r3.corda.doorman.persistence
import net.corda.core.node.NodeInfo
import java.security.cert.CertPath
import javax.persistence.*
interface NodeInfoStorage {
/**
* Retrieve certificate paths using the public key hash.
* @return [CertPath] or null if the public key is not registered with the Doorman.
*/
fun getCertificatePath(publicKeyHash: String): CertPath?
/**
* Obtain list of registered node info hashes.
*/
//TODO: we might want to return [SecureHash] instead of String
fun getNodeInfoHashes(): List<String>
/**
* Retrieve node info using nodeInfo's hash
* @return [NodeInfo] or null if the node info is not registered.
*/
fun getNodeInfo(nodeInfoHash: String): NodeInfo?
/**
* The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
*/
fun putNodeInfo(nodeInfo: NodeInfo)
}
@Entity
@Table(name = "node_info")
class NodeInfoEntity(
@Id
@Column(name = "node_info_hash", length = 64)
var nodeInfoHash: String = "",
@Lob
@Column(name = "node_info")
var nodeInfo: ByteArray = ByteArray(0)
)
@Entity
@Table(name = "public_key_node_info_link")
class PublicKeyNodeInfoLink(
@Id
@Column(name = "public_key_hash", length = 64)
var publicKeyHash: String = "",
@Column(name = "node_info_hash", length = 64)
var nodeInfoHash: String = ""
)

View File

@ -0,0 +1,69 @@
package com.r3.corda.doorman.persistence
import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.hash
import net.corda.core.crypto.sha256
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.PersistentMap
import java.security.cert.CertPath
class PersistenceNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
companion object {
fun makeNodeInfoMap() = PersistentMap<String, NodeInfo, NodeInfoEntity, String>(
toPersistentEntityKey = { it },
toPersistentEntity = { key, nodeInfo ->
val serializedNodeInfo = nodeInfo.serialize()
NodeInfoEntity(key, serializedNodeInfo.bytes)
},
fromPersistentEntity = {
val nodeInfo = it.nodeInfo.deserialize<NodeInfo>()
it.nodeInfoHash to nodeInfo
},
persistentEntityClass = NodeInfoEntity::class.java
)
fun makePublicKeyMap() = PersistentMap<String, String, PublicKeyNodeInfoLink, String>(
toPersistentEntityKey = { it },
toPersistentEntity = { publicKeyHash, nodeInfoHash -> PublicKeyNodeInfoLink(publicKeyHash, nodeInfoHash) },
fromPersistentEntity = { it.publicKeyHash to it.nodeInfoHash },
persistentEntityClass = PublicKeyNodeInfoLink::class.java
)
}
private val nodeInfoMap = database.transaction { makeNodeInfoMap() }
private val publicKeyMap = database.transaction { makePublicKeyMap() }
override fun putNodeInfo(nodeInfo: NodeInfo) {
return database.transaction {
val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hash()
val nodeInfoHash = nodeInfo.serialize().sha256().toString()
val existingNodeInfoHash = publicKeyMap[publicKeyHash]
if (nodeInfoHash != existingNodeInfoHash) {
// Remove node info if exists.
existingNodeInfoHash?.let { nodeInfoMap.remove(it) }
publicKeyMap[publicKeyHash] = nodeInfoHash
nodeInfoMap.put(nodeInfoHash, nodeInfo)
}
}
}
override fun getNodeInfo(nodeInfoHash: String): NodeInfo? = database.transaction { nodeInfoMap[nodeInfoHash] }
override fun getNodeInfoHashes(): List<String> = database.transaction { nodeInfoMap.keys.toList() }
override fun getCertificatePath(publicKeyHash: String): CertPath? {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(ByteArray::class.java).run {
from(CertificateSigningRequest::class.java).run {
select(get<CertificateData>(CertificateSigningRequest::certificateData.name).get<ByteArray>(CertificateData::certificatePath.name))
where(builder.equal(get<CertificateData>(CertificateSigningRequest::certificateData.name).get<String>(CertificateData::publicKeyHash.name), publicKeyHash))
}
}
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
}
}
}

View File

@ -1,100 +1,61 @@
package com.r3.corda.doorman.signer package com.r3.corda.doorman.signer
import com.atlassian.jira.rest.client.api.JiraRestClient import com.r3.corda.doorman.JiraClient
import com.atlassian.jira.rest.client.api.domain.Field import com.r3.corda.doorman.buildCertPath
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import net.corda.core.utilities.country import com.r3.corda.doorman.persistence.RequestStatus
import net.corda.core.utilities.locality import org.bouncycastle.pkcs.PKCS10CertificationRequest
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.organisation
import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
open class DefaultCsrHandler(protected val storage: CertificationRequestStorage, protected val signer: Signer) { interface CsrHandler {
open fun saveRequest(certificationData: CertificationRequestData): String { fun saveRequest(rawRequest: PKCS10CertificationRequest): String
return storage.saveRequest(certificationData) fun processApprovedRequests()
fun getResponse(requestId: String): CertificateResponse
}
class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: Signer?) : CsrHandler {
override fun processApprovedRequests() {
storage.getRequests(RequestStatus.Approved)
.forEach { processRequest(it.requestId, PKCS10CertificationRequest(it.request)) }
} }
open fun sign() { private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
for (id in storage.getApprovedRequestIds()) { if (signer != null) {
signer.sign(id) val certs = signer.sign(request)
storage.putCertificatePath(requestId, certs)
} }
} }
fun getResponse(requestId: String) = storage.getResponse(requestId) override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
} return storage.saveRequest(rawRequest)
class JiraCsrHandler(private val jiraClient: JiraRestClient,
private val projectCode: String,
private val doneTransitionCode: Int,
storage: CertificationRequestStorage, signer: Signer) : DefaultCsrHandler(storage, signer) {
companion object {
private val logger = loggerFor<JiraCsrHandler>()
} }
// The JIRA project must have a Request ID field and the Task issue type. override fun getResponse(requestId: String): CertificateResponse {
private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! val response = storage.getRequest(requestId)
private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! return when (response?.status) {
RequestStatus.New, RequestStatus.Approved, null -> CertificateResponse.NotReady
RequestStatus.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason")
RequestStatus.Signed -> CertificateResponse.Ready(buildCertPath(response.certificateData?.certificatePath ?: throw IllegalArgumentException("Certificate should not be null.")))
}
}
}
override fun saveRequest(certificationData: CertificationRequestData): String { class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
val requestId = super.saveRequest(certificationData) override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(rawRequest)
// Make sure request has been accepted. // Make sure request has been accepted.
val response = storage.getResponse(requestId) if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
if (response !is CertificateResponse.Unauthorised) { jiraClient.createRequestTicket(requestId, rawRequest)
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
val organisation = certificationData.request.subject.organisation
val nearestCity = certificationData.request.subject.locality
val country = certificationData.request.subject.country
val email = certificationData.request.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}")
.setSummary(organisation)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
} }
return requestId return requestId
} }
override fun sign() { override fun processApprovedRequests() {
val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues jiraClient.getApprovedRequests().forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
issues.map { delegate.processApprovedRequests()
val requestId = it.getField(requestIdField.id)?.value?.toString() val signedRequests = storage.getRequests(RequestStatus.Signed).mapNotNull {
if (requestId != null) { it.certificateData?.certificatePath?.let { certs -> it.requestId to buildCertPath(certs) }
var approvedBy = it.assignee?.displayName }.toMap()
if (approvedBy == null) { jiraClient.updateSignedRequests(signedRequests)
approvedBy = "Unknown"
}
storage.approveRequest(requestId, approvedBy)
}
}
super.sign()
// Retrieving certificates for signed CSRs to attach to the jira tasks.
storage.getSignedRequestIds().forEach {
val certificate = (storage.getResponse(it) as? CertificateResponse.Ready)?.certificatePath!!.certificates.first()
// Jira only support ~ (contains) search for custom textfield.
val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $it").claim().issues.firstOrNull()
if (issue != null) {
jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
}
}
} }
} }

View File

@ -1,48 +1,39 @@
package com.r3.corda.doorman.signer package com.r3.corda.doorman.signer
import com.r3.corda.doorman.buildCertPath import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.toX509Certificate import com.r3.corda.doorman.toX509Certificate
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.node.utilities.CertificateAndKeyPair import net.corda.core.internal.toX509CertHolder
import net.corda.core.internal.x500Name
import net.corda.node.utilities.CertificateType import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.security.KeyPair
import java.security.cert.CertPath
import java.security.cert.Certificate import java.security.cert.Certificate
interface Signer { /**
fun sign(requestId: String) * The [Signer] class signs [PKCS10CertificationRequest] using provided CA keypair and certificate path.
} * This is intended to be used in testing environment where hardware signing module is not available.
*/
class LocalSigner(private val storage: CertificationRequestStorage, class Signer(private val caKeyPair: KeyPair, private val caCertPath: Array<Certificate>) {
private val caCertAndKey: CertificateAndKeyPair, fun sign(certificationRequest: PKCS10CertificationRequest): CertPath {
private val rootCACert: Certificate) : Signer { // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
override fun sign(requestId: String) { // We assume all attributes in the subject name has been checked prior approval.
storage.signCertificate(requestId) { // TODO: add validation to subject name.
val request = JcaPKCS10CertificationRequest(request) val request = JcaPKCS10CertificationRequest(certificationRequest)
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(request.subject.toString()).copy(commonName = null).x500Name))), arrayOf())
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA,
// We assume all attributes in the subject name has been checked prior approval. caCertPath.first().toX509CertHolder(),
// TODO: add validation to subject name. caKeyPair,
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(request.subject).copy(commonName = null).x500Name))), arrayOf()) CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN),
val ourCertificate = caCertAndKey.certificate request.publicKey,
val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, nameConstraints = nameConstraints).toX509Certificate()
caCertAndKey.certificate, return buildCertPath(clientCertificate, *caCertPath)
caCertAndKey.keyPair,
CordaX500Name.build(request.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN),
request.publicKey,
nameConstraints = nameConstraints).toX509Certificate()
buildCertPath(clientCertificate, ourCertificate.toX509Certificate(), rootCACert)
}
} }
} }
class ExternalSigner : Signer {
override fun sign(requestId: String) {
// Do nothing
}
}

View File

@ -0,0 +1,80 @@
package com.r3.corda.doorman.webservice
import com.r3.corda.doorman.persistence.NodeInfoStorage
import com.r3.corda.doorman.webservice.NodeInfoWebService.Companion.networkMapPath
import net.corda.core.crypto.Crypto
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.core.utilities.toSHA256Bytes
import org.codehaus.jackson.map.ObjectMapper
import java.io.InputStream
import java.security.InvalidKeyException
import java.security.SignatureException
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.*
import javax.ws.rs.core.Context
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status
@Path(networkMapPath)
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage) {
companion object {
const val networkMapPath = "network-map"
}
@POST
@Path("register")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun registerNode(input: InputStream): Response {
// TODO: Use JSON instead.
val registrationData = input.readBytes().deserialize<SignedData<NodeInfo>>()
val nodeInfo = registrationData.verified()
val digitalSignature = registrationData.sig
val certPath = nodeInfoStorage.getCertificatePath(nodeInfo.legalIdentities.first().owningKey.toSHA256Bytes().toString())
return if (certPath != null) {
try {
require(Crypto.doVerify(certPath.certificates.first().publicKey, digitalSignature.bytes, nodeInfo.serialize().bytes))
// Store the NodeInfo
// TODO: Does doorman need to sign the nodeInfo?
nodeInfoStorage.putNodeInfo(nodeInfo)
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?
return ok(ObjectMapper().writeValueAsString(nodeInfoStorage.getNodeInfoHashes())).build()
}
@GET
@Path("{var}")
fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response {
// TODO: Use JSON instead.
return nodeInfoStorage.getNodeInfo(nodeInfoHash)?.let {
ok(it.serialize().bytes).build()
} ?: status(Response.Status.NOT_FOUND).build()
}
@GET
@Path("my-ip")
fun myIp(@Context request: HttpServletRequest): Response {
// TODO: Verify this returns IP correctly.
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
}
}

View File

@ -1,9 +1,8 @@
package com.r3.corda.doorman package com.r3.corda.doorman.webservice
import com.r3.corda.doorman.DoormanServerStatus
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.signer.CsrHandler
import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.signer.DefaultCsrHandler
import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA
import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
@ -24,25 +23,19 @@ 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("") @Path("certificate")
class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: DoormanServerStatus) { class RegistrationWebService(private val csrHandler: CsrHandler, private val serverStatus: DoormanServerStatus) {
@Context lateinit var request: HttpServletRequest @Context lateinit var request: HttpServletRequest
/** /**
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval. * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval.
* Server returns HTTP 200 response with random generated request Id after request has been persisted. * Server returns HTTP 200 response with random generated request Id after request has been persisted.
*/ */
@POST @POST
@Path("certificate")
@Consumes(MediaType.APPLICATION_OCTET_STREAM) @Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
fun submitRequest(input: InputStream): Response { fun submitRequest(input: InputStream): Response {
val certificationRequest = input.use { val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) }
JcaPKCS10CertificationRequest(it.readBytes()) val requestId = csrHandler.saveRequest(certificationRequest)
}
// TODO: Certificate signing request verifications.
// TODO: Use jira api / slack bot to semi automate the approval process?
// TODO: Acknowledge to user we have received the request via email?
val requestId = csrHandler.saveRequest(CertificationRequestData(request.remoteHost, request.remoteAddr, certificationRequest))
return ok(requestId).build() return ok(requestId).build()
} }
@ -51,7 +44,7 @@ class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: Doo
* Returns HTTP 200 with DER encoded signed certificates if request has been approved else HTTP 204 No content * Returns HTTP 200 with DER encoded signed certificates if request has been approved else HTTP 204 No content
*/ */
@GET @GET
@Path("certificate/{var}") @Path("{var}")
@Produces(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_OCTET_STREAM)
fun retrieveCert(@PathParam("var") requestId: String): Response { fun retrieveCert(@PathParam("var") requestId: String): Response {
val response = csrHandler.getResponse(requestId) val response = csrHandler.getResponse(requestId)

View File

@ -0,0 +1,57 @@
package com.r3.corda.doorman
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import com.r3.corda.doorman.persistence.*
import com.r3.corda.doorman.signer.DefaultCsrHandler
import com.r3.corda.doorman.signer.Signer
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.node.utilities.X509Utilities
import org.junit.Test
import kotlin.test.assertEquals
class DefaultRequestProcessorTest {
@Test
fun `get response`() {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val cert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "Test", country = "GB"), keyPair)
val requestStorage: CertificationRequestStorage = mock {
on { getRequest("New") }.thenReturn(CertificateSigningRequest(status = RequestStatus.New))
on { getRequest("Signed") }.thenReturn(CertificateSigningRequest(status = RequestStatus.Signed, certificateData = CertificateData("", buildCertPath(cert.toX509Certificate()).encoded, CertificateStatus.VALID)))
on { getRequest("Rejected") }.thenReturn(CertificateSigningRequest(status = RequestStatus.Rejected, rejectReason = "Random reason"))
}
val signer: Signer = mock()
val requestProcessor = DefaultCsrHandler(requestStorage, signer)
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random"))
assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New"))
assertEquals(CertificateResponse.Ready(buildCertPath(cert.toX509Certificate())), requestProcessor.getResponse("Signed"))
assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected"))
}
@Test
fun `process request`() {
val request1 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test1", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val request2 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test2", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val request3 = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Test3", country = "GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val requestStorage: CertificationRequestStorage = mock {
on { getRequests(RequestStatus.Approved) }.thenReturn(listOf(
CertificateSigningRequest(requestId = "1", request = request1.encoded),
CertificateSigningRequest(requestId = "2", request = request2.encoded),
CertificateSigningRequest(requestId = "3", request = request3.encoded)
))
}
val signer: Signer = mock()
val requestProcessor = DefaultCsrHandler(requestStorage, signer)
requestProcessor.processApprovedRequests()
verify(signer, times(3)).sign(any())
verify(requestStorage, times(1)).getRequests(any())
}
}

View File

@ -0,0 +1,167 @@
package com.r3.corda.doorman
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import com.r3.corda.doorman.persistence.NodeInfoStorage
import com.r3.corda.doorman.webservice.NodeInfoWebService
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities
import net.corda.nodeapi.internal.serialization.*
import org.bouncycastle.asn1.x500.X500Name
import org.codehaus.jackson.map.ObjectMapper
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileNotFoundException
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import javax.ws.rs.core.MediaType
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class NodeInfoWebServiceTest {
private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), 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)
companion object {
@BeforeClass
@JvmStatic
fun initSerialization() {
try {
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme())
}
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT
SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT
} catch (ignored: Exception) {
// Ignored
}
}
}
@Test
fun `submit nodeInfo`() {
// 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 digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, nodeInfo.serialize().bytes))
val nodeInfoStorage: NodeInfoStorage = mock {
on { getCertificatePath(any()) }.thenReturn(certPath)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
it.start()
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
// Post node info and signature to doorman
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)).use {
it.start()
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
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())
}
}
@Test
fun `get network map`() {
val networkMapList = listOf(SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString())
val nodeInfoStorage: NodeInfoStorage = mock {
on { getNodeInfoHashes() }.thenReturn(networkMapList)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
it.start()
val conn = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}").openConnection() as HttpURLConnection
val response = conn.inputStream.bufferedReader().use { it.readLine() }
val list = ObjectMapper().readValue(response, List::class.java)
verify(nodeInfoStorage, times(1)).getNodeInfoHashes()
assertEquals(networkMapList, list)
}
}
@Test
fun `get 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)
val nodeInfoHash = nodeInfo.serialize().sha256().toString()
val nodeInfoStorage: NodeInfoStorage = mock {
on { getNodeInfo(nodeInfoHash) }.thenReturn(nodeInfo)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
it.start()
val nodeInfoURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/$nodeInfoHash")
val conn = nodeInfoURL.openConnection()
val nodeInfoResponse = conn.inputStream.readBytes().deserialize<NodeInfo>()
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
assertEquals(nodeInfo, nodeInfoResponse)
assertFailsWith(FileNotFoundException::class) {
URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/${SecureHash.randomSHA256()}").openConnection().getInputStream()
}
}
}
private fun doPost(url: URL, payload: ByteArray) {
val conn = url.openConnection() as HttpURLConnection
conn.doOutput = true
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
conn.outputStream.write(payload)
return try {
conn.inputStream.bufferedReader().use { it.readLine() }
} catch (e: IOException) {
throw IOException(conn.errorStream.bufferedReader().readLine(), e)
}
}
}

View File

@ -1,15 +1,13 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.google.common.net.HostAndPort
import com.nhaarman.mockito_kotlin.* import com.nhaarman.mockito_kotlin.*
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.signer.CsrHandler
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.webservice.RegistrationWebService
import com.r3.corda.doorman.signer.DefaultCsrHandler
import com.r3.corda.doorman.signer.LocalSigner
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.node.utilities.CertificateAndKeyPair import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.utilities.CertificateStream import net.corda.node.utilities.CertificateStream
import net.corda.node.utilities.CertificateType import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities
@ -36,17 +34,15 @@ import java.util.zip.ZipInputStream
import javax.ws.rs.core.MediaType import javax.ws.rs.core.MediaType
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DoormanServiceTest { class RegistrationWebServiceTest {
private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), 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 doormanServer: DoormanServer
private fun startSigningServer(storage: CertificationRequestStorage) { private fun startSigningServer(csrHandler: CsrHandler) {
val caCertAndKey = CertificateAndKeyPair(intermediateCACert, intermediateCAKey) doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus()))
val csrManager = DefaultCsrHandler(storage, LocalSigner(storage, caCertAndKey, rootCACert.toX509Certificate()))
doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), csrManager)
doormanServer.start() doormanServer.start()
} }
@ -59,20 +55,20 @@ class DoormanServiceTest {
fun `submit request`() { fun `submit request`() {
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
val storage = mock<CertificationRequestStorage> { val requestProcessor = mock<CsrHandler> {
on { saveRequest(any()) }.then { id } on { saveRequest(any()) }.then { id }
} }
startSigningServer(storage) startSigningServer(requestProcessor)
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
val request = X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName"), "my@mail.com", keyPair) val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair)
// Post request to signing server via http. // Post request to signing server via http.
assertEquals(id, submitRequest(request)) assertEquals(id, submitRequest(request))
verify(storage, times(1)).saveRequest(any()) verify(requestProcessor, times(1)).saveRequest(any())
submitRequest(request) submitRequest(request)
verify(storage, times(2)).saveRequest(any()) verify(requestProcessor, times(2)).saveRequest(any())
} }
@Test @Test
@ -82,40 +78,33 @@ class DoormanServiceTest {
// Mock Storage behaviour. // Mock Storage behaviour.
val certificateStore = mutableMapOf<String, CertPath>() val certificateStore = mutableMapOf<String, CertPath>()
val storage = mock<CertificationRequestStorage> { val requestProcessor = mock<CsrHandler> {
on { getResponse(eq(id)) }.then { on { getResponse(eq(id)) }.then {
certificateStore[id]?.let { certificateStore[id]?.let {
CertificateResponse.Ready(it) CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady } ?: CertificateResponse.NotReady
} }
on { signCertificate(eq(id), any(), any()) }.then { on { processApprovedRequests() }.then {
@Suppress("UNCHECKED_CAST") val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "LegalName", country = "GB"), "my@mail.com", keyPair)
val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
certificateStore[id] = certGen(request) buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
true }
null
} }
on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.approveRequest(id) requestProcessor.processApprovedRequests()
storage.signCertificate(id) {
JcaPKCS10CertificationRequest(request).run {
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
}
}
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(storage, times(2)).getResponse(any()) verify(requestProcessor, times(2)).getResponse(any())
assertEquals(3, certificates.size) assertEquals(3, certificates.size)
certificates.first().run { certificates.first().run {
assertThat(subjectDN.name).contains("CN=LegalName") assertThat(subjectDN.name).contains("O=LegalName")
assertThat(subjectDN.name).contains("L=London") assertThat(subjectDN.name).contains("L=London")
} }
@ -132,34 +121,28 @@ class DoormanServiceTest {
// Mock Storage behaviour. // Mock Storage behaviour.
val certificateStore = mutableMapOf<String, CertPath>() val certificateStore = mutableMapOf<String, CertPath>()
val storage = mock<CertificationRequestStorage> { val storage = mock<CsrHandler> {
on { getResponse(eq(id)) }.then { on { getResponse(eq(id)) }.then {
certificateStore[id]?.let { certificateStore[id]?.let {
CertificateResponse.Ready(it) CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady } ?: CertificateResponse.NotReady
} }
on { signCertificate(eq(id), any(), any()) }.then { on { processApprovedRequests() }.then {
@Suppress("UNCHECKED_CAST") val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair)
val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf())
certificateStore[id] = certGen(request) val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate()
buildCertPath(clientCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
}
true true
} }
on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.approveRequest(id) storage.processApprovedRequests()
storage.signCertificate(id) {
JcaPKCS10CertificationRequest(request).run {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf())
val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate()
buildCertPath(clientCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
}
}
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(storage, times(2)).getResponse(any()) verify(storage, times(2)).getResponse(any())
@ -169,20 +152,18 @@ class DoormanServiceTest {
val sslCert = X509Utilities.createCertificate(CertificateType.TLS, X509CertificateHolder(certificates.first().encoded), keyPair, X500Name("CN=LegalName,L=London"), sslKey.public).toX509Certificate() val sslCert = X509Utilities.createCertificate(CertificateType.TLS, X509CertificateHolder(certificates.first().encoded), keyPair, X500Name("CN=LegalName,L=London"), sslKey.public).toX509Certificate()
// TODO: This is temporary solution, remove all certificate re-shaping after identity refactoring is done. // TODO: This is temporary solution, remove all certificate re-shaping after identity refactoring is done.
X509Utilities.validateCertificateChain(X509CertificateHolder(certificates.last().encoded), sslCert, *certificates.toTypedArray()) X509Utilities.validateCertificateChain(certificates.last(), sslCert, *certificates.toTypedArray())
} }
@Test @Test
fun `request not authorised`() { fun `request not authorised`() {
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
val storage = mock<CertificationRequestStorage> { val requestProcessor = mock<CsrHandler> {
on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") } on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") }
on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.Unauthorised("Not Allowed")) assertThat(pollForResponse(id)).isEqualTo(PollResponse.Unauthorised("Not Allowed"))
} }

View File

@ -1,10 +1,9 @@
package com.r3.corda.doorman.internal.persistence package com.r3.corda.doorman.internal.persistence
import com.r3.corda.doorman.buildCertPath import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import com.r3.corda.doorman.persistence.RequestStatus
import com.r3.corda.doorman.toX509Certificate import com.r3.corda.doorman.toX509Certificate
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -15,17 +14,14 @@ import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.KeyPair import java.security.KeyPair
import java.security.cert.CertPath
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.*
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class DBCertificateRequestStorageTest { class DBCertificateRequestStorageTest {
private lateinit var storage: DBCertificateRequestStorage private lateinit var storage: DBCertificateRequestStorage
@ -47,11 +43,9 @@ class DBCertificateRequestStorageTest {
val request = createRequest("LegalName").first val request = createRequest("LegalName").first
val requestId = storage.saveRequest(request) val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply { assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request.hostName, hostName) assertEquals(request, PKCS10CertificationRequest(this.request))
assertEquals(request.ipAddress, ipAddress)
assertEquals(request.request, this.request)
} }
assertThat(storage.getNewRequestIds()).containsOnly(requestId) assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId)
} }
@Test @Test
@ -60,17 +54,17 @@ class DBCertificateRequestStorageTest {
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(request) val requestId = storage.saveRequest(request)
// Pending request should equals to 1. // Pending request should equals to 1.
assertEquals(1, storage.getNewRequestIds().size) assertEquals(1, storage.getRequests(RequestStatus.New).size)
// Certificate should be empty. // Certificate should be empty.
assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) assertNull(storage.getRequest(requestId)!!.certificateData)
// Store certificate to DB. // Store certificate to DB.
val result = storage.approveRequest(requestId) val result = storage.approveRequest(requestId)
// Check request request has been approved // Check request request has been approved
assertTrue(result) assertTrue(result)
// Check request is not ready yet. // Check request is not ready yet.
assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) // assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady)
// New request should be empty. // New request should be empty.
assertTrue(storage.getNewRequestIds().isEmpty()) assertTrue(storage.getRequests(RequestStatus.New).isEmpty())
} }
@Test @Test
@ -93,28 +87,26 @@ class DBCertificateRequestStorageTest {
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// New request should equals to 1. // New request should equals to 1.
assertEquals(1, storage.getNewRequestIds().size) assertEquals(1, storage.getRequests(RequestStatus.New).size)
// Certificate should be empty. // Certificate should be empty.
assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) assertNull(storage.getRequest(requestId)!!.certificateData)
// Store certificate to DB. // Store certificate to DB.
storage.approveRequest(requestId) storage.approveRequest(requestId)
// Check request is not ready yet. // Check request is not ready yet.
assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady) assertEquals(RequestStatus.Approved, storage.getRequest(requestId)!!.status)
// New request should be empty. // New request should be empty.
assertTrue(storage.getNewRequestIds().isEmpty()) assertTrue(storage.getRequests(RequestStatus.New).isEmpty())
// Sign certificate // Sign certificate
storage.signCertificate(requestId) { storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run {
JcaPKCS10CertificationRequest(csr.request).run {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
} })
}
// Check request is ready // Check request is ready
assertTrue(storage.getResponse(requestId) is CertificateResponse.Ready) assertNotNull(storage.getRequest(requestId)!!.certificateData)
} }
@Test @Test
@ -124,44 +116,47 @@ class DBCertificateRequestStorageTest {
val requestId = storage.saveRequest(csr) val requestId = storage.saveRequest(csr)
// Store certificate to DB. // Store certificate to DB.
storage.approveRequest(requestId) storage.approveRequest(requestId)
val generateCert: CertificationRequestData.() -> CertPath = { storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run {
JcaPKCS10CertificationRequest(csr.request).run {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey) val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
} })
}
// Sign certificate // Sign certificate
storage.signCertificate(requestId, generateCertificate = generateCert)
// When subsequent signature requested // When subsequent signature requested
val result = storage.signCertificate(requestId, generateCertificate = generateCert) assertFailsWith(IllegalArgumentException::class){
// Then check request has not been signed storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run {
assertFalse(result) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", locality = "London", organisation = "R3 LTD", country = "GB"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
})
}
} }
@Test @Test
fun `reject request`() { fun `reject request`() {
val requestId = storage.saveRequest(createRequest("BankA").first) val requestId = storage.saveRequest(createRequest("BankA").first)
storage.rejectRequest(requestId, rejectReason = "Because I said so!") storage.rejectRequest(requestId, rejectReason = "Because I said so!")
assertThat(storage.getNewRequestIds()).isEmpty() assertThat(storage.getRequests(RequestStatus.New)).isEmpty()
val response = storage.getResponse(requestId) as CertificateResponse.Unauthorised assertThat(storage.getRequest(requestId)!!.rejectReason).isEqualTo("Because I said so!")
assertThat(response.message).isEqualTo("Because I said so!")
} }
@Test @Test
fun `request with the same legal name as a pending request`() { fun `request with the same legal name as a pending request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getNewRequestIds()).containsOnly(requestId1) assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1)
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getNewRequestIds()).containsOnly(requestId1) assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1)
val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised assertEquals(RequestStatus.Rejected, storage.getRequest(requestId2)!!.status)
assertThat(response2.message).containsIgnoringCase("duplicate") assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate")
// Make sure the first request is processed properly // Make sure the first request is processed properly
storage.approveRequest(requestId1) storage.approveRequest(requestId1)
assertThat(storage.getResponse(requestId1)).isInstanceOf(CertificateResponse.NotReady::class.java) assertThat(storage.getRequest(requestId1)!!.status).isEqualTo(RequestStatus.Approved)
} }
@Test @Test
@ -169,8 +164,7 @@ class DBCertificateRequestStorageTest {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
storage.approveRequest(requestId1) storage.approveRequest(requestId1)
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate")
assertThat(response2.message).containsIgnoringCase("duplicate")
} }
@Test @Test
@ -178,17 +172,14 @@ class DBCertificateRequestStorageTest {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
storage.rejectRequest(requestId1, rejectReason = "Because I said so!") storage.rejectRequest(requestId1, rejectReason = "Because I said so!")
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getNewRequestIds()).containsOnly(requestId2) assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId2)
storage.approveRequest(requestId2) storage.approveRequest(requestId2)
assertThat(storage.getResponse(requestId2)).isInstanceOf(CertificateResponse.NotReady::class.java) assertThat(storage.getRequest(requestId2)!!.status).isEqualTo(RequestStatus.Approved)
} }
private fun createRequest(legalName: String): Pair<CertificationRequestData, KeyPair> { private fun createRequest(legalName: String): Pair<PKCS10CertificationRequest, KeyPair> {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val request = CertificationRequestData( val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair)
"hostname",
"0.0.0.0",
X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB").x500Name, "my@mail.com", keyPair))
return Pair(request, keyPair) return Pair(request, keyPair)
} }

View File

@ -0,0 +1,139 @@
package com.r3.corda.doorman.internal.persistence
import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.hash
import com.r3.corda.doorman.persistence.*
import com.r3.corda.doorman.toX509Certificate
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase
import net.corda.nodeapi.internal.serialization.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class PersistenceNodeInfoStorageTest {
private lateinit var nodeInfoStorage: NodeInfoStorage
private lateinit var requestStorage: CertificationRequestStorage
private lateinit var persistence: CordaPersistence
private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
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, CordaX500Name(commonName = "Corda Node Intermediate CA", locality = "London", organisation = "R3 LTD", country = "GB"), intermediateCAKey.public)
companion object {
@BeforeClass
@JvmStatic
fun initSerialization() {
try {
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme())
}
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT
SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT
} catch (ignored: Exception) {
// Ignored
}
}
}
@Before
fun startDb() {
persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { DoormanSchemaService() }, createIdentityService = { throw UnsupportedOperationException() })
nodeInfoStorage = PersistenceNodeInfoStorage(persistence)
requestStorage = DBCertificateRequestStorage(persistence)
}
@After
fun closeDb() {
persistence.close()
}
@Test
fun `test get CertificatePath`() {
// 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)
val request = X509Utilities.createCertificateSigningRequest(nodeInfo.legalIdentities.first().name, "my@mail.com", keyPair)
val requestId = requestStorage.saveRequest(request)
requestStorage.approveRequest(requestId)
assertNull(nodeInfoStorage.getCertificatePath(keyPair.public.hash()))
requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()))
val storedCertPath = nodeInfoStorage.getCertificatePath(keyPair.public.hash())
assertNotNull(storedCertPath)
assertEquals(clientCert.toX509Certificate(), storedCertPath!!.certificates.first())
}
@Test
fun `test getNodeInfoHashes`() {
// 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 clientCert2 = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME).public)
val certPath2 = buildCertPath(clientCert2.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
val nodeInfoSame = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
val nodeInfo2 = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath2)), 1, serial = 1L)
nodeInfoStorage.putNodeInfo(nodeInfo)
nodeInfoStorage.putNodeInfo(nodeInfoSame)
// getNodeInfoHashes should contain 1 hash.
assertEquals(listOf(nodeInfo.serialize().sha256().toString()), nodeInfoStorage.getNodeInfoHashes())
nodeInfoStorage.putNodeInfo(nodeInfo2)
// getNodeInfoHashes should contain 2 hash.
assertEquals(listOf(nodeInfo2.serialize().sha256().toString(), nodeInfo.serialize().sha256().toString()).sorted(), nodeInfoStorage.getNodeInfoHashes().sorted())
// Test retrieve NodeInfo.
assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString()))
assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().sha256().toString()))
}
@Test
fun `same pub key with different node info`() {
// 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)
val nodeInfoSamePubKey = NodeInfo(listOf(NetworkHostAndPort("my.company2.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
nodeInfoStorage.putNodeInfo(nodeInfo)
assertEquals(nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString()))
// This should replace the node info.
nodeInfoStorage.putNodeInfo(nodeInfoSamePubKey)
// Old node info should be removed.
assertNull(nodeInfoStorage.getNodeInfo(nodeInfo.serialize().sha256().toString()))
assertEquals(nodeInfoSamePubKey, nodeInfoStorage.getNodeInfo(nodeInfoSamePubKey.serialize().sha256().toString()))
}
}

View File

@ -0,0 +1 @@
mock-maker-inline

View File

@ -12,6 +12,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.node.utilities.MAX_HASH_HEX_SIZE
import net.corda.node.utilities.NODE_DATABASE_PREFIX import net.corda.node.utilities.NODE_DATABASE_PREFIX
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -72,8 +73,8 @@ class PersistentIdentityService(identities: Iterable<PartyAndCertificate> = empt
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities")
class PersistentIdentity( class PersistentIdentity(
@Id @Id
@Column(name = "pk_hash", length = 64) @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE)
var publicKeyHash: String = "", var publicKeyHash: String,
@Lob @Lob
@Column @Column
@ -87,7 +88,7 @@ class PersistentIdentityService(identities: Iterable<PartyAndCertificate> = empt
@Column(name = "name", length = 128) @Column(name = "name", length = 128)
var name: String = "", var name: String = "",
@Column(name = "pk_hash", length = 64) @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE)
var publicKeyHash: String = "" var publicKeyHash: String = ""
) )

View File

@ -8,9 +8,8 @@ import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
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.parsePublicKeyBase58
import net.corda.core.utilities.toBase58String
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.node.utilities.MAX_HASH_HEX_SIZE
import net.corda.node.utilities.NODE_DATABASE_PREFIX import net.corda.node.utilities.NODE_DATABASE_PREFIX
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair import java.security.KeyPair
@ -34,29 +33,32 @@ class PersistentKeyManagementService(val identityService: IdentityService,
@Entity @Entity
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs")
class PersistentKey( class PersistentKey(
@Id @Id
@Column(length = 6000, name = "public_key") @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE)
var publicKey: String = "", var publicKeyHash: String,
@Lob
@Column(name = "public_key")
var publicKey: ByteArray = ByteArray(0),
@Lob @Lob
@Column(name = "private_key") @Column(name = "private_key")
var privateKey: ByteArray = ByteArray(0) var privateKey: ByteArray = ByteArray(0)
) ) {
constructor(publicKey: PublicKey, privateKey: PrivateKey)
: this(publicKey.toStringShort(),
publicKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes,
privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes)
}
private companion object { private companion object {
fun createKeyMap(): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> { fun createKeyMap(): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> {
return AppendOnlyPersistentMap( return AppendOnlyPersistentMap(
toPersistentEntityKey = { it.toBase58String() }, toPersistentEntityKey = { it.toStringShort() },
fromPersistentEntity = { fromPersistentEntity = { Pair(it.publicKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT),
Pair(parsePublicKeyBase58(it.publicKey), it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) },
it.privateKey.deserialize<PrivateKey>(context = SerializationDefaults.STORAGE_CONTEXT))
},
toPersistentEntity = { key: PublicKey, value: PrivateKey -> toPersistentEntity = { key: PublicKey, value: PrivateKey ->
PersistentKey().apply { PersistentKey(key, value)
publicKey = key.toBase58String()
privateKey = value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
}
}, },
persistentEntityClass = PersistentKey::class.java persistentEntityClass = PersistentKey::class.java
) )

View File

@ -1,6 +1,7 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party

View File

@ -1,6 +1,6 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.utilities.toBase58String import net.corda.core.crypto.toStringShort
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
@ -9,12 +9,14 @@ import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.utilities.* import net.corda.node.utilities.MAX_HASH_HEX_SIZE
import net.corda.node.utilities.NODE_DATABASE_PREFIX
import net.corda.node.utilities.PersistentMap
import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisMessagingComponent
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import javax.persistence.*
import java.util.* import java.util.*
import javax.persistence.*
/** /**
* A network map service backed by a database to survive restarts of the node hosting it. * A network map service backed by a database to survive restarts of the node hosting it.
@ -31,8 +33,9 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne
@Entity @Entity
@Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes")
class NetworkNode( class NetworkNode(
@Id @Column(name = "node_party_key") @Id
var publicKey: String = "", @Column(name = "node_party_key_hash", length = MAX_HASH_HEX_SIZE)
var publicKeyHash: String,
@Column @Column
var nodeParty: NodeParty = NodeParty(), var nodeParty: NodeParty = NodeParty(),
@ -58,14 +61,14 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne
fun createNetworkNodesMap(): PersistentMap<PartyAndCertificate, NodeRegistrationInfo, NetworkNode, String> { fun createNetworkNodesMap(): PersistentMap<PartyAndCertificate, NodeRegistrationInfo, NetworkNode, String> {
return PersistentMap( return PersistentMap(
toPersistentEntityKey = { it.owningKey.toBase58String() }, toPersistentEntityKey = { it.owningKey.toStringShort() },
fromPersistentEntity = { fromPersistentEntity = {
Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))), Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))),
it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT))
}, },
toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo ->
NetworkNode( NetworkNode(
publicKey = key.owningKey.toBase58String(), publicKeyHash = key.owningKey.toStringShort(),
nodeParty = NodeParty( nodeParty = NodeParty(
key.name.toString(), key.name.toString(),
key.certificate.encoded, key.certificate.encoded,

View File

@ -20,6 +20,14 @@ import java.util.concurrent.CopyOnWriteArrayList
*/ */
const val NODE_DATABASE_PREFIX = "node_" const val NODE_DATABASE_PREFIX = "node_"
/**
* The maximum supported field-size for hash HEX-encoded outputs (e.g. database fields).
* This value is enough to support hash functions with outputs up to 512 bits (e.g. SHA3-512), in which
* case 128 HEX characters are required.
* 130 was selected instead of 128, to allow for 2 extra characters that will be used as hash-scheme identifiers.
*/
internal const val MAX_HASH_HEX_SIZE = 130
//HikariDataSource implements Closeable which allows CordaPersistence to be Closeable //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable
class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService,
private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable { private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable {

View File

@ -4,7 +4,7 @@ SHELL=/bin/bash
# JAVA_HOME=$(ROOT)/usr/lib/jvm/java-8-openjdk-amd64 # JAVA_HOME=$(ROOT)/usr/lib/jvm/java-8-openjdk-amd64
# JAVA_HOME=$(ROOT)/usr/lib/jvm/java-openjdk # JAVA_HOME=$(ROOT)/usr/lib/jvm/java-openjdk
JDK_IMAGE=$(PWD)/jdk8u/build/linux-x86_64-normal-server-release/images/j2re-image JDK_IMAGE=$(MAKEFILE_DIR)/jdk8u/build/linux-x86_64-normal-server-release/images/j2re-image
.PHONY: all .PHONY: all
all: jvm-enclave/standalone/build/standalone_sgx_verify linux-sgx-driver/isgx.ko all: jvm-enclave/standalone/build/standalone_sgx_verify linux-sgx-driver/isgx.ko
@ -18,7 +18,7 @@ AVIAN_EXTRA_CFLAGS="-I$(ROOT)/usr/include -I$(ROOT)/usr/include/x86_64-linux-gnu
AVIAN_EXTRA_LDFLAGS="-L$(ROOT)/usr/lib -L$(ROOT)/usr/lib/x86_64-linux-gnu -ldl -lpthread -lz" AVIAN_EXTRA_LDFLAGS="-L$(ROOT)/usr/lib -L$(ROOT)/usr/lib/x86_64-linux-gnu -ldl -lpthread -lz"
.PHONY: avian .PHONY: avian
avian: | $(JDK_IMAGE) avian: | $(JDK_IMAGE)
PATH=$(ROOT)/usr/bin:$(PATH) $(MAKE) -C avian build-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-build-cflags+=$(AVIAN_EXTRA_CFLAGS) extra-cflags+=$(AVIAN_EXTRA_CFLAGS) mode=debug openjdk=$(JAVA_HOME) openjdk-src=$(PWD)/jdk8u/jdk/src openjdk-image=$(JDK_IMAGE) system=sgx PATH=$(ROOT)/usr/bin:$(PATH) $(MAKE) -C avian build-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-lflags=$(AVIAN_EXTRA_LDFLAGS) extra-build-cflags+=$(AVIAN_EXTRA_CFLAGS) extra-cflags+=$(AVIAN_EXTRA_CFLAGS) mode=debug openjdk=$(JAVA_HOME) openjdk-src=$(MAKEFILE_DIR)/jdk8u/jdk/src openjdk-image=$(JDK_IMAGE) system=sgx
# The SGX SDK # The SGX SDK
LINUX_SGX_SOURCES=$(shell find linux-sgx \( -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.edl' \) ! \( -name '*_u.?' -o -name '*_t.?' -o -name '*.pb.h' -o -path 'linux-sgx/sdk/cpprt/linux/libunwind/*' -o -path 'linux-sgx/external/rdrand/src/config.h' \)) # Poor man's up-to-date check (they don't have one in the SDK??) LINUX_SGX_SOURCES=$(shell find linux-sgx \( -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.edl' \) ! \( -name '*_u.?' -o -name '*_t.?' -o -name '*.pb.h' -o -path 'linux-sgx/sdk/cpprt/linux/libunwind/*' -o -path 'linux-sgx/external/rdrand/src/config.h' \)) # Poor man's up-to-date check (they don't have one in the SDK??)
@ -29,7 +29,10 @@ jdk8u:
git clone -b deterministic-jvm8 --single-branch https://github.com/corda/openjdk $@ git clone -b deterministic-jvm8 --single-branch https://github.com/corda/openjdk $@
$(JDK_IMAGE): jdk8u $(JDK_IMAGE): jdk8u
cd jdk8u && $(SHELL) ./configure && $(MAKE) JOBS=2 images docs cd jdk8u && \
ALSA_NOT_NEEDED=yes CUPS_NOT_NEEDED=yes FREETYPE_NOT_NEEDED=yes \
PULSE_NOT_NEEDED=yes X11_NOT_NEEDED=yes $(SHELL) ./configure && \
$(MAKE) JOBS=2 images docs
linux-sgx/external/ippcp_internal/inc: linux-sgx/external/ippcp_internal/inc:
cd linux-sgx && $(SHELL) ./download_prebuilt.sh cd linux-sgx && $(SHELL) ./download_prebuilt.sh

View File

@ -26,4 +26,4 @@ Some dependencies are still required to be installed, these are specified in `do
```bash ```bash
$ docker build -t minimal docker-minimal # builds a Docker image using docker-minimal/ $ docker build -t minimal docker-minimal # builds a Docker image using docker-minimal/
$ bash build_in_image.sh minimal # Runs the build inside the `minimal` image $ bash build_in_image.sh minimal # Runs the build inside the `minimal` image
``` ```

View File

@ -1,6 +1,8 @@
FROM ubuntu:xenial FROM ubuntu:xenial
RUN apt-get update -y RUN apt-get update -y
RUN apt-get install -y make gcc autoconf cmake g++ openjdk-8-jdk libtool ocaml python2.7 RUN apt-get install -y \
RUN apt-get install -y mercurial wget autoconf ccache cmake cpio g++ gcc git libtool make \
ocaml openjdk-8-jdk python2.7 unzip wget zip
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64

View File

@ -84,5 +84,5 @@ dependencies {
// Unit testing helpers. // Unit testing helpers.
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':doorman') integrationTestCompile project(':doorman')
} }

View File

@ -1,19 +1,16 @@
package com.r3.corda.signing package com.r3.corda.signing
import com.google.common.net.HostAndPort
import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import com.r3.corda.doorman.DoormanServer
import com.r3.corda.doorman.buildCertPath import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.signer.DefaultCsrHandler import com.r3.corda.doorman.startDoorman
import com.r3.corda.doorman.signer.ExternalSigner
import com.r3.corda.doorman.toX509Certificate import com.r3.corda.doorman.toX509Certificate
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.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.utilities.CertificateType import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities
@ -36,8 +33,6 @@ import java.net.URL
import java.util.* import java.util.*
import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.scheduleAtFixedRate
import kotlin.concurrent.thread import kotlin.concurrent.thread
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage.CertificateSigningRequest as DoormanRequest
import com.r3.corda.signing.persistence.DBCertificateRequestStorage.CertificateSigningRequest as SigningServerRequest
class SigningServiceIntegrationTest { class SigningServiceIntegrationTest {
@ -67,7 +62,7 @@ class SigningServiceIntegrationTest {
// Create all certificates // Create all certificates
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA",
organisation = "R3 Ltd", locality = "London", country = "GB").x500Name, rootCAKey) organisation = "R3 Ltd", locality = "London", country = "GB"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey,
CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB",
@ -91,12 +86,11 @@ class SigningServiceIntegrationTest {
@Test @Test
fun `Signing service communicates with Doorman`() { fun `Signing service communicates with Doorman`() {
//Start doorman server //Start doorman server
val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { val database = configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic. // Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException() throw UnsupportedOperationException()
})) })
val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true)
doorman.start()
// Start Corda network registration. // Start Corda network registration.
val config = testNodeConfiguration( val config = testNodeConfiguration(
@ -139,12 +133,11 @@ class SigningServiceIntegrationTest {
@Ignore @Ignore
fun `DEMO - Create CSR and poll`() { fun `DEMO - Create CSR and poll`() {
//Start doorman server //Start doorman server
val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = { val database = configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic. // Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException() throw UnsupportedOperationException()
})) })
val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true)
doorman.start()
thread(start = true, isDaemon = true) { thread(start = true, isDaemon = true) {
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers") val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")