Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add versioning of confidential initating flows #156

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

* Rebased the `workflows` to use the new `confidential-identities` module.
You will need to add: `TestCordapp.findCordapp("com.r3.corda.lib.ci")` to your tests.
This is also important to version your initiating flows using inlined flows from `token-sdk` with a version equal or more than 2.
Add `@InitiatingFlow(version = 2)` to your initiating flows using new `confiential-identities`, for more information see:
[Flow versioning](https://docs.corda.net/upgrading-cordapps.html#flow-versioning)

#### General

Expand Down
5 changes: 4 additions & 1 deletion workflows/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Corda dependencies.
cordaCompile ("$corda_release_group:corda-core:$corda_release_version") {
cordaCompile("$corda_release_group:corda-core:$corda_release_version") {
changing = true
}
cordaCompile "$confidential_id_release_group:ci-workflows:$confidential_id_release_version"
cordaCompile("$corda_release_group:corda-confidential-identities:$corda_release_version") {
changing = true
}

// Logging.
testCompile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.r3.corda.lib.tokens.workflows.flows.redeem

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole
import com.r3.corda.lib.tokens.workflows.internal.flows.syncKeyVersion
import com.r3.corda.lib.tokens.workflows.utilities.ourSigningKeys
import net.corda.core.flows.CollectSignaturesFlow
import net.corda.core.flows.FlowLogic
Expand Down Expand Up @@ -52,7 +52,7 @@ abstract class AbstractRedeemTokensFlow : FlowLogic<SignedTransaction>() {
// First synchronise identities between issuer and our states.
// TODO: Only do this if necessary.
progressTracker.currentStep = SYNC_IDS
subFlow(SyncKeyMappingFlow(issuerSession, txBuilder.toWireTransaction(serviceHub)))
syncKeyVersion(issuerSession, txBuilder)
val ourSigningKeys = txBuilder.toLedgerTransaction(serviceHub).ourSigningKeys(serviceHub)
val partialStx = serviceHub.signInitialTransaction(txBuilder, ourSigningKeys)
// Call collect signatures flow, issuer should perform all the checks for redeeming states.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.r3.corda.lib.tokens.workflows.flows.redeem

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.ProvideKeyFlow
import com.r3.corda.lib.tokens.contracts.types.TokenType
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole
import com.r3.corda.lib.tokens.workflows.internal.flows.provideKeyVersion
import net.corda.core.contracts.Amount
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
Expand Down Expand Up @@ -33,7 +33,7 @@ constructor(
// Send anonymous identity to the issuer.
issuerSession.send(TransactionRole.PARTICIPANT)
observerSessions.forEach { it.send(TransactionRole.OBSERVER) }
val changeOwner = subFlow(ProvideKeyFlow(issuerSession))
val changeOwner = provideKeyVersion(issuerSession)
return subFlow(RedeemFungibleTokensFlow(
amount = amount,
issuerSession = issuerSession,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.r3.corda.lib.tokens.workflows.flows.redeem
import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.RequestKeyFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole
import com.r3.corda.lib.tokens.workflows.internal.flows.requestKeyVersion
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.utilities.unwrap
Expand All @@ -15,7 +16,7 @@ class ConfidentialRedeemFungibleTokensFlowHandler(val otherSession: FlowSession)
override fun call() {
val role = otherSession.receive<TransactionRole>().unwrap { it }
if (role == TransactionRole.PARTICIPANT) {
subFlow(RequestKeyFlow(otherSession))
requestKeyVersion(otherSession)
}
// Perform checks that the change owner is well known and belongs to the party that inititated the flow
subFlow(RedeemTokensFlowHandler(otherSession))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.r3.corda.lib.tokens.workflows.flows.redeem

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlowHandler
import com.r3.corda.lib.tokens.contracts.states.AbstractToken
import com.r3.corda.lib.tokens.workflows.internal.checkOwner
import com.r3.corda.lib.tokens.workflows.internal.checkSameIssuer
import com.r3.corda.lib.tokens.workflows.internal.checkSameNotary
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlowHandler
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole
import com.r3.corda.lib.tokens.workflows.internal.flows.syncKeyVersionHandler
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.SignTransactionFlow
Expand All @@ -26,7 +26,7 @@ class RedeemTokensFlowHandler(val otherSession: FlowSession) : FlowLogic<Unit>()
if (role == TransactionRole.PARTICIPANT) {
// Synchronise all confidential identities, issuer isn't involved in move transactions, so states holders may
// not be known to this node.
subFlow(SyncKeyMappingFlowHandler(otherSession))
syncKeyVersionHandler(otherSession)
// Perform all the checks to sign the transaction.
subFlow(object : SignTransactionFlow(otherSession) {
override fun checkTransaction(stx: SignedTransaction) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class IssueTokensHandler(val otherSession: FlowSession) : FlowLogic<Unit>() {
*/
@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class ConfidentialIssueTokens
@JvmOverloads
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class MoveNonFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic<Un
*/
@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class ConfidentialMoveFungibleTokens(
val partiesAndAmounts: List<PartyAndAmount<TokenType>>,
val observers: List<Party>,
Expand Down Expand Up @@ -170,7 +170,7 @@ class ConfidentialMoveFungibleTokensHandler(val otherSession: FlowSession) : Flo
*/
@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class ConfidentialMoveNonFungibleTokens(
val partyAndToken: PartyAndToken,
val observers: List<Party>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import net.corda.core.transactions.SignedTransaction

@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class RedeemFungibleTokens
@JvmOverloads
constructor(
Expand All @@ -49,7 +49,7 @@ class RedeemFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic<Uni

@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class RedeemNonFungibleTokens
@JvmOverloads
constructor(
Expand All @@ -75,7 +75,7 @@ class RedeemNonFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic<
// We don't need confidential non fungible redeem, because there are no outputs.
@StartableByService
@StartableByRPC
@InitiatingFlow
@InitiatingFlow(version = 2)
class ConfidentialRedeemFungibleTokens
@JvmOverloads
constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package com.r3.corda.lib.tokens.workflows.internal.flows

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.ProvideKeyFlow
import com.r3.corda.lib.ci.workflows.RequestKeyFlow
import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlow
import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlowHandler
import net.corda.confidential.IdentitySyncFlow
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.verify
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.identity.AbstractParty
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.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.unwrap
import java.security.PublicKey
import java.security.SignatureException

/**
* Internal utilities to handle change of flow versions after introducing new confidential identities.
* Corda doesn't expose nice versioning API for inlined flows nor for flows in the libraries. We introduced only versioning of
* initiating flows from token-sdk. Inlined versions will fallback to new confidential identities CorDapp.
*/
internal const val INITIATING_TOKENS_FLOW = "com.r3.corda.lib.tokens.workflows.flows.rpc."

@Suspendable
internal fun FlowLogic<*>.counterpartyPlatformVersion(session: FlowSession): Int {
val nodeInfo = serviceHub.networkMapCache.getNodeByLegalIdentity(session.counterparty)
return nodeInfo?.platformVersion ?: 0
}

// Internal utilities after introducing new confidential identities. To handle different versions.
@Suspendable
internal fun FlowLogic<*>.provideKeyVersion(session: FlowSession): AbstractParty {
val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion
val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: ""
// This will work only for initiating flow
return if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) {
if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) {
logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " +
"on platformVersion less than 5. Falling back to old CI." +
"If this is not intended behaviour version your flows with version >= 2")
}
// Old Confidential Identities case
subFlow(RequestConfidentialIdentityFlowHandler(session))
} else {
// New Confidential Identities
subFlow(ProvideKeyFlow(session))
}
}

@Suspendable
internal fun FlowLogic<*>.requestKeyVersion(session: FlowSession): AnonymousParty {
val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion
val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: ""
// This will work only for initiating flow
return if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) {
if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) {
logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " +
"on platformVersion less than 5. Falling back to old CI." +
"If this is not intended behaviour version your flows with version >= 2")
}
// Old Confidential Identities case
val key = subFlow(RequestConfidentialIdentityFlow(session)).owningKey
AnonymousParty(key)
} else {
// New Confidential Identities
subFlow(RequestKeyFlow(session))
}
}

@Suspendable
internal fun FlowLogic<*>.syncKeyVersionHandler(session: FlowSession) {
val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion
val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: ""
// This will work only for initiating flow
if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) {
if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) {
logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " +
"on platformVersion less than 5. Falling back to old CI." +
"If this is not intended behaviour version your flows with version >= 2")
}
// Old Confidential Identities case
subFlow(IdentitySyncFlow.Receive(session))
} else {
// New Confidential Identities
subFlow(SyncKeyMappingFlowHandler(session))
}
}

@Suspendable
internal fun FlowLogic<*>.syncKeyVersion(session: FlowSession, txBuilder: TransactionBuilder) {
val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion
val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: ""
// This will work only for initiating flow
if (otherFlowVersion == 1 && counterpartyPlatformVersion(session) < 5) {
if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) {
logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 and runs " +
"on platformVersion less than 5. Falling back to old CI." +
"If this is not intended behaviour version your flows with version >= 2")
}
// Old Confidential Identities case
subFlow(IdentitySyncFlow.Send(session, txBuilder.toWireTransaction(serviceHub)))
} else {
// New Confidential Identities
subFlow(SyncKeyMappingFlow(session, txBuilder.toWireTransaction(serviceHub)))
}
}

@CordaSerializable
internal class ConfidentialIdentityRequest

@CordaSerializable
internal data class IdentityWithSignature(val identity: PartyAndCertificate, val signature: DigitalSignature)

/**
* Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it
* to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says
* the key represents the named entity) and protects against a malicious party incorrectly claiming others'
* keys.
*/
@CordaSerializable
internal data class CertificateOwnershipAssertion(val name: CordaX500Name, val owningKey: PublicKey)

internal class RequestConfidentialIdentityFlow(val session: FlowSession) : FlowLogic<PartyAndCertificate>() {
@Suspendable
override fun call(): PartyAndCertificate {
return session.sendAndReceive<IdentityWithSignature>(ConfidentialIdentityRequest()).unwrap { theirIdentWithSig ->
validateAndRegisterIdentity(
serviceHub = serviceHub,
otherSide = session.counterparty,
theirAnonymousIdentity = theirIdentWithSig.identity,
signature = theirIdentWithSig.signature
)
}
}
}

internal class RequestConfidentialIdentityFlowHandler(val otherSession: FlowSession) : FlowLogic<AnonymousParty>() {
@Suspendable
override fun call(): AnonymousParty {
otherSession.receive<ConfidentialIdentityRequest>().unwrap { it }
val ourAnonymousIdentity: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert(
identity = ourIdentityAndCert,
revocationEnabled = false
)
val data: ByteArray = buildDataToSign(ourAnonymousIdentity)
val signature: DigitalSignature = serviceHub.keyManagementService.sign(
bytes = data,
publicKey = ourAnonymousIdentity.owningKey
).withoutKey()
val ourIdentityWithSig = IdentityWithSignature(ourAnonymousIdentity, signature)
otherSession.send(ourIdentityWithSig)
return ourAnonymousIdentity.party.anonymise()
}
}

/**
* Verifies the confidential identity cert chain and if vaild then stores the identity mapping in the [IdentityService].
*/
@Suspendable
internal fun validateAndRegisterIdentity(
serviceHub: ServiceHub,
otherSide: Party,
theirAnonymousIdentity: PartyAndCertificate,
signature: DigitalSignature
): PartyAndCertificate {
if (theirAnonymousIdentity.name != otherSide.name) {
throw Exception("Certificate subject must match counterparty's well known identity.")
}
try {
theirAnonymousIdentity.owningKey.verify(buildDataToSign(theirAnonymousIdentity), signature)
} catch (ex: SignatureException) {
throw Exception("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 held by the counterparty.
serviceHub.identityService.verifyAndRegisterIdentity(theirAnonymousIdentity)
return theirAnonymousIdentity
}

internal fun buildDataToSign(identity: PartyAndCertificate): ByteArray {
return CertificateOwnershipAssertion(identity.name, identity.owningKey).serialize().bytes
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.r3.corda.lib.tokens.workflows.internal.flows.confidential

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.RequestKeyFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.requestKeyVersion
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.identity.AnonymousParty
Expand All @@ -25,7 +26,7 @@ class AnonymisePartiesFlow(
val party = session.counterparty
if (party in parties) {
session.send(ActionRequest.CREATE_NEW_KEY)
val anonParty = subFlow(RequestKeyFlow(session))
val anonParty = requestKeyVersion(session)
Pair(party, anonParty)
} else {
session.send(ActionRequest.DO_NOTHING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.r3.corda.lib.tokens.workflows.internal.flows.confidential

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.ci.workflows.ProvideKeyFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.provideKeyVersion
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.utilities.unwrap
Expand All @@ -11,7 +12,7 @@ class AnonymisePartiesFlowHandler(val otherSession: FlowSession) : FlowLogic<Uni
override fun call() {
val action = otherSession.receive<ActionRequest>().unwrap { it }
if (action == ActionRequest.CREATE_NEW_KEY) {
subFlow(ProvideKeyFlow(otherSession))
provideKeyVersion(otherSession)
}
}
}
Loading