mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
Merge branch 'master' of https://github.com/corda/enterprise into christians_perftestflows
This commit is contained in:
commit
f579393d88
@ -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<PersistentNodeInfo> = 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<PartyAndCertificate>()
|
||||
return partyCertBinary.deserialize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
|
@ -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<DoormanParameters>()
|
||||
return argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)))
|
||||
.resolve()
|
||||
.parseAs()
|
||||
}
|
||||
|
74
doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt
Normal file
74
doorman/src/main/kotlin/com/r3/corda/doorman/JiraCient.kt
Normal file
@ -0,0 +1,74 @@
|
||||
package com.r3.corda.doorman
|
||||
|
||||
import com.atlassian.jira.rest.client.api.JiraRestClient
|
||||
import com.atlassian.jira.rest.client.api.domain.Field
|
||||
import com.atlassian.jira.rest.client.api.domain.IssueType
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||
import net.corda.core.internal.country
|
||||
import net.corda.core.internal.locality
|
||||
import net.corda.core.internal.organisation
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import java.io.StringWriter
|
||||
import java.security.cert.CertPath
|
||||
|
||||
class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) {
|
||||
companion object {
|
||||
val logger = loggerFor<JiraClient>()
|
||||
}
|
||||
|
||||
// The JIRA project must have a Request ID field and the Task issue type.
|
||||
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
|
||||
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
|
||||
|
||||
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
|
||||
// Make sure request has been accepted.
|
||||
val request = StringWriter()
|
||||
JcaPEMWriter(request).use {
|
||||
it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded))
|
||||
}
|
||||
val organisation = signingRequest.subject.organisation
|
||||
val nearestCity = signingRequest.subject.locality
|
||||
val country = signingRequest.subject.country
|
||||
|
||||
val email = signingRequest.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
|
||||
|
||||
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
|
||||
.setProjectKey(projectCode)
|
||||
.setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}")
|
||||
.setSummary(organisation)
|
||||
.setFieldValue(requestIdField.id, requestId)
|
||||
// This will block until the issue is created.
|
||||
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
|
||||
}
|
||||
|
||||
fun getApprovedRequests(): List<Pair<String, String>> {
|
||||
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||
return issues.map { issue ->
|
||||
issue.getField(requestIdField.id)?.value?.toString().let {
|
||||
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
|
||||
val approvedBy = issue.assignee?.displayName ?: "Unknown"
|
||||
Pair(requestId, approvedBy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSignedRequests(signedRequests: Map<String, CertPath>) {
|
||||
// Retrieving certificates for signed CSRs to attach to the jira tasks.
|
||||
signedRequests.forEach { (id, certPath) ->
|
||||
val certificate = certPath.certificates.first()
|
||||
// Jira only support ~ (contains) search for custom textfield.
|
||||
val issue = restClient.searchClient.searchJql("'Request ID' ~ $id").claim().issues.firstOrNull()
|
||||
if (issue != null) {
|
||||
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
|
||||
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
|
||||
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<DoormanServer>()
|
||||
val serverStatus = DoormanServerStatus()
|
||||
}
|
||||
|
||||
private val server: Server = Server(InetSocketAddress(webServerAddr.host, webServerAddr.port)).apply {
|
||||
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
||||
handler = HandlerCollection().apply {
|
||||
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<String>) {
|
||||
@ -222,9 +225,22 @@ fun main(args: Array<String>) {
|
||||
// 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) {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<CertificateSigningRequest>
|
||||
|
||||
/**
|
||||
* 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<String> = 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<String>
|
||||
|
||||
/**
|
||||
* Retrieve list of approved request IDs.
|
||||
*/
|
||||
fun getApprovedRequestIds(): List<String>
|
||||
|
||||
/**
|
||||
* Retrieve list of signed request IDs.
|
||||
*/
|
||||
fun getSignedRequestIds(): List<String>
|
||||
fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: List<String> = 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<String>? = null,
|
||||
|
||||
@Column(name = "signed_at")
|
||||
var signedAt: Instant? = Instant.now(),
|
||||
|
||||
@Column(name = "rejected_by", length = 64)
|
||||
var rejectedBy: String? = null,
|
||||
|
||||
@Column(name = "rejected_at")
|
||||
var rejectedAt: Instant? = Instant.now(),
|
||||
|
||||
@Column(name = "reject_reason", length = 256, nullable = true)
|
||||
var rejectReason: String? = null,
|
||||
|
||||
// TODO: The certificate data can have its own table.
|
||||
@Embedded
|
||||
var certificateData: CertificateData? = null
|
||||
)
|
||||
|
||||
@Embeddable
|
||||
class CertificateData(
|
||||
@Column(name = "public_key_hash", length = 64, nullable = true)
|
||||
var publicKeyHash: String? = null,
|
||||
|
||||
@Lob
|
||||
@Column(nullable = true)
|
||||
var certificatePath: ByteArray? = null,
|
||||
|
||||
@Column(name = "certificate_status", nullable = true)
|
||||
var certificateStatus: CertificateStatus? = null
|
||||
)
|
||||
|
||||
enum class CertificateStatus {
|
||||
VALID, SUSPENDED, REVOKED
|
||||
}
|
||||
|
||||
enum class RequestStatus {
|
||||
New, Approved, Rejected, Signed
|
||||
}
|
||||
|
||||
sealed class CertificateResponse {
|
||||
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()
|
||||
}
|
@ -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<String>) {
|
||||
return database.transaction {
|
||||
val request = singleRequestWhere { builder, path ->
|
||||
val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
|
||||
val statusEq = builder.equal(path.get<String>(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<String>? = null,
|
||||
|
||||
@Column(name = "signed_at")
|
||||
var signedAt: Instant? = Instant.now(),
|
||||
|
||||
@Column(name = "rejected_by", length = 64)
|
||||
var rejectedBy: String? = null,
|
||||
|
||||
@Column(name = "rejected_at")
|
||||
var rejectedAt: Instant? = Instant.now(),
|
||||
|
||||
@Lob
|
||||
@Column(nullable = true)
|
||||
var certificatePath: ByteArray? = null,
|
||||
|
||||
@Column(name = "reject_reason", length = 256, nullable = true)
|
||||
var rejectReason: String? = null
|
||||
)
|
||||
|
||||
enum class Status {
|
||||
New, Approved, Rejected, Signed
|
||||
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<String>(CertificateSigningRequest::legalName.name), legalName.toString())
|
||||
val statusNewOrApproved = get<String>(CertificateSigningRequest::status.name).`in`(Status.Approved, Status.New)
|
||||
val statusNewOrApproved = get<String>(CertificateSigningRequest::status.name).`in`(Approved, New)
|
||||
criteriaQuery.where(and(nameEq, statusNewOrApproved))
|
||||
}
|
||||
}
|
||||
@ -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<String>(CertificateSigningRequest::requestId.name), requestId)
|
||||
}
|
||||
if (response == null) {
|
||||
CertificateResponse.NotReady
|
||||
} else {
|
||||
when (response.status) {
|
||||
Status.New, Status.Approved -> CertificateResponse.NotReady
|
||||
Status.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason")
|
||||
Status.Signed -> CertificateResponse.Ready(buildCertPath(response.certificatePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun approveRequest(requestId: String, approvedBy: String): Boolean {
|
||||
var approved = false
|
||||
database.transaction {
|
||||
val request = singleRequestWhere { builder, path ->
|
||||
builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
|
||||
builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.New))
|
||||
builder.equal(path.get<String>(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<String>, generateCertificate: CertificationRequestData.() -> CertPath): Boolean {
|
||||
var signed = false
|
||||
database.transaction {
|
||||
val request = singleRequestWhere { builder, path ->
|
||||
builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
|
||||
builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.Approved))
|
||||
}
|
||||
if (request != null) {
|
||||
val now = Instant.now()
|
||||
request.certificatePath = request.toRequestData().generateCertificate().encoded
|
||||
request.status = Status.Signed
|
||||
request.signedAt = now
|
||||
request.signedBy = signedBy
|
||||
session.save(request)
|
||||
signed = true
|
||||
}
|
||||
}
|
||||
return signed
|
||||
}
|
||||
|
||||
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) {
|
||||
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<String>(CertificateSigningRequest::requestId.name), requestId)
|
||||
}
|
||||
}?.toRequestData()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getApprovedRequestIds(): List<String> {
|
||||
return getRequestIdsByStatus(Status.Approved)
|
||||
}
|
||||
|
||||
override fun getNewRequestIds(): List<String> {
|
||||
return getRequestIdsByStatus(Status.New)
|
||||
}
|
||||
|
||||
override fun getSignedRequestIds(): List<String> {
|
||||
return getRequestIdsByStatus(Status.Signed)
|
||||
}
|
||||
|
||||
private fun getRequestIdsByStatus(status: Status): List<String> {
|
||||
override fun getRequests(requestStatus: RequestStatus): List<CertificateSigningRequest> {
|
||||
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<Status>(CertificateSigningRequest::status.name), status))
|
||||
where(builder.equal(get<RequestStatus>(CertificateSigningRequest::status.name), requestStatus))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).resultList
|
||||
}
|
||||
}
|
||||
|
||||
private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> 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<CertificateSigningRequest>) -> 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))
|
||||
}
|
@ -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<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions()))
|
||||
override var schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(DoormanServicesV1, SchemaService.SchemaOptions()))
|
||||
|
||||
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(DoormanServicesV1)
|
||||
|
||||
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException()
|
||||
|
||||
override fun registerCustomSchemas(customSchemas: Set<MappedSchema>) {
|
||||
schemaOptions = schemaOptions.plus(customSchemas.map { mappedSchema ->
|
||||
Pair(mappedSchema, SchemaService.SchemaOptions())
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.r3.corda.doorman.persistence
|
||||
|
||||
import net.corda.core.node.NodeInfo
|
||||
import java.security.cert.CertPath
|
||||
import javax.persistence.*
|
||||
|
||||
interface NodeInfoStorage {
|
||||
/**
|
||||
* Retrieve certificate paths using the public key hash.
|
||||
* @return [CertPath] or null if the public key is not registered with the Doorman.
|
||||
*/
|
||||
fun getCertificatePath(publicKeyHash: String): CertPath?
|
||||
|
||||
/**
|
||||
* Obtain list of registered node info hashes.
|
||||
*/
|
||||
//TODO: we might want to return [SecureHash] instead of String
|
||||
fun getNodeInfoHashes(): List<String>
|
||||
|
||||
/**
|
||||
* Retrieve node info using nodeInfo's hash
|
||||
* @return [NodeInfo] or null if the node info is not registered.
|
||||
*/
|
||||
fun getNodeInfo(nodeInfoHash: String): NodeInfo?
|
||||
|
||||
/**
|
||||
* The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
|
||||
*/
|
||||
fun putNodeInfo(nodeInfo: NodeInfo)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_info")
|
||||
class NodeInfoEntity(
|
||||
@Id
|
||||
@Column(name = "node_info_hash", length = 64)
|
||||
var nodeInfoHash: String = "",
|
||||
|
||||
@Lob
|
||||
@Column(name = "node_info")
|
||||
var nodeInfo: ByteArray = ByteArray(0)
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "public_key_node_info_link")
|
||||
class PublicKeyNodeInfoLink(
|
||||
@Id
|
||||
@Column(name = "public_key_hash", length = 64)
|
||||
var publicKeyHash: String = "",
|
||||
|
||||
@Column(name = "node_info_hash", length = 64)
|
||||
var nodeInfoHash: String = ""
|
||||
)
|
@ -0,0 +1,69 @@
|
||||
package com.r3.corda.doorman.persistence
|
||||
|
||||
import com.r3.corda.doorman.buildCertPath
|
||||
import com.r3.corda.doorman.hash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.PersistentMap
|
||||
import java.security.cert.CertPath
|
||||
|
||||
class PersistenceNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
|
||||
companion object {
|
||||
fun makeNodeInfoMap() = PersistentMap<String, NodeInfo, NodeInfoEntity, String>(
|
||||
toPersistentEntityKey = { it },
|
||||
toPersistentEntity = { key, nodeInfo ->
|
||||
val serializedNodeInfo = nodeInfo.serialize()
|
||||
NodeInfoEntity(key, serializedNodeInfo.bytes)
|
||||
},
|
||||
fromPersistentEntity = {
|
||||
val nodeInfo = it.nodeInfo.deserialize<NodeInfo>()
|
||||
it.nodeInfoHash to nodeInfo
|
||||
},
|
||||
persistentEntityClass = NodeInfoEntity::class.java
|
||||
)
|
||||
|
||||
fun makePublicKeyMap() = PersistentMap<String, String, PublicKeyNodeInfoLink, String>(
|
||||
toPersistentEntityKey = { it },
|
||||
toPersistentEntity = { publicKeyHash, nodeInfoHash -> PublicKeyNodeInfoLink(publicKeyHash, nodeInfoHash) },
|
||||
fromPersistentEntity = { it.publicKeyHash to it.nodeInfoHash },
|
||||
persistentEntityClass = PublicKeyNodeInfoLink::class.java
|
||||
)
|
||||
}
|
||||
|
||||
private val nodeInfoMap = database.transaction { makeNodeInfoMap() }
|
||||
private val publicKeyMap = database.transaction { makePublicKeyMap() }
|
||||
|
||||
override fun putNodeInfo(nodeInfo: NodeInfo) {
|
||||
return database.transaction {
|
||||
val publicKeyHash = nodeInfo.legalIdentities.first().owningKey.hash()
|
||||
val nodeInfoHash = nodeInfo.serialize().sha256().toString()
|
||||
val existingNodeInfoHash = publicKeyMap[publicKeyHash]
|
||||
if (nodeInfoHash != existingNodeInfoHash) {
|
||||
// Remove node info if exists.
|
||||
existingNodeInfoHash?.let { nodeInfoMap.remove(it) }
|
||||
publicKeyMap[publicKeyHash] = nodeInfoHash
|
||||
nodeInfoMap.put(nodeInfoHash, nodeInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNodeInfo(nodeInfoHash: String): NodeInfo? = database.transaction { nodeInfoMap[nodeInfoHash] }
|
||||
|
||||
override fun getNodeInfoHashes(): List<String> = database.transaction { nodeInfoMap.keys.toList() }
|
||||
|
||||
override fun getCertificatePath(publicKeyHash: String): CertPath? {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(ByteArray::class.java).run {
|
||||
from(CertificateSigningRequest::class.java).run {
|
||||
select(get<CertificateData>(CertificateSigningRequest::certificateData.name).get<ByteArray>(CertificateData::certificatePath.name))
|
||||
where(builder.equal(get<CertificateData>(CertificateSigningRequest::certificateData.name).get<String>(CertificateData::publicKeyHash.name), publicKeyHash))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<JiraCsrHandler>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Certificate>) {
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.r3.corda.doorman.webservice
|
||||
|
||||
import com.r3.corda.doorman.persistence.NodeInfoStorage
|
||||
import com.r3.corda.doorman.webservice.NodeInfoWebService.Companion.networkMapPath
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.toSHA256Bytes
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import java.io.InputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.Context
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.ok
|
||||
import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(networkMapPath)
|
||||
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage) {
|
||||
companion object {
|
||||
const val networkMapPath = "network-map"
|
||||
}
|
||||
@POST
|
||||
@Path("register")
|
||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||
fun registerNode(input: InputStream): Response {
|
||||
// TODO: Use JSON instead.
|
||||
val registrationData = input.readBytes().deserialize<SignedData<NodeInfo>>()
|
||||
|
||||
val nodeInfo = registrationData.verified()
|
||||
val digitalSignature = registrationData.sig
|
||||
|
||||
val certPath = nodeInfoStorage.getCertificatePath(nodeInfo.legalIdentities.first().owningKey.toSHA256Bytes().toString())
|
||||
return if (certPath != null) {
|
||||
try {
|
||||
require(Crypto.doVerify(certPath.certificates.first().publicKey, digitalSignature.bytes, nodeInfo.serialize().bytes))
|
||||
// Store the NodeInfo
|
||||
// TODO: Does doorman need to sign the nodeInfo?
|
||||
nodeInfoStorage.putNodeInfo(nodeInfo)
|
||||
ok()
|
||||
} catch (e: Exception) {
|
||||
// Catch exceptions thrown by signature verification.
|
||||
when (e) {
|
||||
is IllegalArgumentException, is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message)
|
||||
// Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status(Response.Status.BAD_REQUEST).entity("Unknown node info, this public key is not registered or approved by Corda Doorman.")
|
||||
}.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
fun getNetworkMap(): Response {
|
||||
// TODO: Cache the response?
|
||||
return ok(ObjectMapper().writeValueAsString(nodeInfoStorage.getNodeInfoHashes())).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{var}")
|
||||
fun getNodeInfo(@PathParam("var") nodeInfoHash: String): Response {
|
||||
// TODO: Use JSON instead.
|
||||
return nodeInfoStorage.getNodeInfo(nodeInfoHash)?.let {
|
||||
ok(it.serialize().bytes).build()
|
||||
} ?: status(Response.Status.NOT_FOUND).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("my-ip")
|
||||
fun myIp(@Context request: HttpServletRequest): Response {
|
||||
// TODO: Verify this returns IP correctly.
|
||||
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
|
||||
}
|
||||
}
|
@ -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)
|
@ -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())
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
package com.r3.corda.doorman
|
||||
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.times
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import com.r3.corda.doorman.persistence.NodeInfoStorage
|
||||
import com.r3.corda.doorman.webservice.NodeInfoWebService
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.utilities.CertificateType
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.nodeapi.internal.serialization.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.ws.rs.core.MediaType
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NodeInfoWebServiceTest {
|
||||
private val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
private val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(locality = "London", organisation = "R3 LTD", country = "GB", commonName = "Corda Node Root CA"), rootCAKey)
|
||||
private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
private val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
|
||||
|
||||
companion object {
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun initSerialization() {
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoServerSerializationScheme())
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
|
||||
SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT
|
||||
SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT
|
||||
} catch (ignored: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submit nodeInfo`() {
|
||||
// Create node info.
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public)
|
||||
val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
|
||||
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
|
||||
|
||||
// Create digital signature.
|
||||
val digitalSignature = DigitalSignature.WithKey(keyPair.public, Crypto.doSign(keyPair.private, nodeInfo.serialize().bytes))
|
||||
|
||||
val nodeInfoStorage: NodeInfoStorage = mock {
|
||||
on { getCertificatePath(any()) }.thenReturn(certPath)
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
|
||||
it.start()
|
||||
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
|
||||
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
|
||||
// Post node info and signature to doorman
|
||||
doPost(registerURL, nodeInfoAndSignature)
|
||||
verify(nodeInfoStorage, times(1)).getCertificatePath(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submit nodeInfo with invalid signature`() {
|
||||
// Create node info.
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public)
|
||||
val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
|
||||
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
|
||||
|
||||
// Create digital signature.
|
||||
val attackerKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val digitalSignature = DigitalSignature.WithKey(attackerKeyPair.public, Crypto.doSign(attackerKeyPair.private, nodeInfo.serialize().bytes))
|
||||
|
||||
val nodeInfoStorage: NodeInfoStorage = mock {
|
||||
on { getCertificatePath(any()) }.thenReturn(certPath)
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
|
||||
it.start()
|
||||
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
|
||||
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
|
||||
// Post node info and signature to doorman
|
||||
assertFailsWith(IOException::class) {
|
||||
doPost(registerURL, nodeInfoAndSignature)
|
||||
}
|
||||
verify(nodeInfoStorage, times(1)).getCertificatePath(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get network map`() {
|
||||
val networkMapList = listOf(SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString())
|
||||
val nodeInfoStorage: NodeInfoStorage = mock {
|
||||
on { getNodeInfoHashes() }.thenReturn(networkMapList)
|
||||
}
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
|
||||
it.start()
|
||||
val conn = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}").openConnection() as HttpURLConnection
|
||||
val response = conn.inputStream.bufferedReader().use { it.readLine() }
|
||||
val list = ObjectMapper().readValue(response, List::class.java)
|
||||
verify(nodeInfoStorage, times(1)).getNodeInfoHashes()
|
||||
assertEquals(networkMapList, list)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get node info`() {
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = "Test", locality = "London", country = "GB"), keyPair.public)
|
||||
val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
|
||||
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
|
||||
|
||||
val nodeInfoHash = nodeInfo.serialize().sha256().toString()
|
||||
|
||||
val nodeInfoStorage: NodeInfoStorage = mock {
|
||||
on { getNodeInfo(nodeInfoHash) }.thenReturn(nodeInfo)
|
||||
}
|
||||
|
||||
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
|
||||
it.start()
|
||||
val nodeInfoURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/$nodeInfoHash")
|
||||
val conn = nodeInfoURL.openConnection()
|
||||
val nodeInfoResponse = conn.inputStream.readBytes().deserialize<NodeInfo>()
|
||||
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
||||
assertEquals(nodeInfo, nodeInfoResponse)
|
||||
|
||||
assertFailsWith(FileNotFoundException::class) {
|
||||
URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/${SecureHash.randomSHA256()}").openConnection().getInputStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doPost(url: URL, payload: ByteArray) {
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.doOutput = true
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
||||
conn.outputStream.write(payload)
|
||||
|
||||
return try {
|
||||
conn.inputStream.bufferedReader().use { it.readLine() }
|
||||
} catch (e: IOException) {
|
||||
throw IOException(conn.errorStream.bufferedReader().readLine(), e)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<CertificationRequestStorage> {
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
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<String, CertPath>()
|
||||
val storage = mock<CertificationRequestStorage> {
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
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<String, CertPath>()
|
||||
val storage = mock<CertificationRequestStorage> {
|
||||
val storage = mock<CsrHandler> {
|
||||
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<CertificationRequestStorage> {
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
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"))
|
||||
}
|
||||
|
@ -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<CertificationRequestData, KeyPair> {
|
||||
private fun createRequest(legalName: String): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
mock-maker-inline
|
@ -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<PartyAndCertificate> = 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<PartyAndCertificate> = 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 = ""
|
||||
)
|
||||
|
||||
|
@ -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<PublicKey, PrivateKey, PersistentKey, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
toPersistentEntityKey = { it.toBase58String() },
|
||||
fromPersistentEntity = {
|
||||
Pair(parsePublicKeyBase58(it.publicKey),
|
||||
it.privateKey.deserialize<PrivateKey>(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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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<PartyAndCertificate, NodeRegistrationInfo, NetworkNode, String> {
|
||||
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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
```
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
}
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user