mirror of
https://github.com/corda/corda.git
synced 2025-04-19 00:27:13 +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:
parent
e486c8b392
commit
b411ad6665
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -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" />
|
||||
|
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
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.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,
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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.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()
|
||||
|
||||
|
@ -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?,
|
||||
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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user