diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index a0e6c80f49..61247d4f80 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -85,13 +85,12 @@ object NodeInfoSchemaV1 : MappedSchema( @Table(name = "node_info_party_cert") data class DBPartyAndCertificate( @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) val name: String, + @Column(name = "owning_key", length = 65535, nullable = false) + val owningKey: String, + @Column(name = "party_cert_binary") @Lob val partyCertBinary: ByteArray, @@ -102,10 +101,10 @@ object NodeInfoSchemaV1 : MappedSchema( private val persistentNodeInfos: Set = emptySet() ) { 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 { - return partyCertBinary.deserialize() + return partyCertBinary.deserialize() } } } diff --git a/doorman/build.gradle b/doorman/build.gradle index 2ddaa94d26..c117d5d615 100644 --- a/doorman/build.gradle +++ b/doorman/build.gradle @@ -1,7 +1,7 @@ ext { // We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are // align with the corresponding Corda release. - corda_dependency_version = '0.16-20170913.101300-6' + corda_dependency_version = '1.0.0' } version "$corda_dependency_version" diff --git a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt index d0485fa66b..88618c6891 100644 --- a/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt +++ b/doorman/src/integration-test/kotlin/com/r3/corda/doorman/DoormanIntegrationTest.kt @@ -1,16 +1,13 @@ package com.r3.corda.doorman -import com.google.common.net.HostAndPort 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.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.LocalSigner +import com.r3.corda.doorman.signer.Signer import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.cert -import net.corda.core.utilities.subject +import net.corda.core.internal.cert +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.utilities.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper @@ -29,10 +26,9 @@ class DoormanIntegrationTest { val tempFolder = TemporaryFolder() @Test - fun `Network Registration With Doorman`() { + fun `initial registration`() { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", organisation = "R3 Ltd", - locality = "London", country = "GB").x500Name, rootCAKey) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootCAKey) val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) 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) @@ -41,16 +37,17 @@ class DoormanIntegrationTest { // Identity service not needed doorman, corda persistence is not very generic. throw UnsupportedOperationException() }) + val signer = Signer(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())) + //Start doorman server - val storage = ApprovingAllCertificateRequestStorage(database) - val doorman = DoormanServer(HostAndPort.fromParts("localhost", 0), DefaultCsrHandler(storage, LocalSigner(storage, CertificateAndKeyPair(intermediateCACert, intermediateCAKey), rootCACert.toX509Certificate()))) - doorman.start() + val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, signer, null) // Start Corda network registration. val config = testNodeConfiguration( baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name).also { whenever(it.certificateSigningService).thenReturn(URL("http://localhost:${doorman.hostAndPort.port}")) + whenever(it.emailAddress).thenReturn("iTest@R3.com") } NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore() @@ -62,24 +59,23 @@ class DoormanIntegrationTest { loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply { 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()) } loadKeyStore(config.sslKeystore, config.keyStorePassword).apply { 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()) } loadKeyStore(config.trustStoreFile, config.trustStorePassword).apply { 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() } - private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt index 54d10cab84..073f23f778 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt @@ -18,12 +18,12 @@ data class DoormanParameters(val basedir: Path, val port: Int, val dataSourceProperties: Properties, val mode: Mode, + val approveAll: Boolean = false, val databaseProperties: Properties? = null, val jiraConfig: JiraConfig? = null, val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks", val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks" ) { - enum class Mode { DOORMAN, CA_KEYGEN, ROOT_KEYGEN } @@ -57,6 +57,7 @@ fun parseParameters(vararg args: String): DoormanParameters { } else { Paths.get(argConfig.getString("basedir")) / "node.conf" } - val config = argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))).resolve() - return config.parseAs() + return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))) + .resolve() + .parseAs() } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt new file mode 100644 index 0000000000..def85565bb --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt @@ -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() + } + + // 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> { + 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) { + // 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() + } + } + } +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index 2db1035d8e..61c1f8c691 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -1,22 +1,24 @@ package com.r3.corda.doorman 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.persistence.ApprovingAllCertificateRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage 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.identity.CordaX500Name import net.corda.core.internal.createDirectories +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.utilities.* -import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA -import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA -import net.corda.node.utilities.X509Utilities.createCertificate -import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector 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.servlet.ServletContainer import java.io.Closeable -import java.lang.Thread.sleep import java.net.InetSocketAddress import java.net.URI +import java.nio.file.Path import java.time.Instant import kotlin.concurrent.thread 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 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 { - val serverStatus = DoormanServerStatus() - +// TODO: Move this class to its own file. +class DoormanServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { companion object { val logger = loggerFor() + 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 { addHandler(buildServletContextHandler()) } } - val hostAndPort: HostAndPort - get() = server.connectors - .map { it as? ServerConnector } - .filterNotNull() - .map { HostAndPort.fromParts(it.host, it.localPort) } + val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } .first() override fun close() { @@ -68,22 +67,6 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle logger.info("Starting Doorman Web Services...") server.start() 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 { @@ -91,18 +74,15 @@ class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandle contextPath = "/" val resourceConfig = ResourceConfig().apply { // Add your API provider classes (annotated for JAX-RS) here - register(DoormanWebService(csrHandler, serverStatus)) - } - val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { - initOrder = 0 // Initialise at server start + webServices.forEach { register(it) } } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start addServlet(jerseyServlet, "/api/*") } } } -data class DoormanServerStatus(var serverStartTime: Instant? = null, - var lastRequestCheckTime: Instant? = null) +data class DoormanServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null) /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */ internal fun readPassword(fmt: String): String { @@ -110,111 +90,134 @@ internal fun readPassword(fmt: String): String { String(System.console().readPassword(fmt)) } else { print(fmt) - readLine()!! + readLine() ?: "" } } -private fun DoormanParameters.generateRootKeyPair() { - if (rootStorePath == null) { - throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!") - } +// Keygen utilities. +// TODO: Move keygen methods to Utilities.kt +fun generateRootKeyPair(rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?) { println("Generating Root CA keypair and certificate.") // 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. rootStorePath.parent.createDirectories() 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)) { - val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA).publicKey - println("Key $CORDA_ROOT_CA already exists in keystore, process will now terminate.") + if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) { + val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey + println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } 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) - rootStore.addOrReplaceKey(CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB", organisationUnit = "Corda", state = null), selfSignKey) + rootStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, selfSignKey.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert)) rootStore.save(rootStorePath, rootKeystorePassword) 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() { - 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!") - } +fun generateCAKeyPair(keystorePath: Path, rootStorePath: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) { println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.") // Get password from console if not in config. - val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") - val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") + val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ") + val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") 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 caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + val keystorePassword = keystorePass ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = caPrivateKeyPass ?: readPassword("CA Private Key Password: ") // Ensure folder exists. keystorePath.parent.createDirectories() val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) - if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA)) { - val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey - println("Key $CORDA_INTERMEDIATE_CA already exists in keystore, process will now terminate.") + if (keyStore.containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA)) { + val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA).publicKey + println("Key ${X509Utilities.CORDA_INTERMEDIATE_CA} already exists in keystore, process will now terminate.") println(oldKey) exitProcess(1) } 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) - keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA, intermediateKey.private, + val intermediateCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootKeyAndCert.certificate, rootKeyAndCert.keyPair, + 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)) keyStore.save(keystorePath, keystorePassword) 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.") - // Create DB connection. - val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, createIdentityService = { - // Identity service not needed doorman, corda persistence is not very generic. - throw UnsupportedOperationException() - }) - 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)) + + val requestService = if (approveAll) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") + ApproveAllCertificateRequestStorage(DBCertificateRequestStorage(database)) } else { - val storage = 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) + DBCertificateRequestStorage(database) } - 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() + + // 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() }) + return doorman } -private fun buildLocalSigner(storage: CertificationRequestStorage, parameters: DoormanParameters): Signer { - checkNotNull(parameters.keystorePath) {"The keystorePath parameter must be specified when using local signing!"} - // Get password from console if not in config. - val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") - val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") - val keystore = loadOrCreateKeyStore(parameters.keystorePath!!, keystorePassword) - val rootCACert = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last() - val caCertAndKey = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword) - return LocalSigner(storage, caCertAndKey, rootCACert) +private fun buildLocalSigner(parameters: DoormanParameters): Signer? { + return parameters.keystorePath?.let { + // Get password from console if not in config. + val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + val keystore = loadOrCreateKeyStore(parameters.keystorePath, keystorePassword) + val caKeyPair = keystore.getKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword) + val caCertPath = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA) + 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) { @@ -222,9 +225,22 @@ fun main(args: Array) { // TODO : Remove config overrides and solely use config file after testnet is finalized. parseParameters(*args).run { when (mode) { - DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() - DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair() - DoormanParameters.Mode.DOORMAN -> startDoorman(keystorePath != null) + DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair( + rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"), + 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) { diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt index 8b61c1f850..451996e532 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Utilities.kt @@ -4,8 +4,10 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.OptionParser +import net.corda.core.crypto.sha256 import org.bouncycastle.cert.X509CertificateHolder import java.io.ByteArrayInputStream +import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.Certificate import java.security.cert.CertificateFactory @@ -44,4 +46,9 @@ fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilitie fun buildCertPath(vararg certificates: Certificate): CertPath { return CertificateFactory.getInstance("X509").generateCertPath(certificates.asList()) -} \ No newline at end of file +} + +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() diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt deleted file mode 100644 index afd7c75cbe..0000000000 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/ApprovingAllCertificateRequestStorage.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt index 9228bc42c3..f219717e97 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt @@ -2,71 +2,127 @@ package com.r3.corda.doorman.persistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.CertPath +import java.time.Instant +import javax.persistence.* /** * Provide certificate signing request storage for the certificate signing server. */ interface CertificationRequestStorage { - 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. */ - 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 /** * 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. */ - fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE.first()): 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 = DOORMAN_SIGNATURE, generateCertificate: CertificationRequestData.() -> CertPath): Boolean + // TODO: Merge status changing methods. + fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE): Boolean /** * 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 - - /** - * Retrieve list of approved request IDs. - */ - fun getApprovedRequestIds(): List - - /** - * Retrieve list of signed request IDs. - */ - fun getSignedRequestIds(): List + fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List = listOf(DOORMAN_SIGNATURE)) } -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? = 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 { object NotReady : CertificateResponse() - class Ready(val certificatePath: CertPath) : CertificateResponse() - class Unauthorised(val message: String) : CertificateResponse() + data class Ready(val certificatePath: CertPath) : CertificateResponse() + data class Unauthorised(val message: String) : CertificateResponse() } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt index 7ba749d562..e5e337d00d 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt @@ -1,91 +1,53 @@ 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.identity.CordaX500Name +import net.corda.core.internal.x500Name import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.DatabaseTransaction import org.bouncycastle.pkcs.PKCS10CertificationRequest -import java.io.ByteArrayInputStream +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import java.security.cert.CertPath -import java.security.cert.CertificateFactory import java.time.Instant -import javax.persistence.* import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.Path import javax.persistence.criteria.Predicate -// TODO Relax the uniqueness requirement to be on the entire X.500 subject rather than just the legal name -open class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { - @Entity - @Table(name = "certificate_signing_request") - class CertificateSigningRequest( - @Id - @Column(name = "request_id", length = 64) - var requestId: String = "", +class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { + override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List) { + return database.transaction { + val request = singleRequestWhere { builder, path -> + val requestIdEq = builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) + val statusEq = builder.equal(path.get(CertificateSigningRequest::status.name), Approved) + builder.and(requestIdEq, statusEq) + } + require(request != null) { "Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId" } - @Column(name = "host_name", length = 100) - var hostName: String = "", - - @Column(name = "ip_address", length = 15) - var ipAddress: String = "", - - @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? = 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 + val publicKeyHash = certificates.certificates.first().publicKey.hash() + request!!.certificateData = CertificateData(publicKeyHash, certificates.encoded, CertificateStatus.VALID) + request.status = Signed + request.signedBy = signedBy + request.signedAt = Instant.now() + session.save(request) + } } - override fun saveRequest(certificationData: CertificationRequestData): String { + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val request = JcaPKCS10CertificationRequest(rawRequest) val requestId = SecureHash.randomSHA256().toString() - database.transaction { + // TODO ensure public key not duplicated. val (legalName, rejectReason) = try { // 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. val query = session.criteriaBuilder.run { val criteriaQuery = createQuery(CertificateSigningRequest::class.java) criteriaQuery.from(CertificateSigningRequest::class.java).run { val nameEq = equal(get(CertificateSigningRequest::legalName.name), legalName.toString()) - val statusNewOrApproved = get(CertificateSigningRequest::status.name).`in`(Status.Approved, Status.New) + val statusNewOrApproved = get(CertificateSigningRequest::status.name).`in`(Approved, New) criteriaQuery.where(and(nameEq, statusNewOrApproved)) } } @@ -96,50 +58,30 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : Pair(legalName.x500Name, null) } } 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, - hostName = certificationData.hostName, - ipAddress = certificationData.ipAddress, legalName = legalName.toString(), - request = certificationData.request.encoded, + request = request.encoded, rejectReason = rejectReason, - status = if (rejectReason == null) Status.New else Status.Rejected - ) - session.save(request) + status = if (rejectReason == null) New else Rejected + )) } return requestId } - override fun getResponse(requestId: String): CertificateResponse { - return database.transaction { - val response = singleRequestWhere { builder, path -> - builder.equal(path.get(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 { var approved = false database.transaction { val request = singleRequestWhere { builder, path -> builder.and(builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId), - builder.equal(path.get(CertificateSigningRequest::status.name), Status.New)) + builder.equal(path.get(CertificateSigningRequest::status.name), New)) } if (request != null) { request.approvedAt = Instant.now() request.approvedBy = approvedBy - request.status = Status.Approved + request.status = Approved session.save(request) approved = true } @@ -147,26 +89,6 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : return approved } - override fun signCertificate(requestId: String, signedBy: List, generateCertificate: CertificationRequestData.() -> CertPath): Boolean { - var signed = false - database.transaction { - val request = singleRequestWhere { builder, path -> - builder.and(builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId), - builder.equal(path.get(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) { database.transaction { val request = singleRequestWhere { builder, path -> @@ -174,7 +96,7 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) : } if (request != null) { request.rejectReason = rejectReason - request.status = Status.Rejected + request.status = Rejected request.rejectedBy = rejectedBy request.rejectedAt = Instant.now() 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 { singleRequestWhere { builder, path -> builder.equal(path.get(CertificateSigningRequest::requestId.name), requestId) } - }?.toRequestData() + } } - override fun getApprovedRequestIds(): List { - return getRequestIdsByStatus(Status.Approved) - } - - override fun getNewRequestIds(): List { - return getRequestIdsByStatus(Status.New) - } - - override fun getSignedRequestIds(): List { - return getRequestIdsByStatus(Status.Signed) - } - - private fun getRequestIdsByStatus(status: Status): List { + override fun getRequests(requestStatus: RequestStatus): List { return database.transaction { 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 { - select(get(CertificateSigningRequest::requestId.name)) - where(builder.equal(get(CertificateSigningRequest::status.name), status)) + where(builder.equal(get(CertificateSigningRequest::status.name), requestStatus)) } } session.createQuery(query).resultList } } - private fun singleRequestWhere(predicate: (CriteriaBuilder, Path) -> Predicate): CertificateSigningRequest? { - return database.transaction { - val builder = session.criteriaBuilder - val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java) - val query = criteriaQuery.from(CertificateSigningRequest::class.java).run { - criteriaQuery.where(predicate(builder, this)) - } - session.createQuery(query).uniqueResultOptional().orElse(null) + private fun DatabaseTransaction.singleRequestWhere(predicate: (CriteriaBuilder, Path) -> Predicate): CertificateSigningRequest? { + val builder = session.criteriaBuilder + val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java) + val query = criteriaQuery.from(CertificateSigningRequest::class.java).run { + criteriaQuery.where(predicate(builder, this)) } + 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)) } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt index 4e765bbd8f..1ee7a39e28 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DoormanSchemaService.kt @@ -10,12 +10,17 @@ class DoormanSchemaService : SchemaService { object DoormanServices 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 = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) + override var schemaOptions: Map = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions())) override fun selectSchemas(state: ContractState): Iterable = setOf(DoormanServicesV1) override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException() + override fun registerCustomSchemas(customSchemas: Set) { + schemaOptions = schemaOptions.plus(customSchemas.map { mappedSchema -> + Pair(mappedSchema, SchemaService.SchemaOptions()) + }) + } } \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt new file mode 100644 index 0000000000..bbdc7b0687 --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/NodeInfoStorage.kt @@ -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 + + /** + * 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 = "" +) \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt new file mode 100644 index 0000000000..2365f501bd --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/PersistenceNodeInfoStorage.kt @@ -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( + toPersistentEntityKey = { it }, + toPersistentEntity = { key, nodeInfo -> + val serializedNodeInfo = nodeInfo.serialize() + NodeInfoEntity(key, serializedNodeInfo.bytes) + }, + fromPersistentEntity = { + val nodeInfo = it.nodeInfo.deserialize() + it.nodeInfoHash to nodeInfo + }, + persistentEntityClass = NodeInfoEntity::class.java + ) + + fun makePublicKeyMap() = PersistentMap( + 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 = 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(CertificateSigningRequest::certificateData.name).get(CertificateData::certificatePath.name)) + where(builder.equal(get(CertificateSigningRequest::certificateData.name).get(CertificateData::publicKeyHash.name), publicKeyHash)) + } + } + session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) } + } + } +} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt index d083c3bc5e..996effda55 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/CsrHandler.kt @@ -1,100 +1,61 @@ package com.r3.corda.doorman.signer -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 com.r3.corda.doorman.JiraClient +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.CertificationRequestStorage -import net.corda.core.utilities.country -import net.corda.core.utilities.locality -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 +import com.r3.corda.doorman.persistence.RequestStatus +import org.bouncycastle.pkcs.PKCS10CertificationRequest -open class DefaultCsrHandler(protected val storage: CertificationRequestStorage, protected val signer: Signer) { - open fun saveRequest(certificationData: CertificationRequestData): String { - return storage.saveRequest(certificationData) +interface CsrHandler { + fun saveRequest(rawRequest: PKCS10CertificationRequest): String + 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() { - for (id in storage.getApprovedRequestIds()) { - signer.sign(id) + private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { + if (signer != null) { + val certs = signer.sign(request) + storage.putCertificatePath(requestId, certs) } } - fun getResponse(requestId: String) = storage.getResponse(requestId) -} - -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() + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + return storage.saveRequest(rawRequest) } - // The JIRA project must have a Request ID field and the Task issue type. - private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! - private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! + override fun getResponse(requestId: String): CertificateResponse { + val response = storage.getRequest(requestId) + 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 { - val requestId = super.saveRequest(certificationData) +class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(rawRequest) // Make sure request has been accepted. - val response = storage.getResponse(requestId) - if (response !is CertificateResponse.Unauthorised) { - 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() + if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { + jiraClient.createRequestTicket(requestId, rawRequest) } return requestId } - override fun sign() { - val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues - issues.map { - val requestId = it.getField(requestIdField.id)?.value?.toString() - if (requestId != null) { - var approvedBy = it.assignee?.displayName - if (approvedBy == null) { - 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() - } - } + override fun processApprovedRequests() { + jiraClient.getApprovedRequests().forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } + delegate.processApprovedRequests() + val signedRequests = storage.getRequests(RequestStatus.Signed).mapNotNull { + it.certificateData?.certificatePath?.let { certs -> it.requestId to buildCertPath(certs) } + }.toMap() + jiraClient.updateSignedRequests(signedRequests) } -} \ No newline at end of file +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt index c303f2f572..a43abbbeea 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/signer/Signer.kt @@ -1,48 +1,39 @@ package com.r3.corda.doorman.signer import com.r3.corda.doorman.buildCertPath -import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.toX509Certificate 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.X509Utilities import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import java.security.KeyPair +import java.security.cert.CertPath import java.security.cert.Certificate -interface Signer { - fun sign(requestId: String) -} - -class LocalSigner(private val storage: CertificationRequestStorage, - private val caCertAndKey: CertificateAndKeyPair, - private val rootCACert: Certificate) : Signer { - - override fun sign(requestId: String) { - storage.signCertificate(requestId) { - val request = JcaPKCS10CertificationRequest(request) - // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, - // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. - // We assume all attributes in the subject name has been checked prior approval. - // TODO: add validation to subject name. - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(request.subject).copy(commonName = null).x500Name))), arrayOf()) - val ourCertificate = caCertAndKey.certificate - val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, - caCertAndKey.certificate, - caCertAndKey.keyPair, - CordaX500Name.build(request.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), - request.publicKey, - nameConstraints = nameConstraints).toX509Certificate() - buildCertPath(clientCertificate, ourCertificate.toX509Certificate(), rootCACert) - } +/** + * 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 Signer(private val caKeyPair: KeyPair, private val caCertPath: Array) { + fun sign(certificationRequest: PKCS10CertificationRequest): CertPath { + // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, + // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. + // We assume all attributes in the subject name has been checked prior approval. + // TODO: add validation to subject name. + val request = JcaPKCS10CertificationRequest(certificationRequest) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.parse(request.subject.toString()).copy(commonName = null).x500Name))), arrayOf()) + val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA, + caCertPath.first().toX509CertHolder(), + caKeyPair, + CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + request.publicKey, + nameConstraints = nameConstraints).toX509Certificate() + return buildCertPath(clientCertificate, *caCertPath) } } - -class ExternalSigner : Signer { - override fun sign(requestId: String) { - // Do nothing - } -} \ No newline at end of file diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt new file mode 100644 index 0000000000..1521956c2b --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/NodeInfoWebService.kt @@ -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>() + + 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() + } +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt similarity index 77% rename from doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt rename to doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt index 4c84cdb9e1..26e2a9ba0f 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/webservice/RegistrationWebService.kt @@ -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.CertificationRequestData -import com.r3.corda.doorman.persistence.CertificationRequestStorage -import com.r3.corda.doorman.signer.DefaultCsrHandler +import com.r3.corda.doorman.signer.CsrHandler 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_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. */ -@Path("") -class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: DoormanServerStatus) { +@Path("certificate") +class RegistrationWebService(private val csrHandler: CsrHandler, private val serverStatus: DoormanServerStatus) { @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. */ @POST - @Path("certificate") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.TEXT_PLAIN) fun submitRequest(input: InputStream): Response { - val certificationRequest = input.use { - JcaPKCS10CertificationRequest(it.readBytes()) - } - // 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)) + val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } + val requestId = csrHandler.saveRequest(certificationRequest) 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 */ @GET - @Path("certificate/{var}") + @Path("{var}") @Produces(MediaType.APPLICATION_OCTET_STREAM) fun retrieveCert(@PathParam("var") requestId: String): Response { val response = csrHandler.getResponse(requestId) diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt new file mode 100644 index 0000000000..e5c9a02a0f --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DefaultRequestProcessorTest.kt @@ -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()) + } +} diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt new file mode 100644 index 0000000000..4ab51a6e0a --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/NodeInfoWebServiceTest.kt @@ -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() + 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) + } + } +} \ No newline at end of file diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt similarity index 63% rename from doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt rename to doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt index 7ce497ceb4..76860b2ad3 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/RegistrationWebServiceTest.kt @@ -1,15 +1,13 @@ package com.r3.corda.doorman -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.* 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.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.LocalSigner +import com.r3.corda.doorman.signer.CsrHandler +import com.r3.corda.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto 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.CertificateType import net.corda.node.utilities.X509Utilities @@ -36,17 +34,15 @@ import java.util.zip.ZipInputStream import javax.ws.rs.core.MediaType import kotlin.test.assertEquals -class DoormanServiceTest { +class RegistrationWebServiceTest { 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 intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public) private lateinit var doormanServer: DoormanServer - private fun startSigningServer(storage: CertificationRequestStorage) { - val caCertAndKey = CertificateAndKeyPair(intermediateCACert, intermediateCAKey) - val csrManager = DefaultCsrHandler(storage, LocalSigner(storage, caCertAndKey, rootCACert.toX509Certificate())) - doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), csrManager) + private fun startSigningServer(csrHandler: CsrHandler) { + doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus())) doormanServer.start() } @@ -59,20 +55,20 @@ class DoormanServiceTest { fun `submit request`() { val id = SecureHash.randomSHA256().toString() - val storage = mock { + val requestProcessor = mock { on { saveRequest(any()) }.then { id } } - startSigningServer(storage) + startSigningServer(requestProcessor) 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. assertEquals(id, submitRequest(request)) - verify(storage, times(1)).saveRequest(any()) + verify(requestProcessor, times(1)).saveRequest(any()) submitRequest(request) - verify(storage, times(2)).saveRequest(any()) + verify(requestProcessor, times(2)).saveRequest(any()) } @Test @@ -82,40 +78,33 @@ class DoormanServiceTest { // Mock Storage behaviour. val certificateStore = mutableMapOf() - val storage = mock { + val requestProcessor = mock { on { getResponse(eq(id)) }.then { certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { signCertificate(eq(id), any(), any()) }.then { - @Suppress("UNCHECKED_CAST") - val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) - certificateStore[id] = certGen(request) - true + on { processApprovedRequests() }.then { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "LegalName", country = "GB"), "my@mail.com", keyPair) + certificateStore[id] = JcaPKCS10CertificationRequest(request).run { + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() + buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) + } + null } - on { getNewRequestIds() }.then { listOf(id) } } - startSigningServer(storage) - + startSigningServer(requestProcessor) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.approveRequest(id) - storage.signCertificate(id) { - JcaPKCS10CertificationRequest(request).run { - val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() - buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) - } - } + requestProcessor.processApprovedRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain - verify(storage, times(2)).getResponse(any()) + verify(requestProcessor, times(2)).getResponse(any()) assertEquals(3, certificates.size) certificates.first().run { - assertThat(subjectDN.name).contains("CN=LegalName") + assertThat(subjectDN.name).contains("O=LegalName") assertThat(subjectDN.name).contains("L=London") } @@ -132,34 +121,28 @@ class DoormanServiceTest { // Mock Storage behaviour. val certificateStore = mutableMapOf() - val storage = mock { + val storage = mock { on { getResponse(eq(id)) }.then { certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { signCertificate(eq(id), any(), any()) }.then { - @Suppress("UNCHECKED_CAST") - val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath) - val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) - certificateStore[id] = certGen(request) + on { processApprovedRequests() }.then { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB"), "my@mail.com", keyPair) + certificateStore[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()) + } true } - on { getNewRequestIds() }.then { listOf(id) } } startSigningServer(storage) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.approveRequest(id) - 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()) - } - } + storage.processApprovedRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain 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() // 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 fun `request not authorised`() { val id = SecureHash.randomSHA256().toString() - val storage = mock { + val requestProcessor = mock { 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")) } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index beb7944ef2..7cff3c854a 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -1,10 +1,9 @@ package com.r3.corda.doorman.internal.persistence 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.DBCertificateRequestStorage +import com.r3.corda.doorman.persistence.RequestStatus import com.r3.corda.doorman.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash @@ -15,17 +14,14 @@ import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.configureDatabase import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After import org.junit.Before import org.junit.Test import java.security.KeyPair -import java.security.cert.CertPath import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* class DBCertificateRequestStorageTest { private lateinit var storage: DBCertificateRequestStorage @@ -47,11 +43,9 @@ class DBCertificateRequestStorageTest { val request = createRequest("LegalName").first val requestId = storage.saveRequest(request) assertNotNull(storage.getRequest(requestId)).apply { - assertEquals(request.hostName, hostName) - assertEquals(request.ipAddress, ipAddress) - assertEquals(request.request, this.request) + assertEquals(request, PKCS10CertificationRequest(this.request)) } - assertThat(storage.getNewRequestIds()).containsOnly(requestId) + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId) } @Test @@ -60,17 +54,17 @@ class DBCertificateRequestStorageTest { // Add request to DB. val requestId = storage.saveRequest(request) // Pending request should equals to 1. - assertEquals(1, storage.getNewRequestIds().size) + assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) + assertNull(storage.getRequest(requestId)!!.certificateData) // Store certificate to DB. val result = storage.approveRequest(requestId) // Check request request has been approved assertTrue(result) // 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. - assertTrue(storage.getNewRequestIds().isEmpty()) + assertTrue(storage.getRequests(RequestStatus.New).isEmpty()) } @Test @@ -93,28 +87,26 @@ class DBCertificateRequestStorageTest { // Add request to DB. val requestId = storage.saveRequest(csr) // New request should equals to 1. - assertEquals(1, storage.getNewRequestIds().size) + assertEquals(1, storage.getRequests(RequestStatus.New).size) // Certificate should be empty. - assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) + assertNull(storage.getRequest(requestId)!!.certificateData) // Store certificate to DB. storage.approveRequest(requestId) // 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. - assertTrue(storage.getNewRequestIds().isEmpty()) + assertTrue(storage.getRequests(RequestStatus.New).isEmpty()) // Sign certificate - storage.signCertificate(requestId) { - JcaPKCS10CertificationRequest(csr.request).run { + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { 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 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()) - } - } + }) // Check request is ready - assertTrue(storage.getResponse(requestId) is CertificateResponse.Ready) + assertNotNull(storage.getRequest(requestId)!!.certificateData) } @Test @@ -124,44 +116,47 @@ class DBCertificateRequestStorageTest { val requestId = storage.saveRequest(csr) // Store certificate to DB. storage.approveRequest(requestId) - val generateCert: CertificationRequestData.() -> CertPath = { - JcaPKCS10CertificationRequest(csr.request).run { + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { 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 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()) - } - } + }) // Sign certificate - storage.signCertificate(requestId, generateCertificate = generateCert) // When subsequent signature requested - val result = storage.signCertificate(requestId, generateCertificate = generateCert) - // Then check request has not been signed - assertFalse(result) + assertFailsWith(IllegalArgumentException::class){ + storage.putCertificatePath(requestId, JcaPKCS10CertificationRequest(csr).run { + 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 fun `reject request`() { val requestId = storage.saveRequest(createRequest("BankA").first) storage.rejectRequest(requestId, rejectReason = "Because I said so!") - assertThat(storage.getNewRequestIds()).isEmpty() - val response = storage.getResponse(requestId) as CertificateResponse.Unauthorised - assertThat(response.message).isEqualTo("Because I said so!") + assertThat(storage.getRequests(RequestStatus.New)).isEmpty() + assertThat(storage.getRequest(requestId)!!.rejectReason).isEqualTo("Because I said so!") } @Test fun `request with the same legal name as a pending request`() { 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) - assertThat(storage.getNewRequestIds()).containsOnly(requestId1) - val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised - assertThat(response2.message).containsIgnoringCase("duplicate") + assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1) + assertEquals(RequestStatus.Rejected, storage.getRequest(requestId2)!!.status) + assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate") // Make sure the first request is processed properly storage.approveRequest(requestId1) - assertThat(storage.getResponse(requestId1)).isInstanceOf(CertificateResponse.NotReady::class.java) + assertThat(storage.getRequest(requestId1)!!.status).isEqualTo(RequestStatus.Approved) } @Test @@ -169,8 +164,7 @@ class DBCertificateRequestStorageTest { val requestId1 = storage.saveRequest(createRequest("BankA").first) storage.approveRequest(requestId1) val requestId2 = storage.saveRequest(createRequest("BankA").first) - val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised - assertThat(response2.message).containsIgnoringCase("duplicate") + assertThat(storage.getRequest(requestId2)!!.rejectReason).containsIgnoringCase("duplicate") } @Test @@ -178,17 +172,14 @@ class DBCertificateRequestStorageTest { val requestId1 = storage.saveRequest(createRequest("BankA").first) storage.rejectRequest(requestId1, rejectReason = "Because I said so!") 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) - assertThat(storage.getResponse(requestId2)).isInstanceOf(CertificateResponse.NotReady::class.java) + assertThat(storage.getRequest(requestId2)!!.status).isEqualTo(RequestStatus.Approved) } - private fun createRequest(legalName: String): Pair { + private fun createRequest(legalName: String): Pair { val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val request = CertificationRequestData( - "hostname", - "0.0.0.0", - X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB").x500Name, "my@mail.com", keyPair)) + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair) return Pair(request, keyPair) } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt new file mode 100644 index 0000000000..166c936866 --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/PersistenceNodeInfoStorageTest.kt @@ -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())) + } +} \ No newline at end of file diff --git a/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/doorman/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 7d3151c63b..18d074cd68 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -12,6 +12,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.MAX_HASH_HEX_SIZE import net.corda.node.utilities.NODE_DATABASE_PREFIX import org.bouncycastle.cert.X509CertificateHolder import java.io.ByteArrayInputStream @@ -72,8 +73,8 @@ class PersistentIdentityService(identities: Iterable = empt @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") class PersistentIdentity( @Id - @Column(name = "pk_hash", length = 64) - var publicKeyHash: String = "", + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE) + var publicKeyHash: String, @Lob @Column @@ -87,7 +88,7 @@ class PersistentIdentityService(identities: Iterable = empt @Column(name = "name", length = 128) var name: String = "", - @Column(name = "pk_hash", length = 64) + @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE) var publicKeyHash: String = "" ) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 4645c1d8e0..9983976146 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -8,9 +8,8 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize 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.MAX_HASH_HEX_SIZE import net.corda.node.utilities.NODE_DATABASE_PREFIX import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -34,29 +33,32 @@ class PersistentKeyManagementService(val identityService: IdentityService, @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( - @Id - @Column(length = 6000, name = "public_key") - var publicKey: String = "", + @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE) + var publicKeyHash: String, + + @Lob + @Column(name = "public_key") + var publicKey: ByteArray = ByteArray(0), @Lob @Column(name = "private_key") 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 { fun createKeyMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( - toPersistentEntityKey = { it.toBase58String() }, - fromPersistentEntity = { - Pair(parsePublicKeyBase58(it.publicKey), - it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) - }, + toPersistentEntityKey = { it.toStringShort() }, + fromPersistentEntity = { Pair(it.publicKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT), + it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> - PersistentKey().apply { - publicKey = key.toBase58String() - privateKey = value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes - } + PersistentKey(key, value) }, persistentEntityClass = PersistentKey::class.java ) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index eb45e7a0df..b3bd8df222 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -1,6 +1,7 @@ package net.corda.node.services.network import net.corda.core.concurrent.CordaFuture +import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 6d3600d398..df03e8c1e4 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,6 +1,6 @@ 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.internal.ThreadBox 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.node.services.api.NetworkMapCacheInternal 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 java.io.ByteArrayInputStream import java.security.cert.CertificateFactory -import javax.persistence.* import java.util.* +import javax.persistence.* /** * 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 @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") class NetworkNode( - @Id @Column(name = "node_party_key") - var publicKey: String = "", + @Id + @Column(name = "node_party_key_hash", length = MAX_HASH_HEX_SIZE) + var publicKeyHash: String, @Column var nodeParty: NodeParty = NodeParty(), @@ -58,14 +61,14 @@ class PersistentNetworkMapService(network: MessagingService, networkMapCache: Ne fun createNetworkNodesMap(): PersistentMap { return PersistentMap( - toPersistentEntityKey = { it.owningKey.toBase58String() }, + toPersistentEntityKey = { it.owningKey.toStringShort() }, fromPersistentEntity = { Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))), it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> NetworkNode( - publicKey = key.owningKey.toBase58String(), + publicKeyHash = key.owningKey.toStringShort(), nodeParty = NodeParty( key.name.toString(), key.certificate.encoded, diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index 77055c6073..a8c85d105d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -20,6 +20,14 @@ import java.util.concurrent.CopyOnWriteArrayList */ 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 class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable { diff --git a/sgx-jvm/Makefile b/sgx-jvm/Makefile index a11c60182f..2b67d641af 100644 --- a/sgx-jvm/Makefile +++ b/sgx-jvm/Makefile @@ -4,7 +4,7 @@ SHELL=/bin/bash # JAVA_HOME=$(ROOT)/usr/lib/jvm/java-8-openjdk-amd64 # 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 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" .PHONY: avian 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 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 $@ $(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: cd linux-sgx && $(SHELL) ./download_prebuilt.sh diff --git a/sgx-jvm/dependencies/README.md b/sgx-jvm/dependencies/README.md index 1c42406d6a..e16398cab2 100644 --- a/sgx-jvm/dependencies/README.md +++ b/sgx-jvm/dependencies/README.md @@ -26,4 +26,4 @@ Some dependencies are still required to be installed, these are specified in `do ```bash $ 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 -``` \ No newline at end of file +``` diff --git a/sgx-jvm/dependencies/docker-minimal/Dockerfile b/sgx-jvm/dependencies/docker-minimal/Dockerfile index 27de27d65d..4de99cea0b 100644 --- a/sgx-jvm/dependencies/docker-minimal/Dockerfile +++ b/sgx-jvm/dependencies/docker-minimal/Dockerfile @@ -1,6 +1,8 @@ FROM ubuntu:xenial 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 mercurial wget +RUN apt-get install -y \ + 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 diff --git a/signing-server/build.gradle b/signing-server/build.gradle index 01fcc51b8f..4a9a76acd8 100644 --- a/signing-server/build.gradle +++ b/signing-server/build.gradle @@ -84,5 +84,5 @@ dependencies { // Unit testing helpers. testCompile 'junit:junit:4.12' testCompile "org.assertj:assertj-core:${assertj_version}" - testCompile project(':doorman') + integrationTestCompile project(':doorman') } \ No newline at end of file diff --git a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt index ff63522880..210493e5ae 100644 --- a/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt +++ b/signing-server/src/integration-test/kotlin/com/r3/corda/signing/SigningServiceIntegrationTest.kt @@ -1,19 +1,16 @@ package com.r3.corda.signing -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import com.r3.corda.doorman.DoormanServer 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.signer.DefaultCsrHandler -import com.r3.corda.doorman.signer.ExternalSigner +import com.r3.corda.doorman.startDoorman import com.r3.corda.doorman.toX509Certificate import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -36,8 +33,6 @@ import java.net.URL import java.util.* import kotlin.concurrent.scheduleAtFixedRate 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 { @@ -67,7 +62,7 @@ class SigningServiceIntegrationTest { // Create all certificates val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) 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 intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB", @@ -91,12 +86,11 @@ class SigningServiceIntegrationTest { @Test fun `Signing service communicates with Doorman`() { //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. throw UnsupportedOperationException() - })) - val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) - doorman.start() + }) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true) // Start Corda network registration. val config = testNodeConfiguration( @@ -139,12 +133,11 @@ class SigningServiceIntegrationTest { @Ignore fun `DEMO - Create CSR and poll`() { //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. throw UnsupportedOperationException() - })) - val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner())) - doorman.start() + }) + val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true) thread(start = true, isDaemon = true) { val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")