Rework package namespace ownership check to verify every package of every class file.

Previous implementation was in LedgerTransaction and focused only on contract classes,
but every package matters.

Also fixes some exception types and does misc refactorings.
This commit is contained in:
Mike Hearn 2019-02-02 12:19:51 +01:00
parent b9ecc5243f
commit 02645f7b9e
24 changed files with 423 additions and 260 deletions

View File

@ -479,6 +479,8 @@ public interface net.corda.core.contracts.Attachment extends net.corda.core.cont
public interface net.corda.core.contracts.AttachmentConstraint
public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
##
public final class net.corda.core.contracts.AttachmentConstraintKt extends java.lang.Object
##
@CordaSerializable
public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException
public <init>(net.corda.core.crypto.SecureHash)
@ -965,12 +967,18 @@ public interface net.corda.core.contracts.TokenizableAssetInfo
public abstract java.math.BigDecimal getDisplayTokenSize()
##
@CordaSerializable
public final class net.corda.core.contracts.TransactionResolutionException extends net.corda.core.flows.FlowException
public class net.corda.core.contracts.TransactionResolutionException extends net.corda.core.flows.FlowException
public <init>(net.corda.core.crypto.SecureHash)
public <init>(net.corda.core.crypto.SecureHash, String)
public <init>(net.corda.core.crypto.SecureHash, String, int, kotlin.jvm.internal.DefaultConstructorMarker)
@NotNull
public final net.corda.core.crypto.SecureHash getHash()
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionResolutionException$UnknownParametersException extends net.corda.core.contracts.TransactionResolutionException
public <init>(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SecureHash)
##
@CordaSerializable
public final class net.corda.core.contracts.TransactionState extends java.lang.Object
public <init>(T, String, net.corda.core.identity.Party)
public <init>(T, String, net.corda.core.identity.Party, Integer)
@ -1026,14 +1034,6 @@ public static final class net.corda.core.contracts.TransactionVerificationExcept
public final String getContractClass()
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$ContractAttachmentNotSignedByPackageOwnerException extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SecureHash, String)
@NotNull
public final net.corda.core.crypto.SecureHash getAttachmentHash()
@NotNull
public final String getContractClass()
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$ContractConstraintRejection extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, String)
@NotNull
@ -1087,8 +1087,18 @@ public static final class net.corda.core.contracts.TransactionVerificationExcept
public final net.corda.core.identity.Party getTxNotary()
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$OverlappingAttachmentsException extends java.lang.Exception
public <init>(String)
public static final class net.corda.core.contracts.TransactionVerificationException$OverlappingAttachmentsException extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, String)
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$PackageOwnershipException extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SecureHash, String, String)
@NotNull
public final net.corda.core.crypto.SecureHash getAttachmentHash()
@NotNull
public final String getContractClass()
@NotNull
public final String getPackageName()
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$SignersMissing extends net.corda.core.contracts.TransactionVerificationException
@ -1128,6 +1138,12 @@ public static final class net.corda.core.contracts.TransactionVerificationExcept
public <init>(net.corda.core.crypto.SecureHash, String, String, String)
##
@CordaSerializable
public static final class net.corda.core.contracts.TransactionVerificationException$UntrustedAttachmentsException extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, java.util.List<? extends net.corda.core.crypto.SecureHash>)
@NotNull
public final java.util.List<net.corda.core.crypto.SecureHash> getIds()
##
@CordaSerializable
public abstract class net.corda.core.contracts.TypeOnlyCommandData extends java.lang.Object implements net.corda.core.contracts.CommandData
public <init>()
public boolean equals(Object)
@ -3537,7 +3553,7 @@ public interface net.corda.core.node.ServicesForResolution
@NotNull
public abstract net.corda.core.node.services.NetworkParametersService getNetworkParametersService()
@NotNull
public abstract net.corda.core.contracts.Attachment loadContractAttachment(net.corda.core.contracts.StateRef, String)
public abstract net.corda.core.contracts.Attachment loadContractAttachment(net.corda.core.contracts.StateRef)
@NotNull
public abstract net.corda.core.contracts.TransactionState<?> loadState(net.corda.core.contracts.StateRef)
@NotNull
@ -7794,7 +7810,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem
@NotNull
public java.sql.Connection jdbcSession()
@NotNull
public net.corda.core.contracts.Attachment loadContractAttachment(net.corda.core.contracts.StateRef, String)
public net.corda.core.contracts.Attachment loadContractAttachment(net.corda.core.contracts.StateRef)
@NotNull
public net.corda.core.contracts.TransactionState<?> loadState(net.corda.core.contracts.StateRef)
@NotNull
@ -7812,6 +7828,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem
public void recordTransactions(boolean, net.corda.core.transactions.SignedTransaction, net.corda.core.transactions.SignedTransaction...)
@NotNull
public Void registerUnloadHandler(kotlin.jvm.functions.Function0<kotlin.Unit>)
public void setNetworkParametersService(net.corda.core.node.services.NetworkParametersService)
@NotNull
public net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder)
@NotNull

View File

