CORDA-3018 Whitelisting attachments by public key - relax signer restrictions (#5358)

* CORDA-3018 Allow loading an untrusted contract jar if another attachment exists that was signed with the same keys and uploaded by a trusted uploader

`TransactionUtils.isAttachmentTrusted` requirements have been relaxed
to allow an untrusted attachment to be loaded as long as another
attachment exists that is signed by the same keys and was uploaded
by a trusted uploader.

The requirement of containing the same contract classes has been
removed. Therefore the contents of the existing trusted attachment
no longer matters.

* CORDA-3018 Allow a subset/intersection of signers in `isAttachmentTrusted`

Allow a subset/intersection of signers to satisfy the signer
requirements of `isAttachmentTrusted`. This allows an "untrusted"
attachment that is signed by one or more keys to be "trusted" as long
as another trusted attachment already exists that is signed by at least
one of the "untrusted" attachments signers.

A cache of trusted and untrusted public keys is now held (replacing the
previous cache of `List<PublicKey>`.

Tests have been added to `NodeAttachmentServiceTest` to confirm that
an attachment query using an `EQUAL` statement will actually return
attachments that are signed by any of the keys passed into the query.

Confirming this allowed an `EQUAL` query to satisfy the search that
had to be done as part of this change.

`MockAttachmentStorage`'s query criteria was updated to better match
the real `NodeAttachmentService` implementation.

* CORDA-3018 Update cache name and kdoc on `isAttachmentTrusted`

* CORDA-3018 Verify that chains of trust do not occur

* CORDA-3018 Switch keys around to improve chain of trust tests
This commit is contained in:
Dan Newton 2019-08-08 09:33:45 +01:00 committed by Shams Asari
parent 44428b6048
commit fc265ee472
4 changed files with 448 additions and 27 deletions

View File

@ -3,6 +3,7 @@ package net.corda.coretests.transactions
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.declaredField
import net.corda.core.internal.inputStream
@ -199,4 +200,178 @@ class AttachmentsClassLoaderTests {
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return jar.use { storage.importAttachment(jar, uploader, filename) }
}
@Test
fun `Allow loading an untrusted contract jar if another attachment exists that was signed with the same keys and uploaded by a trusted uploader`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone trusted"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"rpc",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
}
@Test
fun `Allow loading an untrusted contract jar if another attachment exists that was signed by a trusted uploader - intersection of keys match existing attachment`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val keyPairC = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone trusted"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"rpc",
it,
signers = listOf(keyPairA.public, keyPairC.public)
)
}
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
}
@Test
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
}
}
@Test
fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairA.public, keyPairB.public)
)
}
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
}
}
@Test
fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() {
val keyPairA = Crypto.generateKeyPair()
val keyPairB = Crypto.generateKeyPair()
val keyPairC = Crypto.generateKeyPair()
val classJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted with the same keys"
).inputStream()
classJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"app",
it,
signers = listOf(keyPairA.public)
)
}
val inheritedTrustClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone who inherits trust"
).inputStream()
val inheritedTrustAttachment = inheritedTrustClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairB.public, keyPairA.public)
)
}
val untrustedClassJar = fakeAttachment(
"/com/example/something/UntrustedClass.class",
"Signed by someone untrusted"
).inputStream()
val untrustedAttachment = untrustedClassJar.use {
storage.importContractAttachment(
listOf("UntrustedClass.class"),
"untrusted",
it,
signers = listOf(keyPairB.public, keyPairC.public)
)
}
make(arrayOf(inheritedTrustAttachment).map { storage.openAttachment(it)!! })
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! })
}
}
}

View File

