diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt index 7257620d19..575c1f8d52 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt @@ -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 { progressTracker.currentStep = AWAITING_KEY val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(legalIdentityAnonymous.serialize().bytes) // Special case that if we're both parties, a single identity is generated val identities = LinkedHashMap() @@ -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(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(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, 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) \ No newline at end of file diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt index 753d9a3927..a5a17ccdf8 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt @@ -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(legalIdentityAnonymous).unwrap { confidentialIdentity -> - SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity) - } + val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(ourConfidentialIdentity.serialize().bytes) + val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity) + val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey) + otherSideSession.sendAndReceive(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey())) + .unwrap { (theirConfidentialIdentityBytes, theirSigBytes) -> + val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize() + SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes) + } } } \ No newline at end of file diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 5554b8ea34..55e9ef9d26 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -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("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("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("Signature does not match the given identity and nonce.") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousBob, signature.withoutKey()) + } + } + + mockNet.stopNodes() + } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index d343913712..0db44ba841 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -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) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index b0499938ef..f77e3ae122 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -196,7 +196,6 @@ fun 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(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() }