CORDA-2375 Ensure node has unique attachment contract classname/version from signed JARs (#4535)

Corda Node ensures a given contract class and version can be sourced from only one signed and trusted Attachment (JAR).
An attempt to import a signed JAR as a trusted uploader (or promote to be trusted) with a class and version already present in the other trusted Attachment will raise DuplicateContractClassException.

Minor fixes to Hibernate Attachment Query parser (original query to select attachment without signers would always return no attachments)
This commit is contained in:
szymonsztuka 2019-01-10 14:13:00 +00:00 committed by GitHub
parent e87a8ed496
commit 9b8fda0d6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 246 additions and 31 deletions

View File

@ -212,7 +212,7 @@ data class Sort(val columns: Collection<SortColumn>) : BaseSort() {
data class AttachmentSort(val columns: Collection<AttachmentSortColumn>) : BaseSort() { data class AttachmentSort(val columns: Collection<AttachmentSortColumn>) : BaseSort() {
enum class AttachmentSortAttribute(val columnName: String) { enum class AttachmentSortAttribute(val columnName: String) {
INSERTION_DATE("insertion_date"), INSERTION_DATE("insertionDate"),
UPLOADER("uploader"), UPLOADER("uploader"),
FILENAME("filename"), FILENAME("filename"),
VERSION ("version") VERSION ("version")

View File

@ -230,6 +230,20 @@ The contract attachment non-downgrade rule is enforced in two locations:
A version number is stored in the manifest information of the enclosing JAR file. This version identifier should be a whole number starting A version number is stored in the manifest information of the enclosing JAR file. This version identifier should be a whole number starting
from 1. This information should be set using the Gradle cordapp plugin, or manually, as described in :doc:`versioning`. from 1. This information should be set using the Gradle cordapp plugin, or manually, as described in :doc:`versioning`.
Uniqueness requirement Contract and Version for Signature Constraint
--------------------------------------------------------------------
CorDapps in Corda 4 may be signed (to use new signature constraints functionality) or unsigned, and versioned.
The following controls are enforced for these different types of jars within the attachment store of a node:
- Signed contract JARs must be uniquely versioned per contract class (or group of).
At runtime the node will throw a `DuplicateContractClassException`` exception if this condition is violated.
- Unsigned contract JARs: there may exist multiple instances of the same contract jar.
At run-time the node will warn of duplicates encountered.
The most recent version given by insertionDate into the attachment storage will be used upon transaction building/resolution.
Issues when using the HashAttachmentConstraint Issues when using the HashAttachmentConstraint
---------------------------------------------- ----------------------------------------------

View File

@ -11,6 +11,12 @@ import net.corda.core.serialization.CordaSerializable
*/ */
class DuplicateAttachmentException(attachmentHash: String) : java.nio.file.FileAlreadyExistsException(attachmentHash), ClientRelevantError class DuplicateAttachmentException(attachmentHash: String) : java.nio.file.FileAlreadyExistsException(attachmentHash), ClientRelevantError
/**
* Thrown to indicate that a contract class name of the same version was already uploaded to a Corda node.
*/
class DuplicateContractClassException(contractClassName: String, version: Int, attachmentHashes: List<String>) :
Exception("Contract $contractClassName version '$version' already present in the attachments $attachmentHashes"), ClientRelevantError
/** /**
* Thrown to indicate that a flow was not designed for RPC and should be started from an RPC client. * Thrown to indicate that a flow was not designed for RPC and should be started from an RPC client.
*/ */

View File

@ -80,18 +80,24 @@ class FlowCheckpointVersionNodeStartupCheckTest {
@Test @Test
fun `restart node with incompatible version of suspended flow due to different jar hash`() { fun `restart node with incompatible version of suspended flow due to different jar hash`() {
driver(parametersForRestartingNodes()) { driver(parametersForRestartingNodes()) {
val uniqueName = "different-jar-hash-test-${UUID.randomUUID()}" val uniqueWorkflowJarName = "different-jar-hash-test-${UUID.randomUUID()}"
val cordapp = defaultCordapp.copy(name = uniqueName) val uniqueContractJarName = "contract-$uniqueWorkflowJarName"
val defaultWorkflowJar = cordappWithPackages(SendMessageFlow::class.packageName)
val defaultContractJar = cordappWithPackages(MessageState::class.packageName)
val contractJar = defaultContractJar.copy(name = uniqueContractJarName)
val workflowJar = defaultWorkflowJar.copy(name = uniqueWorkflowJarName)
val bobBaseDir = createSuspendedFlowInBob(setOf(cordapp)) val bobBaseDir = createSuspendedFlowInBob(setOf(workflowJar, contractJar))
val cordappsDir = bobBaseDir / "cordapps" val cordappsDir = bobBaseDir / "cordapps"
val cordappJar = cordappsDir.list().single { it.toString().endsWith(".jar") } val cordappJar = cordappsDir.list().single {
! it.toString().contains(uniqueContractJarName) && it.toString().endsWith(".jar")
}
// Make sure we're dealing with right jar // Make sure we're dealing with right jar
assertThat(cordappJar.fileName.toString()).contains(uniqueName) assertThat(cordappJar.fileName.toString()).contains(uniqueWorkflowJarName)
// The name is part of the MANIFEST so changing it is sufficient to change the jar hash // The name is part of the MANIFEST so changing it is sufficient to change the jar hash
val modifiedCordapp = cordapp.copy(name = "${cordapp.name}-modified") val modifiedCordapp = workflowJar.copy(name = "${workflowJar.name}-modified")
val modifiedCordappJar = CustomCordapp.getJarFile(modifiedCordapp) val modifiedCordappJar = CustomCordapp.getJarFile(modifiedCordapp)
modifiedCordappJar.moveTo(cordappJar, REPLACE_EXISTING) modifiedCordappJar.moveTo(cordappJar, REPLACE_EXISTING)
@ -125,8 +131,8 @@ class FlowCheckpointVersionNodeStartupCheckTest {
)).getOrThrow() )).getOrThrow()
} }
val logDir = baseDirectory(BOB_NAME) / NodeStartup.LOGS_DIRECTORY_NAME val logDir = baseDirectory(BOB_NAME)
val logFile = logDir.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() } val logFile = logDir.list { it.filter { it.fileName.toString().endsWith("out.log") }.findAny().get() }
val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() } val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() }
assertEquals(1, matchingLineCount) assertEquals(1, matchingLineCount)
} }

View File

@ -29,6 +29,7 @@ import net.corda.node.utilities.InfrequentlyMutatedCache
import net.corda.node.utilities.NonInvalidatingCache import net.corda.node.utilities.NonInvalidatingCache
import net.corda.node.utilities.NonInvalidatingWeightBasedCache import net.corda.node.utilities.NonInvalidatingWeightBasedCache
import net.corda.nodeapi.exceptions.DuplicateAttachmentException import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import net.corda.nodeapi.exceptions.DuplicateContractClassException
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.nodeapi.internal.persistence.currentDBSession
@ -305,6 +306,22 @@ class NodeAttachmentService(
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
} }
private fun verifyVersionUniquenessForSignedAttachments(contractClassNames: List<ContractClassName>, contractVersion: Int, signers: List<PublicKey>?){
if (signers != null && signers.isNotEmpty()) {
contractClassNames.forEach {
val existingContractsImplementations = queryAttachments(AttachmentQueryCriteria.AttachmentsQueryCriteria(
contractClassNamesCondition = Builder.equal(listOf(it)),
versionCondition = Builder.equal(contractVersion),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS),
isSignedCondition = Builder.equal(true))
)
if (existingContractsImplementations.isNotEmpty()) {
throw DuplicateContractClassException(it, contractVersion, existingContractsImplementations.map { it.toString() })
}
}
}
}
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks. // TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId { private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
return database.transaction { return database.transaction {
@ -324,6 +341,9 @@ class NodeAttachmentService(
val jarSigners = getSigners(bytes) val jarSigners = getSigners(bytes)
val contractVersion = getVersion(bytes) val contractVersion = getVersion(bytes)
val session = currentDBSession() val session = currentDBSession()
verifyVersionUniquenessForSignedAttachments(contractClassNames, contractVersion, jarSigners)
val attachment = NodeAttachmentService.DBAttachment( val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(), attId = id.toString(),
content = bytes, content = bytes,
@ -344,6 +364,7 @@ class NodeAttachmentService(
val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
// update the `uploader` field (as the existing attachment may have been resolved from a peer) // update the `uploader` field (as the existing attachment may have been resolved from a peer)
if (attachment.uploader != uploader) { if (attachment.uploader != uploader) {
verifyVersionUniquenessForSignedAttachments(contractClassNames, attachment.version, attachment.signers)
attachment.uploader = uploader attachment.uploader = uploader
session.saveOrUpdate(attachment) session.saveOrUpdate(attachment)
log.info("Updated attachment $id with uploader $uploader") log.info("Updated attachment $id with uploader $uploader")
@ -430,7 +451,8 @@ class NodeAttachmentService(
private fun getContractAttachmentVersions(contractClassName: String): NavigableMap<Version, AttachmentIds> = contractsCache.get(contractClassName) { name -> private fun getContractAttachmentVersions(contractClassName: String): NavigableMap<Version, AttachmentIds> = contractsCache.get(contractClassName) { name ->
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)), val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)),
versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)) versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS))
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC),
AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.INSERTION_DATE, Sort.Direction.DESC)))
database.transaction { database.transaction {
val session = currentDBSession() val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder val criteriaBuilder = session.criteriaBuilder
@ -447,15 +469,17 @@ class NodeAttachmentService(
val query = session.createQuery(criteriaQuery) val query = session.createQuery(criteriaQuery)
// execution // execution
TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it) }.toMap()) TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it, name) }.toMap())
} }
} }
private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>): Pair<Version, AttachmentIds> { private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>, contractClassName: String): Pair<Version, AttachmentIds> {
check(it.value.size <= 2) val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }.singleOrNull() check (signed.size <= 1) //sanity check
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }.singleOrNull() val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }
return it.key to AttachmentIds(signed, unsigned) if (unsigned.size > 1) //TODO cater better for whiltelisted JARs - CORDA-2405
log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
return it.key to AttachmentIds(signed.singleOrNull(), unsigned.firstOrNull())
} }
override fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? { override fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? {

View File

@ -231,11 +231,12 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: Crite
} }
criteria.isSignedCondition?.let { isSigned -> criteria.isSignedCondition?.let { isSigned ->
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers") if (isSigned == Builder.equal(true)) {
if (isSigned == Builder.equal(true)) val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNotNull)) predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNotNull))
else } else {
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNull)) predicateSet.add(criteriaBuilder.equal(criteriaBuilder.size(root.get<List<PublicKey>?>("signers")),0))
}
} }
criteria.versionCondition?.let { criteria.versionCondition?.let {

View File

@ -18,6 +18,7 @@ import net.corda.core.node.services.vault.Sort
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.exceptions.DuplicateAttachmentException import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import net.corda.nodeapi.exceptions.DuplicateContractClassException
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
@ -33,6 +34,7 @@ import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.* import org.assertj.core.api.Assertions.*
import org.junit.After import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -268,10 +270,18 @@ class NodeAttachmentServiceTest {
storage.queryAttachments(AttachmentsQueryCriteria(signersCondition = Builder.equal(listOf(publicKey)))).size storage.queryAttachments(AttachmentsQueryCriteria(signersCondition = Builder.equal(listOf(publicKey)))).size
) )
assertEquals( val allAttachments = storage.queryAttachments(AttachmentsQueryCriteria())
3, assertEquals(6, allAttachments.size)
storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true))).size
) val signedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true)))
assertEquals(3, signedAttachments.size)
val unsignedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(false)))
assertEquals(3, unsignedAttachments.size)
assertNotEquals(signedAttachments.toSet(), unsignedAttachments.toSet())
assertEquals(signedAttachments.toSet() + unsignedAttachments.toSet(), allAttachments.toSet())
assertEquals( assertEquals(
1, 1,
@ -315,6 +325,138 @@ class NodeAttachmentServiceTest {
} }
} }
@Test
fun `cannot import jar with duplicated contract class, version and signers for trusted uploader`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
}
}
}
@Test
fun `can import jar with duplicated contract class, version and signers - when one uploader is trusted and other isnt`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val anotherAttachmentId = anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
assertNotEquals(attachmentId, anotherAttachmentId)
}
}
@Test
fun `can promote to trusted uploader for the same attachment`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val reimportedAttachmentId = contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertEquals(attachmentId, reimportedAttachmentId)
}
}
@Test
fun `cannot promote to trusted uploader if other trusted attachment already has duplicated contract class, version and signers`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
}
}
}
@Test
fun `cannot promote to trusted uploder the same jar if other trusted uplodaer `() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
}
}
}
@Test
fun `can import duplicated contract class and signers if versions differ`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract", 2)
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderA", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
assertTrue("com.example.MyContract" in (storage.openAttachment(it) as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version from unsiged attachment if a signed attachment already exists`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version from siged attachment if an unsigned attachment already exists`() {
SelfCleaningDir().use { file ->
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version for unsigned attachments`() {
SelfCleaningDir().use { file ->
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test @Test
fun `sorting and compound conditions work`() { fun `sorting and compound conditions work`() {
val (jarA, hashA) = makeTestJar(listOf(Pair("a", "a"))) val (jarA, hashA) = makeTestJar(listOf(Pair("a", "a")))

View File

@ -44,15 +44,20 @@ object ContractJarTestUtils {
} }
} }
@JvmOverloads private fun Path.signWithDummyKey(jarName: Path): PublicKey{
fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1): Pair<Path, PublicKey> {
val alias = "testAlias" val alias = "testAlias"
val pwd = "testPassword" val pwd = "testPassword"
workingDir.generateKey(alias, pwd, ALICE_NAME.toString()) this.generateKey(alias, pwd, ALICE_NAME.toString())
val signer = this.signJar(jarName.toAbsolutePath().toString(), alias, pwd)
(this / "_shredder").delete()
(this / "_teststore").delete()
return signer
}
@JvmOverloads
fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1): Pair<Path, PublicKey> {
val jarName = makeTestContractJar(workingDir, contractName, true, version) val jarName = makeTestContractJar(workingDir, contractName, true, version)
val signer = workingDir.signJar(jarName.toAbsolutePath().toString(), alias, pwd) val signer = workingDir.signWithDummyKey(jarName)
(workingDir / "_shredder").delete()
(workingDir / "_teststore").delete()
return workingDir.resolve(jarName) to signer return workingDir.resolve(jarName) to signer
} }
@ -67,6 +72,23 @@ object ContractJarTestUtils {
return workingDir.resolve(jarName) return workingDir.resolve(jarName)
} }
@JvmOverloads
fun makeTestContractJar(workingDir: Path, contractNames: List<String>, signed: Boolean = false, version: Int = 1, generateManifest: Boolean = true, jarFileName : String? = null): Path {
contractNames.forEach {
val packages = it.split(".")
val className = packages.last()
createTestClass(workingDir, className, packages.subList(0, packages.size - 1))
}
val packages = contractNames.first().split(".")
val jarName = jarFileName ?: "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar"
workingDir.createJar(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray() )
if (generateManifest)
workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString()))
if (signed)
workingDir.signWithDummyKey(workingDir.resolve(jarName))
return workingDir.resolve(jarName)
}
private fun createTestClass(workingDir: Path, className: String, packages: List<String>): Path { private fun createTestClass(workingDir: Path, className: String, packages: List<String>): Path {
val newClass = """package ${packages.joinToString(".")}; val newClass = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*; import net.corda.core.contracts.*;

View File

@ -84,7 +84,7 @@ object JarSignatureTestUtils {
manifest.mainAttributes[attributeName] = value manifest.mainAttributes[attributeName] = value
} }
val output = JarOutputStream(FileOutputStream((this / fileName).toFile()), manifest) val output = JarOutputStream(FileOutputStream((this / fileName).toFile()), manifest)
var entry= input.nextEntry var entry = input.nextEntry
val buffer = ByteArray(1 shl 14) val buffer = ByteArray(1 shl 14)
while (true) { while (true) {
output.putNextEntry(entry) output.putNextEntry(entry)