@ -18,7 +18,15 @@ import java.security.PublicKey
* @property hash Merkle root of the transaction being resolved, see [net.corda.core.transactions.WireTransaction.id]
*/
@KeepForDJVM
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
open class TransactionResolutionException @JvmOverloads constructor(val hash: SecureHash, message: String = "Transaction resolution failure for $hash") : FlowException(message) {
/**
* Thrown if a transaction specifies a set of parameters that aren't stored locally yet verification is requested.
* This should never normally happen because before verification comes resolution, and if a peer can't provide a
* new set of parameters, [TransactionResolutionException] will have already been thrown beforehand.
*/
class UnknownParametersException(txId: SecureHash, paramsHash: SecureHash) : TransactionResolutionException(txId,
"Transaction specified network parameters $paramsHash but these parameters are not known.")
}
/**
* The node asked a remote peer for the attachment identified by [hash] because it is a dependency of a transaction
@ -246,23 +254,40 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
class InvalidNotaryChange(txId: SecureHash)
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
/**
* Thrown to indicate that a contract attachment is not signed by the network-wide package owner.
*/
class ContractAttachmentNotSignedByPackageOwnerException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String) : TransactionVerificationException(txId,
"""The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner specified in the network parameters.
Please check the source of this attachment and if it is malicious contact your zone operator to report this incident.
For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null)
/**
* Thrown when multiple attachments provide the same file when building the AttachmentsClassloader for a transaction.
*/
@CordaSerializable
@KeepForDJVM
class OverlappingAttachmentsException(path: String) : Exception("Multiple attachments define a file at path `$path`.")
class OverlappingAttachmentsException(txId: SecureHash, path: String) : TransactionVerificationException(txId, "Multiple attachments define a file at $path.", null)
/**
* Thrown when a transaction appears to be trying to downgrade a state to an earlier version of the app that defines it.
* This could be an attempt to exploit a bug in the app, so we prevent it.
*/
@KeepForDJVM
class TransactionVerificationVersionException(txId: SecureHash, contractClassName: ContractClassName, inputVersion: String, outputVersion: String)
: TransactionVerificationException(txId, " No-Downgrade Rule has been breached for contract class $contractClassName. " +
"The output state contract version '$outputVersion' is lower that the version of the input state '$inputVersion'.", null)
: TransactionVerificationException(txId, "No-Downgrade Rule has been breached for contract class $contractClassName. " +
"The output state contract version '$outputVersion' is lower than the version of the input state '$inputVersion'.", null)
/**
* Thrown to indicate that a contract attachment is not signed by the network-wide package owner. Please note that
* the [txId] will always be [SecureHash.zeroHash] because package ownership is an error with a particular attachment,
* and because attachment classloaders are reused this is independent of any particular transaction.
*/
@CordaSerializable
class PackageOwnershipException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String, val packageName: String) : TransactionVerificationException(txId,
"""The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner of package $packageName specified in the network parameters.
Please check the source of this attachment and if it is malicious contact your zone operator to report this incident.
For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null)
/** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */
@KeepForDJVM
@CordaSerializable
class UntrustedAttachmentsException(txId: SecureHash, val ids: List<SecureHash>) :
TransactionVerificationException(txId, "Attempting to load untrusted transaction attachments: $ids. " +
"At this time these are not loadable because the DJVM sandbox has not yet been integrated. " +
"You will need to install that app version yourself, to whitelist it for use. " +
"Please follow the operational steps outlined in https://docs.corda.net/cordapp-build-systems.html#cordapp-contract-attachments to learn more and continue.",
null)
}

View File

@ -21,7 +21,12 @@ const val RPC_UPLOADER = "rpc"
const val P2P_UPLOADER = "p2p"
const val UNKNOWN_UPLOADER = "unknown"
val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)
// We whitelist sources of transaction JARs for now as a temporary state until the DJVM and other security sandboxes
// have been integrated, at which point we'll be able to run untrusted code downloaded over the network and this mechanism
// can be removed. Because we ARE downloading attachments over the P2P network in anticipation of this upgrade, we
// track the source of each attachment in our store. TestDSL is used by LedgerDSLInterpreter when custom attachments
// are added in unit test code.
val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, "TestDSL")
fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS

View File

