[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:
Florian Friemel 2018-11-22 13:40:50 +00:00 committed by GitHub
parent e486c8b392
commit b411ad6665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 5578 additions and 27 deletions

4
.idea/compiler.xml generated
View File

@ -49,6 +49,10 @@
<module name="client_test" target="1.8" />
<module name="cliutils_main" 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_test" target="1.8" />
<module name="common-validation_main" target="1.8" />

View File

@ -0,0 +1,7 @@
Configuring the node to use the Utimaco HSM
==================
.. contents::
TODO
-------

View File

@ -38,6 +38,8 @@ import java.util.*
import javax.security.auth.x500.X500Principal
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_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256

View File

@ -75,6 +75,10 @@ dependencies {
compile project(':common-validation')
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)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${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-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)
def DB_PROVIDER = System.getProperty("custom.databaseProvider")
switch (DB_PROVIDER) {

View File

@ -51,9 +51,11 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) {
)
from configurations.capsuleRuntime.files.collect { zipTree(it) }
// Exclude the binaries for the Utimaco HSM as we cannot distribute them.
exclude('**/CryptoServer*.jar')
capsuleManifest {
applicationVersion = corda_release_version
// See experimental/quasar-hook/README.md for how to generate.
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
systemProperties['visualvm.display.name'] = 'CordaEnterprise'

Binary file not shown.

View File

@ -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)
)
}

View File

@ -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) }
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -9,11 +9,8 @@ import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isCRLDistributionPointBlacklisted
import net.corda.core.crypto.*
import net.corda.core.crypto.internal.AliasPrivateKey
import net.corda.core.crypto.newSecureRandom
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
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.KeyManagementServiceInternal
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.MessagingService
import net.corda.node.services.network.NetworkMapClient
@ -952,7 +950,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
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,
services: ServicesForResolution,

View File

@ -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.MutualSslConfiguration
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.persistence.DatabaseConfig
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
// to allow for pluggable implementations.
val cryptoServiceName: SupportedCryptoServices?
val cryptoServiceConf: String? // Location for the cryptoService conf file.
val cryptoServiceConf: Path? // Location for the cryptoService conf file.
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings
@ -115,8 +119,9 @@ interface NodeConfiguration {
fun makeCryptoService(): CryptoService {
return when(cryptoServiceName) {
// Pick default BCCryptoService when null.
SupportedCryptoServices.BC_SIMPLE, null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore)
SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore)
SupportedCryptoServices.UTIMACO -> UtimacoCryptoService.fromConfigurationFile(cryptoServiceConf)
null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore) // Pick default BCCryptoService when null.
}
}
}

View File

@ -82,7 +82,7 @@ data class NodeConfigurationImpl(
override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = Defaults.cordappSignerKeyFingerprintBlacklist,
override val cryptoServiceName: SupportedCryptoServices? = Defaults.cryptoServiceName,
override val cryptoServiceConf: String? = Defaults.cryptoServiceConf,
override val cryptoServiceConf: Path? = Defaults.cryptoServiceConf,
override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings
) : NodeConfiguration {
internal object Defaults {
@ -119,7 +119,7 @@ data class NodeConfigurationImpl(
val enableSNI: Boolean = true
val useOpenSsl: Boolean = false
val cryptoServiceName: SupportedCryptoServices? = null
val cryptoServiceConf: String? = null
val cryptoServiceConf: Path? = null
val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings()
fun cordappsDirectories(baseDirectory: Path) = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT)

View File

@ -75,7 +75,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
private val cordappDirectories by string().mapValid(::toPath).list().optional()
private val cordappSignerKeyFingerprintBlacklist by string().list().optional().withDefaultValue(Defaults.cordappSignerKeyFingerprintBlacklist)
private val cryptoServiceName by enum(SupportedCryptoServices::class).optional()
private val cryptoServiceConf by string().optional()
private val cryptoServiceConf by string().mapValid(::toPath).optional()
@Suppress("unused")
private val custom by nestedObject().optional()
private val relay by nested(RelayConfigurationSpec).optional()

View File

@ -2,8 +2,8 @@ package net.corda.node.services.keys.cryptoservice
enum class SupportedCryptoServices {
/** Identifier for [BCCryptoService]. */
BC_SIMPLE
// UTIMACO, // Utimaco HSM.
BC_SIMPLE,
UTIMACO, // Utimaco HSM.
// GEMALTO_LUNA, // Gemalto Luna HSM.
// AZURE_KV // Azure key Vault.
}

View File

@ -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
}
}

View 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)
}
}

View File

@ -19,6 +19,7 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.io.File
import java.net.InetAddress
import java.net.URI
import java.net.URL
@ -223,7 +224,7 @@ class NodeConfigurationImplTest {
@Test
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()

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
dGVzdHBhc3N3b3Jk

View File

@ -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
}

View File

@ -196,7 +196,7 @@ class DriverDSLImpl(
}
}
override fun startNode(defaultParameters: NodeParameters,
override fun startNode(defaultParameters: NodeParameters,
providedName: CordaX500Name?,
rpcUsers: List<User>,
verifierType: VerifierType,
@ -262,7 +262,7 @@ class DriverDSLImpl(
val registrationFuture = if (compatibilityZone?.rootCert != null) {
// 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 {
doneFuture(Unit)
}
@ -324,21 +324,23 @@ class DriverDSLImpl(
private fun startNodeRegistration(
providedName: CordaX500Name,
rootCert: X509Certificate,
networkServicesConfig: NetworkServicesConfig
networkServicesConfig: NetworkServicesConfig,
customOverrides: Map<String, Any?> = mapOf()
): CordaFuture<NodeConfig> {
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(
baseDirectory = baseDirectory,
allowMissingConfig = true,
configOverrides = 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)
configOverrides = overrides
)).checkAndOverrideForInMemoryDB()
val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1")