diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index 100fc2f2f7..4439cb0be5 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -20,7 +20,6 @@ import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAPublicKey -import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.x500.X500Name import java.math.BigDecimal import java.security.PublicKey @@ -39,28 +38,28 @@ object JacksonSupport { interface PartyObjectMapper { @Deprecated("Use partyFromX500Name instead") fun partyFromName(partyName: String): Party? - fun partyFromPrincipal(principal: X500Name): Party? + fun partyFromX500Name(name: X500Name): Party? fun partyFromKey(owningKey: PublicKey): Party? } class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName) - override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal) + override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) } class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName) - override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal) + override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) } class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException() - override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException() + override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException() override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() } @@ -170,7 +169,7 @@ object JacksonSupport { // how to parse the content return if (parser.text.contains("=")) { val principal = X500Name(parser.text) - mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") + mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") } else { val key = try { parsePublicKeyBase58(parser.text) diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index f8a3ec7c5d..706ffdbd26 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -1,6 +1,8 @@ package net.corda.core.crypto import net.corda.core.crypto.Crypto.generateKeyPair +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder @@ -24,6 +26,7 @@ import java.time.temporal.ChronoUnit import java.util.* object X509Utilities { + val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512 val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 // Aliases for private keys and certificates. diff --git a/core/src/main/kotlin/net/corda/core/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/core/flows/TxKeyFlow.kt new file mode 100644 index 0000000000..32ba268709 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/TxKeyFlow.kt @@ -0,0 +1,95 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap +import org.bouncycastle.asn1.x500.X500Name +import java.security.cert.CertPath +import java.security.cert.X509Certificate + +/** + * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction. + * This is intended for use as a subflow of another flow. + */ +object TxKeyFlow { + abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic>() { + fun validateIdentity(untrustedIdentity: Pair): AnonymousIdentity { + val (wellKnownCert, certPath) = untrustedIdentity + val theirCert = certPath.certificates.last() + // TODO: Don't trust self-signed certificates + return if (theirCert is X509Certificate) { + val certName = X500Name(theirCert.subjectDN.name) + if (certName == otherSide.name) { + val anonymousParty = AnonymousParty(theirCert.publicKey) + serviceHub.identityService.registerPath(wellKnownCert, anonymousParty, certPath) + AnonymousIdentity(certPath, theirCert, anonymousParty) + } else + throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${certName}") + } else + throw IllegalStateException("Expected an X.509 certificate but received ${theirCert.javaClass.name}") + } + } + + @StartableByRPC + @InitiatingFlow + class Requester(otherSide: Party, + revocationEnabled: Boolean, + override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide, revocationEnabled) { + constructor(otherSide: Party, + revocationEnabled: Boolean) : this(otherSide, revocationEnabled, tracker()) + + companion object { + object AWAITING_KEY : ProgressTracker.Step("Awaiting key") + + fun tracker() = ProgressTracker(AWAITING_KEY) + } + + @Suspendable + override fun call(): Map { + progressTracker.currentStep = AWAITING_KEY + val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled) + val theirIdentity = receive>(otherSide).unwrap { validateIdentity(it) } + send(otherSide, myIdentityFragment) + return mapOf(Pair(otherSide, AnonymousIdentity(myIdentityFragment)), + Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + } + } + + /** + * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the + * counterparty and as the result from the flow. + */ + class Provider(otherSide: Party, + revocationEnabled: Boolean, + override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide,revocationEnabled) { + constructor(otherSide: Party, + revocationEnabled: Boolean = false) : this(otherSide, revocationEnabled, tracker()) + + companion object { + object SENDING_KEY : ProgressTracker.Step("Sending key") + + fun tracker() = ProgressTracker(SENDING_KEY) + } + + @Suspendable + override fun call(): Map { + progressTracker.currentStep = SENDING_KEY + val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled) + send(otherSide, myIdentityFragment) + val theirIdentity = receive>(otherSide).unwrap { validateIdentity(it) } + return mapOf(Pair(otherSide, AnonymousIdentity(myIdentityFragment)), + Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + } + } + + data class AnonymousIdentity( + val certPath: CertPath, + val certificate: X509Certificate, + val identity: AnonymousParty) { + constructor(myIdentity: Pair) : this(myIdentity.second, + myIdentity.first, + AnonymousParty(myIdentity.second.certificates.last().publicKey)) + } +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 4dafa52ef0..d066d1007c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,6 +1,7 @@ package net.corda.core.node.services import net.corda.core.contracts.PartyAndReference +import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -55,9 +56,31 @@ interface IdentityService { fun partyFromName(name: String): Party? fun partyFromX500Name(principal: X500Name): Party? + /** + * Resolve the well known identity of a party. If the party passed in is already a well known identity + * (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity, or null if unknown. + */ fun partyFromAnonymous(party: AbstractParty): Party? + + /** + * Resolve the well known identity of a party. If the party passed in is already a well known identity + * (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity, or null if unknown. + */ fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + /** + * Resolve the well known identity of a party. Throws an exception if the party cannot be identified. + * If the party passed in is already a well known identity (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity. + * @throws IllegalArgumentException + */ + fun requirePartyFromAnonymous(party: AbstractParty): Party + /** * Get the certificate chain showing an anonymous party is owned by the given party. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 6116441b0a..65fd283927 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,10 +3,10 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* -import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.* import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria @@ -20,6 +20,8 @@ import net.corda.core.transactions.WireTransaction import rx.Observable import java.io.InputStream import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.time.Instant import java.util.* @@ -385,6 +387,17 @@ interface KeyManagementService { @Suspendable fun freshKey(): PublicKey + /** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding + * [X509Certificate] and adds it to the identity service. + * + * @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for. + * @param revocationEnabled whether to check revocation status of certificates in the certificate path. + * @return X.509 certificate and path to the trust root. + */ + @Suspendable + fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair + /** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. * @param bytes The data to sign over using the chosen key. * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity, diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt deleted file mode 100644 index 8e7d85b240..0000000000 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.unwrap -import java.security.PublicKey -import java.security.cert.Certificate - -object TxKeyFlowUtilities { - /** - * Receive a key from a counterparty. This would normally be triggered by a flow as part of a transaction assembly - * process. - */ - @Suspendable - fun receiveKey(flow: FlowLogic<*>, otherSide: Party): Pair { - val untrustedKey = flow.receive(otherSide) - return untrustedKey.unwrap { - // TODO: Verify the certificate connects the given key to the counterparty, once we have certificates - Pair(it.key, it.certificate) - } - } - - /** - * Generates a new key and then returns it to the counterparty and as the result from the function. Note that this - * is an expensive operation, and should only be called once the calling flow has confirmed it wants to be part of - * a transaction with the counterparty, in order to avoid a DoS risk. - */ - @Suspendable - fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey { - val key = flow.serviceHub.keyManagementService.freshKey() - // TODO: Generate and sign certificate for the key, once we have signing support for composite keys - // (in this case the legal identity key) - flow.send(otherSide, ProvidedTransactionKey(key, null)) - return key - } - - @CordaSerializable - data class ProvidedTransactionKey(val key: PublicKey, val certificate: Certificate?) -} diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt deleted file mode 100644 index 78f18c3171..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.corda.core.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.identity.Party -import net.corda.core.utilities.ProgressTracker -import net.corda.flows.TxKeyFlowUtilities -import java.security.PublicKey -import java.security.cert.Certificate - -/** - * Very basic flow which requests a transaction key from a counterparty, used for testing [TxKeyFlowUtilities]. - * This MUST not be provided on any real node, as the ability for arbitrary parties to request keys would enable - * DoS of the node, as key generation/storage is vastly more expensive than submitting a request. - */ -object TxKeyFlow { - - @InitiatingFlow - class Requester(val otherSide: Party, - override val progressTracker: ProgressTracker) : FlowLogic>() { - constructor(otherSide: Party) : this(otherSide, tracker()) - - companion object { - object AWAITING_KEY : ProgressTracker.Step("Awaiting key") - - fun tracker() = ProgressTracker(AWAITING_KEY) - } - - @Suspendable - override fun call(): Pair { - progressTracker.currentStep = AWAITING_KEY - return TxKeyFlowUtilities.receiveKey(this, otherSide) - } - } - - /** - * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the - * counterparty and as the result from the flow. - */ - class Provider(val otherSide: Party, - override val progressTracker: ProgressTracker) : FlowLogic() { - constructor(otherSide: Party) : this(otherSide, tracker()) - - companion object { - object SENDING_KEY : ProgressTracker.Step("Sending key") - - fun tracker() = ProgressTracker(SENDING_KEY) - } - - @Suspendable - override fun call(): PublicKey { - progressTracker.currentStep = SENDING_KEY - return TxKeyFlowUtilities.provideKey(this, otherSide) - } - } -} diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowTests.kt new file mode 100644 index 0000000000..44c1f86731 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowTests.kt @@ -0,0 +1,53 @@ +package net.corda.core.flows + +import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.node.MockNetwork +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class TxKeyFlowTests { + lateinit var net: MockNetwork + + @Before + fun before() { + net = MockNetwork(false) + } + + @Test + fun `issue key`() { + // We run this in parallel threads to help catch any race conditions that may exist. + net = MockNetwork(false, true) + + // Set up values we'll need + val revocationEnabled = false + val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) + val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) + val alice: Party = aliceNode.services.myInfo.legalIdentity + val bob: Party = bobNode.services.myInfo.legalIdentity + aliceNode.services.identityService.registerIdentity(bob) + aliceNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity) + bobNode.services.identityService.registerIdentity(alice) + bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity) + + // Run the flows + bobNode.registerServiceFlow(TxKeyFlow.Requester::class) { TxKeyFlow.Provider(it) } + val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob, revocationEnabled)) + + // Get the results + val actual: Map = requesterFlow.resultFuture.getOrThrow() + assertEquals(2, actual.size) + // Verify that the generated anonymous identities do not match the well known identities + val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() + val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() + assertNotEquals(alice, aliceAnonymousIdentity.identity) + assertNotEquals(bob, bobAnonymousIdentity.identity) + } +} diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt deleted file mode 100644 index 662a807269..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.core.flows - -import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.testing.node.MockNetwork -import org.junit.Before -import org.junit.Test -import java.security.PublicKey -import kotlin.test.assertNotNull - -class TxKeyFlowUtilitiesTests { - lateinit var net: MockNetwork - - @Before - fun before() { - net = MockNetwork(false) - } - - @Test - fun `issue key`() { - // We run this in parallel threads to help catch any race conditions that may exist. - net = MockNetwork(false, true) - - // Set up values we'll need - val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) - val bobKey: Party = bobNode.services.myInfo.legalIdentity - - // Run the flows - bobNode.registerServiceFlow(TxKeyFlow.Requester::class) { TxKeyFlow.Provider(it) } - val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bobKey)) - - // Get the results - val actual: PublicKey = requesterFlow.resultFuture.get().first - assertNotNull(actual) - } -} diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index bbb6e1aaf1..637d5d62a5 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -58,7 +58,7 @@ class CashTests { database = dataSourceAndDatabase.second database.transaction { services = object : MockServices() { - override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) + override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) override val vaultService: VaultService = makeVaultService(dataSourceProps) override fun recordTransactions(txs: Iterable) { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index fb50a92c07..dbe68f221c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -306,7 +306,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - keyManagement = makeKeyManagementService() + keyManagement = makeKeyManagementService(identity) scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) @@ -501,7 +501,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "has any other map node been configured.") } - protected open fun makeKeyManagementService(): KeyManagementService = PersistentKeyManagementService(partyKeys) + protected open fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { + return PersistentKeyManagementService(identityService, partyKeys) + } open protected fun makeNetworkMapService() { inNodeNetworkMapService = PersistentNetworkMapService(services, configuration.minimumPlatformVersion) diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 25ebc637a4..a1f16b0b7d 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -54,8 +54,17 @@ class InMemoryIdentityService(identities: Iterable = emptySet(), @Deprecated("Use partyFromX500Name") override fun partyFromName(name: String): Party? = principalToParties[X500Name(name)] override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal] - override fun partyFromAnonymous(party: AbstractParty): Party? = partyFromKey(party.owningKey) + override fun partyFromAnonymous(party: AbstractParty): Party? { + return if (party is Party) { + party + } else { + partyFromKey(party.owningKey) + } + } override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + override fun requirePartyFromAnonymous(party: AbstractParty): Party { + return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") + } @Throws(IdentityService.UnknownAnonymousPartyException::class) override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index 6030118332..cb4d6ca321 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -5,11 +5,15 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.keys import net.corda.core.crypto.sign +import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.util.* import javax.annotation.concurrent.ThreadSafe @@ -25,7 +29,8 @@ import javax.annotation.concurrent.ThreadSafe * etc. */ @ThreadSafe -class E2ETestKeyManagementService(initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { +class E2ETestKeyManagementService(val identityService: IdentityService, + initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { private class InnerState { val keys = HashMap() } @@ -51,6 +56,8 @@ class E2ETestKeyManagementService(initialKeys: Set) : SingletonSerializ return keyPair.public } + override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair = freshKeyAndCert(this, identityService, identity, revocationEnabled) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { return mutex.locked { val pk = publicKey.keys.first { keys.containsKey(it) } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt new file mode 100644 index 0000000000..24d7fd51bc --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -0,0 +1,37 @@ +package net.corda.node.services.keys + +import net.corda.core.crypto.CertificateType +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.X509Utilities +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import java.security.cert.CertPath +import java.security.cert.X509Certificate + +/** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding + * [X509Certificate] and adds it to the identity service. + * + * @param keyManagementService key service to use when generating the new key. + * @param identityService identity service to use when registering the certificate. + * @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for. + * @param revocationEnabled whether to check revocation status of certificates in the certificate path. + * @return X.509 certificate and path to the trust root. + */ +fun freshKeyAndCert(keyManagementService: KeyManagementService, + identityService: IdentityService, + identity: Party, + revocationEnabled: Boolean = false): Pair { + val ourPublicKey = keyManagementService.freshKey() + // FIXME: Use the actual certificate for the identity the flow is presenting themselves as + val issuerKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME) + val issuerCertificate = X509Utilities.createSelfSignedCACertificate(identity.name, issuerKey) + val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate, issuerKey, identity.name, ourPublicKey) + val ourCertPath = X509Utilities.createCertificatePath(issuerCertificate, ourCertificate, revocationEnabled = revocationEnabled) + identityService.registerPath(issuerCertificate, + AnonymousParty(ourPublicKey), + ourCertPath) + return Pair(issuerCertificate, ourCertPath) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 643dcdf05d..532e05605b 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -5,6 +5,8 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.keys import net.corda.core.crypto.sign +import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.utilities.* @@ -13,6 +15,8 @@ import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. @@ -21,7 +25,8 @@ import java.security.PublicKey * * This class needs database transactions to be in-flight during method calls and init. */ -class PersistentKeyManagementService(initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { +class PersistentKeyManagementService(val identityService: IdentityService, + initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}our_key_pairs") { val publicKey = publicKey("public_key") @@ -62,6 +67,8 @@ class PersistentKeyManagementService(initialKeys: Set) : SingletonSeria return keyPair.public } + override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair = freshKeyAndCert(this, identityService, identity, revocationEnabled) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { return mutex.locked { val pk = publicKey.keys.first { keys.containsKey(it) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 9e249ef0f0..c20c5d84c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -348,7 +348,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val serviceFlowInfo = serviceHub.getServiceFlowFactory(sessionInit.clientFlowClass) if (serviceFlowInfo == null) { logger.warn("${sessionInit.clientFlowClass} has not been registered with a service flow: $sessionInit") - sendSessionReject("Don't know ${sessionInit.clientFlowClass.name}") + sendSessionReject("${sessionInit.clientFlowClass.name} has not been registered with a service flow") return } diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 51c9f06c92..28366cb7dd 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -12,6 +12,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.ALICE_KEY import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.services.MockServiceHubInternal +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.StateMachineManager @@ -75,9 +76,10 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val dataSourceAndDatabase = configureDatabase(dataSourceProps) dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second + val identityService = InMemoryIdentityService() + val kms = MockKeyManagementService(identityService, ALICE_KEY) database.transaction { - val kms = MockKeyManagementService(ALICE_KEY) val nullIdentity = X500Name("cn=None") val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging( false, diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index ce8c330e1d..03672b3833 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -115,4 +115,14 @@ class InMemoryIdentityServiceTests { service.assertOwnership(bob, anonymousAlice) } } + + /** + * Ensure if we feed in a full identity, we get the same identity back. + */ + @Test + fun `deanonymising a well known identity`() { + val expected = ALICE + val actual = InMemoryIdentityService().partyFromAnonymous(expected) + assertEquals(expected, actual) + } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 4905acec1d..b1606ec9fb 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -171,8 +171,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - override fun makeKeyManagementService(): KeyManagementService { - return E2ETestKeyManagementService(partyKeys + (overrideServices?.values ?: emptySet())) + override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { + return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) } override fun startMessagingService(rpcOps: RPCOps) { @@ -370,6 +370,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, repeat(numPartyNodes) { nodes += createPartyNode(mapNode.info.address) } + nodes.forEach { node -> + nodes.map { it.info.legalIdentity }.forEach { identity -> + node.services.identityService.registerIdentity(identity) + } + } return BasketOfNodes(nodes, notaryNode, mapNode) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 7eb9e8c5dc..c50f00a420 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,11 +1,8 @@ package net.corda.testing.node import net.corda.core.contracts.Attachment -import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo @@ -15,6 +12,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.keys.freshKeyAndCert import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService @@ -23,7 +21,6 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.testing.MEGA_CORP import net.corda.testing.MINI_CORP import net.corda.testing.MOCK_VERSION_INFO -import org.bouncycastle.asn1.x500.X500Name import rx.Observable import rx.subjects.PublishSubject import java.io.ByteArrayInputStream @@ -38,7 +35,6 @@ import java.security.cert.CertPath import java.security.cert.X509Certificate import java.time.Clock import java.util.* -import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream import javax.annotation.concurrent.ThreadSafe @@ -64,8 +60,8 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { } override val storageService: TxWritableStorageService = MockStorageService() - override val identityService: IdentityService = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) - override val keyManagementService: KeyManagementService = MockKeyManagementService(*keys) + override final val identityService: IdentityService = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) + override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) override val vaultService: VaultService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() @@ -81,7 +77,8 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { } } -class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { +class MockKeyManagementService(val identityService: IdentityService, + vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) override val keys: Set get() = keyStore.keys @@ -94,6 +91,8 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerialize return k.public } + override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair = freshKeyAndCert(this, identityService, identity, revocationEnabled) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { val pk = publicKey.keys.first { keyStore.containsKey(it) } return KeyPair(pk, keyStore[pk]!!) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 29c4d8042d..ea70e70a25 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -6,10 +6,12 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.commonName import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient @@ -26,7 +28,7 @@ import kotlin.concurrent.thread /** * This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or - * any other such task that would make it functionable in a network and thus left to the user to do so manually. + * any other such task that would make it functional in a network and thus left to the user to do so manually. */ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), rpcAddress: HostAndPort = freeLocalHostAndPort()) : AutoCloseable { @@ -35,7 +37,8 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL val userService = RPCUserServiceImpl(config.rpcUsers) val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() - val keyService: KeyManagementService = E2ETestKeyManagementService(setOf(identity)) + val identityService: IdentityService = InMemoryIdentityService() + val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) val broker = ArtemisMessagingServer(config, address, rpcAddress, InMemoryNetworkMapCache(), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create()