@ -53,7 +53,6 @@ val ContractState.requiredContractClassName: String? get() {
*
* 2. Java package namespace of signed contract jar is registered in the CZ network map with same public keys (as used to sign contract jar)
*/
// TODO - SignatureConstraint third party signers.
fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: AttachmentWithContext): Boolean {
val output = this
return when {

View File

@ -20,7 +20,7 @@ object JarSignatureCollector {
private val unsignableEntryName = "META-INF/(?:(?:.*[.](?:SF|DSA|RSA|EC)|SIG-.*)|INDEX\\.LIST)".toRegex()
/**
* Returns an ordered list of every [Party] which has signed every signable item in the given [JarInputStream].
* Returns an ordered list of every [PublicKey] which has signed every signable item in the given [JarInputStream].
*
* @param jar The open [JarInputStream] to collect signing parties from.
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.

View File

@ -4,7 +4,6 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
import net.corda.core.transactions.LedgerTransaction
@ -27,8 +26,11 @@ fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = this.i
/**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
* wrong object instance. This class helps avoid that.
*
* @param inputVersions A map linking each contract class name to the advertised version of the JAR that defines it. Used for downgrade protection.
*/
class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>) {
class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader,
private val inputVersions: Map<ContractClassName, Version>) {
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
private val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract()
@ -43,7 +45,6 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
checkNoNotaryChange()
checkEncumbrancesValid()
validateContractVersions()
validatePackageOwnership()
validateStatesAgainstContract()
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
verifyConstraints(hashToSignatureConstrainedContracts)
@ -212,7 +213,7 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
private fun validateContractVersions() {
contractAttachmentsByContract.forEach { contractClassName, attachments ->
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: CordappImpl.DEFAULT_CORDAPP_VERSION
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
inputVersions[contractClassName]?.let {
if (it > outputVersion) {
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
}
@ -220,26 +221,6 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
}
}
/**
* Verify that for each contract the network wide package owner is respected.
*
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
*/
private fun validatePackageOwnership() {
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
}.toMap()
contractsAndOwners.forEach { contract, owner ->
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
if (!owner.isFulfilledBy(attachment.signerKeys))
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
}
}
/**
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
@ -270,8 +251,8 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
/**
* Enforces the validity of the actual constraints.
* * Constraints should be one of the valid supported ones.
* * Constraints should propagate correctly if not marked otherwise.
* - Constraints should be one of the valid supported ones.
* - Constraints should propagate correctly if not marked otherwise.
*
* Returns set of contract classes that identify hash -> signature constraint switchover
*/
@ -293,10 +274,10 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the
// corresponding input states.
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
outputConstraints?.forEach { outputConstraint ->
inputConstraints?.forEach { inputConstraint ->
val inputConstraints = (inputContractGroups[contractClassName] ?: emptyList()).map { it.state.constraint }.toSet()
val outputConstraints = (outputContractGroups[contractClassName] ?: emptyList()).map { it.constraint }.toSet()
outputConstraints.forEach { outputConstraint ->
inputConstraints.forEach { inputConstraint ->
val constraintAttachment = resolveAttachment(contractClassName)
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
throw TransactionVerificationException.ConstraintPropagationRejection(

View File

@ -18,6 +18,7 @@ import java.time.Instant
/**
* Network parameters are a set of values that every node participating in the zone needs to agree on and use to
* correctly interoperate with each other.
*
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
* @property notaries List of well known and trusted notary identities with information on validation type.
* @property maxMessageSize This is currently ignored. However, it will be wired up in a future release.

View File

@ -4,11 +4,14 @@ import net.corda.core.CordaException
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.targetPlatformVersion
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.*
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
import net.corda.core.utilities.contextLogger
@ -23,19 +26,20 @@ import java.util.*
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
* file paths.
* file paths. In addition, every JAR is scanned to ensure that it doesn't violate the package namespace ownership
* rules.
*
* @property params The network parameters fetched from the transaction for which this classloader was built.
* @property sampleTxId The transaction ID that triggered the creation of this classloader. Because classloaders are cached
* this tx may be stale, that is, classloading might be triggered by the verification of some other transaction
* if not all code is invoked every time, however we want a txid for errors in case of attachment bogusness.
*/
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
class AttachmentsClassLoader(attachments: List<Attachment>,
val params: NetworkParameters,
private val sampleTxId: SecureHash,
parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
init {
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }.map(ContractAttachment::id)
if(untrusted.isNotEmpty()) {
throw UntrustedAttachmentsException(untrusted)
}
requireNoDuplicates(attachments)
}
companion object {
private val log = contextLogger()
@ -44,97 +48,11 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
setOrDecorateURLStreamHandlerFactory()
}
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
// In the AttachmentsClassLoader we just ignore any class in those 2 packages.
// In the AttachmentsClassLoader we just block any class in those 2 packages.
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") }
// This function attempts to strike a balance between security and usability when it comes to the no-overlap rule.
// TODO - investigate potential exploits.
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
require(path.toLowerCase() == path)
require(!path.contains("\\"))
return when {
path.endsWith("/") -> false // Directories (packages) can overlap.
targetPlatformVersion < 4 && ignoreDirectories.any { path.startsWith(it) } -> false // Ignore jolokia and json-simple for old cordapps.
path.endsWith(".class") -> true // All class files need to be unique.
!path.startsWith("meta-inf") -> true // All files outside of META-INF need to be unique.
(path == "meta-inf/services/net.corda.core.serialization.serializationwhitelist") -> false // Allow overlapping on the SerializationWhitelist.
path.startsWith("meta-inf/services") -> true // Services can't overlap to prevent a malicious party from injecting additional implementations of an interface used by a contract.
else -> false // This allows overlaps over any non-class files in "META-INF" - except 'services'.
}
}
private fun requireNoDuplicates(attachments: List<Attachment>) {
require(attachments.isNotEmpty()) { "attachments list is empty" }
if (attachments.size == 1) return
// Here is where we enforce the no-overlap rule. This rule states that a transaction which has multiple
// attachments defining different files for the same file path is invalid. It's an important part of the
// security model and blocks various sorts of attacks.
//
// Consider the case of a transaction with two attachments, A and B. Attachment B satisfies the constraint
// on the transaction's states, and thus should be bound by the logic imposed by the contract logic in that
// attachment. But if attachment A were to supply a different class file with the same file name, then the
// usual Java classpath semantics would apply and it'd end up being contract A that gets executed, not B.
// This would prevent you from reasoning about the semantics and transitional logic applied to a state; in
// effect the ledger would be open to arbitrary malicious changes.
//
// There are several variants of this attack that mean we must enforce the no-overlap rule on every file.
// For instance the attacking attachment may override an inner class of the contract class, or a dependency.
//
// We hash each file and ignore overlaps where the contents are actually identical. This is to simplify
// migration from hash to signature constraints. In such a migration transaction the same JAR may be
// attached twice, one signed and one unsigned. The signature files are ignored for the purposes of
// overlap checking as they are expected to have similar names and don't affect the semantics of the
// code, and the class files will be identical so that also doesn't affect lookup. Thus both constraints
// can be satisfied with different attachments that are actually behaviourally identical.
//
// It also avoids a problem where the same dependency has been fat-jarred into multiple apps. This can
// happen because we don't have (as of writing, Feb 2019) any infrastructure for tracking or managing
// dependencies between attachments, so, dependent libraries get bundled up together. Detecting duplicates
// avoids accidental triggering of the no-overlap rule in benign circumstances.
val classLoaderEntries = mutableMapOf<String, Attachment>()
for (attachment in attachments) {
attachment.openAsJAR().use { jar ->
val targetPlatformVersion = jar.manifest?.targetPlatformVersion ?: 1
while (true) {
val entry = jar.nextJarEntry ?: break
if (entry.isDirectory) continue
// We already verified that paths are not strange/game playing when we inserted the attachment
// into the storage service. So we don't need to repeat it here.
//
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
//
// Also convert to Unix path separators as all resource/class lookups will expect this.
val path = entry.name.toLowerCase().replace('\\', '/')
// Some files don't need overlap checking because they don't affect the way the code runs.
if (!shouldCheckForNoOverlap(path, targetPlatformVersion)) continue
// If 2 entries have the same content hash, it means the same file is present in both attachments, so that is ok.
if (path in classLoaderEntries.keys) {
val contentHash = readAttachment(attachment, path).sha256()
val originalAttachment = classLoaderEntries[path]!!
val originalContentHash = readAttachment(originalAttachment, path).sha256()
if (contentHash == originalContentHash) {
log.debug { "Duplicate entry $path has same content hash $contentHash" }
continue
} else {
log.debug { "Content hash differs for $path" }
throw OverlappingAttachmentsException(path)
}
}
log.debug { "Adding new entry for $path" }
classLoaderEntries[path] = attachment
}
}
log.debug { "${classLoaderEntries.size} classloaded entries for $attachment" }
}
}
@VisibleForTesting
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use {
@ -188,6 +106,142 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
}
}
init {
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }
.map(ContractAttachment::id)
if (untrusted.isNotEmpty())
throw TransactionVerificationException.UntrustedAttachmentsException(sampleTxId, untrusted)
checkAttachments(attachments)
}
// This function attempts to strike a balance between security and usability when it comes to the no-overlap rule.
// TODO - investigate potential exploits.
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
require(path.toLowerCase() == path)
require(!path.contains("\\"))
return when {
path.endsWith("/") -> false // Directories (packages) can overlap.
targetPlatformVersion < 4 && ignoreDirectories.any { path.startsWith(it) } -> false // Ignore jolokia and json-simple for old cordapps.
path.endsWith(".class") -> true // All class files need to be unique.
!path.startsWith("meta-inf") -> true // All files outside of META-INF need to be unique.
(path == "meta-inf/services/net.corda.core.serialization.serializationwhitelist") -> false // Allow overlapping on the SerializationWhitelist.
path.startsWith("meta-inf/services") -> true // Services can't overlap to prevent a malicious party from injecting additional implementations of an interface used by a contract.
else -> false // This allows overlaps over any non-class files in "META-INF" - except 'services'.
}
}
private fun checkAttachments(attachments: List<Attachment>) {
require(attachments.isNotEmpty()) { "attachments list is empty" }
// Here is where we enforce the no-overlap and package ownership rules.
//
// The no-overlap rule states that a transaction which has multiple attachments defining different files for
// the same file path is invalid. It's an important part of the security model and blocks various sorts of
// attacks.
//
// Consider the case of a transaction with two attachments, A and B. Attachment B satisfies the constraint
// on the transaction's states, and thus should be bound by the logic imposed by the contract logic in that
// attachment. But if attachment A were to supply a different class file with the same file name, then the
// usual Java classpath semantics would apply and it'd end up being contract A that gets executed, not B.
// This would prevent you from reasoning about the semantics and transitional logic applied to a state; in
// effect the ledger would be open to arbitrary malicious changes.
//
// There are several variants of this attack that mean we must enforce the no-overlap rule on every file.
// For instance the attacking attachment may override an inner class of the contract class, or a dependency.
// However some files do normally overlap between JARs, like manifest files and others under META-INF. Those
// do not affect code execution and are excluded.
//
// Package ownership rules are intended to avoid attacks in which the adversaries define classes in victim
// namespaces. Whilst the constraints and attachments mechanism would keep these logically separated on the
// ledger itself, once such states are serialised and deserialised again e.g. across RPC, to XML or JSON
// then the origin of the code may be lost and only the fully qualified class name may remain. To avoid
// attacks on externally connected systems that only consider type names, we allow people to formally
// claim their parts of the Java package namespace via registration with the zone operator.
val classLoaderEntries = mutableMapOf<String, Attachment>()
for (attachment in attachments) {
// We may have been given an attachment loaded from the database in which case, important info like
// signers is already calculated.
val signers = if (attachment is ContractAttachment) {
attachment.signerKeys
} else {
// The call below reads the entire JAR and calculates all the public keys that signed the JAR.
// It also verifies that there are no mismatches, like a JAR with two signers where some files
// are signed by key A and others only by key B.
//
// The process of iterating every file of an attachment is important because JAR signature
// checks are only applied during a file read. Merely opening a signed JAR does not imply
// the files within it are correctly signed, but, we wish to verify package ownership
// at this point during construction because otherwise we may conclude a JAR is properly
// signed by the owners of the packages, even if it's not. We'd eventually discover that fact
// when trying to read the class file to use it, but if we'd made any decisions based on
// perceived correctness of the signatures or package ownership already, that would be too late.
attachment.openAsJAR().use { JarSignatureCollector.collectSigners(it) }
}
// Now open it again to compute the overlap and package ownership data.
attachment.openAsJAR().use { jar ->
val targetPlatformVersion = jar.manifest?.targetPlatformVersion ?: 1
while (true) {
val entry = jar.nextJarEntry ?: break
if (entry.isDirectory) continue
// We already verified that paths are not strange/game playing when we inserted the attachment
// into the storage service. So we don't need to repeat it here.
//
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
//
// Also convert to Unix path separators as all resource/class lookups will expect this.
val path = entry.name.toLowerCase(Locale.US).replace('\\', '/')
// Namespace ownership. We only check class files: resources are loaded relative to a JAR anyway.
if (path.endsWith(".class")) {
// Get the package name from the file name. Inner classes separate their names with $ not /
// in file names so they are not a problem.
val pkgName= path
.dropLast(".class".length)
.replace('/', '.')
.split('.')
.dropLast(1)
.joinToString(".")
for ((namespace, pubkey) in params.packageOwnership) {
// Note that due to the toLowerCase() call above, we'll be comparing against a lowercased
// version of the ownership claim.
val ns = namespace.toLowerCase(Locale.US)
// We need an additional . to avoid matching com.foo.Widget against com.foobar.Zap
if (pkgName == ns || pkgName.startsWith("$ns.")) {
if (pubkey !in signers)
throw PackageOwnershipException(sampleTxId, attachment.id, path, pkgName)
}
}
}
// Some files don't need overlap checking because they don't affect the way the code runs.
if (!shouldCheckForNoOverlap(path, targetPlatformVersion)) continue
// If 2 entries have the same content hash, it means the same file is present in both attachments, so that is ok.
if (path in classLoaderEntries.keys) {
val contentHash = readAttachment(attachment, path).sha256()
val originalAttachment = classLoaderEntries[path]!!
val originalContentHash = readAttachment(originalAttachment, path).sha256()
if (contentHash == originalContentHash) {
log.debug { "Duplicate entry $path has same content hash $contentHash" }
continue
} else {
log.debug { "Content hash differs for $path" }
throw OverlappingAttachmentsException(sampleTxId, path)
}
}
log.debug { "Adding new entry for $path" }
classLoaderEntries[path] = attachment
}
}
log.debug { "${classLoaderEntries.size} classloaded entries for $attachment" }
}
}
/**
* Required to prevent classes that were excluded from the no-overlap check from being loaded by contract code.
* As it can lead to non-determinism.
@ -201,28 +255,42 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
}
/**
* This is just a factory that provides caches to optimise expensive construction/loading of classloaders, serializers, whitelisted classes.
* This is just a factory that provides caches to optimise expensive construction/loading of classloaders, serializers,
* whitelisted classes.
*/
@VisibleForTesting
internal object AttachmentsClassLoaderBuilder {
private const val CACHE_SIZE = 1000
// This runs in the DJVM so it can't use caffeine.
private val cache: MutableMap<Set<SecureHash>, SerializationContext> = createSimpleCache<Set<SecureHash>, SerializationContext>(CACHE_SIZE).toSynchronised()
// We use a set here because the ordering of attachments doesn't affect code execution, due to the no
// overlap rule, and attachments don't have any particular ordering enforced by the builders. So we
// can just do unordered comparisons here. But the same attachments run with different network parameters
// may behave differently, so that has to be a part of the cache key.
private data class Key(val hashes: Set<SecureHash>, val params: NetworkParameters)
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
// This runs in the DJVM so it can't use caffeine.
private val cache: MutableMap<Key, SerializationContext> = createSimpleCache<Key, SerializationContext>(CACHE_SIZE).toSynchronised()
/**
* Runs the given block with serialization execution context set up with a (possibly cached) attachments classloader.
*
* @param txId The transaction ID that triggered this request; it's unused except for error messages and exceptions that can occur during setup.
*/
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, params: NetworkParameters, txId: SecureHash, block: (ClassLoader) -> T): T {
val attachmentIds = attachments.map { it.id }.toSet()
val serializationContext = cache.computeIfAbsent(attachmentIds) {
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
// Create classloader and load serializers, whitelisted classes
val transactionClassLoader = AttachmentsClassLoader(attachments)
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId)
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
.flatMap { it.whitelist }
.toList()
// Create a new serializationContext for the current Transaction.
// Create a new serializationContext for the current transaction. In this context we will forbid
// deserialization of objects from the future, i.e. disable forwards compatibility. This is to ensure
// that app logic doesn't ignore newly added fields or accidentally downgrade data from newer state
// schemas to older schemas by discarding fields.
SerializationFactory.defaultFactory.defaultContext
.withPreventDataLoss()
.withClassLoader(transactionClassLoader)
@ -274,13 +342,4 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
connected = true
}
}
}
/** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */
@KeepForDJVM
@CordaSerializable
class UntrustedAttachmentsException(val ids: List<SecureHash>) :
CordaException("Attempting to load untrusted Contract Attachments: $ids" +
"These may have been received over the p2p network from a remote node." +
"Please follow the operational steps outlined in https://docs.corda.net/cordapp-build-systems.html#cordapp-contract-attachments to continue."
)
}

View File

@ -38,7 +38,6 @@ data class ContractUpgradeWireTransaction(
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
val privacySalt: PrivacySalt = PrivacySalt()
) : CoreTransaction() {
companion object {
/**
* Runs the explicit upgrade logic.
@ -127,8 +126,8 @@ data class ContractUpgradeWireTransaction(
}
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java as Class<UpgradedContract<ContractState, ContractState>>)
.newInstance()
@Suppress("UNCHECKED_CAST")
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java).newInstance() as UpgradedContract<ContractState, ContractState>
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, className, e)
}
@ -137,15 +136,15 @@ data class ContractUpgradeWireTransaction(
* Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction.
*/
@CordaInternal
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
val binaryInput = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!!
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef, params: NetworkParameters): SerializedBytes<TransactionState<ContractState>> {
val binaryInput: SerializedBytes<TransactionState<ContractState>> = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!!
val legacyAttachment = services.attachments.openAttachment(legacyContractAttachmentId)
?: throw MissingContractAttachments(emptyList())
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
?: throw MissingContractAttachments(emptyList())
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(listOf(legacyAttachment, upgradedAttachment)) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize<TransactionState<ContractState>>()
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(listOf(legacyAttachment, upgradedAttachment), params, id) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize()
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
outputState.serialize()

View File

@ -2,8 +2,10 @@ package net.corda.core.transactions
import net.corda.core.CordaInternal
import net.corda.core.KeepForDJVM
import net.corda.core.StubOutForDJVM
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
@ -12,6 +14,7 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.utilities.contextLogger
import java.lang.UnsupportedOperationException
import java.util.*
import java.util.function.Predicate
@ -35,6 +38,7 @@ private constructor(
// DOCSTART 1
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<ContractState>>,
/** The outputs created by the transaction. */
override val outputs: List<TransactionState<ContractState>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<CommandWithParties<CommandData>>,
@ -42,13 +46,24 @@ private constructor(
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
/** The notary that the tx uses, this must be the same as the notary of all the inputs, or null if there are no inputs. */
override val notary: Party?,
/** The time window within which the tx is valid, will be checked against notary pool member clocks. */
val timeWindow: TimeWindow?,
/** Random data used to make the transaction hash unpredictable even if the contents can be predicted; needed to avoid some obscure attacks. */
val privacySalt: PrivacySalt,
/** Network parameters that were in force when the transaction was notarised. */
/**
* Network parameters that were in force when the transaction was constructed. This is nullable only for backwards
* compatibility for serialized transactions. In reality this field will always be set when on the normal codepaths.
*/
override val networkParameters: NetworkParameters?,
/** Referenced states, which are like inputs but won't be consumed. */
override val references: List<StateAndRef<ContractState>>,
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>
/**
* The versions of the app JARs attached to the transactions that defined the inputs, grouped by contract class name.
* This is used to stop adversaries downgrading apps to versions that have exploitable bugs.
*/
private val inputVersions: Map<ContractClassName, Version>
//DOCEND 1
) : FullTransaction() {
// These are not part of the c'tor above as that defines LedgerTransaction's serialisation format
@ -80,9 +95,9 @@ private constructor(
componentGroups: List<ComponentGroup>? = null,
serializedInputs: List<SerializedStateAndRef>? = null,
serializedReferences: List<SerializedStateAndRef>? = null,
inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>
inputVersions: Map<ContractClassName, Version>
): LedgerTransaction {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, inputStatesContractClassNameToMaxVersion).apply {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, inputVersions).apply {
this.componentGroups = componentGroups
this.serializedInputs = serializedInputs
this.serializedReferences = serializedReferences
@ -103,7 +118,7 @@ private constructor(
/**
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
* The contract verification logic is run in a custom [AttachmentsClassLoader] created for the current transaction.
* The contract verification logic is run in a custom classloader created for the current transaction.
* This classloader is only used during verification and does not leak to the client code.
*
* The reason for this is that classes (contract states) deserialized in this classloader would actually be a different type from what
@ -113,21 +128,40 @@ private constructor(
*/
@Throws(TransactionVerificationException::class)
fun verify() {
if (networkParameters == null) {
// For backwards compatibility only.
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
}
val verifier = internalPrepareVerify(emptyList())
verifier.verify()
internalPrepareVerify(emptyList()).verify()
}
/**
* This method has to be called in a context where it has access to the database.
*/
@CordaInternal
internal fun internalPrepareVerify(extraAttachments: List<Attachment>) = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments) { transactionClassLoader ->
Verifier(createLtxForVerification(), transactionClassLoader, inputStatesContractClassNameToMaxVersion)
internal fun internalPrepareVerify(extraAttachments: List<Attachment>): Verifier {
// Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules
// like no-overlap, package namespace ownership and (in future) deterministic Java.
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments, getParamsWithGoo(), id) { transactionClassLoader ->
Verifier(createLtxForVerification(), transactionClassLoader, inputVersions)
}
}
// Read network parameters with backwards compatibility goo.
private fun getParamsWithGoo(): NetworkParameters {
var params = networkParameters
if (params == null) {
// This path is triggered if someone used old constructors that were accidentally exposed; darn Kotlin's lack of package-private
// visibility! We did originally try to maintain verification codepaths that supported lack of network parameters, but, it
// got too convoluted and people kept just !! asserting the nullity away because on normal codepaths this is always set.
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method would not be accurate.")
// Roll the dice - we're probably in flow context if we got here at all, which means we can fish the current params out.
try {
params = getParamsFromFlowLogic()
} catch (e: UnsupportedOperationException) {
// Inside DJVM, ignore.
}
if (params == null)
throw UnsupportedOperationException("Cannot verify a LedgerTransaction created using deprecated constructors outside of flow context.")
}
return params
}
@StubOutForDJVM
@ -147,6 +181,7 @@ private constructor(
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true)
val authenticatedDeserializedCommands = deserializedCommands.map { cmd ->
@Suppress("DEPRECATION") // Deprecated feature.
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
CommandWithParties(cmd.signers, parties, cmd.value)
}
@ -162,7 +197,7 @@ private constructor(
privacySalt = this.privacySalt,
networkParameters = this.networkParameters,
references = deserializedReferences,
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
inputVersions = this.inputVersions
)
} else {
// This branch is only present for backwards compatibility.
@ -562,7 +597,7 @@ private constructor(
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references,
inputStatesContractClassNameToMaxVersion = emptyMap()
inputVersions = emptyMap()
)
}
@ -588,7 +623,7 @@ private constructor(
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references,
inputStatesContractClassNameToMaxVersion = emptyMap()
inputVersions = emptyMap()
)
}
}

View File

@ -98,7 +98,7 @@ data class NotaryChangeWireTransaction(
* TODO - currently this uses the main classloader.
*/
@CordaInternal
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef, params: NetworkParameters): SerializedBytes<TransactionState<ContractState>> {
return services.loadState(stateRef).serialize()
}

View File

@ -123,7 +123,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
/**
* Looks up identities, attachments and dependent input states using the provided lookup functions in order to
* construct a [LedgerTransaction]. Note that identity lookup failure does *not* cause an exception to be thrown.
* This invocation doesn't cheeks contact class version downgrade rule.
* This invocation doesn't check various rules like no-downgrade or package namespace ownership.
*
* @throws AttachmentResolutionException if a required attachment was not found using [resolveAttachment].
* @throws TransactionResolutionException if an input was not found not using [resolveStateRef].
@ -143,7 +143,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
{ null },
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
{ it -> resolveAttachment(it.txhash) ?: missingAttachment }
{ resolveAttachment(it.txhash) ?: missingAttachment }
)
}
@ -159,7 +159,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveAttachment,
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
resolveParameters,
{ it -> resolveAttachment(it.txhash) ?: missingAttachment }
{ resolveAttachment(it.txhash) ?: missingAttachment }
)
}
@ -188,7 +188,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) }
val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException(id)
val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException.UnknownParametersException(id, networkParametersHash!!)
// For each contract referenced in the inputs, figure out the highest version being used. The outputs must be
// at least that version or higher, to prevent adversaries from downgrading the app to an old version that has
@ -351,17 +351,25 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
/**
* This is the main logic that knows how to retrieve the binary representation of [StateRef]s.
*
* For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the correct classloader independent of the node's classpath.
* For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the
* correct classloader independent of the node's classpath.
*/
@CordaInternal
fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>? {
return if (services is ServiceHub) {
val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction
?: throw TransactionResolutionException(stateRef.txhash)
// Get the network parameters from the tx or whatever the default params are.
val paramsHash = coreTransaction.networkParametersHash ?: services.networkParametersService.defaultHash
val params = services.networkParametersService.lookup(paramsHash) ?: throw IllegalStateException("Should have been able to fetch parameters by this point: $paramsHash")
@Suppress("UNCHECKED_CAST")
when (coreTransaction) {
is WireTransaction -> coreTransaction.componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }?.components?.get(stateRef.index) as SerializedBytes<TransactionState<ContractState>>?
is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
is WireTransaction -> coreTransaction.componentGroups
.firstOrNull { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }
?.components
?.get(stateRef.index) as SerializedBytes<TransactionState<ContractState>>?
is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params)
is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params)
else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} transaction. This is not supported.")
}
} else {

View File

@ -33,6 +33,7 @@ import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.internal.MockCordappProvider
import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.MockNetworkParametersStorage
import net.corda.testing.node.ledger
import org.junit.*
import java.security.PublicKey
@ -90,7 +91,6 @@ class ConstraintsPropagationTests {
.copy(whitelistedContractImplementations = mapOf(
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
noPropagationContractClassName to listOf(SecureHash.zeroHash)),
packageOwnership = mapOf("net.corda.finance.contracts.asset" to hashToSignatureConstraintsKey),
notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
) {
override fun loadContractAttachment(stateRef: StateRef) = servicesForResolution.loadContractAttachment(stateRef)
@ -117,6 +117,7 @@ class ConstraintsPropagationTests {
}
@Test
@Ignore // TODO(mike): rework
fun `Happy path for Hash to Signature Constraint migration`() {
val cordapps = (ledgerServices.cordappProvider as MockCordappProvider).cordapps
val cordappAttachmentIds =
@ -403,7 +404,7 @@ class ConstraintsPropagationTests {
input("c1")
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
command(ALICE_PUBKEY, Cash.Commands.Move())
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower that the version of the input state '2'.")
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower than the version of the input state '2'.")
}
}
}
@ -466,7 +467,7 @@ class ConstraintsPropagationTests {
input("c2")
output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(2000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
command(ALICE_PUBKEY, Cash.Commands.Move())
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower that the version of the input state '2'.")
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower than the version of the input state '2'.")
}
}
}
@ -485,7 +486,7 @@ class ConstraintsPropagationTests {
input("c1")
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
command(ALICE_PUBKEY, Cash.Commands.Move())
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower that the version of the input state '2'.")
failsWith("No-Downgrade Rule has been breached for contract class net.corda.finance.contracts.asset.Cash. The output state contract version '1' is lower than the version of the input state '2'.")
}
}
}

