mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Add signature exchange to transaction key flow (#1417)
Require a signature on a deterministic data blob (which includes X.500 name and public key) when exchanging new confidential identities, in order to ensure that the owner of the key pair wants it to represent the specified name, not just that the certificate owner states the key represents the given identity.
This commit is contained in:
parent
7340a2e32f
commit
7ad754fe78
@ -1,15 +1,32 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import org.bouncycastle.asn1.DERSet
|
||||
import org.bouncycastle.asn1.pkcs.CertificationRequestInfo
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.security.cert.CertPath
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
||||
@ -27,8 +44,32 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_KEY)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService, otherSide: Party, anonymousOtherSide: PartyAndCertificate): PartyAndCertificate {
|
||||
require(anonymousOtherSide.name == otherSide.name)
|
||||
/**
|
||||
* Generate the determinstic data blob the confidential identity's key holder signs to indicate they want to
|
||||
* represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes,
|
||||
* but only the signature is sent. The blob is built independently on each node and the received signature
|
||||
* verified against the expected blob, rather than exchanging the blob.
|
||||
*/
|
||||
fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray {
|
||||
val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey)
|
||||
return certOwnerAssert.serialize().bytes
|
||||
}
|
||||
|
||||
@Throws(SwapIdentitiesException::class)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService,
|
||||
otherSide: Party,
|
||||
anonymousOtherSideBytes: PartyAndCertificate,
|
||||
sigBytes: DigitalSignature): PartyAndCertificate {
|
||||
val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes
|
||||
if (anonymousOtherSide.name != otherSide.name) {
|
||||
throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.")
|
||||
}
|
||||
val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes)
|
||||
try {
|
||||
signature.verify(buildDataToSign(anonymousOtherSideBytes))
|
||||
} catch(ex: SignatureException) {
|
||||
throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex)
|
||||
}
|
||||
// Validate then store their identity so that we can prove the key in the transaction is owned by the
|
||||
// counterparty.
|
||||
identityService.verifyAndRegisterIdentity(anonymousOtherSide)
|
||||
@ -40,6 +81,7 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
progressTracker.currentStep = AWAITING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(legalIdentityAnonymous.serialize().bytes)
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
@ -47,13 +89,33 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val otherSession = initiateFlow(otherParty)
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
val data = buildDataToSign(legalIdentityAnonymous)
|
||||
val ourSig: DigitalSignature.WithKey = serviceHub.keyManagementService.sign(data, legalIdentityAnonymous.owningKey)
|
||||
val ourIdentWithSig = IdentityWithSignature(serializedIdentity, ourSig.withoutKey())
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig)
|
||||
.unwrap { (confidentialIdentityBytes, theirSigBytes) ->
|
||||
val confidentialIdentity: PartyAndCertificate = confidentialIdentityBytes.bytes.deserialize()
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherParty, confidentialIdentity, theirSigBytes)
|
||||
}
|
||||
identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise())
|
||||
identities.put(otherParty, anonymousOtherSide.party.anonymise())
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class IdentityWithSignature(val identity: SerializedBytes<PartyAndCertificate>, val signature: DigitalSignature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class used only in the context of asserting the owner of the private key for the listed key wants to use it
|
||||
* to represent the named entity. This is pairs with an X.509 certificate (which asserts the signing identity says
|
||||
* the key represents the named entity), but protects against a certificate authority incorrectly claiming others'
|
||||
* keys.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class CertificateOwnershipAssertion(val x500Name: CordaX500Name,
|
||||
val publicKey: PublicKey)
|
||||
|
||||
open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null)
|
||||
: FlowException(message, cause)
|
@ -4,6 +4,9 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
@ -20,9 +23,14 @@ class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEna
|
||||
override fun call() {
|
||||
val revocationEnabled = false
|
||||
progressTracker.currentStep = SENDING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
otherSideSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(ourConfidentialIdentity.serialize().bytes)
|
||||
val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity)
|
||||
val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey)
|
||||
otherSideSession.sendAndReceive<SwapIdentitiesFlow.IdentityWithSignature>(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey()))
|
||||
.unwrap { (theirConfidentialIdentityBytes, theirSigBytes) ->
|
||||
val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize()
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,16 +4,10 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.*
|
||||
|
||||
class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
@ -52,4 +46,69 @@ class SwapIdentitiesFlowTests {
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that flow is actually validating the name on the certificate presented by the counterparty.
|
||||
*/
|
||||
@Test
|
||||
fun `verifies identity name`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val notBob = notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(notBob)
|
||||
val signature = notaryNode.services.keyManagementService.sign(sigData, notBob.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Certificate subject must match counterparty's well known identity.") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey())
|
||||
}
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that flow is actually validating its the signature presented by the counterparty.
|
||||
*/
|
||||
@Test
|
||||
fun `verifies signature`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
// Check that the wrong signature is rejected
|
||||
notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}.let { anonymousNotary ->
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousNotary)
|
||||
val signature = notaryNode.services.keyManagementService.sign(sigData, anonymousNotary.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousNotary, signature.withoutKey())
|
||||
}
|
||||
}
|
||||
// Check that the right signing key, but wrong identity is rejected
|
||||
val anonymousAlice = aliceNode.database.transaction {
|
||||
aliceNode.services.keyManagementService.freshKeyAndCert(aliceNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}
|
||||
bobNode.database.transaction {
|
||||
bobNode.services.keyManagementService.freshKeyAndCert(bobNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}.let { anonymousBob ->
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice)
|
||||
val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce.") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousBob, signature.withoutKey())
|
||||
}
|
||||
}
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
}
|
||||
|
@ -46,5 +46,6 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun isValid(content: ByteArray) = by.isValid(content, this)
|
||||
fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes)
|
||||
}
|
||||
}
|
||||
|
@ -196,7 +196,6 @@ fun <T : Any> T.serialize(serializationFactory: SerializationFactory = Serializa
|
||||
* A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize]
|
||||
* to get the original object back.
|
||||
*/
|
||||
@Suppress("unused") // Type parameter is just for documentation purposes.
|
||||
class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
// It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer.
|
||||
val hash: SecureHash by lazy { bytes.sha256() }
|
||||
|
Loading…
Reference in New Issue
Block a user