mirror of
https://github.com/corda/corda.git
synced 2025-06-16 06:08:13 +00:00
Merging signing service and doorman (#72)
* Merging signing service and doorman * Addressing review comments * Removing redundant package name space from method call * Adding description field to gradle
This commit is contained in:
@ -0,0 +1,223 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.toX509Certificate
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
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 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.hibernate.envers.AuditReaderFactory
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
import kotlin.test.*
|
||||
|
||||
class DBCertificateRequestStorageTest {
|
||||
private lateinit var storage: DBCertificateRequestStorage
|
||||
private lateinit var persistence: CordaPersistence
|
||||
|
||||
@Before
|
||||
fun startDb() {
|
||||
persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { throw UnsupportedOperationException() }, SchemaService())
|
||||
storage = DBCertificateRequestStorage(persistence)
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
persistence.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `valid request`() {
|
||||
val request = createRequest("LegalName").first
|
||||
val requestId = storage.saveRequest(request)
|
||||
assertNotNull(storage.getRequest(requestId)).apply {
|
||||
assertEquals(request, PKCS10CertificationRequest(this.request))
|
||||
}
|
||||
assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approve request`() {
|
||||
val (request, _) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(request)
|
||||
// Pending request should equals to 1.
|
||||
assertEquals(1, storage.getRequests(RequestStatus.New).size)
|
||||
// Certificate should be empty.
|
||||
assertNull(storage.getRequest(requestId)!!.certificateData)
|
||||
// Store certificate to DB.
|
||||
val result = storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
// Check request request has been approved
|
||||
assertTrue(result)
|
||||
// Check request is not ready yet.
|
||||
// assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady)
|
||||
// New request should be empty.
|
||||
assertTrue(storage.getRequests(RequestStatus.New).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approve request ignores subsequent approvals`() {
|
||||
// Given
|
||||
val (request, _) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(request)
|
||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
|
||||
// When subsequent approval is performed
|
||||
val result = storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
// Then check request has not been approved
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sign request`() {
|
||||
val (csr, _) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// New request should equals to 1.
|
||||
assertEquals(1, storage.getRequests(RequestStatus.New).size)
|
||||
// Certificate should be empty.
|
||||
assertNull(storage.getRequest(requestId)!!.certificateData)
|
||||
// Store certificate to DB.
|
||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
// Check request is not ready yet.
|
||||
assertEquals(RequestStatus.Approved, storage.getRequest(requestId)!!.status)
|
||||
// New request should be empty.
|
||||
assertTrue(storage.getRequests(RequestStatus.New).isEmpty())
|
||||
// Sign certificate
|
||||
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())
|
||||
}, listOf(DOORMAN_SIGNATURE))
|
||||
// Check request is ready
|
||||
assertNotNull(storage.getRequest(requestId)!!.certificateData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sign request ignores subsequent sign requests`() {
|
||||
val (csr, _) = createRequest("LegalName")
|
||||
// Add request to DB.
|
||||
val requestId = storage.saveRequest(csr)
|
||||
// Store certificate to DB.
|
||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||
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())
|
||||
}, listOf(DOORMAN_SIGNATURE))
|
||||
// Sign certificate
|
||||
// When subsequent signature requested
|
||||
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())
|
||||
}, listOf(DOORMAN_SIGNATURE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reject request`() {
|
||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||
storage.rejectRequest(requestId, DOORMAN_SIGNATURE, "Because I said so!")
|
||||
assertThat(storage.getRequests(RequestStatus.New)).isEmpty()
|
||||
assertThat(storage.getRequest(requestId)!!.remark).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.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId1)
|
||||
assertEquals(RequestStatus.Rejected, storage.getRequest(requestId2)!!.status)
|
||||
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
|
||||
// Make sure the first request is processed properly
|
||||
storage.approveRequest(requestId1, DOORMAN_SIGNATURE)
|
||||
assertThat(storage.getRequest(requestId1)!!.status).isEqualTo(RequestStatus.Approved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a previously approved request`() {
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA").first)
|
||||
storage.approveRequest(requestId1, DOORMAN_SIGNATURE)
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
assertThat(storage.getRequest(requestId2)!!.remark).containsIgnoringCase("duplicate")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request with the same legal name as a previously rejected request`() {
|
||||
val requestId1 = storage.saveRequest(createRequest("BankA").first)
|
||||
storage.rejectRequest(requestId1, DOORMAN_SIGNATURE, "Because I said so!")
|
||||
val requestId2 = storage.saveRequest(createRequest("BankA").first)
|
||||
assertThat(storage.getRequests(RequestStatus.New).map { it.requestId }).containsOnly(requestId2)
|
||||
storage.approveRequest(requestId2, DOORMAN_SIGNATURE)
|
||||
assertThat(storage.getRequest(requestId2)!!.status).isEqualTo(RequestStatus.Approved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `audit data is available for CSRs`() {
|
||||
// given
|
||||
val approver = "APPROVER"
|
||||
|
||||
// when
|
||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||
storage.approveRequest(requestId, approver)
|
||||
|
||||
// then
|
||||
persistence.transaction {
|
||||
val auditReader = AuditReaderFactory.get(persistence.entityManagerFactory.createEntityManager())
|
||||
val newRevision = auditReader.find(CertificateSigningRequest::class.java, requestId, 1)
|
||||
assertEquals(RequestStatus.New, newRevision.status)
|
||||
assertTrue(newRevision.modifiedBy.isEmpty())
|
||||
val approvedRevision = auditReader.find(CertificateSigningRequest::class.java, requestId, 2)
|
||||
assertEquals(RequestStatus.Approved, approvedRevision.status)
|
||||
assertEquals(approver, approvedRevision.modifiedBy.first())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRequest(legalName: String): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val request = X509Utilities.createCertificateSigningRequest(CordaX500Name(organisation = legalName, locality = "London", country = "GB"), "my@mail.com", keyPair)
|
||||
return Pair(request, keyPair)
|
||||
}
|
||||
|
||||
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
|
||||
val props = Properties()
|
||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
||||
props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
|
||||
props.setProperty("dataSource.user", "sa")
|
||||
props.setProperty("dataSource.password", "")
|
||||
return props
|
||||
}
|
||||
|
||||
private fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties {
|
||||
val props = Properties()
|
||||
props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String)
|
||||
if (key != null) {
|
||||
props.setProperty(key, value)
|
||||
}
|
||||
return props
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package com.r3.corda.networkmanage.common.persistence
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.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(), { throw UnsupportedOperationException() }, SchemaService())
|
||||
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, DOORMAN_SIGNATURE)
|
||||
|
||||
assertNull(nodeInfoStorage.getCertificatePath(keyPair.public.hash()))
|
||||
|
||||
requestStorage.putCertificatePath(requestId, buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()), listOf(DOORMAN_SIGNATURE))
|
||||
|
||||
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,59 @@
|
||||
package com.r3.corda.networkmanage.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.networkmanage.common.persistence.*
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.toX509Certificate
|
||||
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
|
||||
import com.r3.corda.networkmanage.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, remark = "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,50 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.typesafe.config.ConfigException
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class DoormanParametersTest {
|
||||
private val testDummyPath = ".${File.separator}testDummyPath.jks"
|
||||
private val validConfigPath = File(javaClass.getResource("/doorman.conf").toURI()).absolutePath
|
||||
private val invalidConfigPath = File(javaClass.getResource("/doorman_fail.conf").toURI()).absolutePath
|
||||
|
||||
@Test
|
||||
fun `parse mode flag arg correctly`() {
|
||||
assertEquals(DoormanParameters.Mode.CA_KEYGEN, parseParameters("--mode", "CA_KEYGEN", "--configFile", validConfigPath).mode)
|
||||
assertEquals(DoormanParameters.Mode.ROOT_KEYGEN, parseParameters("--mode", "ROOT_KEYGEN", "--configFile", validConfigPath).mode)
|
||||
assertEquals(DoormanParameters.Mode.DOORMAN, parseParameters("--mode", "DOORMAN", "--configFile", validConfigPath).mode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `command line arg should override config file`() {
|
||||
val params = parseParameters("--keystorePath", testDummyPath, "--port", "1000", "--configFile", validConfigPath)
|
||||
assertEquals(testDummyPath, params.keystorePath.toString())
|
||||
assertEquals(1000, params.port)
|
||||
|
||||
val params2 = parseParameters("--configFile", validConfigPath)
|
||||
assertEquals(Paths.get("/opt/doorman/certificates/caKeystore.jks"), params2.keystorePath)
|
||||
assertEquals(8080, params2.port)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when config missing`() {
|
||||
// dataSourceProperties is missing from node_fail.conf and it should fail during parsing, and shouldn't use default from reference.conf.
|
||||
assertFailsWith<ConfigException.Missing> {
|
||||
parseParameters("--configFile", invalidConfigPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse jira config correctly`() {
|
||||
val parameter = parseParameters("--configFile", validConfigPath)
|
||||
assertEquals("https://doorman-jira-host.com/", parameter.jiraConfig?.address)
|
||||
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
||||
assertEquals("username", parameter.jiraConfig?.username)
|
||||
assertEquals("password", parameter.jiraConfig?.password)
|
||||
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
package com.r3.corda.networkmanage.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.networkmanage.common.persistence.NodeInfoStorage
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.toX509Certificate
|
||||
import com.r3.corda.networkmanage.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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
||||
import com.r3.corda.networkmanage.common.utils.toX509Certificate
|
||||
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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
|
||||
import net.corda.node.utilities.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.HttpURLConnection.*
|
||||
import java.net.URL
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.ws.rs.core.MediaType
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class RegistrationWebServiceTest {
|
||||
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, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
|
||||
private lateinit var doormanServer: DoormanServer
|
||||
|
||||
private fun startSigningServer(csrHandler: CsrHandler) {
|
||||
doormanServer = DoormanServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, DoormanServerStatus()))
|
||||
doormanServer.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun close() {
|
||||
doormanServer.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submit request`() {
|
||||
val id = SecureHash.randomSHA256().toString()
|
||||
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
on { saveRequest(any()) }.then { id }
|
||||
}
|
||||
|
||||
startSigningServer(requestProcessor)
|
||||
|
||||
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
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(requestProcessor, times(1)).saveRequest(any())
|
||||
submitRequest(request)
|
||||
verify(requestProcessor, times(2)).saveRequest(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieve certificate`() {
|
||||
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val id = SecureHash.randomSHA256().toString()
|
||||
|
||||
// Mock Storage behaviour.
|
||||
val certificateStore = mutableMapOf<String, CertPath>()
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
on { getResponse(eq(id)) }.then {
|
||||
certificateStore[id]?.let {
|
||||
CertificateResponse.Ready(it)
|
||||
} ?: CertificateResponse.NotReady
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
startSigningServer(requestProcessor)
|
||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||
|
||||
requestProcessor.processApprovedRequests()
|
||||
|
||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||
verify(requestProcessor, times(2)).getResponse(any())
|
||||
assertEquals(3, certificates.size)
|
||||
|
||||
certificates.first().run {
|
||||
assertThat(subjectDN.name).contains("O=LegalName")
|
||||
assertThat(subjectDN.name).contains("L=London")
|
||||
}
|
||||
|
||||
certificates.last().run {
|
||||
assertThat(subjectDN.name).contains("CN=Corda Node Root CA")
|
||||
assertThat(subjectDN.name).contains("L=London")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieve certificate and create valid TLS certificate`() {
|
||||
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val id = SecureHash.randomSHA256().toString()
|
||||
|
||||
// Mock Storage behaviour.
|
||||
val certificateStore = mutableMapOf<String, CertPath>()
|
||||
val storage = mock<CsrHandler> {
|
||||
on { getResponse(eq(id)) }.then {
|
||||
certificateStore[id]?.let {
|
||||
CertificateResponse.Ready(it)
|
||||
} ?: CertificateResponse.NotReady
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
startSigningServer(storage)
|
||||
|
||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||
|
||||
storage.processApprovedRequests()
|
||||
|
||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||
verify(storage, times(2)).getResponse(any())
|
||||
assertEquals(3, certificates.size)
|
||||
|
||||
val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
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(certificates.last(), sslCert, *certificates.toTypedArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request not authorised`() {
|
||||
val id = SecureHash.randomSHA256().toString()
|
||||
|
||||
val requestProcessor = mock<CsrHandler> {
|
||||
on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") }
|
||||
}
|
||||
|
||||
startSigningServer(requestProcessor)
|
||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.Unauthorised("Not Allowed"))
|
||||
}
|
||||
|
||||
private fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||
val conn = URL("http://${doormanServer.hostAndPort}/api/certificate").openConnection() as HttpURLConnection
|
||||
conn.doOutput = true
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
||||
conn.outputStream.write(request.encoded)
|
||||
return conn.inputStream.bufferedReader().use { it.readLine() }
|
||||
}
|
||||
|
||||
private fun pollForResponse(id: String): PollResponse {
|
||||
val url = URL("http://${doormanServer.hostAndPort}/api/certificate/$id")
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
|
||||
return when (conn.responseCode) {
|
||||
HTTP_OK -> ZipInputStream(conn.inputStream).use {
|
||||
val stream = CertificateStream(it)
|
||||
val certificates = ArrayList<X509Certificate>()
|
||||
while (it.nextEntry != null) {
|
||||
certificates.add(stream.nextCertificate())
|
||||
}
|
||||
PollResponse.Ready(certificates)
|
||||
}
|
||||
HTTP_NO_CONTENT -> PollResponse.NotReady
|
||||
HTTP_UNAUTHORIZED -> PollResponse.Unauthorised(IOUtils.toString(conn.errorStream))
|
||||
else -> throw IOException("Cannot connect to Certificate Signing Server, HTTP response code : ${conn.responseCode}")
|
||||
}
|
||||
}
|
||||
|
||||
private interface PollResponse {
|
||||
object NotReady : PollResponse
|
||||
data class Ready(val certChain: List<X509Certificate>) : PollResponse
|
||||
data class Unauthorised(val message: String) : PollResponse
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.r3.corda.networkmanage.hsm.authentication
|
||||
|
||||
import CryptoServerCXI.CryptoServerCXI
|
||||
import CryptoServerJCE.CryptoServerProvider
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.Console
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AuthenticatorTest {
|
||||
|
||||
private lateinit var provider: CryptoServerProvider
|
||||
private lateinit var console: Console
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
provider = mock()
|
||||
whenever(provider.cryptoServer).thenReturn(mock<CryptoServerCXI>())
|
||||
console = mock()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `connectAndAuthenticate aborts when user inputs Q`() {
|
||||
// given
|
||||
givenUserConsoleInputOnReadLine("Q")
|
||||
var executed = false
|
||||
|
||||
// when
|
||||
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
|
||||
|
||||
// then
|
||||
assertFalse(executed)
|
||||
verify(provider, never()).loginPassword(any<String>(), any<String>())
|
||||
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `connectAndAuthenticate authenticates user with password`() {
|
||||
// given
|
||||
val username = "TEST_USER"
|
||||
val password = "TEST_PASSWORD"
|
||||
givenUserConsoleInputOnReadLine(username)
|
||||
givenUserConsoleInputOnReadPassword(password)
|
||||
givenAuthenticationResult(true)
|
||||
var executed = false
|
||||
|
||||
// when
|
||||
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
|
||||
|
||||
// then
|
||||
verify(provider).loginPassword(username, password)
|
||||
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
|
||||
assertTrue(executed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `connectAndAuthenticate authenticates user with card reader`() {
|
||||
// given
|
||||
val username = "TEST_USER"
|
||||
givenUserConsoleInputOnReadLine(username)
|
||||
givenAuthenticationResult(true)
|
||||
var executed = false
|
||||
|
||||
// when
|
||||
Authenticator(provider = provider, console = console, mode = AuthMode.CARD_READER).connectAndAuthenticate { _, _ -> executed = true }
|
||||
|
||||
// then
|
||||
verify(provider).loginSign(username, ":cs2:cyb:USB0", null)
|
||||
verify(provider, never()).loginPassword(any<String>(), any<String>())
|
||||
assertTrue(executed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `connectAndAuthenticate authenticates multiple users with password`() {
|
||||
// given
|
||||
val username = "TEST_USER"
|
||||
val password = "TEST_PASSWORD"
|
||||
givenUserConsoleInputOnReadLine(username)
|
||||
givenUserConsoleInputOnReadPassword(password)
|
||||
givenAuthenticationResult(false, false, true)
|
||||
var executed = false
|
||||
|
||||
// when
|
||||
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
|
||||
|
||||
// then
|
||||
verify(provider, times(3)).loginPassword(username, password)
|
||||
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
|
||||
assertTrue(executed)
|
||||
}
|
||||
|
||||
private fun givenAuthenticationResult(sufficient: Boolean, vararg subsequent: Boolean) {
|
||||
val stub = whenever(provider.cryptoServer.authState).thenReturn(if (sufficient) 3 else 0)
|
||||
subsequent.forEach {
|
||||
stub.thenReturn(if (it) 3 else 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenUserConsoleInputOnReadPassword(input: String) {
|
||||
whenever(console.readPassword(any<String>())).thenReturn(input.toCharArray())
|
||||
}
|
||||
|
||||
private fun givenUserConsoleInputOnReadLine(input: String) {
|
||||
whenever(console.readLine()).thenReturn(input)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.r3.corda.networkmanage.hsm.configuration
|
||||
|
||||
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
|
||||
import com.typesafe.config.ConfigException
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ConfigurationTest {
|
||||
private val validConfigPath = File(javaClass.getResource("/hsm.conf").toURI()).absolutePath
|
||||
private val invalidConfigPath = File(javaClass.getResource("/hsm_fail.conf").toURI()).absolutePath
|
||||
|
||||
@Test
|
||||
fun `authMode is parsed correctly`() {
|
||||
val paramsWithPassword = parseParameters("--configFile", validConfigPath, "--authMode", AuthMode.CARD_READER.name)
|
||||
assertEquals(AuthMode.CARD_READER, paramsWithPassword.authMode)
|
||||
val paramsWithCardReader = parseParameters("--configFile", validConfigPath, "--authMode", AuthMode.PASSWORD.name)
|
||||
assertEquals(AuthMode.PASSWORD, paramsWithCardReader.authMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validDays duration is parsed correctly`() {
|
||||
val expectedDuration = 360
|
||||
val paramsWithPassword = parseParameters("--configFile", validConfigPath, "--validDays", expectedDuration.toString())
|
||||
assertEquals(expectedDuration, paramsWithPassword.validDays)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when config missing database source properties`() {
|
||||
// dataSourceProperties is missing from node_fail.conf and it should fail during parsing, and shouldn't use default from reference.conf.
|
||||
assertFailsWith<ConfigException.Missing> {
|
||||
parseParameters("--configFile", invalidConfigPath)
|
||||
}
|
||||
}
|
||||
}
|
21
network-management/src/test/resources/doorman.conf
Normal file
21
network-management/src/test/resources/doorman.conf
Normal file
@ -0,0 +1,21 @@
|
||||
keystorePath = "/opt/doorman/certificates/caKeystore.jks"
|
||||
keyStorePassword = "password"
|
||||
caPrivateKeyPassword = "password"
|
||||
host = "localhost"
|
||||
port = 8080
|
||||
h2port = 0
|
||||
|
||||
dataSourceProperties {
|
||||
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
|
||||
jiraConfig{
|
||||
address = "https://doorman-jira-host.com/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
6
network-management/src/test/resources/doorman_fail.conf
Normal file
6
network-management/src/test/resources/doorman_fail.conf
Normal file
@ -0,0 +1,6 @@
|
||||
keystorePath = "/opt/doorman/certificates/caKeystore.jks"
|
||||
keyStorePassword = "password"
|
||||
caPrivateKeyPassword = "password"
|
||||
host = "localhost"
|
||||
port = 8080
|
||||
h2port = 0
|
12
network-management/src/test/resources/hsm.conf
Normal file
12
network-management/src/test/resources/hsm.conf
Normal file
@ -0,0 +1,12 @@
|
||||
device = "3001@127.0.0.1"
|
||||
keyGroup = "*"
|
||||
keySpecifier = -1
|
||||
authMode = PASSWORD
|
||||
|
||||
h2port = 0
|
||||
dataSourceProperties {
|
||||
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
4
network-management/src/test/resources/hsm_fail.conf
Normal file
4
network-management/src/test/resources/hsm_fail.conf
Normal file
@ -0,0 +1,4 @@
|
||||
device = "3001@127.0.0.1"
|
||||
keyGroup = "*"
|
||||
keySpecifier = -1
|
||||
authMode = PASSWORD
|
@ -0,0 +1 @@
|
||||
mock-maker-inline
|
Reference in New Issue
Block a user