View File

@ -14,7 +14,6 @@ import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import org.junit.Rule
@ -30,7 +29,7 @@ class PackageOwnershipVerificationTests {
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
val BOB_PARTY get() = BOB.party
val BOB_PUBKEY get() = BOB.publicKey
val dummyContract = "net.corda.core.contracts.DummyContract"
const val DUMMY_CONTRACT = "net.corda.core.contracts.DummyContract"
val OWNER_KEY_PAIR = Crypto.generateKeyPair()
}
@ -46,7 +45,10 @@ class PackageOwnershipVerificationTests {
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
},
networkParameters = testNetworkParameters(
packageOwnership = mapOf("net.corda.core.contracts" to OWNER_KEY_PAIR.public),
packageOwnership = mapOf(
"net.corda.core.contracts" to OWNER_KEY_PAIR.public,
"net.corda.isolated.workflows" to BOB_PUBKEY
),
notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))
)
)
@ -55,8 +57,8 @@ class PackageOwnershipVerificationTests {
fun `Happy path - Transaction validates when package signed by owner`() {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachment(dummyContract, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public))
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
attachment(DUMMY_CONTRACT, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public))
output(DUMMY_CONTRACT, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
command(ALICE_PUBKEY, DummyIssue())
verifies()
}
@ -67,10 +69,26 @@ class PackageOwnershipVerificationTests {
fun `Transaction validation fails when the selected attachment is not signed by the owner`() {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachment(dummyContract, SecureHash.allOnesHash, listOf(ALICE_PUBKEY))
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
attachment(DUMMY_CONTRACT, SecureHash.allOnesHash, listOf(ALICE_PUBKEY))
output(DUMMY_CONTRACT, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
command(ALICE_PUBKEY, DummyIssue())
failsWith("is not signed by the owner specified in the network parameters")
failsWith("is not signed by the owner")
}
}
}
@Test
fun `packages that do not have contracts in are still ownable`() {
// The first version of this feature was incorrectly concerned with contract classes and only contract
// classes, but for the feature to work it must apply to any package. This tests that by using a package
// in isolated.jar that doesn't include any contracts.
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachment(DUMMY_CONTRACT, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public))
attachment(attachment(javaClass.getResourceAsStream("/isolated.jar")))
output(DUMMY_CONTRACT, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
command(ALICE_PUBKEY, DummyIssue())
failsWith("is not signed by the owner")
}
}
}