@ -184,10 +184,9 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
// For now we don't check whether the attached network parameters match the current ones.
}
private data class AttachmentAttributeKey(val signers: List<PublicKey>, val contractClasses: List<ContractClassName>?)
// A cache for caching whether a particular attachment ID is trusted
private val attachmentTrustedCache: MutableMap<AttachmentAttributeKey, Boolean> = createSimpleCache<AttachmentAttributeKey, Boolean>(100).toSynchronised()
// A cache for caching whether a particular set of signers are trusted
private val trustedKeysCache: MutableMap<PublicKey, Boolean> =
createSimpleCache<PublicKey, Boolean>(100).toSynchronised()
/**
* Establishes whether an attachment should be trusted. This logic is required in order to verify transactions, as transaction
@ -195,8 +194,8 @@ private val attachmentTrustedCache: MutableMap<AttachmentAttributeKey, Boolean>
*
* Attachments are trusted if one of the following is true:
* - They are uploaded by a trusted uploader
* - There is another attachment in the attachment store signed by the same keys (and with the same contract classes if the attachment is a
* contract attachment) that is trusted
* - There is another attachment in the attachment store, that is trusted and is signed by at least one key that the input
* attachment is also signed with
*/
fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Boolean {
val trustedByUploader = when (attachment) {
@ -208,22 +207,14 @@ fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Bo
if (trustedByUploader) return true
return if (service != null && attachment.signerKeys.isNotEmpty()) {
val signers = attachment.signerKeys
val contractClasses = if (attachment is ContractAttachment) {
attachment.allContracts.toList()
} else {
null
}
val key = AttachmentAttributeKey(signers, contractClasses)
attachmentTrustedCache.computeIfAbsent(key) {
val contractClassCondition = it.contractClasses?.let { classes -> Builder.equal(classes) }
val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
contractClassNamesCondition = contractClassCondition,
signersCondition = Builder.equal(signers),
attachment.signerKeys.any { signer ->
trustedKeysCache.computeIfAbsent(signer) {
val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(signer)),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)
)
service.queryAttachments(queryCriteria).isNotEmpty()
)
service.queryAttachments(queryCriteria).isNotEmpty()
}
}
} else {
false

View File

@ -14,10 +14,8 @@ import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.AttachmentQueryCriteria.AttachmentsQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder
import net.corda.core.node.services.vault.Sort
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
@ -801,7 +799,7 @@ class NodeAttachmentServiceTest {
}
@Test
fun `jar not trusted if same key but different contract`() {
fun `jar trusted if same key but different contract`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
@ -817,7 +815,114 @@ class NodeAttachmentServiceTest {
// Sanity check.
assertEquals(key1, key2, "Different public keys used to sign jars")
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a subset of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are an intersection of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
val alias3 = "yetAnotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
file.path.generateKey(alias3, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias3, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar trusted if the signing keys are a superset of an existing trusted jar's signers`() {
SelfCleaningDir().use { file ->
val alias = "testAlias"
val password = "testPassword"
val alias2 = "anotherTestAlias"
file.path.generateKey(alias, password)
file.path.generateKey(alias2, password)
val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password)
val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password)
file.path.signJar(jarV2.toAbsolutePath().toString(), alias2, password)
val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted")
}
}
@Test
fun `jar with inherited trust does not grant trust to other jars (no chain of trust)`() {
SelfCleaningDir().use { file ->
val aliasA = "Daredevil"
val aliasB = "The Punisher"
val aliasC = "Jessica Jones"
val password = "i am a netflix series"
file.path.generateKey(aliasA, password)
file.path.generateKey(aliasB, password)
file.path.generateKey(aliasC, password)
val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract")
file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2)
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
val jarSignedByBC = makeTestContractJar(file.path, "foo.bar.AnotherContract", version = 2)
file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password)
file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") }
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentA)!!, storage), "Contract $attachmentA should be trusted")
assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentB)!!, storage), "Contract $attachmentB should inherit trust")
assertFalse(isAttachmentTrusted(storage.openAttachment(attachmentC)!!, storage), "Contract $attachmentC should not be trusted (no chain of trust)")
}
}
@ -940,6 +1045,147 @@ class NodeAttachmentServiceTest {
}
}
@Test
fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() {
SelfCleaningDir().use { file ->
val aliasA = "Luke Skywalker"
val aliasB = "Han Solo"
val aliasC = "Chewbacca"
val aliasD = "Princess Leia"
val password = "may the force be with you"
file.path.generateKey(aliasA, password)
file.path.generateKey(aliasB, password)
file.path.generateKey(aliasC, password)
file.path.generateKey(aliasD, password)
val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract")
val keyA = file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract")
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
val keyB = file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
val jarSignedByABC = makeTestContractJar(file.path, "foo.bar.ExpensiveContract")
file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasA, password)
file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasB, password)
val keyC = file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasC, password)
val jarSignedByABCD = makeTestContractJar(file.path, "foo.bar.DidNotReadThisContract")
file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasA, password)
file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasB, password)
file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasC, password)
val keyD = file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasD, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "app", "B.jar") }
val attachmentC = jarSignedByABC.read { storage.privilegedImportAttachment(it, "app", "C.jar") }
val attachmentD = jarSignedByABCD.read { storage.privilegedImportAttachment(it, "app", "D.jar") }
assertEquals(
listOf(attachmentA, attachmentB, attachmentC, attachmentD),
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyA))
)
)
)
assertEquals(
listOf(attachmentB, attachmentC, attachmentD),
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyB))
)
)
)
assertEquals(
listOf(attachmentC, attachmentD),
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyC))
)
)
)
assertEquals(
listOf(attachmentD),
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyD))
)
)
)
}
}
@Test
fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing multiple public keys`() {
SelfCleaningDir().use { file ->
val aliasA = "Ironman"
val aliasB = "Captain America"
val aliasC = "Blackwidow"
val aliasD = "Thor"
val password = "avengers, assemble!"
file.path.generateKey(aliasA, password)
file.path.generateKey(aliasB, password)
file.path.generateKey(aliasC, password)
file.path.generateKey(aliasD, password)
val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract")
val keyA = file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password)
val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract")
file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password)
val keyB = file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password)
val jarSignedByBC = makeTestContractJar(file.path, "foo.bar.ExpensiveContract")
file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password)
val keyC = file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password)
val jarSignedByCD = makeTestContractJar(file.path, "foo.bar.DidNotReadThisContract")
file.path.signJar(jarSignedByCD.toAbsolutePath().toString(), aliasC, password)
val keyD = file.path.signJar(jarSignedByCD.toAbsolutePath().toString(), aliasD, password)
val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") }
val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "app", "B.jar") }
val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "app", "C.jar") }
val attachmentD = jarSignedByCD.read { storage.privilegedImportAttachment(it, "app", "D.jar") }
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyA, keyC))
)
).let { result ->
assertEquals(4, result.size)
assertEquals(
listOf(attachmentA, attachmentB, attachmentC, attachmentD),
result
)
}
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyA, keyB))
)
).let { result ->
// made a [Set] due to [NodeAttachmentService.queryAttachments] not returning distinct results
assertEquals(3, result.toSet().size)
assertEquals(setOf(attachmentA, attachmentB, attachmentC), result.toSet())
}
storage.queryAttachments(
AttachmentsQueryCriteria(
signersCondition = Builder.equal(listOf(keyB, keyC, keyD))
)
).let { result ->
// made a [Set] due to [NodeAttachmentService.queryAttachments] not returning distinct results
assertEquals(3, result.toSet().size)
assertEquals(setOf(attachmentB, attachmentC, attachmentD), result.toSet())
}
}
}
// Not the real FetchAttachmentsFlow!
private class FetchAttachmentsFlow : FlowLogic<Unit>() {
@Suspendable

View File

@ -47,7 +47,16 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
// and not all predicate types are covered here.
private fun <C> criteriaFilter(metadata: C, predicate: ColumnPredicate<C>?): Boolean {
return when (predicate) {
is ColumnPredicate.EqualityComparison -> predicate.rightLiteral == metadata
// real implementation allows an intersection of signers to return results from the query
is ColumnPredicate.EqualityComparison -> {
val rightLiteral = predicate.rightLiteral
when (rightLiteral) {
is Collection<*> -> rightLiteral.any { value ->
(metadata as Collection<*>).contains(value)
}
else -> rightLiteral == metadata
}
}
is ColumnPredicate.CollectionExpression -> predicate.rightLiteral.contains(metadata)
else -> true
}