From a165c69d3a8c833c52826e7add8b85adfdb622b6 Mon Sep 17 00:00:00 2001 From: "adam.houston" Date: Fri, 18 Feb 2022 16:24:37 +0000 Subject: [PATCH] Example of encrypted backchain - WIP for investigation purposes --- .../net/corda/common/logging/Constants.kt | 2 +- constants.properties | 2 +- .../net/corda/core/flows/FinalityFlow.kt | 5 +- .../core/flows/ReceiveTransactionFlow.kt | 59 ++++++- .../corda/core/flows/SendTransactionFlow.kt | 51 ++++-- .../net/corda/core/internal/FetchDataFlow.kt | 7 + .../core/internal/ResolveTransactionsFlow.kt | 49 +++++- .../core/internal/ServiceHubCoreInternal.kt | 8 + .../TransactionVerifierServiceInternal.kt | 4 + .../kotlin/net/corda/core/node/ServiceHub.kt | 4 + .../services/EncryptedTransactionService.kt | 149 +++++++++++++++++ .../core/node/services/TransactionStorage.kt | 7 + .../core/transactions/EncryptedTransaction.kt | 29 ++++ .../core/transactions/SignedTransaction.kt | 64 ++++++++ .../core/transactions/WireTransaction.kt | 25 +++ .../finance/flows/CashPaymentFlowTests.kt | 27 ++- .../net/corda/node/internal/AbstractNode.kt | 7 +- .../internal/ServicesForResolutionImpl.kt | 4 +- .../MigrationServicesForResolution.kt | 4 +- .../node/services/DbTransactionsResolver.kt | 134 +++++++++++++++ .../node/services/api/ServiceHubInternal.kt | 19 +++ .../persistence/DBTransactionStorage.kt | 155 ++++++++++++++++-- .../node-core.changelog-v22-encryption.xml | 14 ++ .../node/messaging/TwoPartyTradeFlowTests.kt | 26 +++ .../persistence/DBTransactionStorageTests.kt | 61 +++++++ .../net/corda/testing/node/MockServices.kt | 5 +- .../node/internal/MockTransactionStorage.kt | 24 +++ 27 files changed, 900 insertions(+), 45 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt create mode 100644 core/src/main/kotlin/net/corda/core/transactions/EncryptedTransaction.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v22-encryption.xml diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt index e14ac91de3..168e94d887 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt @@ -9,4 +9,4 @@ package net.corda.common.logging * (originally added to source control for ease of use) */ -internal const val CURRENT_MAJOR_RELEASE = "4.8.5" +internal const val CURRENT_MAJOR_RELEASE = "4.8.5" \ No newline at end of file diff --git a/constants.properties b/constants.properties index 37dcdce0f7..a24cf70f49 100644 --- a/constants.properties +++ b/constants.properties @@ -21,7 +21,7 @@ jdkClassifier11=jdk11 dockerJavaVersion=3.2.5 proguardVersion=6.1.1 bouncycastleVersion=1.68 -classgraphVersion=4.8.90 +classgraphVersion=4.8.135 disruptorVersion=3.4.2 typesafeConfigVersion=1.3.4 jsr305Version=3.0.2 diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 810b143dac..33976659d1 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -180,7 +180,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, oldV3Broadcast(notarised, oldParticipants.toSet()) for (session in sessions) { try { - subFlow(SendTransactionFlow(session, notarised)) + // PoC send encrypted + subFlow(SendTransactionFlow(session, notarised, true)) logger.info("Party ${session.counterparty} received the transaction.") } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( @@ -282,7 +283,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) { + return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord, encrypted = true) { override fun checkBeforeRecording(stx: SignedTransaction) { require(expectedTxId == null || expectedTxId == stx.id) { "We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" + diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 413f01db3f..8ce493c495 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -4,8 +4,10 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.checkParameterHash +import net.corda.core.internal.dependencies import net.corda.core.internal.pushToLoggingContext import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.RawDependency import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap @@ -30,7 +32,8 @@ import java.security.SignatureException */ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, private val checkSufficientSignatures: Boolean = true, - private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic() { + private val statesToRecord: StatesToRecord = StatesToRecord.NONE, + private val encrypted : Boolean = false) : FlowLogic() { @Suppress("KDocMissingDocumentation") @Suspendable @Throws(SignatureException::class, @@ -47,11 +50,59 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid it.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") checkParameterHash(it.networkParametersHash) - subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord)) + + subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord, encrypted = encrypted)) + logger.info("Transaction dependencies resolution completed.") try { - it.verify(serviceHub, checkSufficientSignatures) - it + if (encrypted) { + + val validatedTxSvc = serviceHub.validatedTransactions + + val encryptedTxs = it.dependencies.mapNotNull { + validatedTxId -> + validatedTxSvc.getEncryptedTransaction(validatedTxId)?.let { etx -> + etx.id to etx + } + }.toMap() + + val signedTxs = it.dependencies.mapNotNull { + validatedTxId -> + validatedTxSvc.getTransaction(validatedTxId)?.let { stx -> + stx.id to stx + } + }.toMap() + + val networkParameters = it.dependencies.mapNotNull { depTxId -> + val npHash = when { + encryptedTxs[depTxId] != null -> serviceHub.encryptedTransactionService.getNetworkParameterHash(encryptedTxs[depTxId]!!) + ?: serviceHub.networkParametersService.defaultHash + signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash + ?: serviceHub.networkParametersService.defaultHash + else -> null + } + + npHash?.let { depTxId to npHash } + }.associate { + netParams -> + netParams.first to serviceHub.networkParametersService.lookup(netParams.second) + } + + val rawDependencies = it.dependencies.associate { + txId -> + txId to RawDependency( + encryptedTxs[txId], + signedTxs[txId], + networkParameters[txId] + ) + } + + serviceHub.encryptedTransactionService.verifyTransaction(it, serviceHub, checkSufficientSignatures, rawDependencies) + it + } else { + it.verify(serviceHub, checkSufficientSignatures) + it + } } catch (e: Exception) { logger.warn("Transaction verification failed.") throw e diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 233a89236b..173eab31c6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -67,7 +67,7 @@ class MaybeSerializedSignedTransaction(override val id: SecureHash, val serializ * @param otherSide the target party. * @param stx the [SignedTransaction] being sent to the [otherSideSession]. */ -open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : DataVendingFlow(otherSide, stx) +open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction, override val encrypted: Boolean = false) : DataVendingFlow(otherSide, stx, encrypted) /** * The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify @@ -80,7 +80,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : */ open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List>) : DataVendingFlow(otherSideSession, stateAndRefs) -open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { +open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, open val encrypted: Boolean = false) : FlowLogic() { @Suspendable protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive(payload) @@ -91,6 +91,9 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) @Suspendable override fun call(): Void? { + + val encryptSvc = serviceHub.encryptedTransactionService + val networkMaxMessageSize = serviceHub.networkParameters.maxMessageSize val maxPayloadSize = networkMaxMessageSize / 2 @@ -146,20 +149,48 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) var numSent = 0 payload = when (dataRequest.dataType) { FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId -> - logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size})" } + + logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size}), encrypted: $encrypted" } + if (!authorisedTransactions.isAuthorised(txId)) { throw FetchDataFlow.IllegalTransactionRequest(txId) } - val tx = serviceHub.validatedTransactions.getTransaction(txId) - ?: throw FetchDataFlow.HashNotFound(txId) - authorisedTransactions.removeAuthorised(tx.id) - authorisedTransactions.addAuthorised(getInputTransactions(tx)) - totalByteCount += tx.txBits.size - numSent++ - tx + + if (encrypted) { + var encryptedTx = serviceHub.validatedTransactions.getEncryptedTransaction(txId) + + if (encryptedTx == null) { + val tx = serviceHub.validatedTransactions.getTransaction(txId) + ?: throw FetchDataFlow.HashNotFound(txId) + + encryptedTx = encryptSvc.encryptTransaction(tx) + } + + authorisedTransactions.removeAuthorised(encryptedTx.id) + authorisedTransactions.addAuthorised(encryptSvc.getDependencies(encryptedTx)) + + totalByteCount += encryptedTx.bytes.size + numSent++ + encryptedTx + } else { + + val tx = serviceHub.validatedTransactions.getTransaction(txId) + ?: throw FetchDataFlow.HashNotFound(txId) + authorisedTransactions.removeAuthorised(tx.id) + authorisedTransactions.addAuthorised(getInputTransactions(tx)) + totalByteCount += tx.txBits.size + numSent++ + tx + } } // Loop on all items returned using dataRequest.hashes.map: FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId -> + + // TODO: adding this for PoC to ensure that we don't fall into this + if (encrypted) { + throw FlowException("Batch mode disabled for encryption PoC") + } + if (!authorisedTransactions.isAuthorised(txId)) { throw FetchDataFlow.IllegalTransactionRequest(txId) } diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 8d69db366f..23c014e07c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -18,6 +18,7 @@ import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.SerializationToken import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsTokenContext +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.UntrustworthyData @@ -273,6 +274,12 @@ class FetchTransactionsFlow(requests: Set, otherSide: FlowSession) : override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid) } +class FetchEncryptedTransactionsFlow(requests: Set, otherSide: FlowSession) : + FetchDataFlow(requests, otherSide, DataType.TRANSACTION) { + + override fun load(txid: SecureHash): EncryptedTransaction? = serviceHub.validatedTransactions.getEncryptedTransaction(txid) +} + class FetchBatchTransactionsFlow(requests: Set, otherSide: FlowSession) : FetchDataFlow(requests, otherSide, DataType.BATCH_TRANSACTION) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 66b525692f..078806fceb 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.node.StatesToRecord import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.debug @@ -21,7 +22,8 @@ class ResolveTransactionsFlow private constructor( val initialTx: SignedTransaction?, val txHashes: Set, val otherSide: FlowSession, - val statesToRecord: StatesToRecord + val statesToRecord: StatesToRecord, + val encrypted: Boolean = false ) : FlowLogic() { constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) @@ -36,6 +38,10 @@ class ResolveTransactionsFlow private constructor( constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) : this(transaction, transaction.dependencies, otherSide, statesToRecord) + // TODO: PoC constructor + constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE, encrypted: Boolean) + : this(transaction, transaction.dependencies, otherSide, statesToRecord, encrypted) + private var fetchNetParamsFromCounterpart = false @Suppress("MagicNumber") @@ -49,16 +55,25 @@ class ResolveTransactionsFlow private constructor( // Fetch missing parameters flow was added in version 4. This check is needed so we don't end up with node V4 sending parameters // request to node V3 that doesn't know about this protocol. fetchNetParamsFromCounterpart = counterpartyPlatformVersion >= PlatformVersionSwitches.FETCH_MISSING_NETWORK_PARAMETERS - val batchMode = counterpartyPlatformVersion >= PlatformVersionSwitches.BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN + + // disable batch mode for encrypted + val batchMode = counterpartyPlatformVersion >= PlatformVersionSwitches.BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN && !encrypted + logger.debug { "ResolveTransactionsFlow.call(): Otherside Platform Version = '$counterpartyPlatformVersion': Batch mode = $batchMode" } + // TODO: attachments and net params are not encrypted (yet) if (initialTx != null) { fetchMissingAttachments(initialTx) fetchMissingNetworkParameters(initialTx) } val resolver = (serviceHub as ServiceHubCoreInternal).createTransactionsResolver(this) - resolver.downloadDependencies(batchMode) + + if (encrypted) { + resolver.downloadEncryptedDependencies() + } else { + resolver.downloadDependencies(batchMode) + } logger.trace { "ResolveTransactionsFlow: Sending END." } otherSide.send(FetchDataFlow.Request.End) // Finish fetching data. @@ -66,7 +81,12 @@ class ResolveTransactionsFlow private constructor( // If transaction resolution is performed for a transaction where some states are relevant, then those should be // recorded if this has not already occurred. val usedStatesToRecord = if (statesToRecord == StatesToRecord.NONE) StatesToRecord.ONLY_RELEVANT else statesToRecord - resolver.recordDependencies(usedStatesToRecord) + + if (encrypted) { + resolver.recordEncryptedDependencies(usedStatesToRecord) + } else { + resolver.recordDependencies(usedStatesToRecord) + } } /** @@ -108,4 +128,25 @@ class ResolveTransactionsFlow private constructor( false } } + + // PoC variants. + // TODO: no support for contract upgrade! + @Suspendable + fun fetchMissingAttachments(transaction: EncryptedTransaction): Boolean { + val attachmentIds = serviceHub.encryptedTransactionService.getAttachmentIds(transaction) + val downloads = subFlow(FetchAttachmentsFlow(attachmentIds, otherSide)).downloaded + return (downloads.isNotEmpty()) + } + + @Suspendable + fun fetchMissingNetworkParameters(transaction: EncryptedTransaction): Boolean { + return if (fetchNetParamsFromCounterpart) { + serviceHub.encryptedTransactionService.getNetworkParameterHash(transaction)?.let { + val downloads = subFlow(FetchNetworkParametersFlow(setOf(it), otherSide)).downloaded + downloads.isNotEmpty() + } ?: false + } else { + false + } + } } diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 4b7d856699..7aca7dbd4d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -32,4 +32,12 @@ interface TransactionsResolver { @Suspendable fun recordDependencies(usedStatesToRecord: StatesToRecord) + + // for Poc we will create a completely parallel set of functions, perhaps a different implementation of TransactionsResolver is + // preferable long term + @Suspendable + fun downloadEncryptedDependencies() + + @Suspendable + fun recordEncryptedDependencies(usedStatesToRecord: StatesToRecord) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index e7ca576618..88c69bec76 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -438,3 +438,7 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi } } } + +// BOB +// E32DC1E8E08D41FE635B27DED128193B75833DC2053BD6A8BB52FB21448EF045 -> 68BB0EA190E6CCD4A5E39CCCEE18A27 +// E32DC1E8E08D41FE635B27DED128193B75833DC2053BD6A8BB52FB21448EF045 -> 68BB0EA190E6CCD4A5E39CCCEE18A27 diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index d63b63edf4..d8a5b4c789 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -49,6 +49,8 @@ interface ServicesForResolution { /** Returns the network parameters the node is operating under. */ val networkParameters: NetworkParameters + val encryptedTransactionService: EncryptedTransactionService + /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * @@ -120,6 +122,8 @@ interface ServiceHub : ServicesForResolution { // NOTE: Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid // their internal state from being serialized in checkpoints. + override val encryptedTransactionService: EncryptedTransactionService + /** * The vault service lets you observe, soft lock and add notes to states that involve you or are relevant to your * node in some way. Additionally you may query and track states that correspond to various criteria. diff --git a/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt b/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt new file mode 100644 index 0000000000..6e0d324d83 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt @@ -0,0 +1,149 @@ +package net.corda.core.node.services + +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.dependencies +import net.corda.core.node.ServiceHub +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.transactions.EncryptedTransaction +import net.corda.core.transactions.RawDependency +import net.corda.core.transactions.RawDependencyMap +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.SignedTransactionDependencies +import net.corda.core.transactions.SignedTransactionDependencyMap +import net.corda.core.utilities.toHexString +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.IvParameterSpec + +// TODO: this should be an interface +class EncryptedTransactionService() : SingletonSerializeAsToken() { + companion object { + + private const val CRYPTO_TRANSFORMATION = "AES/CBC/PKCS5PADDING" + private const val CRYPTO_ALGORITHM = "AES" + + private const val IV_BYTE_LENGTH = 16 + + // we will use this when we want to sign over an encrypted transaction to attest that we've verified it + private val enclaveKeyPair = Crypto.generateKeyPair() + + // this key we will use internally for encryption/decryption n.b. this is JUST for a quick PoC, as we will lose this key on restart + private val encryptionKey = KeyGenerator + .getInstance(CRYPTO_ALGORITHM) + .also { it.init(256) } + .generateKey() + + private fun generateRandomBytes(length: Int): ByteArray { + val arr = ByteArray(length) + SecureRandom().nextBytes(arr) + return arr + } + + fun getPublicEnclaveKey() = enclaveKeyPair.public + } + + fun getDependencies(encryptedTransaction: EncryptedTransaction): Set { + + val stx = decryptTransaction(encryptedTransaction) + return stx.dependencies + } + + fun getAttachmentIds(encryptedTransaction: EncryptedTransaction): Set { + + val stx = decryptTransaction(encryptedTransaction) + return stx.tx.attachments.toSet() + } + + fun getNetworkParameterHash(encryptedTransaction: EncryptedTransaction): SecureHash? { + + val stx = decryptTransaction(encryptedTransaction) + return stx.tx.networkParametersHash + } + + // TODO: this is glossing over a lot of difficulty here. toLedgerTransaction resolves a lot of stuff via services which wont be available within an enclave + fun verifyTransaction(encryptedTransaction: EncryptedTransaction, serviceHub : ServiceHub, checkSufficientSignatures: Boolean, rawDependencies: RawDependencyMap): ByteArray { + + println("Verifying encrypted ${encryptedTransaction.id} ${serviceHub.myInfo.legalIdentities.single()}") + + val stx = decryptTransaction(encryptedTransaction) + + val dependencies = extractDependencies(stx.inputs + stx.references, rawDependencies) + + // will throw if cannot verify + stx.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify() + + return Crypto.doSign(enclaveKeyPair.private, stx.txBits.bytes) + } + + fun verifyTransaction(signedTransaction: SignedTransaction, serviceHub: ServiceHub, checkSufficientSignatures: Boolean, rawDependencies: RawDependencyMap) { + + println("Verifying ${signedTransaction.id} ${serviceHub.myInfo.legalIdentities.single()}") + + val dependencies = extractDependencies(signedTransaction.inputs + signedTransaction.references, rawDependencies) + + // will throw if cannot verify + signedTransaction.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify() + } + + fun encryptTransaction(signedTransaction: SignedTransaction): EncryptedTransaction { + + println("Encrypting ${signedTransaction.id} with key: $encryptionKey ${encryptionKey.encoded.toHexString()}") + + val ivBytes = generateRandomBytes(IV_BYTE_LENGTH) + val initialisationVector = IvParameterSpec(ivBytes) + val encryptionCipher = Cipher + .getInstance(CRYPTO_TRANSFORMATION) + .also { it.init(Cipher.ENCRYPT_MODE, encryptionKey, initialisationVector) } + + val encryptedTxBytes = encryptionCipher.doFinal(ivBytes + signedTransaction.serialize().bytes) + return EncryptedTransaction(signedTransaction.id, encryptedTxBytes) + } + + private fun decryptTransaction(encryptedTransaction: EncryptedTransaction): SignedTransaction { + + val encryptedBytes = encryptedTransaction.bytes + + // first IV_BYTE_LENGTH bytes are the IV + val initialisationVector = IvParameterSpec(encryptedTransaction.bytes.copyOf(IV_BYTE_LENGTH)) + + // remainder is the transaction + val encryptedTransactionBytes = encryptedTransaction.bytes.copyOfRange(IV_BYTE_LENGTH, encryptedBytes.size) + + val decryptionCipher = Cipher + .getInstance(CRYPTO_TRANSFORMATION) + .also { it.init(Cipher.DECRYPT_MODE, encryptionKey, initialisationVector) } + + println("Decrypting ${encryptedTransaction.id} with key: $encryptionKey ${encryptionKey.encoded.toHexString()}") + + return decryptionCipher.doFinal(encryptedTransactionBytes).deserialize() + } + + private fun extractDependencies(requiredStateRefs: List, rawDependencies: RawDependencyMap) : SignedTransactionDependencyMap { + + val requiredStates = requiredStateRefs.map { it.txhash }.distinct() + + val dependencies = requiredStateRefs.map { + val rawDependency = rawDependencies[it.txhash] ?: throw IllegalArgumentException("Missing raw dependency for ${it.txhash}") + val tx = rawDependency.getTransaction() ?: throw IllegalArgumentException("Missing raw dependency data for ${it.txhash} $rawDependency") + it to tx.coreTransaction.outputs[it.index] + }.groupBy { it.first.txhash } + + + return requiredStates.map { + val resolvedDependencies = dependencies[it] ?: throw IllegalArgumentException("Missing encrypted transaction resolved reference for $it") + it to SignedTransactionDependencies( + inputsAndRefs = resolvedDependencies.toMap(), + networkParameters = rawDependencies[it]?.networkParameters + ) + }.toMap() + } + + private fun RawDependency.getTransaction() : SignedTransaction? { + return signedTransaction ?: encryptedTransaction?.let { decryptTransaction(it) } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 5bdb494be6..4a0baa7b54 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -5,7 +5,9 @@ import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.debug import rx.Observable /** @@ -19,6 +21,11 @@ interface TransactionStorage { */ fun getTransaction(id: SecureHash): SignedTransaction? + /** + * Return the encrypted transaction with the given [id], or null if no such transaction exists. + */ + fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? + /** * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * incorporate the update. diff --git a/core/src/main/kotlin/net/corda/core/transactions/EncryptedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/EncryptedTransaction.kt new file mode 100644 index 0000000000..33e3af3c7d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/transactions/EncryptedTransaction.kt @@ -0,0 +1,29 @@ +package net.corda.core.transactions + +import net.corda.core.contracts.NamedByHash +import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +data class EncryptedTransaction ( + override val id : SecureHash, + val bytes : ByteArray + // TODO: will need to also store the signature of who verified this tx + ) : NamedByHash{ + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EncryptedTransaction + + if (id != other.id) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + return 31 * (id.hashCode() + bytes.contentHashCode()) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index ec1067d815..7611814adb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.core.internal.TransactionDeserialisationException import net.corda.core.internal.TransactionVerifierServiceInternal import net.corda.core.internal.VisibleForTesting +import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable @@ -363,6 +364,47 @@ data class SignedTransaction(val txBits: SerializedBytes, class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) + + //region Added for encryption PoC + @JvmOverloads + @DeleteForDJVM + @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) + fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true, dependencyMap: SignedTransactionDependencyMap): LedgerTransaction { + if (checkSufficientSignatures) { + verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid(). + } else { + checkSignaturesAreValid() + } + // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. + resolveAndCheckNetworkParameters(services, dependencyMap) + return tx.toLedgerTransaction(services, dependencyMap) + } + + @DeleteForDJVM + private fun resolveAndCheckNetworkParameters(services: ServiceHub, dependencyMap: SignedTransactionDependencyMap) { + + val defaultNetworkParameters = services.networkParametersService.lookup(services.networkParametersService.defaultHash) + val txNetworkParameters = if (networkParametersHash != null) { + services.networkParametersService.lookup(networkParametersHash!!) + } else { + defaultNetworkParameters + } ?: throw TransactionResolutionException(id) + + val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash } + groupedInputsAndRefs.map { entry -> + + val dependencies = dependencyMap[entry.key] ?: throw TransactionResolutionException(id) + + val params = (dependencies.networkParameters ?: defaultNetworkParameters) ?: throw TransactionResolutionException(id) + + if (txNetworkParameters.epoch < params.epoch) { + throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params) + } + } + } + //endregion + + //region Deprecated /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ @Deprecated("No replacement, this should not be used outside of Corda core") @@ -373,3 +415,25 @@ data class SignedTransaction(val txBits: SerializedBytes, fun isNotaryChangeTransaction() = this.coreTransaction is NotaryChangeWireTransaction //endregion } + +typealias SignedTransactionDependencyMap = Map + +data class SignedTransactionDependencies( + val inputsAndRefs : Map>, + val networkParameters : NetworkParameters? +) + +//data class RawDependencies( +// val encryptedTransactions: Map, +// val signedTransactions: Map +// val networkParameters: NetworkParameters +//) + + +typealias RawDependencyMap = Map + +data class RawDependency( + val encryptedTransaction: EncryptedTransaction?, + val signedTransaction: SignedTransaction?, + val networkParameters: NetworkParameters? +) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 22bfb19be2..1db94b9411 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -123,6 +123,31 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ) } + @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) + @DeleteForDJVM + fun toLedgerTransaction(services: ServicesForResolution, dependencyMap: SignedTransactionDependencyMap): LedgerTransaction { + return services.specialise( + toLedgerTransactionInternal( + resolveIdentity = { services.identityService.partyFromKey(it) }, + resolveAttachment = { services.attachments.openAttachment(it) }, + resolveStateRefAsSerialized = { + + val dependencies = dependencyMap[it.txhash] ?: throw TransactionResolutionException(it.txhash) + val stateRef = dependencies.inputsAndRefs[it] ?: throw TransactionResolutionException(it.txhash) + + stateRef.serialize() + }, + resolveParameters = { + val hashToResolve = it ?: services.networkParametersService.defaultHash + services.networkParametersService.lookup(hashToResolve) + }, + // `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal] + isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true }, + attachmentsClassLoaderCache = (services as? ServiceHubCoreInternal)?.attachmentsClassLoaderCache + ) + ) + } + // Helper for deprecated toLedgerTransaction // TODO: revisit once Deterministic JVM code updated @Suppress("UNUSED") // not sure if this field can be removed safely?? diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 788911a89f..656ed7b03a 100644 --- a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -4,8 +4,10 @@ import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.trackBy import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.toHexString import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash @@ -29,6 +31,9 @@ class CashPaymentFlowTests { private lateinit var bankOfCordaNode: StartedMockNode private lateinit var bankOfCorda: Party private lateinit var aliceNode: StartedMockNode + private lateinit var bobNode: StartedMockNode + + private lateinit var issuanceTx: SignedTransaction @Before fun start() { @@ -36,8 +41,9 @@ class CashPaymentFlowTests { bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) aliceNode = mockNet.createPartyNode(ALICE_NAME) + bobNode = mockNet.createPartyNode(BOB_NAME) val future = bankOfCordaNode.startFlow(CashIssueFlow(initialBalance, ref, mockNet.defaultNotaryIdentity)) - future.getOrThrow() + issuanceTx = future.getOrThrow().stx } @After @@ -58,7 +64,12 @@ class CashPaymentFlowTests { val future = bankOfCordaNode.startFlow(CashPaymentFlow(expectedPayment, payTo)) mockNet.runNetwork() - future.getOrThrow() + val payTx = future.getOrThrow().stx + + // pay Bob (not anonymously as we want to check that Bob owns it) + val futureBob = aliceNode.startFlow(CashPaymentFlow(expectedPayment, bobNode.info.singleIdentity(), false)) + mockNet.runNetwork() + val bobTx = futureBob.getOrThrow().stx // Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary // and $1,500 back to us, so we expect to consume one state, produce one state for our own vault @@ -80,6 +91,18 @@ class CashPaymentFlowTests { assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) } } + + + listOf(bobNode, aliceNode, bankOfCordaNode).forEach { node -> + listOf(issuanceTx, payTx, bobTx).forEach { stx -> + println("${node.info.singleIdentity()} UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}") + println("${node.info.singleIdentity()} ENCRYPTED: ${node.services.validatedTransactions.getEncryptedTransaction(stx.id)?.let { "${stx.id} -> ${it.bytes.toHexString()}"}}") + } + } + + bobNode.services.vaultService.queryBy(Cash.State::class.java).states.forEach { + println("BOB: ${it.state.data}") + } } @Test(timeout=300_000) 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 88662aec01..94a21a04b2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -53,6 +53,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.ContractUpgradeService import net.corda.core.node.services.CordaService +import net.corda.core.node.services.EncryptedTransactionService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.TransactionVerifierService @@ -290,7 +291,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory) @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { + + val encryptedTransactionService = EncryptedTransactionService().tokenize() + + val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, encryptedTransactionService).also { attachments.servicesForResolution = it } @Suppress("LeakingThis") @@ -1134,6 +1138,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val diagnosticsService: DiagnosticsService get() = this@AbstractNode.diagnosticsService override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor override val notaryService: NotaryService? get() = this@AbstractNode.notaryService + override val encryptedTransactionService: EncryptedTransactionService = this@AbstractNode.encryptedTransactionService private lateinit var _myInfo: NodeInfo override val myInfo: NodeInfo get() = _myInfo diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt index f5836c0cc5..f70f93b374 100644 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.SerializedStateAndRef import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.EncryptedTransactionService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.TransactionStorage @@ -19,7 +20,8 @@ data class ServicesForResolutionImpl( override val attachments: AttachmentStorage, override val cordappProvider: CordappProvider, override val networkParametersService: NetworkParametersService, - private val validatedTransactions: TransactionStorage + private val validatedTransactions: TransactionStorage, + override val encryptedTransactionService: EncryptedTransactionService ) : ServicesForResolution { override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?: throw IllegalArgumentException("No current parameters in network parameters storage") diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt index 0186b9659c..b1bc5e2e50 100644 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt +++ b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.readObject import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.EncryptedTransactionService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.TransactionStorage @@ -38,7 +39,8 @@ class MigrationServicesForResolution( override val attachments: AttachmentStorageInternal, private val transactions: TransactionStorage, private val cordaDB: CordaPersistence, - cacheFactory: MigrationNamedCacheFactory + cacheFactory: MigrationNamedCacheFactory, + override val encryptedTransactionService: EncryptedTransactionService = EncryptedTransactionService() ): ServicesForResolution { companion object { diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index bc6cf3d2af..933011bd9c 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -3,11 +3,14 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.internal.FetchEncryptedTransactionsFlow import net.corda.core.internal.FetchTransactionsFlow import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.TransactionsResolver import net.corda.core.internal.dependencies import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.EncryptedTransaction +import net.corda.core.transactions.RawDependency import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug import net.corda.core.utilities.trace @@ -113,6 +116,131 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa } } + @Suspendable + override fun downloadEncryptedDependencies() { + logger.debug { "Downloading encrypted dependencies for transactions ${flow.txHashes}" } + val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage + val encryptSvc = flow.serviceHub.encryptedTransactionService + + val nextRequests = LinkedHashSet(flow.txHashes) // Keep things unique but ordered, for unit test stability. + val topologicalSort = TopologicalSort() + + while (nextRequests.isNotEmpty()) { + logger.debug { "Main fetch loop: size_remaining=${nextRequests.size}" } + // Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the + // graph we're traversing. + nextRequests.removeAll(topologicalSort.transactionIds) + if (nextRequests.isEmpty()) { + // Done early. + break + } + + // Request the standalone transaction data (which may refer to things we don't yet have). + val (existingTxIds, downloadedTxs) = fetchEncryptedRequiredTransactions(Collections.singleton(nextRequests.first())) // Fetch first item only + for (tx in downloadedTxs) { + val dependencies = encryptSvc.getDependencies(tx) + topologicalSort.add(tx.id, dependencies) + } + + var suspended = true + for (downloaded in downloadedTxs) { + suspended = false + val dependencies = encryptSvc.getDependencies(downloaded) + // Do not keep in memory as this bloats the checkpoint. Write each item to the database. + transactionStorage.addUnverifiedEncryptedTransaction(downloaded) + + // The write locks are only released over a suspend, so need to keep track of whether the flow has been suspended to ensure + // that locks are not held beyond each while loop iteration (as doing this would result in a deadlock due to claiming locks + // in the wrong order) + val suspendedViaAttachments = flow.fetchMissingAttachments(downloaded) + val suspendedViaParams = flow.fetchMissingNetworkParameters(downloaded) + suspended = suspended || suspendedViaAttachments || suspendedViaParams + + // Add all input states and reference input states to the work queue. + nextRequests.addAll(dependencies) + } + + // If the flow did not suspend on the last iteration of the downloaded loop above, perform a suspend here to ensure that + // all data is flushed to the database. + if (!suspended) { + FlowLogic.sleep(0.seconds) + } + + // It's possible that the node has a transaction in storage already. Dependencies should also be present for this transaction, + // so just remove these IDs from the set of next requests. + nextRequests.removeAll(existingTxIds) + } + + sortedDependencies = topologicalSort.complete() + logger.debug { "Downloaded ${sortedDependencies?.size} dependencies from remote peer for transactions ${flow.txHashes}" } + } + + @Suspendable + override fun recordEncryptedDependencies(usedStatesToRecord: StatesToRecord) { + val sortedDependencies = checkNotNull(this.sortedDependencies) + val encryptSvc = flow.serviceHub.encryptedTransactionService + logger.trace { "Recording ${sortedDependencies.size} dependencies for ${flow.txHashes.size} transactions" } + val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage + for (txId in sortedDependencies) { + // Retrieve and delete the transaction from the unverified store. + val (tx, isVerified) = checkNotNull(transactionStorage.getEncryptedTransactionInternal(txId)) { + "Somehow the unverified transaction ($txId) that we stored previously is no longer there." + } + if (!isVerified) { + + val dependencies = encryptSvc.getDependencies(tx) + + val encryptedTxs = dependencies.mapNotNull { + depTxId -> + transactionStorage.getEncryptedTransaction(depTxId)?.let { etx -> + etx.id to etx + } + }.toMap() + + val signedTxs = dependencies.mapNotNull { + depTxId -> + transactionStorage.getTransaction(depTxId)?.let { stx -> + stx.id to stx + } + }.toMap() + + val services = flow.serviceHub + val networkParameters = dependencies.mapNotNull { depTxId -> + val npHash = when { + encryptedTxs[depTxId] != null -> encryptSvc.getNetworkParameterHash(encryptedTxs[depTxId]!!) + ?: services.networkParametersService.defaultHash + signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash + ?: services.networkParametersService.defaultHash + else -> null + } + + npHash?.let { depTxId to npHash } + }.associate { + it.first to services.networkParametersService.lookup(it.second) + } + + val rawDependencies = dependencies.associate { + it to RawDependency( + encryptedTxs[it], + signedTxs[it], + networkParameters[it] + ) + } + + encryptSvc.verifyTransaction(tx, flow.serviceHub, true, rawDependencies) + + // TODO: why does this usually go through the serviceHub's recordTransactions function and not + // direct to the validatedTransactions service?? + // flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx)) + + val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage + transactionStorage.addEncryptedTransaction(tx) + } else { + logger.debug { "No need to record $txId as it's already been verified" } + } + } + } + // The transactions already present in the database do not need to be checkpointed on every iteration of downloading // dependencies for other transactions, so strip these down to just the IDs here. @Suspendable @@ -121,6 +249,12 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa return Pair(requestedTxs.fromDisk.map { it.id }, requestedTxs.downloaded) } + @Suspendable + private fun fetchEncryptedRequiredTransactions(requests: Set): Pair, List> { + val requestedTxs = flow.subFlow(FetchEncryptedTransactionsFlow(requests, flow.otherSide)) + return Pair(requestedTxs.fromDisk.map { it.id }, requestedTxs.downloaded) + } + /** * Provides a way to topologically sort SignedTransactions represented just their [SecureHash] IDs. This means that given any two transactions * T1 and T2 in the list returned by [complete] if T1 is a dependency of T2 then T1 will occur earlier than T2. diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 8139b3b0a4..f8e5e18135 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -14,8 +14,10 @@ import net.corda.core.node.StatesToRecord import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCacheBase import net.corda.core.node.services.TransactionStorage +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.DbTransactionsResolver @@ -23,6 +25,7 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.persistence.AttachmentStorageInternal +import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -253,17 +256,33 @@ interface WritableTransactionStorage : TransactionStorage { // TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry. fun addTransaction(transaction: SignedTransaction): Boolean + /** + * Add a new encrypted transaction to the store + */ + fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean + /** * Add a new *unverified* transaction to the store. */ fun addUnverifiedTransaction(transaction: SignedTransaction) + /** + * Add a new *unverified* encrypted transaction to the store. + */ + fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) + /** * Return the transaction with the given ID from the store, and a flag of whether it's verified. Returns null if no transaction with the * ID exists. */ fun getTransactionInternal(id: SecureHash): Pair? + /** + * Return the transaction with the given ID from the store, and a flag of whether it's verified. Returns null if no transaction with the + * ID exists. + */ + fun getEncryptedTransactionInternal(id: SecureHash): Pair? + /** * Returns a future that completes with the transaction corresponding to [id] once it has been committed. Do not warn when run inside * a DB transaction. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index aeeea1dba8..fa3fa94f8c 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -13,6 +13,7 @@ import net.corda.core.serialization.* import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.toFuture import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -53,8 +54,13 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val status: TransactionStatus, @Column(name = "timestamp", nullable = false) - val timestamp: Instant - ) + val timestamp: Instant, + + @Column(name = "encrypted", nullable = false) + val encrypted: Boolean = false + + // TODO: will need to also store the signature of who verified this tx + ) enum class TransactionStatus { UNVERIFIED, @@ -120,17 +126,33 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: name = "DBTransactionStorage_transactions", toPersistentEntityKey = SecureHash::toString, fromPersistentEntity = { - SecureHash.create(it.txId) to TxCacheValue( - it.transaction.deserialize(context = contextToUse()), - it.status) + if (it.encrypted) { + SecureHash.create(it.txId) to TxCacheValue( + EncryptedTransaction( + SecureHash.parse(it.txId), + it.transaction + ), + it.status + ) + } else { + SecureHash.create(it.txId) to TxCacheValue( + it.transaction.deserialize(context = contextToUse()), + it.status + ) + } }, toPersistentEntity = { key: SecureHash, value: TxCacheValue -> DBTransaction( txId = key.toString(), stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id?.uuid?.toString(), - transaction = value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes, + transaction = if( value.encrypted ) { + value.txBits + } else { + value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes + }, status = value.status, - timestamp = clock.instant() + timestamp = clock.instant(), + encrypted = value.encrypted ) }, persistentEntityClass = DBTransaction::class.java, @@ -138,6 +160,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: ) } + // TODO: weight of transactions will be wrong at this stage for encrypted transactions private fun weighTx(tx: AppendOnlyPersistentMapBase.Transactional): Int { val actTx = tx.peekableValue ?: return 0 return actTx.sigs.sumBy { it.size + transactionSignatureOverheadEstimate } + actTx.txBits.size @@ -187,7 +210,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: override fun getTransaction(id: SecureHash): SignedTransaction? { return database.transaction { - txStorage.content[id]?.let { if (it.status.isVerified()) it.toSignedTx() else null } + txStorage.content[id]?.let { if (it.status.isVerified() && !it.encrypted ) it.toSignedTx() else null } } } @@ -207,7 +230,58 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: override fun getTransactionInternal(id: SecureHash): Pair? { return database.transaction { - txStorage.content[id]?.let { it.toSignedTx() to it.status.isVerified() } + txStorage.content[id]?.let { + if (!it.encrypted) { + it.toSignedTx() to it.status.isVerified() + } else null + } + } + } + + override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean { + val transactionId = encryptedTransaction.id + return database.transaction { + txStorage.locked { + val cachedValue = TxCacheValue(encryptedTransaction, TransactionStatus.VERIFIED) + val addedOrUpdated = addOrUpdate(transactionId, cachedValue) { k, _ -> updateTransaction(k) } + if (addedOrUpdated) { + logger.debug { "Transaction $transactionId has been recorded as verified" } + } else { + logger.debug { "Transaction $transactionId is already recorded as verified, so no need to re-record" } + } + addedOrUpdated + } + } + } + + override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? { + return database.transaction { + txStorage.content[id]?.let { if (it.status.isVerified() && it.encrypted ) it.toEncryptedTx() else null } + } + } + + override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) { + val transactionId = encryptedTransaction.id + database.transaction { + txStorage.locked { + val cacheValue = TxCacheValue(encryptedTransaction, status = TransactionStatus.UNVERIFIED) + val added = addWithDuplicatesAllowed(transactionId, cacheValue) + if (added) { + logger.debug { "Encrypted Transaction $transactionId recorded as unverified." } + } else { + logger.info("Encrypted Transaction $transactionId already exists so no need to record.") + } + } + } + } + + override fun getEncryptedTransactionInternal(id: SecureHash): Pair? { + return database.transaction { + txStorage.content[id]?.let { + if (it.encrypted) { + it.toEncryptedTx() to it.status.isVerified() + } else null + } } } @@ -262,21 +336,70 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: private fun snapshot(): List { return txStorage.content.allPersisted.use { - it.filter { it.second.status.isVerified() }.map { it.second.toSignedTx() }.toList() + it.filter { it.second.status.isVerified() && !it.second.encrypted }.map { it.second.toSignedTx() }.toList() } } // Cache value type to just store the immutable bits of a signed transaction plus conversion helpers private data class TxCacheValue( - val txBits: SerializedBytes, + val id: SecureHash, + val txBits: ByteArray, val sigs: List, - val status: TransactionStatus + val status: TransactionStatus, + val encrypted: Boolean ) { constructor(stx: SignedTransaction, status: TransactionStatus) : this( - stx.txBits, - Collections.unmodifiableList(stx.sigs), - status) + stx.id, + stx.txBits.bytes, + stx.sigs, + status, + false) - fun toSignedTx() = SignedTransaction(txBits, sigs) + constructor(encryptedTransaction: EncryptedTransaction, status: TransactionStatus) : this( + encryptedTransaction.id, + encryptedTransaction.bytes, + emptyList(), + status, + true) + + fun toSignedTx() : SignedTransaction { + return if (!encrypted) { + val txBitsAsSerialized = SerializedBytes(txBits) + SignedTransaction(txBitsAsSerialized, sigs) + } else { + throw IllegalArgumentException("Cannot get signed transaction for encrypted tx") + } + } + + fun toEncryptedTx() : EncryptedTransaction { + return if (encrypted) { + // TODO: EncryptedTransaction will be extended to include verification signature + EncryptedTransaction(id, txBits) + } else { + throw IllegalArgumentException("Cannot get encrypted transaction for signed tx") + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TxCacheValue + + if (!txBits.contentEquals(other.txBits)) return false + if (sigs != other.sigs) return false + if (status != other.status) return false + if (encrypted != other.encrypted) return false + + return true + } + + override fun hashCode(): Int { + var result = txBits.contentHashCode() + result = 31 * result + sigs.hashCode() + result = 31 * result + status.hashCode() + result = 31 * result + encrypted.hashCode() + return result + } } } diff --git a/node/src/main/resources/migration/node-core.changelog-v22-encryption.xml b/node/src/main/resources/migration/node-core.changelog-v22-encryption.xml new file mode 100644 index 0000000000..0718265bd5 --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v22-encryption.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 0c49ee44ac..b04963619d 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -18,6 +18,7 @@ import net.corda.core.node.services.Vault import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.toFuture +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -786,6 +787,31 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { delegate.getTransactionInternal(id) } } + + // TODO: these Encrypted transactions may need an overhaul is probably indicative that overloading the current storage was a bad idea + override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean { + return database.transaction { + delegate.addEncryptedTransaction(encryptedTransaction) + } + } + + override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) { + return database.transaction { + delegate.addUnverifiedEncryptedTransaction(encryptedTransaction) + } + } + + override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? { + return database.transaction { + delegate.getEncryptedTransaction(id) + } + } + + override fun getEncryptedTransactionInternal(id: SecureHash): Pair? { + return database.transaction { + delegate.getEncryptedTransactionInternal(id) + } + } } interface TxRecord { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 51b400c321..0e3f28d15a 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -7,7 +7,13 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.core.serialization.serialize import net.corda.core.toFuture +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.node.CordaClock import net.corda.node.MutableClock @@ -15,6 +21,7 @@ import net.corda.node.SimpleClock import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.serialization.internal.CordaSerializationEncoding import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule @@ -32,12 +39,17 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import rx.plugins.RxJavaHooks +import java.security.SecureRandom import java.time.Clock import java.time.Instant import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.IvParameterSpec import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlin.test.assertNotNull class DBTransactionStorageTests { private companion object { @@ -390,6 +402,47 @@ class DBTransactionStorageTests { assertThat(warning).isEqualTo(DBTransactionStorage.TRANSACTION_ALREADY_IN_PROGRESS_WARNING) } + @Test(timeout=300_000) + fun `encrypted transaction`() { + val now = Instant.ofEpochSecond(111222333L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction() + + val keygen = KeyGenerator.getInstance("AES") + keygen.init(256) + val key = keygen.generateKey() + + val cipherTransformation = "AES/CBC/PKCS5PADDING" + val encryptionCipher = Cipher.getInstance(cipherTransformation) + val iv = generateIv() + encryptionCipher.init(Cipher.ENCRYPT_MODE, key, iv) + + val encryptedTxBytes = encryptionCipher.doFinal(transaction.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes) + val encryptedTx = EncryptedTransaction(transaction.id, encryptedTxBytes) + + transactionStorage.addEncryptedTransaction(encryptedTx) + + val storedTx = transactionStorage.getEncryptedTransaction(transaction.id) + + val decryptionCipher = Cipher.getInstance(cipherTransformation) + decryptionCipher.init(Cipher.DECRYPT_MODE, key, iv) + + assertNotNull(storedTx, "Could not find stored encrypted message") + + val decryptedTx = decryptionCipher.doFinal(storedTx!!.bytes).deserialize(context = contextToUse()) + + assertEquals(decryptedTx, transaction) + + assertEquals(now, readTransactionTimestampFromDB(transaction.id)) + } + + fun generateIv(): IvParameterSpec? { + val iv = ByteArray(16) + SecureRandom().nextBytes(iv) + return IvParameterSpec(iv) + } + private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { transactionStorage = DBTransactionStorage(database, TestingNamedCacheFactory(cacheSizeBytesOverride ?: 1024), clock) @@ -413,4 +466,12 @@ class DBTransactionStorageTests { listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID))) ) } + + private fun contextToUse(): SerializationContext { + return if (effectiveSerializationEnv.serializationFactory.currentContext?.useCase == SerializationContext.UseCase.Storage) { + effectiveSerializationEnv.serializationFactory.currentContext!! + } else { + SerializationDefaults.STORAGE_CONTEXT + } + } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index efd5813736..74d3fe20e2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -86,7 +86,8 @@ open class MockServices private constructor( override val keyManagementService: KeyManagementService = MockKeyManagementService( identityService, *arrayOf(initialIdentity.keyPair) + moreKeys - ) + ), + override val encryptedTransactionService : EncryptedTransactionService = EncryptedTransactionService() ) : ServiceHub { companion object { @@ -457,7 +458,7 @@ open class MockServices private constructor( override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() protected val servicesForResolution: ServicesForResolution - get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) + get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, encryptedTransactionService) internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index c1cebf95e1..443482a03a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.toFuture +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage import net.corda.testing.node.MockServices @@ -30,6 +31,7 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } private val txns = HashMap() + private val encryptedTxns = HashMap() private val _updatesPublisher = PublishSubject.create() @@ -61,5 +63,27 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun getTransactionInternal(id: SecureHash): Pair? = txns[id]?.let { Pair(it.stx, it.isVerified) } + override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean { + val current = encryptedTxns.putIfAbsent(encryptedTransaction.id, EncryptedTxHolder(encryptedTransaction, isVerified = true)) + return if (current == null) { + true + } else if (!current.isVerified) { + current.isVerified = true + true + } else { + false + } + } + + override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) { + encryptedTxns.putIfAbsent(encryptedTransaction.id, EncryptedTxHolder(encryptedTransaction, isVerified = false)) + } + + override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? = encryptedTxns[id]?.let { if (it.isVerified) it.etx else null } + + override fun getEncryptedTransactionInternal(id: SecureHash): Pair? = + encryptedTxns[id]?.let { Pair(it.etx, it.isVerified) } + private class TxHolder(val stx: SignedTransaction, var isVerified: Boolean) + private class EncryptedTxHolder(val etx: EncryptedTransaction, var isVerified: Boolean) } \ No newline at end of file