View File

@ -10,6 +10,7 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.isolated.contracts.DummyContractBackdoor
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
@ -46,7 +47,7 @@ class AttachmentsClassLoaderSerializationTests {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }) { classLoader ->
val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)

View File

@ -3,10 +3,13 @@ package net.corda.core.transactions
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.declaredField
import net.corda.core.internal.inputStream
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
@ -14,13 +17,10 @@ import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.net.URL
import java.nio.file.Paths
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderTests {
@ -39,6 +39,9 @@ class AttachmentsClassLoaderTests {
}
private val storage = MockAttachmentStorage()
private val networkParameters = testNetworkParameters()
private fun make(attachments: List<Attachment>, params: NetworkParameters = networkParameters) = AttachmentsClassLoader(attachments, params, SecureHash.zeroHash)
@Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
@ -51,7 +54,7 @@ class AttachmentsClassLoaderTests {
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
val classloader = make(listOf(storage.openAttachment(isolatedId)!!))
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
@ -63,7 +66,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@ -74,7 +77,7 @@ class AttachmentsClassLoaderTests {
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
// does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
make(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
}
@Test
@ -83,7 +86,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
// does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
@Test
@ -91,7 +94,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val cl = make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
@ -104,7 +107,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val cl = make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("same data", txt)
}
@ -115,7 +118,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@ -124,7 +127,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.serializationwhitelist", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.serializationwhitelist", "some other data").inputStream(), "app", "file2.jar")
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
@Test
@ -133,7 +136,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(fakeAttachment("meta-inf/services/com.example.something", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@ -143,7 +146,7 @@ class AttachmentsClassLoaderTests {
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@ -154,7 +157,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", ISOLATED_CONTRACTS_JAR_PATH.file)
val att2 = importAttachment(fakeAttachment("net/corda/finance/contracts/isolated/AnotherDummyContract\$State.class", "some attackdata").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
make(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}

View File

@ -13,7 +13,6 @@ import net.corda.testing.core.*
import net.corda.testing.internal.createWireTransaction
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.rigorousMock
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Rule
import org.junit.Test
import java.io.InputStream
@ -140,7 +139,7 @@ class TransactionTests {
privacySalt,
testNetworkParameters(),
emptyList(),
inputStatesContractClassNameToMaxVersion = emptyMap()
inputVersions = emptyMap()
)
transaction.verify()
@ -192,7 +191,7 @@ class TransactionTests {
privacySalt,
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
emptyList(),
inputStatesContractClassNameToMaxVersion = emptyMap()
inputVersions = emptyMap()
)
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }

View File

@ -17,13 +17,16 @@ import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.internal.NetworkParametersReader
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.*
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.driver.*
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User
import net.corda.testing.node.internal.cordappWithPackages
@ -247,6 +250,7 @@ class CordappConstraintsTests {
}
@Test
@Ignore // TODO(mike): rework
fun `issue cash and transfer using hash to signature constraints migration`() {
// signing key setup
val keyStoreDir = SelfCleaningDir()
@ -255,10 +259,7 @@ class CordappConstraintsTests {
driver(DriverParameters(
cordappsForAllNodes = listOf(UNSIGNED_FINANCE_CORDAPP),
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
networkParameters = testNetworkParameters(
minimumPlatformVersion = 4,
packageOwnership = mapOf("net.corda.finance.contracts.asset" to packageOwnerKey)
),
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
inMemoryDB = false
)) {
val (alice, bob) = listOf(
@ -266,23 +267,31 @@ class CordappConstraintsTests {
startNode(providedName = BOB_NAME, rpcUsers = listOf(user))
).map { it.getOrThrow() }
val notary = defaultNotaryHandle.nodeHandles.get().first()
// Issue Cash
val issueTx = alice.rpc.startFlow(::CashIssueFlow, 1000.DOLLARS, OpaqueBytes.of(1), defaultNotaryIdentity).returnValue.getOrThrow()
val issueTx = alice.rpc.startFlow(::CashIssueFlow, 1000.DOLLARS, OpaqueBytes.of(1), defaultNotaryIdentity)
.returnValue.getOrThrow()
println("Issued transaction: $issueTx")
// Query vault
val states = alice.rpc.vaultQueryBy<Cash.State>().states
printVault(alice, states)
// Restart the node and re-query the vault
println("Shutting down the node for $ALICE_NAME ...")
(alice as OutOfProcess).process.destroyForcibly()
alice.stop()
// Claim the package, publish the new network parameters , and restart all nodes.
val parameters = NetworkParametersReader(DEV_ROOT_CA.certificate, null, notary.baseDirectory).read().networkParameters
// Restart the node and re-query the vault
println("Shutting down the node for $BOB_NAME ...")
(bob as OutOfProcess).process.destroyForcibly()
bob.stop()
val newParams = parameters.copy(
packageOwnership = mapOf("net.corda.finance.contracts.asset" to packageOwnerKey)
)
listOf(alice, bob, notary).forEach { node ->
println("Shutting down the node for ${node} ... ")
(node as OutOfProcess).process.destroyForcibly()
node.stop()
NetworkParametersCopier(newParams, overwriteFile = true).install(node.baseDirectory)
}
startNode(providedName = defaultNotaryIdentity.name)
println("Restarting the node for $ALICE_NAME ...")
(baseDirectory(ALICE_NAME) / "cordapps").deleteRecursively()

View File

@ -9,7 +9,6 @@ import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow
import net.corda.core.serialization.internal.UntrustedAttachmentsException
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
@ -64,7 +63,7 @@ class AttachmentLoadingTests {
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
// ConsumeAndBroadcastResponderFlow re-throws any non-FlowExceptions with just their class name in the message so that
// we can verify here Bob threw the correct exception
.hasMessage(UntrustedAttachmentsException::class.java.name)
.hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name)
}
}

View File

@ -264,7 +264,7 @@ class NodeAttachmentService(
if (content.isPresent) {
return content.get().first
}
// if no attachement has been found, we don't want to cache that - it might arrive later
// If no attachment has been found, we don't want to cache that - it might arrive later.
attachmentContentCache.invalidate(key)
return null
}

View File

@ -30,7 +30,9 @@ interface SimpleFieldAccess {
@DeleteForDJVM
class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
ClassLoader(parentClassLoader) {
fun load(name: String, bytes: ByteArray): Class<*> = defineClass(name, bytes, 0, bytes.size)
fun load(name: String, bytes: ByteArray): Class<*> {
return defineClass(name, bytes, 0, bytes.size)
}
}
class InterfaceMismatchNonGetterException(val clazz: Class<*>, val method: Method) : InterfaceMismatchException(

View File

@ -9,15 +9,17 @@ import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.core.serialization.internal.UntrustedAttachmentsException
import net.corda.node.serialization.kryo.CordaClassResolver
import net.corda.node.serialization.kryo.CordaKryo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.rigorousMock
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Rule
@ -211,16 +213,16 @@ class CordaClassResolverTests {
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash)
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@Test(expected = UntrustedAttachmentsException::class)
@Test(expected = TransactionVerificationException.UntrustedAttachmentsException::class)
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash)
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}

View File

@ -118,7 +118,7 @@ open class MockServices private constructor(
val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService::wellKnownPartyFromX500Name, identityService::wellKnownPartyFromAnonymous, schemaService, schemaService.internalSchemas())
val mockService = database.transaction {
object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys) {
override val networkParametersService: NetworkParametersService = MockNetworkParametersStorage(networkParameters)
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(networkParameters)
override val vaultService: VaultService = makeVaultService(schemaService, database, cordappLoader)
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
ServiceHubInternal.recordTransactions(statesToRecord, txs,
@ -312,7 +312,7 @@ open class MockServices private constructor(
it.start()
}
override val cordappProvider: CordappProvider get() = mockCordappProvider
override val networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
protected val servicesForResolution: ServicesForResolution
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions)

View File

@ -70,7 +70,7 @@ class MockCordappProvider(
private val attachmentsCache = mutableMapOf<String, ByteArray>()
private fun fakeAttachmentCached(contractClass: String, manifestAttributes: Map<String,String> = emptyMap()): ByteArray {
return attachmentsCache.computeIfAbsent(contractClass + manifestAttributes.toSortedMap()) {
fakeAttachment(contractClass, contractClass, manifestAttributes)
fakeAttachment(contractClass.replace('.', '/') + ".class", "fake class file for $contractClass", manifestAttributes)
}
}
}