From aeeb08f79944f1cf87dd484ae991da53172bdf94 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Tue, 17 Dec 2024 12:59:01 +0100 Subject: [PATCH] COSE: Ensure verification of signed structures --- signum | 2 +- .../wallet/lib/oidvci/CredentialIssuer.kt | 2 +- .../asitplus/wallet/lib/cbor/CoseService.kt | 19 +++++++++------- .../wallet/lib/cbor/CoseServiceTest.kt | 20 ++++++++++++----- .../cbor/DeviceSignedItemSerializationTest.kt | 19 +++++++++++++--- .../cbor/IssuerSignedItemSerializationTest.kt | 11 ++++++++-- .../wallet/lib/iso/Tag24SerializationTest.kt | 22 +++++++++---------- .../wallet/lib/cbor/CoseServiceJvmTest.kt | 4 ++-- 8 files changed, 66 insertions(+), 33 deletions(-) diff --git a/signum b/signum index 57d29861..9af03ffa 160000 --- a/signum +++ b/signum @@ -1 +1 @@ -Subproject commit 57d2986198e68e75d1b3e08e3538609c84903f71 +Subproject commit 9af03ffa8434b67bc31c6cdf6a87eae9c4af3273 diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt index 203e690c..dc0b16cd 100644 --- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt +++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt @@ -228,7 +228,7 @@ class CredentialIssuer( if (cwt.nonce == null || !authorizationService.verifyClientNonce(cwt.nonce!!.decodeToString())) throw OAuth2Exception(Errors.INVALID_PROOF) .also { Napier.w("client did provide invalid nonce in CWT in proof: ${cwt.nonce}") } - val header = coseSigned.protectedHeader.value + val header = coseSigned.protectedHeader if (header.contentType != PROOF_CWT_TYPE) throw OAuth2Exception(Errors.INVALID_PROOF) .also { Napier.w("client did provide invalid header type in CWT in proof: $header") } diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt index 2300f500..aa010b3d 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt @@ -64,13 +64,13 @@ class DefaultCoseService(private val cryptoService: CryptoService) : CoseService addCertificate: Boolean, ): KmmResult> = catching { protectedHeader.withAlgorithmAndKeyId(addKeyId).let { coseHeader -> - calcSignature(coseHeader, payload, serializer).let { (rawPayload, signature) -> - CoseSigned

( + calcSignature(coseHeader, payload, serializer).let { signature -> + CoseSigned.create( protectedHeader = coseHeader, unprotectedHeader = unprotectedHeader.withCertificateIfExists(addCertificate), payload = payload, signature = signature, - rawPayload = rawPayload + payloadSerializer = serializer, ) } } @@ -108,13 +108,16 @@ class DefaultCoseService(private val cryptoService: CryptoService) : CoseService protectedHeader: CoseHeader, payload: P?, serializer: KSerializer

, - ): Pair = - CoseSigned.prepareCoseSignatureInput

(protectedHeader, payload, serializer).let { signatureInput -> + ): CryptoSignature.RawByteEncodable = + CoseSigned.prepare

( + protectedHeader = protectedHeader, + externalAad = byteArrayOf(), + payload = payload, + payloadSerializer = serializer + ).let { signatureInput -> cryptoService.sign(signatureInput.serialize()).asKmmResult().getOrElse { Napier.w("No signature from native code", it) throw it - }.let { signature -> - signatureInput.payload to signature } } } @@ -132,7 +135,7 @@ class DefaultVerifierCoseService( externalAad: ByteArray, ) = catching { val signatureInput = coseSigned.prepareCoseSignatureInput(externalAad = externalAad) - val algorithm = coseSigned.protectedHeader.value.algorithm + val algorithm = coseSigned.protectedHeader.algorithm ?: throw IllegalArgumentException("Algorithm not specified") val publicKey = signer.toCryptoPublicKey().getOrElse { ex -> throw IllegalArgumentException("Signer not convertible") diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt index 0cecf0b6..7f40e8db 100644 --- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt +++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt @@ -5,9 +5,12 @@ import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.DefaultCryptoService import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert import at.asitplus.wallet.lib.iso.* +import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.builtins.ByteArraySerializer @@ -28,7 +31,8 @@ class CoseServiceTest : FreeSpec({ cryptoService = DefaultCryptoService(keyMaterial) coseService = DefaultCoseService(cryptoService) verifierCoseService = DefaultVerifierCoseService() - randomPayload = Random.nextBytes(32) + // Prevent COSE-special bytes at the start of the payload + randomPayload = "This is the content: ".encodeToByteArray() + Random.nextBytes(32) coseKey = keyMaterial.publicKey.toCoseKey().getOrThrow() } @@ -43,8 +47,14 @@ class CoseServiceTest : FreeSpec({ signed.payload shouldBe randomPayload signed.signature.shouldNotBeNull() - val parsed = CoseSigned.deserialize(parameterSerializer, signed.serialize(parameterSerializer)).getOrThrow() - parsed shouldBe signed + val serialized = signed.serialize(parameterSerializer) + val parsed = CoseSigned.deserialize(parameterSerializer, serialized).getOrThrow() + withClue( + "signed.payload ${signed.wireFormat.payload?.encodeToString(Base16())} " + + "vs parsed.payload: ${parsed.payload?.encodeToString(Base16())}" + ) { + parsed shouldBe signed + } val result = verifierCoseService.verifyCose(parsed, coseKey) result.isSuccess shouldBe true @@ -76,7 +86,7 @@ class CoseServiceTest : FreeSpec({ signed.payload shouldBe mso signed.signature.shouldNotBeNull() - val parsed = CoseSigned.deserialize(parameterSerializer,signed.serialize(parameterSerializer)).getOrThrow() + val parsed = CoseSigned.deserialize(parameterSerializer, signed.serialize(parameterSerializer)).getOrThrow() parsed shouldBe signed val result = verifierCoseService.verifyCose(parsed, coseKey) @@ -94,7 +104,7 @@ class CoseServiceTest : FreeSpec({ signed.payload shouldBe null signed.signature.shouldNotBeNull() - val parsed = CoseSigned.deserialize(parameterSerializer,signed.serialize(parameterSerializer)).getOrThrow() + val parsed = CoseSigned.deserialize(parameterSerializer, signed.serialize(parameterSerializer)).getOrThrow() parsed shouldBe signed val result = verifierCoseService.verifyCose(parsed, coseKey) diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/DeviceSignedItemSerializationTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/DeviceSignedItemSerializationTest.kt index 2decf99a..03c05605 100644 --- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/DeviceSignedItemSerializationTest.kt +++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/DeviceSignedItemSerializationTest.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.cbor import at.asitplus.signum.indispensable.CryptoSignature +import at.asitplus.signum.indispensable.cosef.CoseAlgorithm import at.asitplus.signum.indispensable.cosef.CoseHeader import at.asitplus.signum.indispensable.cosef.CoseSigned import at.asitplus.wallet.lib.iso.* @@ -47,9 +48,21 @@ class DeviceSignedItemSerializationTest : FreeSpec({ key = elementId, value = Random.nextBytes(32), ) - val protectedHeader = CoseHeader() - val issuerAuth = CoseSigned(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()), null) - val deviceAuth = CoseSigned(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()), null) + val protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256) + val issuerAuth = CoseSigned.create( + protectedHeader, + null, + null, + CryptoSignature.RSAorHMAC(byteArrayOf()), + MobileSecurityObject.serializer() + ) + val deviceAuth = CoseSigned.create( + protectedHeader, + null, + null, + CryptoSignature.RSAorHMAC(byteArrayOf()), + ByteArraySerializer() + ) val doc = Document( docType = uuid4().toString(), diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/IssuerSignedItemSerializationTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/IssuerSignedItemSerializationTest.kt index 357b381f..e4f05f8d 100644 --- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/IssuerSignedItemSerializationTest.kt +++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/IssuerSignedItemSerializationTest.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.cbor import at.asitplus.signum.indispensable.CryptoSignature +import at.asitplus.signum.indispensable.cosef.CoseAlgorithm import at.asitplus.signum.indispensable.cosef.CoseHeader import at.asitplus.signum.indispensable.cosef.CoseSigned import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper @@ -46,8 +47,14 @@ class IssuerSignedItemSerializationTest : FreeSpec({ elementIdentifier = elementId, elementValue = Random.nextBytes(32), ) - val protectedHeader = CoseHeader() - val issuerAuth = CoseSigned(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()), null) + val protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256) + val issuerAuth = CoseSigned.create( + protectedHeader, + null, + null, + CryptoSignature.RSAorHMAC(byteArrayOf()), + MobileSecurityObject.serializer() + ) val doc = Document( docType = uuid4().toString(), issuerSigned = IssuerSigned.fromIssuerSignedItems( diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/Tag24SerializationTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/Tag24SerializationTest.kt index e1d92b8b..557db756 100644 --- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/Tag24SerializationTest.kt +++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/Tag24SerializationTest.kt @@ -22,6 +22,7 @@ import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.encodeToByteArray import kotlin.random.Random @@ -48,14 +49,13 @@ class Tag24SerializationTest : FreeSpec({ ) ), deviceAuth = DeviceAuth( - deviceSignature = CoseSigned( - protectedHeader = CoseHeader(), + deviceSignature = CoseSigned.create( + protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, - payload = byteArrayOf(), + payload = null, signature = CryptoSignature.RSAorHMAC(byteArrayOf()), - rawPayload = byteArrayOf() + payloadSerializer = ByteArraySerializer() ) - ) ) @@ -121,12 +121,12 @@ class Tag24SerializationTest : FreeSpec({ validityInfo = ValidityInfo(Clock.System.now(), Clock.System.now(), Clock.System.now()) ) val serializedMso = mso.serializeForIssuerAuth() - val input = CoseSigned( - protectedHeader = CoseHeader(), + val input = CoseSigned.create( + protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = mso, signature = CryptoSignature.RSAorHMAC(byteArrayOf()), - rawPayload = serializedMso, + payloadSerializer = MobileSecurityObject.serializer(), ) val serialized = vckCborSerializer.encodeToByteArray(input) @@ -172,12 +172,12 @@ private fun MobileSecurityObject.Companion.deserializeFromIssuerAuth(it: ByteArr private fun deviceKeyInfo() = DeviceKeyInfo(CoseKey(CoseKeyType.EC2, keyParams = CoseKeyParams.EcYBoolParams(CoseEllipticCurve.P256))) -private fun issuerAuth() = CoseSigned( - protectedHeader = CoseHeader(), +private fun issuerAuth() = CoseSigned.create( + protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = null, signature = CryptoSignature.RSAorHMAC(byteArrayOf()), - rawPayload = null, + payloadSerializer = MobileSecurityObject.serializer(), ) private fun issuerSignedItem() = IssuerSignedItem(0u, Random.nextBytes(16), "identifier", "value") diff --git a/vck/src/jvmTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceJvmTest.kt b/vck/src/jvmTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceJvmTest.kt index 7ba757c4..00baa56e 100644 --- a/vck/src/jvmTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceJvmTest.kt +++ b/vck/src/jvmTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceJvmTest.kt @@ -2,12 +2,12 @@ package at.asitplus.wallet.lib.cbor import at.asitplus.signum.indispensable.* import at.asitplus.signum.indispensable.cosef.* -import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper import at.asitplus.signum.supreme.HazardousMaterials import at.asitplus.signum.supreme.hazmat.jcaPrivateKey import at.asitplus.signum.supreme.sign.EphemeralKey import at.asitplus.wallet.lib.agent.DefaultCryptoService import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert +import at.asitplus.wallet.lib.iso.vckCborSerializer import com.authlete.cbor.CBORByteArray import com.authlete.cbor.CBORDecoder import com.authlete.cbor.CBORTaggedItem @@ -165,7 +165,7 @@ class CoseServiceJvmTest : FreeSpec({ SigStructureBuilder().sign1(parsedCoseSign1).build().encode().encodeToString(Base16()) val signatureInput = CoseSignatureInput( contextString = "Signature1", - protectedHeader = ByteStringWrapper(CoseHeader(algorithm = coseAlgorithm)), + protectedHeader = vckCborSerializer.encodeToByteArray(CoseHeader.serializer(), CoseHeader(algorithm = coseAlgorithm)), externalAad = byteArrayOf(), payload = randomPayload.encodeToByteArray(), ).serialize().encodeToString(Base16())