mirror of
https://github.com/corda/corda.git
synced 2025-06-19 15:43:52 +00:00
[ENT-2539] Implement support for holding node Legal Identity keys in Ultimaco HSM (#1571)
* Add dependencies for Utimaco HSM. The CryptoServerJCE.jar was added in node/lib. The node/capsule/build.gradle excludes this jar from the final corda.jar. * Copy the HSM Simulator utility from Network Services. HsmSimulator.kt uses the spotify docker client to pull an image with a pre-configured hsm simulator from our docker registry and run it in integration tests. * Implementation of the CryptoService interface for Utimaco HSM. * Integration test for Utimaco CryptoService. * Unit tests for UtimacoCryptoService (only config parsing). * Integrate Utimaco CryptoService in AbstractNode and NodeConfiguration. * Respond to Feedback: Remove copyright notice. * Respond to PR Feedback: Improve integration test. * Use custom overrides in DriverDSLImpl.startNodeRegistration * Make Utimaco dependencies compileOnly and testCompile. * Add integration test for registering a node that is backed by utimaco HSM. * Respond to feedback: move HsmSimulator to different package. * Make NodeConfiguration.cryptoServiceConf a Path instead of String. * Add Keyfile-based login. * Respond to feedback -- default signing algorithm. * Respond to feedback: naming. * UtimacoNodeRegistrationTest: explicitly verify that tx signature is valid. * Respond to feedback: Static import assertThat. * Rename key file for test login so it's not ignored.
This commit is contained in:
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -49,6 +49,10 @@
|
|||||||
<module name="client_test" target="1.8" />
|
<module name="client_test" target="1.8" />
|
||||||
<module name="cliutils_main" target="1.8" />
|
<module name="cliutils_main" target="1.8" />
|
||||||
<module name="cliutils_test" target="1.8" />
|
<module name="cliutils_test" target="1.8" />
|
||||||
|
<module name="com.r3.corda_buildSrc_main" target="1.8" />
|
||||||
|
<module name="com.r3.corda_buildSrc_test" target="1.8" />
|
||||||
|
<module name="com.r3.corda_canonicalizer_main" target="1.8" />
|
||||||
|
<module name="com.r3.corda_canonicalizer_test" target="1.8" />
|
||||||
<module name="common-configuration-parsing_main" target="1.8" />
|
<module name="common-configuration-parsing_main" target="1.8" />
|
||||||
<module name="common-configuration-parsing_test" target="1.8" />
|
<module name="common-configuration-parsing_test" target="1.8" />
|
||||||
<module name="common-validation_main" target="1.8" />
|
<module name="common-validation_main" target="1.8" />
|
||||||
|
7
docs/source/corda-hsm.rst
Normal file
7
docs/source/corda-hsm.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Configuring the node to use the Utimaco HSM
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
TODO
|
||||||
|
-------
|
@ -38,6 +38,8 @@ import java.util.*
|
|||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
object X509Utilities {
|
object X509Utilities {
|
||||||
|
// Note that this default value only applies to BCCryptoService. Other implementations of CryptoService may have to use different
|
||||||
|
// schemes (for instance `UtimacoCryptoService.DEFAULT_IDENTITY_SIGNATURE_SCHEME`).
|
||||||
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
|
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
|
||||||
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
||||||
|
|
||||||
|
@ -75,6 +75,10 @@ dependencies {
|
|||||||
compile project(':common-validation')
|
compile project(':common-validation')
|
||||||
compile project(':common-configuration-parsing')
|
compile project(':common-configuration-parsing')
|
||||||
|
|
||||||
|
// Utimaco HSM
|
||||||
|
compileOnly fileTree(dir: 'lib', include: 'CryptoServer*.jar')
|
||||||
|
testCompile fileTree(dir: 'lib', include: 'CryptoServer*.jar')
|
||||||
|
|
||||||
// Log4J: logging framework (with SLF4J bindings)
|
// Log4J: logging framework (with SLF4J bindings)
|
||||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
||||||
compile "org.apache.logging.log4j:log4j-web:${log4j_version}"
|
compile "org.apache.logging.log4j:log4j-web:${log4j_version}"
|
||||||
@ -183,6 +187,9 @@ dependencies {
|
|||||||
testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
||||||
testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
||||||
|
|
||||||
|
// Spotify Docker client for using docker containers in integration tests
|
||||||
|
testCompile "com.spotify:docker-client:8.9.1"
|
||||||
|
|
||||||
// Add runtime-only dependency on the JDBC driver for the specified DB provider (used in database integration tests)
|
// Add runtime-only dependency on the JDBC driver for the specified DB provider (used in database integration tests)
|
||||||
def DB_PROVIDER = System.getProperty("custom.databaseProvider")
|
def DB_PROVIDER = System.getProperty("custom.databaseProvider")
|
||||||
switch (DB_PROVIDER) {
|
switch (DB_PROVIDER) {
|
||||||
|
@ -51,9 +51,11 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) {
|
|||||||
)
|
)
|
||||||
from configurations.capsuleRuntime.files.collect { zipTree(it) }
|
from configurations.capsuleRuntime.files.collect { zipTree(it) }
|
||||||
|
|
||||||
|
// Exclude the binaries for the Utimaco HSM as we cannot distribute them.
|
||||||
|
exclude('**/CryptoServer*.jar')
|
||||||
|
|
||||||
capsuleManifest {
|
capsuleManifest {
|
||||||
applicationVersion = corda_release_version
|
applicationVersion = corda_release_version
|
||||||
|
|
||||||
// See experimental/quasar-hook/README.md for how to generate.
|
// See experimental/quasar-hook/README.md for how to generate.
|
||||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
||||||
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
|
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
|
||||||
|
BIN
node/lib/CryptoServerJCE.jar
Normal file
BIN
node/lib/CryptoServerJCE.jar
Normal file
Binary file not shown.
@ -0,0 +1,34 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice.utimaco
|
||||||
|
|
||||||
|
fun testConfig(
|
||||||
|
port: Int,
|
||||||
|
host: String = "127.0.0.1",
|
||||||
|
connectionTimeout: Int = 30000,
|
||||||
|
timeout: Int = 60000,
|
||||||
|
endSessionOnShutdown: Boolean = false,
|
||||||
|
keepSessionAlive: Boolean = false,
|
||||||
|
keyGroup: String = "TEST.CORDACONNECT.ROOT",
|
||||||
|
keySpecifier: Int = 1,
|
||||||
|
storeKeysExternal: Boolean = false,
|
||||||
|
username: String = "INTEGRATION_TEST",
|
||||||
|
password: String = "INTEGRATION_TEST"): UtimacoCryptoService.UtimacoConfig {
|
||||||
|
return UtimacoCryptoService.UtimacoConfig(
|
||||||
|
UtimacoCryptoService.ProviderConfig(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
connectionTimeout,
|
||||||
|
timeout,
|
||||||
|
endSessionOnShutdown,
|
||||||
|
keepSessionAlive,
|
||||||
|
keyGroup,
|
||||||
|
keySpecifier,
|
||||||
|
storeKeysExternal
|
||||||
|
),
|
||||||
|
UtimacoCryptoService.KeyGenerationConfiguration(
|
||||||
|
keyGroup = "TEST.CORDACONNECT.ROOT",
|
||||||
|
keySpecifier = 1
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
UtimacoCryptoService.Credentials(username, password)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice.utimaco
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.core.utilities.days
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
|
import net.corda.testing.core.getTestPartyAndCertificate
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import net.corda.node.hsm.HsmSimulator
|
||||||
|
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
|
||||||
|
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class UtimacoCryptoServiceIntegrationTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val hsmSimulator: HsmSimulator = HsmSimulator()
|
||||||
|
|
||||||
|
val config = testConfig(hsmSimulator.port)
|
||||||
|
val login = { UtimacoCryptoService.UtimacoCredentials("INTEGRATION_TEST", "INTEGRATION_TEST".toByteArray()) }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When credentials are incorrect, should throw UtimacoHSMException`() {
|
||||||
|
val config = testConfig(hsmSimulator.port)
|
||||||
|
assertFailsWith<UtimacoCryptoService.UtimacoHSMException> {
|
||||||
|
UtimacoCryptoService.fromConfig(config) {
|
||||||
|
UtimacoCryptoService.UtimacoCredentials("invalid", "invalid".toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When credentials become incorrect, should throw UtimacoHSMException`() {
|
||||||
|
var pw = "INTEGRATION_TEST"
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config) { UtimacoCryptoService.UtimacoCredentials("INTEGRATION_TEST", pw.toByteArray()) }
|
||||||
|
cryptoService.logOff()
|
||||||
|
pw = "foo"
|
||||||
|
assertFailsWith<UtimacoCryptoService.UtimacoHSMException> { cryptoService.generateKeyPair("foo", Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When connection cannot be established, should throw ConnectionException`() {
|
||||||
|
val invalidConfig = testConfig(1)
|
||||||
|
assertFailsWith<IOException> {
|
||||||
|
UtimacoCryptoService.fromConfig(invalidConfig, login)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When alias contains illegal characters, should throw `() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = "a".repeat(257)
|
||||||
|
assertFailsWith<UtimacoCryptoService.UtimacoHSMException> { cryptoService.generateKeyPair(alias, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Handles re-authentication properly`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
cryptoService.logOff()
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID)
|
||||||
|
assertTrue { cryptoService.containsKey(alias) }
|
||||||
|
val data = UUID.randomUUID().toString().toByteArray()
|
||||||
|
cryptoService.logOff()
|
||||||
|
val signed = cryptoService.sign(alias, data)
|
||||||
|
Crypto.doVerify(pubKey, signed, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Generate ECDSA key with r1 curve, then sign and verify data`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID)
|
||||||
|
assertTrue { cryptoService.containsKey(alias) }
|
||||||
|
val data = UUID.randomUUID().toString().toByteArray()
|
||||||
|
val signed = cryptoService.sign(alias, data)
|
||||||
|
Crypto.doVerify(pubKey, signed, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Generate ECDSA key with k1 curve, then sign and verify data`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, Crypto.ECDSA_SECP256K1_SHA256.schemeNumberID)
|
||||||
|
assertTrue { cryptoService.containsKey(alias) }
|
||||||
|
val data = UUID.randomUUID().toString().toByteArray()
|
||||||
|
val signed = cryptoService.sign(alias, data)
|
||||||
|
Crypto.doVerify(pubKey, signed, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Generate RSA key, then sign and verify data`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, Crypto.RSA_SHA256.schemeNumberID)
|
||||||
|
assertTrue { cryptoService.containsKey(alias) }
|
||||||
|
val data = UUID.randomUUID().toString().toByteArray()
|
||||||
|
val signed = cryptoService.sign(alias, data)
|
||||||
|
Crypto.doVerify(pubKey, signed, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When key does not exist, signing should throw`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
assertFalse { cryptoService.containsKey(alias) }
|
||||||
|
val data = UUID.randomUUID().toString().toByteArray()
|
||||||
|
assertFailsWith<CryptoServiceException> { cryptoService.sign(alias, data) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When key does not exist, getPublicKey should return null`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
assertFalse { cryptoService.containsKey(alias) }
|
||||||
|
assertNull(cryptoService.getPublicKey(alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When key does not exist, getContentSigner should throw`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config)
|
||||||
|
{ UtimacoCryptoService.UtimacoCredentials("INTEGRATION_TEST", "INTEGRATION_TEST".toByteArray()) }
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
assertFalse { cryptoService.containsKey(alias) }
|
||||||
|
assertFailsWith<CryptoServiceException> { cryptoService.getSigner(alias) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Content signer works with X509Utilities`() {
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(config, login)
|
||||||
|
val alias = UUID.randomUUID().toString()
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID)
|
||||||
|
val signer = cryptoService.getSigner(alias)
|
||||||
|
val otherAlias = UUID.randomUUID().toString()
|
||||||
|
val otherPubKey = cryptoService.generateKeyPair(otherAlias, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID)
|
||||||
|
val issuer = Party(DUMMY_BANK_A_NAME, pubKey)
|
||||||
|
val partyAndCert = getTestPartyAndCertificate(issuer)
|
||||||
|
val issuerCert = partyAndCert.certificate
|
||||||
|
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCert)
|
||||||
|
val ourCertificate = X509Utilities.createCertificate(
|
||||||
|
CertificateType.CONFIDENTIAL_LEGAL_IDENTITY,
|
||||||
|
issuerCert.subjectX500Principal,
|
||||||
|
issuerCert.publicKey,
|
||||||
|
signer,
|
||||||
|
partyAndCert.name.x500Principal,
|
||||||
|
otherPubKey,
|
||||||
|
window)
|
||||||
|
ourCertificate.checkValidity()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `login with key file`() {
|
||||||
|
// the admin user of the simulator is set up with key-file login
|
||||||
|
val keyFile = UtimacoCryptoServiceIntegrationTest::class.java.getResource("ADMIN.keykey").toPath()
|
||||||
|
val username = "ADMIN"
|
||||||
|
val pw = "utimaco".toByteArray()
|
||||||
|
val conf = config.copy(authThreshold = 0) // because auth state for the admin user is 570425344
|
||||||
|
val cryptoService = UtimacoCryptoService.fromConfig(conf) { UtimacoCryptoService.UtimacoCredentials(username, pw, keyFile) }
|
||||||
|
// the admin user does not have permission to access or create keys, so this operation will fail
|
||||||
|
assertFailsWith<UtimacoCryptoService.UtimacoHSMException> { cryptoService.generateKeyPair("no", Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice.utimaco
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
|
import net.corda.node.utilities.registration.TestDoorman
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.internal.DEV_ROOT_CA
|
||||||
|
import net.corda.testing.internal.IntegrationTest
|
||||||
|
import net.corda.testing.internal.IntegrationTestSchemas
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.internal.DriverDSLImpl
|
||||||
|
import net.corda.testing.node.internal.SharedCompatibilityZoneParams
|
||||||
|
import net.corda.testing.node.internal.internalDriver
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import net.corda.node.hsm.HsmSimulator
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
class UtimacoNodeRegistrationTest : IntegrationTest() {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val doorman: TestDoorman = TestDoorman()
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val configFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val hsmSimulator: HsmSimulator = HsmSimulator()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@ClassRule
|
||||||
|
@JvmField
|
||||||
|
val databaseSchemas = IntegrationTestSchemas("NotaryService", "Alice", "Genevieve")
|
||||||
|
|
||||||
|
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
|
||||||
|
private val aliceName = CordaX500Name("Alice", "London", "GB")
|
||||||
|
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `node registration with one node backed by Utimaco HSM`() {
|
||||||
|
|
||||||
|
val tmpUtimacoConfig = createTempUtimacoConfig()
|
||||||
|
|
||||||
|
val compatibilityZone = SharedCompatibilityZoneParams(
|
||||||
|
URL("http://${doorman.serverHostAndPort}"),
|
||||||
|
null,
|
||||||
|
publishNotaries = { doorman.server.networkParameters = testNetworkParameters(it) },
|
||||||
|
rootCert = DEV_ROOT_CA.certificate)
|
||||||
|
internalDriver(
|
||||||
|
portAllocation = doorman.portAllocation,
|
||||||
|
compatibilityZone = compatibilityZone,
|
||||||
|
initialiseSerialization = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||||
|
cordappsForAllNodes = DriverDSLImpl.cordappsInCurrentAndAdditionalPackages("net.corda.finance"),
|
||||||
|
notaryCustomOverrides = mapOf("devMode" to false)
|
||||||
|
) {
|
||||||
|
val (alice, genevieve) = listOf(
|
||||||
|
startNode(providedName = aliceName, customOverrides = mapOf(
|
||||||
|
"devMode" to false,
|
||||||
|
"cryptoServiceName" to "UTIMACO",
|
||||||
|
"cryptoServiceConf" to tmpUtimacoConfig
|
||||||
|
)),
|
||||||
|
startNode(providedName = genevieveName, customOverrides = mapOf("devMode" to false))
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
|
Assertions.assertThat(doorman.registrationHandler.idsPolled).containsOnly(
|
||||||
|
aliceName.organisation,
|
||||||
|
genevieveName.organisation,
|
||||||
|
notaryName.organisation)
|
||||||
|
|
||||||
|
// Check the nodes can communicate among themselves (and the notary).
|
||||||
|
val anonymous = false
|
||||||
|
val result = alice.rpc.startFlow(
|
||||||
|
::CashIssueAndPaymentFlow,
|
||||||
|
1000.DOLLARS,
|
||||||
|
OpaqueBytes.of(12),
|
||||||
|
genevieve.nodeInfo.singleIdentity(),
|
||||||
|
anonymous,
|
||||||
|
defaultNotaryIdentity
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
|
||||||
|
// make sure the transaction was actually signed by the key in the hsm
|
||||||
|
val utimacoCryptoService = UtimacoCryptoService.fromConfigurationFile(File(tmpUtimacoConfig).toPath())
|
||||||
|
val alicePubKey = utimacoCryptoService.getPublicKey("identity-private-key")
|
||||||
|
assertThat(alicePubKey).isNotNull()
|
||||||
|
assertThat(result.stx.sigs.map { it.by.encoded }.filter { it.contentEquals(alicePubKey!!.encoded) }).hasSize(1)
|
||||||
|
assertThat(result.stx.sigs.single { it.by.encoded.contentEquals(alicePubKey!!.encoded) }.isValid(result.stx.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTempUtimacoConfig(): String {
|
||||||
|
val utimacoConfig = ConfigFactory.parseFile(javaClass.getResource("utimaco_config.yml").toPath().toFile())
|
||||||
|
val portConfig = ConfigFactory.parseMap(mapOf("provider.port" to hsmSimulator.port))
|
||||||
|
val config = portConfig.withFallback(utimacoConfig)
|
||||||
|
val tmpConfigFile = configFolder.newFile("utimaco_config.yml")
|
||||||
|
FileUtils.writeStringToFile(tmpConfigFile, config.root().render(), Charset.defaultCharset())
|
||||||
|
return tmpConfigFile.absolutePath
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
package net.corda.node.utilities.registration
|
||||||
|
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.logElapsedTime
|
||||||
|
import net.corda.core.internal.readFully
|
||||||
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.driver.PortAllocation
|
||||||
|
import net.corda.testing.internal.DEV_ROOT_CA
|
||||||
|
import net.corda.testing.internal.IntegrationTest
|
||||||
|
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.rules.ExternalResource
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.cert.CertPath
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
import javax.ws.rs.*
|
||||||
|
import javax.ws.rs.core.MediaType
|
||||||
|
import javax.ws.rs.core.Response
|
||||||
|
|
||||||
|
class TestDoorman: ExternalResource() {
|
||||||
|
|
||||||
|
internal val portAllocation = PortAllocation.Incremental(13000)
|
||||||
|
internal val registrationHandler = RegistrationHandler(DEV_ROOT_CA)
|
||||||
|
internal lateinit var server: NetworkMapServer
|
||||||
|
internal lateinit var serverHostAndPort: NetworkHostAndPort
|
||||||
|
|
||||||
|
override fun before() {
|
||||||
|
server = NetworkMapServer(
|
||||||
|
pollInterval = 1.seconds,
|
||||||
|
hostAndPort = portAllocation.nextHostAndPort(),
|
||||||
|
myHostNameValue = "localhost",
|
||||||
|
additionalServices = *arrayOf(registrationHandler))
|
||||||
|
serverHostAndPort = server.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun after() {
|
||||||
|
server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("certificate")
|
||||||
|
class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair) {
|
||||||
|
private val certPaths = ConcurrentHashMap<String, CertPath>()
|
||||||
|
val idsPolled = ConcurrentSkipListSet<String>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val log = loggerFor<RegistrationHandler>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
fun registration(input: InputStream): Response {
|
||||||
|
return log.logElapsedTime("Registration") {
|
||||||
|
val certificationRequest = JcaPKCS10CertificationRequest(input.readFully())
|
||||||
|
val (certPath, name) = createSignedClientCertificate(
|
||||||
|
certificationRequest,
|
||||||
|
rootCertAndKeyPair.keyPair,
|
||||||
|
listOf(rootCertAndKeyPair.certificate))
|
||||||
|
require(!name.organisation.contains("\\s".toRegex())) { "Whitespace in the organisation name not supported" }
|
||||||
|
certPaths[name.organisation] = certPath
|
||||||
|
Response.ok(name.organisation).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
fun reply(@PathParam("id") id: String): Response {
|
||||||
|
return log.logElapsedTime("Reply by Id") {
|
||||||
|
idsPolled += id
|
||||||
|
buildResponse(certPaths[id]!!.certificates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildResponse(certificates: List<Certificate>): Response {
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
ZipOutputStream(baos).use { zip ->
|
||||||
|
listOf(X509Utilities.CORDA_CLIENT_CA, X509Utilities.CORDA_INTERMEDIATE_CA, X509Utilities.CORDA_ROOT_CA).zip(certificates).forEach {
|
||||||
|
zip.putNextEntry(ZipEntry("${it.first}.cer"))
|
||||||
|
zip.write(it.second.encoded)
|
||||||
|
zip.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Response.ok(baos.toByteArray())
|
||||||
|
.type("application/zip")
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest,
|
||||||
|
caKeyPair: KeyPair,
|
||||||
|
caCertPath: List<X509Certificate>): Pair<CertPath, CordaX500Name> {
|
||||||
|
val request = JcaPKCS10CertificationRequest(certificationRequest)
|
||||||
|
val name = CordaX500Name.parse(request.subject.toString())
|
||||||
|
val nodeCaCert = X509Utilities.createCertificate(
|
||||||
|
CertificateType.NODE_CA,
|
||||||
|
caCertPath[0],
|
||||||
|
caKeyPair,
|
||||||
|
name.x500Principal,
|
||||||
|
request.publicKey,
|
||||||
|
nameConstraints = null)
|
||||||
|
val certPath = X509Utilities.buildCertPath(nodeCaCert, caCertPath)
|
||||||
|
return Pair(certPath, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# RSA key
|
||||||
|
MOD=B7F3893AAB150BAFECC1931097893C38751AD728DD56DEB8F1A41097755B5E0664FF32FD902B04EDCFD5E2EF8330FDF07C15F9C2229E53F71446EEDBC82BEA3D1679B2BBC07B269D0832D098B3478189CB1FD9F770ED5231EE9AA05BEBE2D0F13F4813F919EB8B3B14AEEE0EE22EDEB152CB5B5798712CDE28273B7E5AB232EB
|
||||||
|
PEXP=010001
|
||||||
|
SEXP=0C69C84467C01B524B5942B9D76800E2D47033BDC3B5F580A879C84ED8320AB5C6C1FBE8657EA9ADFC9CF3DBF2CFEF0AF7ECA9B6828C89A0FE42CD2292AEF7F6FB0B8BC61EAE635CE3ACAADACBB0609666266D28B2760483F169C05E672C5C88D2B5B0F66C6474AA7E75A3D526EFBD865D4CD8457DD8F9D31C4B095827C6B3AD
|
||||||
|
P=D19916EC3E718F393467AD608813306B58F763EF6F1A8FE1251AAAE720D1A6F0E552F95DE53C0FECDFFE0ED9E541FC00F83393C9E1B26789D3A779ACA9A5C905
|
||||||
|
Q=E0ACED5548DFF0A24147FEDE87B22505DC11FBC4F080C3E17A11BA588AE2A40AFCFDF352F9031F8F344E909C2ECCD912E2BA6B864C2DE6CFB4F50E03C17F0F2F
|
||||||
|
U=9EC636117E558F3A1C9E03E54A1FADD9F0A6728F34C5842B6F557D58C92BCB243FDB62AA9751B5AA24B4B5129B253ED97D3A69818C7AD2AA6483C2473C1E52F7
|
||||||
|
dP=9D165DC5C5AF1AA6C70E05355A06F7BD1CBA9D5DB0297A3845B4CCEDD8FD085F77A04E60FF139AE3EFA4DBC0974072FCCF08E8F4DF80F474A9FAD50881454D79
|
||||||
|
dQ=7332D781EA1AC0A4413AAC08E7A4C4ECEB38E151CA4B0BA499D56B29A914AA2DE42845D1DE51E6A5A39940F683DC8ED4EB21D0AE0C7360AC5149710525FA830B
|
||||||
|
OWNER=Utimaco IS GmbH / Init-Dev-1-Key
|
@ -0,0 +1,21 @@
|
|||||||
|
provider = {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 3001
|
||||||
|
connectionTimeout = 60000
|
||||||
|
endSessionOnShutdown = true
|
||||||
|
keepSessionAlive = true
|
||||||
|
keyGroup = "*"
|
||||||
|
keySpecifier = 2
|
||||||
|
storeKeysExternal = false
|
||||||
|
}
|
||||||
|
keyGeneration = {
|
||||||
|
keyOverride = true
|
||||||
|
keyExport = false
|
||||||
|
keyGenMechanism = 4
|
||||||
|
keySpecifier = 2
|
||||||
|
}
|
||||||
|
authThreshold = 1
|
||||||
|
authentication = {
|
||||||
|
username = INTEGRATION_TEST
|
||||||
|
password = INTEGRATION_TEST
|
||||||
|
}
|
@ -9,11 +9,8 @@ import net.corda.confidential.SwapIdentitiesHandler
|
|||||||
import net.corda.core.CordaException
|
import net.corda.core.CordaException
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.isCRLDistributionPointBlacklisted
|
|
||||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||||
import net.corda.core.crypto.newSecureRandom
|
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
@ -55,6 +52,7 @@ import net.corda.node.services.identity.PersistentIdentityService
|
|||||||
import net.corda.node.services.keys.BasicHSMKeyManagementService
|
import net.corda.node.services.keys.BasicHSMKeyManagementService
|
||||||
import net.corda.node.services.keys.KeyManagementServiceInternal
|
import net.corda.node.services.keys.KeyManagementServiceInternal
|
||||||
import net.corda.node.services.keys.cryptoservice.BCCryptoService
|
import net.corda.node.services.keys.cryptoservice.BCCryptoService
|
||||||
|
import net.corda.node.services.keys.cryptoservice.utimaco.UtimacoCryptoService
|
||||||
import net.corda.node.services.messaging.DeduplicationHandler
|
import net.corda.node.services.messaging.DeduplicationHandler
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
import net.corda.node.services.network.NetworkMapClient
|
import net.corda.node.services.network.NetworkMapClient
|
||||||
@ -952,7 +950,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath))
|
return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun generateKeyPair(alias: String) = cryptoService.generateKeyPair(alias, X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME.schemeNumberID)
|
protected open fun generateKeyPair(alias: String) = when (cryptoService) {
|
||||||
|
is UtimacoCryptoService -> cryptoService.generateKeyPair(alias, UtimacoCryptoService.DEFAULT_IDENTITY_SIGNATURE_SCHEME.schemeNumberID)
|
||||||
|
else -> cryptoService.generateKeyPair(alias, X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME.schemeNumberID)
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun makeVaultService(keyManagementService: KeyManagementService,
|
protected open fun makeVaultService(keyManagementService: KeyManagementService,
|
||||||
services: ServicesForResolution,
|
services: ServicesForResolution,
|
||||||
|
@ -14,6 +14,10 @@ import net.corda.node.services.keys.cryptoservice.SupportedCryptoServices
|
|||||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
|
import net.corda.node.services.keys.cryptoservice.utimaco.UtimacoCryptoService
|
||||||
|
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||||
|
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
|
||||||
|
import net.corda.nodeapi.internal.config.*
|
||||||
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
import net.corda.tools.shell.SSHDConfiguration
|
||||||
@ -89,7 +93,7 @@ interface NodeConfiguration {
|
|||||||
// TODO At the moment this is just an identifier for the desired CryptoService engine. Consider using a classname to
|
// TODO At the moment this is just an identifier for the desired CryptoService engine. Consider using a classname to
|
||||||
// to allow for pluggable implementations.
|
// to allow for pluggable implementations.
|
||||||
val cryptoServiceName: SupportedCryptoServices?
|
val cryptoServiceName: SupportedCryptoServices?
|
||||||
val cryptoServiceConf: String? // Location for the cryptoService conf file.
|
val cryptoServiceConf: Path? // Location for the cryptoService conf file.
|
||||||
|
|
||||||
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
|
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
|
||||||
|
|
||||||
@ -115,8 +119,9 @@ interface NodeConfiguration {
|
|||||||
|
|
||||||
fun makeCryptoService(): CryptoService {
|
fun makeCryptoService(): CryptoService {
|
||||||
return when(cryptoServiceName) {
|
return when(cryptoServiceName) {
|
||||||
// Pick default BCCryptoService when null.
|
SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore)
|
||||||
SupportedCryptoServices.BC_SIMPLE, null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore)
|
SupportedCryptoServices.UTIMACO -> UtimacoCryptoService.fromConfigurationFile(cryptoServiceConf)
|
||||||
|
null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore) // Pick default BCCryptoService when null.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ data class NodeConfigurationImpl(
|
|||||||
override val flowOverrides: FlowOverrideConfig?,
|
override val flowOverrides: FlowOverrideConfig?,
|
||||||
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
|
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
|
||||||
override val cryptoServiceName: SupportedCryptoServices? = Defaults.cryptoServiceName,
|
override val cryptoServiceName: SupportedCryptoServices? = Defaults.cryptoServiceName,
|
||||||
override val cryptoServiceConf: String? = Defaults.cryptoServiceConf,
|
override val cryptoServiceConf: Path? = Defaults.cryptoServiceConf,
|
||||||
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
|
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
|
||||||
) : NodeConfiguration {
|
) : NodeConfiguration {
|
||||||
internal object Defaults {
|
internal object Defaults {
|
||||||
@ -119,7 +119,7 @@ data class NodeConfigurationImpl(
|
|||||||
val enableSNI: Boolean = true
|
val enableSNI: Boolean = true
|
||||||
val useOpenSsl: Boolean = false
|
val useOpenSsl: Boolean = false
|
||||||
val cryptoServiceName: SupportedCryptoServices? = null
|
val cryptoServiceName: SupportedCryptoServices? = null
|
||||||
val cryptoServiceConf: String? = null
|
val cryptoServiceConf: Path? = null
|
||||||
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
|
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
|
||||||
|
|
||||||
fun cordappsDirectories(baseDirectory: Path) = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT)
|
fun cordappsDirectories(baseDirectory: Path) = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT)
|
||||||
|
@ -75,7 +75,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
|||||||
private val cordappDirectories by string().mapValid(::toPath).list().optional()
|
private val cordappDirectories by string().mapValid(::toPath).list().optional()
|
||||||
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
|
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
|
||||||
private val cryptoServiceName by enum(SupportedCryptoServices::class).optional()
|
private val cryptoServiceName by enum(SupportedCryptoServices::class).optional()
|
||||||
private val cryptoServiceConf by string().optional()
|
private val cryptoServiceConf by string().mapValid(::toPath).optional()
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
private val custom by nestedObject().optional()
|
private val custom by nestedObject().optional()
|
||||||
private val relay by nested(RelayConfigurationSpec).optional()
|
private val relay by nested(RelayConfigurationSpec).optional()
|
||||||
|
@ -2,8 +2,8 @@ package net.corda.node.services.keys.cryptoservice
|
|||||||
|
|
||||||
enum class SupportedCryptoServices {
|
enum class SupportedCryptoServices {
|
||||||
/** Identifier for [BCCryptoService]. */
|
/** Identifier for [BCCryptoService]. */
|
||||||
BC_SIMPLE
|
BC_SIMPLE,
|
||||||
// UTIMACO, // Utimaco HSM.
|
UTIMACO, // Utimaco HSM.
|
||||||
// GEMALTO_LUNA, // Gemalto Luna HSM.
|
// GEMALTO_LUNA, // Gemalto Luna HSM.
|
||||||
// AZURE_KV // Azure key Vault.
|
// AZURE_KV // Azure key Vault.
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,318 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice.utimaco
|
||||||
|
|
||||||
|
import CryptoServerCXI.CryptoServerCXI
|
||||||
|
import CryptoServerJCE.CryptoServerProvider
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.nodeapi.internal.config.parseAs
|
||||||
|
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
||||||
|
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
|
||||||
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||||
|
import org.bouncycastle.operator.ContentSigner
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.security.*
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of CryptoService for the Utimaco HSM.
|
||||||
|
*/
|
||||||
|
class UtimacoCryptoService(private val cryptoServerProvider: CryptoServerProvider, private val keyConfig: KeyGenerationConfiguration, private val authThreshold: Int, private val auth: () -> UtimacoCredentials) : CryptoService {
|
||||||
|
|
||||||
|
private val keyStore: KeyStore
|
||||||
|
private val keyTemplate: CryptoServerCXI.KeyAttributes
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
keyTemplate = toKeyTemplate(keyConfig)
|
||||||
|
authenticate(auth())
|
||||||
|
val authState = cryptoServerProvider.cryptoServer.authState
|
||||||
|
require((authState and 0x0000000F) >= authThreshold) {
|
||||||
|
"Insufficient authentication: auth state is $authState, at least $authThreshold is required."
|
||||||
|
}
|
||||||
|
keyStore = KeyStore.getInstance("CryptoServer", cryptoServerProvider)
|
||||||
|
keyStore.load(null, null)
|
||||||
|
} catch (e: CryptoServerAPI.CryptoServerException) {
|
||||||
|
throw UtimacoHSMException(HsmErrors.errors[e.ErrorCode], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> withAuthentication(block: () -> T): T {
|
||||||
|
return withErrorMapping {
|
||||||
|
if (cryptoServerProvider.cryptoServer.authState and 0x0000000F >= authThreshold) {
|
||||||
|
block()
|
||||||
|
} else {
|
||||||
|
authenticate(auth())
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> withErrorMapping(block: () -> T): T {
|
||||||
|
try {
|
||||||
|
return block()
|
||||||
|
} catch (e: CryptoServerAPI.CryptoServerException) {
|
||||||
|
throw UtimacoHSMException(HsmErrors.errors[e.ErrorCode], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey {
|
||||||
|
return generateKeyPair(alias, schemeNumberID, keyTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun containsKey(alias: String): Boolean {
|
||||||
|
try {
|
||||||
|
return withAuthentication {
|
||||||
|
keyStore.containsAlias(alias)
|
||||||
|
}
|
||||||
|
} catch (e: CryptoServerAPI.CryptoServerException) {
|
||||||
|
HsmErrors.errors[e.ErrorCode]
|
||||||
|
throw UtimacoHSMException(HsmErrors.errors[e.ErrorCode], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPublicKey(alias: String): PublicKey? {
|
||||||
|
try {
|
||||||
|
return withAuthentication {
|
||||||
|
keyStore.getCertificate(alias)?.publicKey?.let {
|
||||||
|
KeyFactory.getInstance(it.algorithm).generatePublic(X509EncodedKeySpec(it.encoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: CryptoServerAPI.CryptoServerException) {
|
||||||
|
HsmErrors.errors[e.ErrorCode]
|
||||||
|
throw UtimacoHSMException(HsmErrors.errors[e.ErrorCode], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sign(alias: String, data: ByteArray): ByteArray {
|
||||||
|
try {
|
||||||
|
return withAuthentication {
|
||||||
|
(keyStore.getKey(alias, null) as PrivateKey?)?.let {
|
||||||
|
val algorithm = if (it.algorithm == "RSA") {
|
||||||
|
"SHA256withRSA"
|
||||||
|
} else {
|
||||||
|
"SHA256withECDSA"
|
||||||
|
}
|
||||||
|
val signature = Signature.getInstance(algorithm, cryptoServerProvider)
|
||||||
|
signature.initSign(it)
|
||||||
|
signature.update(data)
|
||||||
|
signature.sign()
|
||||||
|
} ?: throw CryptoServiceException("No key found for alias $alias")
|
||||||
|
}
|
||||||
|
} catch (e: CryptoServerAPI.CryptoServerException) {
|
||||||
|
HsmErrors.errors[e.ErrorCode]
|
||||||
|
throw UtimacoHSMException(HsmErrors.errors[e.ErrorCode], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSigner(alias: String): ContentSigner {
|
||||||
|
return object : ContentSigner {
|
||||||
|
private val publicKey: PublicKey = getPublicKey(alias) ?: throw CryptoServiceException("No key found for alias $alias")
|
||||||
|
private val sigAlgID: AlgorithmIdentifier = Crypto.findSignatureScheme(publicKey).signatureOID
|
||||||
|
|
||||||
|
private val baos = ByteArrayOutputStream()
|
||||||
|
override fun getAlgorithmIdentifier(): AlgorithmIdentifier = sigAlgID
|
||||||
|
override fun getOutputStream(): OutputStream = baos
|
||||||
|
override fun getSignature(): ByteArray = sign(alias, baos.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(alias: String, schemeId: Int, keyTemplate: CryptoServerCXI.KeyAttributes): PublicKey {
|
||||||
|
return withAuthentication {
|
||||||
|
val keyAttributes = attributesForScheme(keyTemplate, schemeId)
|
||||||
|
keyAttributes.name = alias
|
||||||
|
val overwrite = if (keyConfig.keyOverride) CryptoServerCXI.FLAG_OVERWRITE else 0
|
||||||
|
cryptoServerProvider.cryptoServer.generateKey(overwrite, keyAttributes, keyConfig.keyGenMechanism)
|
||||||
|
getPublicKey(alias) ?: throw CryptoServiceException("Key generation for alias $alias succeeded, but key could not be accessed afterwards.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logOff() {
|
||||||
|
cryptoServerProvider.logoff()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authenticate(credentials: UtimacoCredentials) {
|
||||||
|
if (credentials.keyFile != null) {
|
||||||
|
cryptoServerProvider.loginSign(credentials.username, credentials.keyFile.toFile().absolutePath, String(credentials.password))
|
||||||
|
} else {
|
||||||
|
cryptoServerProvider.loginPassword(credentials.username, credentials.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UtimacoHSMException(message: String?, cause: Throwable? = null) : CryptoServiceException(message, cause)
|
||||||
|
|
||||||
|
data class UtimacoCredentials(val username: String, val password: ByteArray, val keyFile: Path? = null)
|
||||||
|
|
||||||
|
data class UtimacoConfig(
|
||||||
|
val provider: ProviderConfig,
|
||||||
|
val keyGeneration: KeyGenerationConfiguration = KeyGenerationConfiguration(),
|
||||||
|
val authThreshold: Int = 1,
|
||||||
|
val authentication: Credentials,
|
||||||
|
val keyFile: Path? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ProviderConfig(
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val connectionTimeout: Int = 30000,
|
||||||
|
val timeout: Int = 60000,
|
||||||
|
val endSessionOnShutdown: Boolean = true,
|
||||||
|
val keepSessionAlive: Boolean = false,
|
||||||
|
val keyGroup: String = "*",
|
||||||
|
val keySpecifier: Int = -1,
|
||||||
|
val storeKeysExternal: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KeyGenerationConfiguration(
|
||||||
|
val keyGroup: String = "*",
|
||||||
|
val keySpecifier: Int = -1,
|
||||||
|
val keyOverride: Boolean = false,
|
||||||
|
val keyExport: Boolean = false,
|
||||||
|
val keyGenMechanism: Int = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Credentials(val username: String, val password: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taken from network-services.
|
||||||
|
* Configuration class for [CryptoServerProvider]
|
||||||
|
* Currently not supported: DefaultUser,KeyStorePath,LogFile,LogLevel,LogSize
|
||||||
|
*/
|
||||||
|
internal data class CryptoServerProviderConfig(
|
||||||
|
val Device: String,
|
||||||
|
val ConnectionTimeout: Int,
|
||||||
|
val Timeout: Int,
|
||||||
|
val EndSessionOnShutdown: Int, // todo does this actually exist? docs don't mention it
|
||||||
|
val KeepSessionAlive: Int,
|
||||||
|
val KeyGroup: String,
|
||||||
|
val KeySpecifier: Int,
|
||||||
|
val StoreKeysExternal: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun attributesForScheme(keyTemplate: CryptoServerCXI.KeyAttributes, schemeId: Int): CryptoServerCXI.KeyAttributes {
|
||||||
|
if (schemeId !in keyAttributeForScheme.keys) {
|
||||||
|
throw NoSuchAlgorithmException("No mapping for scheme ID $schemeId.")
|
||||||
|
}
|
||||||
|
val schemeAttributes = keyAttributeForScheme[schemeId]!!
|
||||||
|
return CryptoServerCXI.KeyAttributes().apply {
|
||||||
|
specifier = keyTemplate.specifier
|
||||||
|
group = keyTemplate.group
|
||||||
|
export = keyTemplate.export
|
||||||
|
algo = schemeAttributes.algo
|
||||||
|
curve = schemeAttributes.curve
|
||||||
|
size = schemeAttributes.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
|
||||||
|
|
||||||
|
private val keyAttributeForScheme: Map<Int, CryptoServerCXI.KeyAttributes> = mapOf(
|
||||||
|
Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID to CryptoServerCXI.KeyAttributes().apply {
|
||||||
|
algo = CryptoServerCXI.KEY_ALGO_ECDSA
|
||||||
|
setCurve("secp256r1")
|
||||||
|
},
|
||||||
|
Crypto.ECDSA_SECP256K1_SHA256.schemeNumberID to CryptoServerCXI.KeyAttributes().apply {
|
||||||
|
algo = CryptoServerCXI.KEY_ALGO_ECDSA
|
||||||
|
setCurve("secp256k1")
|
||||||
|
},
|
||||||
|
Crypto.RSA_SHA256.schemeNumberID to CryptoServerCXI.KeyAttributes().apply {
|
||||||
|
algo = CryptoServerCXI.KEY_ALGO_RSA
|
||||||
|
size = Crypto.RSA_SHA256.keySize!!
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parseConfigFile(configFile: Path): UtimacoConfig {
|
||||||
|
val config = ConfigFactory.parseFile(configFile.toFile())
|
||||||
|
return config.parseAs(UtimacoConfig::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username and password are stored in files, base64-encoded
|
||||||
|
*/
|
||||||
|
fun fileBasedAuth(usernameFile: Path, passwordFile: Path): () -> UtimacoCredentials = {
|
||||||
|
val username = String(Base64.getDecoder().decode(usernameFile.toFile().readLines().first()))
|
||||||
|
val pw = if (usernameFile == passwordFile) {
|
||||||
|
Base64.getDecoder().decode(passwordFile.toFile().readLines().get(1))
|
||||||
|
} else {
|
||||||
|
Base64.getDecoder().decode(passwordFile.toFile().readLines().get(0))
|
||||||
|
}
|
||||||
|
UtimacoCredentials(username, pw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromConfigurationFile(configFile: Path?): UtimacoCryptoService {
|
||||||
|
val config = parseConfigFile(configFile!!)
|
||||||
|
return fromConfig(config) { UtimacoCredentials(config.authentication.username, config.authentication.password.toByteArray(), config.keyFile) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromConfig(configuration: UtimacoConfig, auth: () -> UtimacoCredentials): UtimacoCryptoService {
|
||||||
|
val providerConfig = toCryptoServerProviderConfig(configuration.provider)
|
||||||
|
val cryptoServerProvider = createProvider(providerConfig)
|
||||||
|
return UtimacoCryptoService(cryptoServerProvider, configuration.keyGeneration, configuration.authThreshold, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that some attributes cannot be determined at this point, as they depend on the scheme ID
|
||||||
|
*/
|
||||||
|
private fun toKeyTemplate(config: KeyGenerationConfiguration): CryptoServerCXI.KeyAttributes {
|
||||||
|
return CryptoServerCXI.KeyAttributes().apply {
|
||||||
|
specifier = config.keySpecifier
|
||||||
|
group = config.keyGroup
|
||||||
|
export = if (config.keyExport) 1 else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toCryptoServerProviderConfig(config: ProviderConfig): CryptoServerProviderConfig {
|
||||||
|
return CryptoServerProviderConfig(
|
||||||
|
"${config.port}@${config.host}",
|
||||||
|
config.connectionTimeout,
|
||||||
|
config.timeout,
|
||||||
|
if (config.endSessionOnShutdown) 1 else 0,
|
||||||
|
if (config.keepSessionAlive) 1 else 0,
|
||||||
|
config.keyGroup,
|
||||||
|
config.keySpecifier,
|
||||||
|
config.storeKeysExternal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taken from network-services.
|
||||||
|
* Creates an instance of [CryptoServerProvider] configured accordingly to the passed configuration.
|
||||||
|
*
|
||||||
|
* @param config crypto server provider configuration.
|
||||||
|
*
|
||||||
|
* @return preconfigured instance of [CryptoServerProvider]
|
||||||
|
*/
|
||||||
|
private fun createProvider(config: CryptoServerProviderConfig): CryptoServerProvider {
|
||||||
|
val cfgBuffer = ByteArrayOutputStream()
|
||||||
|
val writer = cfgBuffer.writer(Charsets.UTF_8)
|
||||||
|
for (property in CryptoServerProviderConfig::class.memberProperties) {
|
||||||
|
writer.write("${property.name} = ${property.get(config)}\n")
|
||||||
|
}
|
||||||
|
writer.close()
|
||||||
|
val cfg = cfgBuffer.toByteArray().inputStream()
|
||||||
|
return CryptoServerProvider(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This code (incl. the hsm_errors file) is duplicated with the Network-Management module
|
||||||
|
object HsmErrors {
|
||||||
|
val errors: Map<Int, String> by lazy(HsmErrors::load)
|
||||||
|
fun load(): Map<Int, String> {
|
||||||
|
val errors = HashMap<Int, String>()
|
||||||
|
val hsmErrorsStream = HsmErrors::class.java.getResourceAsStream("hsm_errors")
|
||||||
|
hsmErrorsStream.bufferedReader().lines().reduce(null) { previous, current ->
|
||||||
|
if (previous == null) {
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
errors[java.lang.Long.decode(previous).toInt()] = current
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
213
node/src/test/kotlin/net/corda/node/hsm/HsmSimulator.kt
Normal file
213
node/src/test/kotlin/net/corda/node/hsm/HsmSimulator.kt
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package net.corda.node.hsm
|
||||||
|
|
||||||
|
import CryptoServerAPI.CryptoServerException
|
||||||
|
import CryptoServerJCE.CryptoServerProvider
|
||||||
|
import com.spotify.docker.client.DefaultDockerClient
|
||||||
|
import com.spotify.docker.client.DockerClient
|
||||||
|
import com.spotify.docker.client.messages.ContainerConfig
|
||||||
|
import com.spotify.docker.client.messages.HostConfig
|
||||||
|
import com.spotify.docker.client.messages.PortBinding
|
||||||
|
import com.spotify.docker.client.messages.RegistryAuth
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.testing.driver.PortAllocation
|
||||||
|
import org.junit.Assume.assumeFalse
|
||||||
|
import org.junit.rules.ExternalResource
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
data class CryptoUserCredentials(val username: String, val password: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HSM Simulator rule allowing to use the HSM Simulator within the integration tests. It is designed to be used mainly
|
||||||
|
* on the TeamCity, but if the required setup is available it can be executed locally as well.
|
||||||
|
* It will bind to the simulator to the local port
|
||||||
|
* ToDocker engine needs to be installed on the machine
|
||||||
|
* 2) Environment variables (AZURE_CR_USER and AZURE_CR_PASS) are available and hold valid credentials to the corda.azurecr.io
|
||||||
|
* repository
|
||||||
|
* 3) HSM requires Unlimited Strength Jurisdiction extension to be installed on the machine connecting with the HSM box.
|
||||||
|
* See http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
|
||||||
|
* use it locally, the following pre-requisites need to be met:
|
||||||
|
* 1)
|
||||||
|
* Since the above setup is not a strong requirement for the integration tests to be executed it is intended that this
|
||||||
|
* rule is used together with the assumption mechanism in tests.
|
||||||
|
*/
|
||||||
|
class HsmSimulator(private val serverAddress: String = DEFAULT_SERVER_ADDRESS,
|
||||||
|
private val imageRepoTag: String = DEFAULT_IMAGE_REPO_TAG,
|
||||||
|
private val imageVersion: String = DEFAULT_IMAGE_VERSION,
|
||||||
|
private val pullImage: Boolean = DEFAULT_PULL_IMAGE,
|
||||||
|
private val registryUser: String? = REGISTRY_USERNAME,
|
||||||
|
private val registryPass: String? = REGISTRY_PASSWORD) : ExternalResource() {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val DEFAULT_SERVER_ADDRESS = "corda.azurecr.io"
|
||||||
|
/*
|
||||||
|
* Currently we have following images:
|
||||||
|
* 1) corda.azurecr.io/network-management/hsm-simulator - having only one user configured:
|
||||||
|
* - INTEGRATION_TEST (password: INTEGRATION_TEST) with the CXI_GROUP="*"
|
||||||
|
* 2)corda.azurecr.io/network-management/hsm-simulator-with-groups - having following users configured:
|
||||||
|
* - INTEGRATION_TEST (password: INTEGRATION_TEST) with the CXI_GROUP=*
|
||||||
|
* - INTEGRATION_TEST_SUPER (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT
|
||||||
|
* - INTEGRATION_TEST_ROOT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.ROOT
|
||||||
|
* - INTEGRATION_TEST_OPS (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS
|
||||||
|
* - INTEGRATION_TEST_SUPER_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.*
|
||||||
|
* - INTEGRATION_TEST_ROOT_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.ROOT.*
|
||||||
|
* - INTEGRATION_TEST_OPS_ (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.*
|
||||||
|
* - INTEGRATION_TEST_OPS_CERT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.CERT
|
||||||
|
* - INTEGRATION_TEST_OPS_NETMAP (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.NETMAP
|
||||||
|
* - INTEGRATION_TEST_OPS_CERT (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.CERT.*
|
||||||
|
* - INTEGRATION_TEST_OPS_NETMAP (password: INTEGRATION_TEST) with the CXI_GROUP=TEST.CORDACONNECT.OPS.NETMAP.*
|
||||||
|
*/
|
||||||
|
val DEFAULT_IMAGE_REPO_TAG = "corda.azurecr.io/network-management/hsm-simulator-with-groups"
|
||||||
|
val DEFAULT_IMAGE_VERSION = "latest"
|
||||||
|
val DEFAULT_PULL_IMAGE = true
|
||||||
|
|
||||||
|
val HSM_SIMULATOR_PORT = "3001/tcp"
|
||||||
|
val CONTAINER_KILL_TIMEOUT_SECONDS = 10
|
||||||
|
|
||||||
|
val CRYPTO_USER = System.getProperty("CRYPTO_USER", "INTEGRATION_TEST")
|
||||||
|
val CRYPTO_PASSWORD = System.getProperty("CRYPTO_PASSWORD", "INTEGRATION_TEST")
|
||||||
|
|
||||||
|
val REGISTRY_USERNAME = System.getenv("AZURE_CR_USER")
|
||||||
|
val REGISTRY_PASSWORD = System.getenv("AZURE_CR_PASS")
|
||||||
|
|
||||||
|
val log = loggerFor<HsmSimulator>()
|
||||||
|
|
||||||
|
private val HSM_STARTUP_SLEEP_INTERVAL_MS = 500L
|
||||||
|
private val HSM_STARTUP_POLL_MAX_COUNT = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private val localHostAndPortBinding = PortAllocation.Incremental(10000).nextHostAndPort()
|
||||||
|
private lateinit var docker: DockerClient
|
||||||
|
private var containerId: String? = null
|
||||||
|
|
||||||
|
override fun before() {
|
||||||
|
assumeFalse("Docker registry username is not set!. Skipping the test.", registryUser.isNullOrBlank())
|
||||||
|
assumeFalse("Docker registry password is not set!. Skipping the test.", registryPass.isNullOrBlank())
|
||||||
|
docker = DefaultDockerClient.fromEnv().build()
|
||||||
|
if (pullImage) {
|
||||||
|
docker.pullHsmSimulatorImageFromRepository()
|
||||||
|
}
|
||||||
|
containerId = docker.createContainer()
|
||||||
|
docker.startHsmSimulatorContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun after() {
|
||||||
|
docker.stopAndRemoveHsmSimulatorContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the port at which simulator is listening at.
|
||||||
|
*/
|
||||||
|
val port get(): Int = localHostAndPortBinding.port
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the host IP address, which the simulator is listening at.
|
||||||
|
*/
|
||||||
|
val host get(): String = localHostAndPortBinding.host
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HSM user credentials. Those are supposed to be preconfigured on the HSM itself. Thus, when
|
||||||
|
* tests are executed these credentials can be used to access HSM crypto user's functionality.
|
||||||
|
* It is assumed that the docker image state has those configured already and they should match the ones returned.
|
||||||
|
*/
|
||||||
|
fun cryptoUserCredentials(): CryptoUserCredentials {
|
||||||
|
return CryptoUserCredentials(CRYPTO_USER, CRYPTO_PASSWORD)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DockerClient.stopAndRemoveHsmSimulatorContainer() {
|
||||||
|
if (containerId != null) {
|
||||||
|
log.debug("Stopping container $containerId...")
|
||||||
|
this.stopContainer(containerId, CONTAINER_KILL_TIMEOUT_SECONDS)
|
||||||
|
log.debug("Removing container $containerId...")
|
||||||
|
this.removeContainer(containerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DockerClient.startHsmSimulatorContainer() {
|
||||||
|
if (containerId != null) {
|
||||||
|
log.debug("Starting container $containerId...")
|
||||||
|
this.startContainer(containerId)
|
||||||
|
pollAndWaitForHsmSimulator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pollAndWaitForHsmSimulator() {
|
||||||
|
val config = CryptoServerProviderConfig(
|
||||||
|
Device = "${localHostAndPortBinding.port}@${localHostAndPortBinding.host}",
|
||||||
|
KeyGroup = "*",
|
||||||
|
KeySpecifier = -1
|
||||||
|
)
|
||||||
|
var pollCount = HSM_STARTUP_POLL_MAX_COUNT
|
||||||
|
while (pollCount > 0) {
|
||||||
|
val provider = createProvider(config)
|
||||||
|
try {
|
||||||
|
provider.loginPassword(CRYPTO_USER, CRYPTO_PASSWORD)
|
||||||
|
provider.cryptoServer.authState
|
||||||
|
return
|
||||||
|
} catch (e: CryptoServerException) {
|
||||||
|
pollCount--
|
||||||
|
Thread.sleep(HSM_STARTUP_SLEEP_INTERVAL_MS)
|
||||||
|
} finally {
|
||||||
|
provider.logoff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Unable to obtain connection to initialised HSM Simulator")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getImageFullName() = "$imageRepoTag:$imageVersion"
|
||||||
|
|
||||||
|
private fun DockerClient.pullHsmSimulatorImageFromRepository(): DockerClient {
|
||||||
|
this.pull(imageRepoTag,
|
||||||
|
RegistryAuth.builder()
|
||||||
|
.serverAddress(serverAddress)
|
||||||
|
.username(registryUser)
|
||||||
|
.password(registryPass)
|
||||||
|
.build())
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DockerClient.createContainer(): String? {
|
||||||
|
val portBindings = mapOf(HSM_SIMULATOR_PORT to listOf(PortBinding.create(localHostAndPortBinding.host, localHostAndPortBinding.port.toString())))
|
||||||
|
val hostConfig = HostConfig.builder().portBindings(portBindings).build()
|
||||||
|
val containerConfig = ContainerConfig.builder()
|
||||||
|
.hostConfig(hostConfig)
|
||||||
|
.portSpecs()
|
||||||
|
.image(getImageFullName())
|
||||||
|
.exposedPorts(HSM_SIMULATOR_PORT)
|
||||||
|
.build()
|
||||||
|
val containerCreation = this.createContainer(containerConfig)
|
||||||
|
return containerCreation.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configuration class for [CryptoServerProvider]
|
||||||
|
*/
|
||||||
|
data class CryptoServerProviderConfig(
|
||||||
|
val Device: String = "3001@127.0.0.1",
|
||||||
|
val ConnectionTimeout: Int = 30000,
|
||||||
|
val Timeout: Int = 60000,
|
||||||
|
val EndSessionOnShutdown: Int = 1,
|
||||||
|
val KeepSessionAlive: Int = 0,
|
||||||
|
val KeyGroup: String = "*",
|
||||||
|
val KeySpecifier: Int = -1,
|
||||||
|
val StoreKeysExternal: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of [CryptoServerProvider] configured accordingly to the passed configuration.
|
||||||
|
*
|
||||||
|
* @param config crypto server provider configuration.
|
||||||
|
*
|
||||||
|
* @return preconfigured instance of [CryptoServerProvider]
|
||||||
|
*/
|
||||||
|
private fun createProvider(config: CryptoServerProviderConfig): CryptoServerProvider {
|
||||||
|
val cfgBuffer = ByteArrayOutputStream()
|
||||||
|
val writer = cfgBuffer.writer(Charsets.UTF_8)
|
||||||
|
for (property in CryptoServerProviderConfig::class.memberProperties) {
|
||||||
|
writer.write("${property.name} = ${property.get(config)}\n")
|
||||||
|
}
|
||||||
|
writer.close()
|
||||||
|
val cfg = cfgBuffer.toByteArray().inputStream()
|
||||||
|
return CryptoServerProvider(cfg)
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
|
|||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@ -223,7 +224,7 @@ class NodeConfigurationImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validation has error on non-null cryptoServiceConf for null cryptoServiceName`() {
|
fun `validation has error on non-null cryptoServiceConf for null cryptoServiceName`() {
|
||||||
val configuration = testConfiguration.copy(cryptoServiceConf = "unsupported.conf")
|
val configuration = testConfiguration.copy(cryptoServiceConf = File("unsupported.conf").toPath())
|
||||||
|
|
||||||
val errors = configuration.validate()
|
val errors = configuration.validate()
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice.utimaco
|
||||||
|
|
||||||
|
import net.corda.core.internal.toPath
|
||||||
|
import net.corda.node.services.keys.cryptoservice.utimaco.UtimacoCryptoService.Companion.fileBasedAuth
|
||||||
|
import net.corda.node.services.keys.cryptoservice.utimaco.UtimacoCryptoService.Companion.parseConfigFile
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class UtimacoCryptoServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Parse config file`() {
|
||||||
|
val config = parseConfigFile(javaClass.getResource("utimaco_config.yml").toPath())
|
||||||
|
assertEquals(true, config.provider.keepSessionAlive)
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun `File based auth`() {
|
||||||
|
val auth = fileBasedAuth(javaClass.getResource("uname").toPath(), javaClass.getResource("pw").toPath())
|
||||||
|
val credentials = auth()
|
||||||
|
assertEquals("testpassword", String(credentials.password))
|
||||||
|
assertEquals("testuser", credentials.username)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
dGVzdHBhc3N3b3Jk
|
@ -0,0 +1 @@
|
|||||||
|
dGVzdHVzZXI=
|
@ -0,0 +1,20 @@
|
|||||||
|
provider = {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 3001
|
||||||
|
connectionTimeout = 60000
|
||||||
|
endSessionOnShutdown = false
|
||||||
|
keepSessionAlive = true
|
||||||
|
keyGroup = "*"
|
||||||
|
keySpecifier = 2
|
||||||
|
storeKeysExternal = false
|
||||||
|
}
|
||||||
|
keyGeneration = {
|
||||||
|
keyOverride = false
|
||||||
|
keyExport = false
|
||||||
|
keyGenMechanism = 4
|
||||||
|
}
|
||||||
|
authThreshold = 1
|
||||||
|
authentication = {
|
||||||
|
username = foo
|
||||||
|
password = bar
|
||||||
|
}
|
@ -196,7 +196,7 @@ class DriverDSLImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startNode(defaultParameters: NodeParameters,
|
override fun startNode(defaultParameters: NodeParameters,
|
||||||
providedName: CordaX500Name?,
|
providedName: CordaX500Name?,
|
||||||
rpcUsers: List<User>,
|
rpcUsers: List<User>,
|
||||||
verifierType: VerifierType,
|
verifierType: VerifierType,
|
||||||
@ -262,7 +262,7 @@ class DriverDSLImpl(
|
|||||||
|
|
||||||
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
||||||
// We don't need the network map to be available to be able to register the node
|
// We don't need the network map to be available to be able to register the node
|
||||||
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config())
|
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config(), customOverrides)
|
||||||
} else {
|
} else {
|
||||||
doneFuture(Unit)
|
doneFuture(Unit)
|
||||||
}
|
}
|
||||||
@ -324,21 +324,23 @@ class DriverDSLImpl(
|
|||||||
private fun startNodeRegistration(
|
private fun startNodeRegistration(
|
||||||
providedName: CordaX500Name,
|
providedName: CordaX500Name,
|
||||||
rootCert: X509Certificate,
|
rootCert: X509Certificate,
|
||||||
networkServicesConfig: NetworkServicesConfig
|
networkServicesConfig: NetworkServicesConfig,
|
||||||
|
customOverrides: Map<String, Any?> = mapOf()
|
||||||
): CordaFuture<NodeConfig> {
|
): CordaFuture<NodeConfig> {
|
||||||
val baseDirectory = baseDirectory(providedName).createDirectories()
|
val baseDirectory = baseDirectory(providedName).createDirectories()
|
||||||
|
val overrides = configOf(
|
||||||
|
"p2pAddress" to portAllocation.nextHostAndPort().toString(),
|
||||||
|
"compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(),
|
||||||
|
"myLegalName" to providedName.toString(),
|
||||||
|
"rpcSettings" to mapOf(
|
||||||
|
"address" to portAllocation.nextHostAndPort().toString(),
|
||||||
|
"adminAddress" to portAllocation.nextHostAndPort().toString()
|
||||||
|
),
|
||||||
|
"devMode" to false) + customOverrides
|
||||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||||
baseDirectory = baseDirectory,
|
baseDirectory = baseDirectory,
|
||||||
allowMissingConfig = true,
|
allowMissingConfig = true,
|
||||||
configOverrides = configOf(
|
configOverrides = overrides
|
||||||
"p2pAddress" to portAllocation.nextHostAndPort().toString(),
|
|
||||||
"compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(),
|
|
||||||
"myLegalName" to providedName.toString(),
|
|
||||||
"rpcSettings" to mapOf(
|
|
||||||
"address" to portAllocation.nextHostAndPort().toString(),
|
|
||||||
"adminAddress" to portAllocation.nextHostAndPort().toString()
|
|
||||||
),
|
|
||||||
"devMode" to false)
|
|
||||||
)).checkAndOverrideForInMemoryDB()
|
)).checkAndOverrideForInMemoryDB()
|
||||||
|
|
||||||
val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1")
|
val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1")
|
||||||
|
Reference in New Issue
Block a user