Merge pull request #2 from corda/feature/CORDA-1947/package_ownership

CORDA-1947 added packageOwnership
This commit is contained in:
Tudor Malene 2018-10-19 15:24:13 +01:00 committed by GitHub
commit 0234c1fcf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 561 additions and 110 deletions

View File

@ -191,10 +191,10 @@ class NodeMonitorModel : AutoCloseable {
val retryInterval = 5.seconds
val client = CordaRPCClient(
nodeHostAndPort,
CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = retryInterval
)
nodeHostAndPort,
CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = retryInterval
)
)
do {
val connection = try {

View File

@ -2,6 +2,7 @@ package net.corda.core.contracts
import net.corda.core.KeepForDJVM
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
/**
* Wrap an attachment in this if it is to be used as an executable contract attachment
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
*/
@KeepForDJVM
@CordaSerializable
class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment {
class ContractAttachment @JvmOverloads constructor(
val attachment: Attachment,
val contract: ContractClassName,
val additionalContracts: Set<ContractClassName> = emptySet(),
val uploader: String? = null,
override val signers: List<PublicKey> = emptyList()) : Attachment by attachment {
val allContracts: Set<ContractClassName> get() = additionalContracts + contract

View File

@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NonEmptySet
import java.security.PublicKey
@ -163,4 +164,12 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
@DeleteForDJVM
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)
}

View File

@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.utilities.days
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
@ -22,6 +23,7 @@ import java.time.Instant
* of parameters.
* @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class.
* This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html).
* @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
* @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen
* during this period
*/
@ -35,7 +37,8 @@ data class NetworkParameters(
val modifiedTime: Instant,
val epoch: Int,
val whitelistedContractImplementations: Map<String, List<AttachmentId>>,
val eventHorizon: Duration
val eventHorizon: Duration,
val packageOwnership: Map<JavaPackageName, PublicKey>
) {
@DeprecatedConstructorForDeserialization(1)
constructor (minimumPlatformVersion: Int,
@ -52,7 +55,28 @@ data class NetworkParameters(
modifiedTime,
epoch,
whitelistedContractImplementations,
Int.MAX_VALUE.days
Int.MAX_VALUE.days,
emptyMap()
)
@DeprecatedConstructorForDeserialization(2)
constructor (minimumPlatformVersion: Int,
notaries: List<NotaryInfo>,
maxMessageSize: Int,
maxTransactionSize: Int,
modifiedTime: Instant,
epoch: Int,
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
eventHorizon: Duration
) : this(minimumPlatformVersion,
notaries,
maxMessageSize,
maxTransactionSize,
modifiedTime,
epoch,
whitelistedContractImplementations,
eventHorizon,
emptyMap()
)
init {
@ -63,6 +87,7 @@ data class NetworkParameters(
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" }
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
}
fun copy(minimumPlatformVersion: Int,
@ -83,20 +108,47 @@ data class NetworkParameters(
eventHorizon = eventHorizon)
}
fun copy(minimumPlatformVersion: Int,
notaries: List<NotaryInfo>,
maxMessageSize: Int,
maxTransactionSize: Int,
modifiedTime: Instant,
epoch: Int,
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
eventHorizon: Duration
): NetworkParameters {
return copy(minimumPlatformVersion = minimumPlatformVersion,
notaries = notaries,
maxMessageSize = maxMessageSize,
maxTransactionSize = maxTransactionSize,
modifiedTime = modifiedTime,
epoch = epoch,
whitelistedContractImplementations = whitelistedContractImplementations,
eventHorizon = eventHorizon)
}
override fun toString(): String {
return """NetworkParameters {
minimumPlatformVersion=$minimumPlatformVersion
notaries=$notaries
maxMessageSize=$maxMessageSize
maxTransactionSize=$maxTransactionSize
whitelistedContractImplementations {
${whitelistedContractImplementations.entries.joinToString("\n ")}
}
eventHorizon=$eventHorizon
modifiedTime=$modifiedTime
epoch=$epoch
}"""
minimumPlatformVersion=$minimumPlatformVersion
notaries=$notaries
maxMessageSize=$maxMessageSize
maxTransactionSize=$maxTransactionSize
whitelistedContractImplementations {
${whitelistedContractImplementations.entries.joinToString("\n ")}
}
eventHorizon=$eventHorizon
modifiedTime=$modifiedTime
epoch=$epoch,
packageOwnership= {
${packageOwnership.keys.joinToString()}}
}
}"""
}
/**
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
*/
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull()
}
/**
@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
* version.
*/
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
/**
* A wrapper for a legal java package. Used by the network parameters to store package ownership.
*/
@CordaSerializable
data class JavaPackageName(val name: String) {
init {
require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" }
}
/**
* Returns true if the [fullClassName] is in a subpackage of the current package.
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
*
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
*/
fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true)
}
// Check if a string is a legal Java package name.
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
}
// Make sure that packages don't overlap so that ownership is clear.
private fun noOverlap(packages: Collection<JavaPackageName>) = packages.all { currentPackage ->
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") }
}

View File

@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction(
private fun verifyConstraints() {
val attachmentForConstraintVerification = AttachmentWithContext(
legacyContractAttachment as? ContractAttachment
?: ContractAttachment(legacyContractAttachment, legacyContractClassName),
?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers),
upgradedContract.legacyContract,
networkParameters.whitelistedContractImplementations
)

View File

@ -3,6 +3,7 @@ package net.corda.core.transactions
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.castIfPossible
@ -81,6 +82,7 @@ data class LedgerTransaction @JvmOverloads constructor(
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
// TODO - verify for version downgrade
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract()
verifyConstraintsValidity(contractAttachmentsByContract)
verifyConstraints(contractAttachmentsByContract)
@ -105,6 +107,30 @@ data class LedgerTransaction @JvmOverloads constructor(
}
}
/**
* Verify that for each contract the network wide package owner is respected.
*
* TODO - revisit once transaction contains network parameters.
*/
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// This should never happen once we have network parameters in the transaction.
if (networkParameters == null) {
return
}
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it }
}.toMap()
contractsAndOwners.forEach { contract, owner ->
val attachment = contractAttachmentsByContract[contract]!!
if (!owner.isFulfilledBy(attachment.signers)) {
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
}
}
}
/**
* Enforces the validity of the actual constraints.
* * Constraints should be one of the valid supported ones.

View File

@ -0,0 +1,45 @@
package net.corda.core
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.div
import net.corda.nodeapi.internal.crypto.loadKeyStore
import java.io.FileInputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
import java.util.jar.JarInputStream
import kotlin.test.assertEquals
object JarSignatureTestUtils {
val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
fun Path.executeProcess(vararg command: String) {
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
assertEquals(0, ProcessBuilder()
.inheritIO()
.redirectOutput(shredder)
.redirectError(shredder)
.directory(this.toFile())
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
.start()
.waitFor())
}
fun Path.generateKey(alias: String, password: String, name: String) =
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name)
fun Path.createJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
fun Path.updateJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
return ks.getCertificate(alias).publicKey
}
fun Path.getJarSigners(fileName: String) =
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
}

View File

@ -0,0 +1,95 @@
package net.corda.core.contracts
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.JavaPackageName
import net.corda.core.transactions.LedgerTransaction
import net.corda.finance.POUNDS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
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
import org.junit.Test
class PackageOwnershipVerificationTests {
private companion object {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
val ALICE_PARTY get() = ALICE.party
val ALICE_PUBKEY get() = ALICE.publicKey
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"
val OWNER_KEY_PAIR = Crypto.generateKeyPair()
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val ledgerServices = MockServices(
cordappPackages = listOf("net.corda.finance.contracts.asset"),
initialIdentity = ALICE,
identityService = rigorousMock<IdentityServiceInternal>().also {
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
},
networkParameters = testNetworkParameters()
.copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public))
)
@Test
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())
command(ALICE_PUBKEY, DummyIssue())
verifies()
}
}
}
@Test
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())
command(ALICE_PUBKEY, DummyIssue())
failsWith("is not signed by the owner specified in the network parameters")
}
}
}
}
@BelongsToContract(DummyContract::class)
class DummyContractState : ContractState {
override val participants: List<AbstractParty>
get() = emptyList()
}
class DummyContract : Contract {
interface Commands : CommandData
class Create : Commands
override fun verify(tx: LedgerTransaction) {
//do nothing
}
}
class DummyIssue : TypeOnlyCommandData()

View File

@ -1,8 +1,11 @@
package net.corda.core.internal
import net.corda.core.identity.CordaX500Name
import net.corda.core.JarSignatureTestUtils.createJar
import net.corda.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.getJarSigners
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.JarSignatureTestUtils.updateJar
import net.corda.core.identity.Party
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import org.assertj.core.api.Assertions.assertThat
@ -10,30 +13,14 @@ import org.junit.After
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileInputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.PublicKey
import java.util.jar.JarInputStream
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class JarSignatureCollectorTest {
companion object {
private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName)
private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
private val shredder = (dir / "_shredder").toFile() // No need to delete after each test.
fun execute(vararg command: String) {
assertEquals(0, ProcessBuilder()
.inheritIO()
.redirectOutput(shredder)
.directory(dir.toFile())
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
.start()
.waitFor())
}
private const val FILENAME = "attachment.jar"
private const val ALICE = "alice"
@ -41,14 +28,11 @@ class JarSignatureCollectorTest {
private const val BOB = "bob"
private const val BOB_PASS = "bobpass"
private fun generateKey(alias: String, password: String, name: CordaX500Name) =
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name.toString())
@BeforeClass
@JvmStatic
fun beforeClass() {
generateKey(ALICE, ALICE_PASS, ALICE_NAME)
generateKey(BOB, BOB_PASS, BOB_NAME)
dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
(dir / "_signable1").writeLines(listOf("signable1"))
(dir / "_signable2").writeLines(listOf("signable2"))
@ -75,49 +59,49 @@ class JarSignatureCollectorTest {
@Test
fun `empty jar has no signers`() {
(dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this.
createJar("META-INF")
assertEquals(emptyList(), getJarSigners())
dir.createJar(FILENAME, "META-INF")
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
signAsAlice()
assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign.
assertEquals(emptyList(), dir.getJarSigners(FILENAME)) // There needs to have been a file for ALICE to sign.
}
@Test
fun `unsigned jar has no signers`() {
createJar("_signable1")
assertEquals(emptyList(), getJarSigners())
dir.createJar(FILENAME, "_signable1")
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
updateJar("_signable2")
assertEquals(emptyList(), getJarSigners())
dir.updateJar(FILENAME, "_signable2")
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
}
@Test
fun `one signer`() {
createJar("_signable1", "_signable2")
dir.createJar(FILENAME, "_signable1", "_signable2")
val key = signAsAlice()
assertEquals(listOf(key), getJarSigners())
assertEquals(listOf(key), dir.getJarSigners(FILENAME))
(dir / "my-dir").createDirectory()
updateJar("my-dir")
assertEquals(listOf(key), getJarSigners()) // Unsigned directory is irrelevant.
dir.updateJar(FILENAME, "my-dir")
assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant.
}
@Test
fun `two signers`() {
createJar("_signable1", "_signable2")
dir.createJar(FILENAME, "_signable1", "_signable2")
val key1 = signAsAlice()
val key2 = signAsBob()
assertEquals(setOf(key1, key2), getJarSigners().toSet())
assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet())
}
@Test
fun `all files must be signed by the same set of signers`() {
createJar("_signable1")
dir.createJar(FILENAME, "_signable1")
val key1 = signAsAlice()
assertEquals(listOf(key1), getJarSigners())
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
updateJar("_signable2")
dir.updateJar(FILENAME, "_signable2")
signAsBob()
assertFailsWith<InvalidJarSignersException>(
"""
@ -126,41 +110,23 @@ class JarSignatureCollectorTest {
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
constraints applied to attachment signatures.
""".trimIndent().replace('\n', ' ')
) { getJarSigners() }
) { dir.getJarSigners(FILENAME) }
}
@Test
fun `bad signature is caught even if the party would not qualify as a signer`() {
(dir / "volatile").writeLines(listOf("volatile"))
createJar("volatile")
dir.createJar(FILENAME, "volatile")
val key1 = signAsAlice()
assertEquals(listOf(key1), getJarSigners())
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
(dir / "volatile").writeLines(listOf("garbage"))
updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad.
dir.updateJar(FILENAME, "volatile", "_signable1") // ALICE's signature on volatile is now bad.
signAsBob()
// The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature:
assertFailsWith<SecurityException> { getJarSigners() }
assertFailsWith<SecurityException> { dir.getJarSigners(FILENAME) }
}
//region Helper functions
private fun createJar(vararg contents: String) =
execute(*(arrayOf("jar", "cvf", FILENAME) + contents))
private fun updateJar(vararg contents: String) =
execute(*(arrayOf("jar", "uvf", FILENAME) + contents))
private fun signJar(alias: String, password: String): PublicKey {
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias)
val ks = loadKeyStore(dir.resolve("_teststore"), "storepass")
return ks.getCertificate(alias).publicKey
}
private fun signAsAlice() = signJar(ALICE, ALICE_PASS)
private fun signAsBob() = signJar(BOB, BOB_PASS)
private fun getJarSigners() =
JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigners)
//endregion
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
}

View File

@ -134,5 +134,5 @@ class TransactionBuilderTest {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
}, DummyContract.PROGRAM_ID)
}, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey })
}

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.*
import net.corda.testing.internal.createWireTransaction
@ -129,7 +130,8 @@ class TransactionTests {
id,
null,
timeWindow,
privacySalt
privacySalt,
testNetworkParameters()
)
transaction.verify()

View File

@ -125,7 +125,14 @@ The current set of network parameters:
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners.
Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same.
Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported.
The transaction verification logic will throw an exception when this happens.
Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`.
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.
Network parameters update process

View File

@ -164,7 +164,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage)
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
attachments.servicesForResolution = it
}
@Suppress("LeakingThis")
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
@ -1052,7 +1054,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters)
}

View File

@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
output.writeString(obj.contract)
kryo.writeClassAndObject(output, obj.additionalContracts)
output.writeString(obj.uploader)
kryo.writeClassAndObject(output, obj.signers)
}
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
if (kryo.serializationContext() != null) {
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val context = kryo.serializationContext()!!
val attachmentStorage = context.serviceHub.attachments
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
override val id = attachmentHash
}
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
} else {
val attachment = GeneratedAttachment(input.readBytesWithLength())
val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
return ContractAttachment(attachment, contract, additionalContracts, uploader)
val signers = kryo.readClassAndObject(input) as List<PublicKey>
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
}
}
}

View File

@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream
import net.corda.core.ClientRelevantError
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.sha256
import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
@ -31,6 +34,7 @@ import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.file.Paths
import java.security.PublicKey
import java.time.Instant
import java.util.*
import java.util.jar.JarInputStream
@ -46,6 +50,10 @@ class NodeAttachmentService(
cacheFactory: NamedCacheFactory,
private val database: CordaPersistence
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
// This is to break the circular dependency.
lateinit var servicesForResolution: ServicesForResolution
companion object {
private val log = contextLogger()
@ -95,7 +103,13 @@ class NodeAttachmentService(
@Column(name = "contract_class_name", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null
var contractClassNames: List<ContractClassName>? = null,
@ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER)
@Column(name = "signer", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__signers__attachments"))
var signers: List<PublicKey>? = null
)
@VisibleForTesting
@ -213,11 +227,13 @@ class NodeAttachmentService(
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
return database.transaction {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
?: return@transaction null
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader)
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers
?: emptyList())
} else {
it
}
@ -291,14 +307,19 @@ class NodeAttachmentService(
val id = bytes.sha256()
if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream())
val jarSigners = getSigners(bytes)
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(),
content = bytes,
uploader = uploader,
filename = filename,
contractClassNames = contractClassNames
contractClassNames = contractClassNames,
signers = jarSigners
)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
@ -310,6 +331,9 @@ class NodeAttachmentService(
}
}
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
@Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try {
@ -340,4 +364,4 @@ class NodeAttachmentService(
query.resultList.map { AttachmentId.parse(it.attId) }
}
}
}
}

View File

@ -0,0 +1,18 @@
package net.corda.node.services.persistence
import net.corda.core.crypto.Crypto
import net.corda.core.utilities.hexToByteArray
import net.corda.core.utilities.toHex
import java.security.PublicKey
import javax.persistence.AttributeConverter
import javax.persistence.Converter
/**
* Converts to and from a Public key into a hex encoded string.
* Used by JPA to automatically map a [PublicKey] to a text column
*/
@Converter(autoApply = true)
class PublicKeyToTextConverter : AttributeConverter<PublicKey, String> {
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex()
override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) }
}

View File

@ -14,4 +14,17 @@
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
</changeSet>
<changeSet author="R3.Corda" id="add_signers">
<createTable tableName="node_attachments_signers">
<column name="att_id" type="NVARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="signer" type="NVARCHAR(1024)"/>
</createTable>
<addForeignKeyConstraint baseColumnNames="att_id" baseTableName="node_attachments_signers"
constraintName="FK__signers__attachments"
referencedColumnNames="att_id" referencedTableName="node_attachments"/>
</changeSet>
</databaseChangeLog>

View File

@ -2,6 +2,8 @@ package net.corda.node.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.generateKeyPair
import net.corda.core.node.JavaPackageName
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.core.node.NotaryInfo
import net.corda.core.utilities.days
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -65,7 +68,8 @@ class NetworkParametersTest {
fun `choosing notary not specified in network parameters will fail`() {
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
val notary = NotaryConfig(false)
doReturn(notary).whenever(it).notary}))
doReturn(notary).whenever(it).notary
}))
val fakeNotaryId = fakeNotary.info.singleIdentity()
val alice = mockNet.createPartyNode(ALICE_NAME)
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
@ -87,6 +91,62 @@ class NetworkParametersTest {
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
}
@Test
fun `package ownership checks are correct`() {
val key1 = generateKeyPair().public
val key2 = generateKeyPair().public
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.!example.stuff") to key2
)
)
}.withMessageContaining("Attempting to whitelist illegal java package")
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.example.stuff") to key2
)
)
}.withMessage("multiple packages added to the packageOwnership overlap.")
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.examplestuff") to key2
)
)
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
}
// Helpers
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
NetworkParametersCopier(params).install(dir)

View File

@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.JarSignatureTestUtils.createJar
import net.corda.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder
@ -18,32 +24,39 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.TestingNamedCacheFactory
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.*
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem
import java.nio.file.Path
import java.nio.file.*
import java.security.PublicKey
import java.util.jar.JarEntry
import java.util.jar.JarOutputStream
import javax.tools.JavaFileObject
import javax.tools.SimpleJavaFileObject
import javax.tools.StandardLocation
import javax.tools.ToolProvider
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class NodeAttachmentServiceTest {
// Use an in memory file system for testing attachment storage.
private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private val services = rigorousMock<ServicesForResolution>()
@Before
fun setUp() {
@ -52,18 +65,40 @@ class NodeAttachmentServiceTest {
val dataSourceProperties = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
fs = Jimfs.newFileSystem(Configuration.unix())
doReturn(testNetworkParameters()).whenever(services).networkParameters
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
database.transaction {
it.start()
}
}
storage.servicesForResolution = services
}
@After
fun tearDown() {
dir.list { subdir ->
subdir.forEach(Path::deleteRecursively)
}
database.close()
}
@Test
fun `importing a signed jar saves the signers to the storage`() {
val jarAndSigner = makeTestSignedContractJar("com.example.MyContract")
val signedJar = jarAndSigner.first
val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null)
assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash })
}
@Test
fun `importing a non-signed jar will save no signers`() {
val jarName = makeTestContractJar("com.example.MyContract")
val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null)
assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size)
}
@Test
fun `insert and retrieve`() {
val (testJar, expectedHash) = makeTestJar()
@ -289,7 +324,20 @@ class NodeAttachmentServiceTest {
return Pair(file, file.readAll().sha256())
}
private companion object {
companion object {
private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName)
@BeforeClass
@JvmStatic
fun beforeClass() {
}
@AfterClass
@JvmStatic
fun afterClass() {
dir.deleteRecursively()
}
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
output.use {
val jar = JarOutputStream(it)
@ -305,5 +353,48 @@ class NodeAttachmentServiceTest {
jar.closeEntry()
}
}
private fun makeTestSignedContractJar(contractName: String): Pair<Path, PublicKey> {
val alias = "testAlias"
val pwd = "testPassword"
dir.generateKey(alias, pwd, ALICE_NAME.toString())
val jarName = makeTestContractJar(contractName)
val signer = dir.signJar(jarName, alias, pwd)
return dir.resolve(jarName) to signer
}
private fun makeTestContractJar(contractName: String): String {
val packages = contractName.split(".")
val jarName = "testattachment.jar"
val className = packages.last()
createTestClass(className, packages.subList(0, packages.size - 1))
dir.createJar(jarName, "${contractName.replace(".", "/")}.class")
return jarName
}
private fun createTestClass(className: String, packages: List<String>): Path {
val newClass = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*;
import net.corda.core.transactions.*;
public class $className implements Contract {
@Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
}
}
""".trimIndent()
val compiler = ToolProvider.getSystemJavaCompiler()
val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) {
override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence {
return newClass
}
}
val fileManager = compiler.getStandardFileManager(null, null, null)
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile()))
val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call()
return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name)
}
}
}

View File

@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException
import net.corda.serialization.internal.GeneratedAttachment
import net.corda.serialization.internal.amqp.CustomSerializer
import net.corda.serialization.internal.amqp.SerializerFactory
import java.security.PublicKey
/**
* A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly.
@ -23,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
} catch (e: Exception) {
throw MissingAttachmentsException(listOf(obj.id))
}
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader)
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signers)
}
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers)
}
@KeepForDJVM
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?)
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?, val signers: List<PublicKey>)
}

View File

@ -1,11 +1,13 @@
package net.corda.testing.contracts
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty
/**
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
*/
@BelongsToContract(DummyContract::class)
data class DummyState @JvmOverloads constructor (
/** Some information that the state represents for test purposes. **/
val magicNumber: Int = 0,

View File

@ -7,6 +7,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.readFully
import net.corda.core.node.services.AttachmentId
@ -71,7 +72,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers)
_files[sha256] = Pair(attachment, bytes)
}
return sha256