From a9b807d33c96362435f90e4344946d09d46bb404 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Tue, 18 Jul 2023 11:34:04 +0200 Subject: [PATCH] Implement simple COSE signing and verification --- .../wallet/lib/agent/CryptoService.kt | 13 +++ .../asitplus/wallet/lib/cbor/CoseAlgorithm.kt | 6 ++ .../wallet/lib/cbor/CoseEllipticCurve.kt | 31 +++++-- .../at/asitplus/wallet/lib/cbor/CoseKey.kt | 52 +++++++++++ .../asitplus/wallet/lib/cbor/CoseService.kt | 89 +++++++++++++++++++ .../at/asitplus/wallet/lib/cbor/CoseSigned.kt | 45 ++++++++++ .../wallet/lib/jws/MultibaseHelper.kt | 14 +++ .../wallet/lib/cbor/CoseServiceTest.kt | 44 +++++++++ .../wallet/lib/agent/DefaultCryptoService.kt | 39 ++++++++ .../wallet/lib/agent/DefaultCryptoService.kt | 80 +++++++++++++++-- 10 files changed, 399 insertions(+), 14 deletions(-) create mode 100644 vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt create mode 100644 vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index 060cd734a..077ecf1f5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -1,6 +1,8 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.cbor.CoseAlgorithm +import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm @@ -45,8 +47,12 @@ interface CryptoService { val jwsAlgorithm: JwsAlgorithm + val coseAlgorithm: CoseAlgorithm + fun toJsonWebKey(): JsonWebKey + fun toCoseKey(): CoseKey + } interface VerifierCryptoService { @@ -58,6 +64,13 @@ interface VerifierCryptoService { publicKey: JsonWebKey ): KmmResult + fun verify( + input: ByteArray, + signature: ByteArray, + algorithm: CoseAlgorithm, + publicKey: CoseKey + ): KmmResult + } expect object CryptoUtils { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt index b745074cc..819c54942 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt @@ -15,6 +15,12 @@ enum class CoseAlgorithm(val value: Int) { ES384(-35), ES512(-36); + val signatureValueLength + get() = when (this) { + ES256 -> 256 / 8 + ES384 -> 384 / 8 + ES512 -> 512 / 8 + } } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt index a9fea9342..b495e0130 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt @@ -13,11 +13,32 @@ enum class CoseEllipticCurve(val value: Int) { P256(1), P384(2), - P521(3), - X25519(4), - X448(5), - Ed25519(6), - Ed448(7); + P521(3); + //X25519(4), + //X448(5), + //Ed25519(6), + //Ed448(7); + + val keyLengthBits + get() = when (this) { + P256 -> 256 + P384 -> 384 + P521 -> 521 + } + + val coordinateLengthBytes + get() = when (this) { + P256 -> 256 / 8 + P384 -> 384 / 8 + P521 -> 521 / 8 + } + + val signatureLengthBytes + get() = when (this) { + P256 -> 256 / 8 + P384 -> 384 / 8 + P521 -> 521 / 8 + } } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt index c978a7df0..de40478b2 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt @@ -1,6 +1,11 @@ package at.asitplus.wallet.lib.cbor +import at.asitplus.KmmResult import at.asitplus.wallet.lib.iso.cborSerializer +import at.asitplus.wallet.lib.jws.EcCurve +import at.asitplus.wallet.lib.jws.JsonWebKey +import at.asitplus.wallet.lib.jws.JwkType +import at.asitplus.wallet.lib.jws.MultibaseHelper import io.github.aakira.napier.Napier import io.matthewnelson.component.base64.encodeBase64 import kotlinx.serialization.ExperimentalSerializationApi @@ -46,6 +51,12 @@ data class CoseKey( ) { fun serialize() = cborSerializer.encodeToByteArray(this) + fun toAnsiX963ByteArray(): KmmResult { + if (x != null && y != null) + return KmmResult.success(byteArrayOf(0x04.toByte()) + x + y); + return KmmResult.failure(IllegalArgumentException()) + } + companion object { fun deserialize(it: ByteArray) = kotlin.runCatching { @@ -55,6 +66,47 @@ data class CoseKey( null } + fun fromAnsiX963Bytes(type: CoseKeyType, curve: CoseEllipticCurve, it: ByteArray): CoseKey? { + if (type != CoseKeyType.EC2 || curve != CoseEllipticCurve.P256) { + return null + } + if (it.size != 1 + 32 + 32 || it[0] != 0x04.toByte()) { + return null + } + val xCoordinate = it.sliceArray(1 until 33) + val yCoordinate = it.sliceArray(33 until 65) + val keyId = MultibaseHelper.calcKeyId(curve, xCoordinate, yCoordinate) + ?: return null + return CoseKey( + type = type, + keyId = keyId.encodeToByteArray(), + algorithm = CoseAlgorithm.ES256, + curve = curve, + x = xCoordinate, + y = yCoordinate, + ) + } + + fun fromCoordinates( + type: CoseKeyType, + curve: CoseEllipticCurve, + x: ByteArray, + y: ByteArray + ): CoseKey? { + if (type != CoseKeyType.EC2 || curve != CoseEllipticCurve.P256) { + return null + } + val keyId = MultibaseHelper.calcKeyId(curve, x, y) + ?: return null + return CoseKey( + type = type, + keyId = keyId.encodeToByteArray(), + algorithm = CoseAlgorithm.ES256, + curve = curve, + x = x, + y = y + ) + } } override fun equals(other: Any?): Boolean { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt new file mode 100644 index 000000000..a58dc7d5d --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt @@ -0,0 +1,89 @@ +package at.asitplus.wallet.lib.cbor + +import at.asitplus.KmmResult +import at.asitplus.wallet.lib.agent.CryptoService +import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService +import at.asitplus.wallet.lib.agent.VerifierCryptoService +import at.asitplus.wallet.lib.jws.JwsExtensions.extractSignatureValues +import io.github.aakira.napier.Napier +import kotlinx.serialization.cbor.ByteStringWrapper + +/** + * Creates and parses COSE objects. + */ +interface CoseService { + + /** + * Appends correct values for [CoseHeader.kid], [CoseHeader.algorithm], + * if the corresponding options are set + */ + suspend fun createSignedCose( + protectedHeader: CoseHeader, + unprotectedHeader: CoseHeader, + payload: ByteArray, + addKeyId: Boolean = true, + ): KmmResult +} + +interface VerifierCoseService { + + fun verifyCose(coseSigned: CoseSigned, signer: CoseKey): KmmResult + +} + +class DefaultCoseService(private val cryptoService: CryptoService) : CoseService { + + override suspend fun createSignedCose( + protectedHeader: CoseHeader, + unprotectedHeader: CoseHeader, + payload: ByteArray, + addKeyId: Boolean, + ): KmmResult { + var copy = protectedHeader.copy(algorithm = cryptoService.coseAlgorithm) + if (addKeyId) + copy = copy.copy(kid = cryptoService.identifier) + + val signatureInput = CoseSignatureInput( + contextString = "Signature1", + protectedHeader = ByteStringWrapper(copy), + payload = payload, + ).serialize() + + val signature = cryptoService.sign(signatureInput).getOrElse { + Napier.w("No signature from native code", it) + return KmmResult.failure(it) + } + val rawSignature = signature.extractSignatureValues(cryptoService.coseAlgorithm.signatureValueLength) + return KmmResult.success( + CoseSigned(ByteStringWrapper(copy), unprotectedHeader, payload, rawSignature) + ) + } +} + +class DefaultVerifierCoseService( + private val cryptoService: VerifierCryptoService = DefaultVerifierCryptoService() +) : VerifierCoseService { + + /** + * Verifiers the signature of [coseSigned] by using [signer]. + */ + override fun verifyCose(coseSigned: CoseSigned, signer: CoseKey): KmmResult { + val signatureInput = CoseSignatureInput( + contextString = "Signature1", + protectedHeader = ByteStringWrapper(coseSigned.protectedHeader.value), + payload = coseSigned.payload, + ).serialize() + + val algorithm = coseSigned.protectedHeader.value.algorithm + ?: return KmmResult.failure(IllegalArgumentException("Algorithm not specified")) + val verified = cryptoService.verify(signatureInput, coseSigned.signature, algorithm, signer) + val result = verified.getOrElse { + Napier.w("No verification from native code", it) + return KmmResult.failure(it) + } + return KmmResult.success(result) + } +} + + + diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt index ded40947e..88fefd6bd 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt @@ -67,6 +67,51 @@ data class CoseSigned( } } +@OptIn(ExperimentalSerializationApi::class) +@Serializable +@CborArray +data class CoseSignatureInput( + val contextString: String = "Signature1", + @Serializable(with = ByteStringWrapperSerializer::class) + @ByteString + val protectedHeader: ByteStringWrapper, + @ByteString + val externalAad: ByteArray = byteArrayOf(), + @ByteString + val payload: ByteArray, +){ + fun serialize() = cborSerializer.encodeToByteArray(this) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as CoseSignatureInput + + if (contextString != other.contextString) return false + if (protectedHeader != other.protectedHeader) return false + if (!externalAad.contentEquals(other.externalAad)) return false + return payload.contentEquals(other.payload) + } + + override fun hashCode(): Int { + var result = contextString.hashCode() + result = 31 * result + protectedHeader.hashCode() + result = 31 * result + externalAad.contentHashCode() + result = 31 * result + payload.contentHashCode() + return result + } + + companion object { + fun deserialize(it: ByteArray) = kotlin.runCatching { + cborSerializer.decodeFromByteArray(it) + }.getOrElse { + Napier.w("deserialize failed", it) + null + } + } +} + object ByteStringWrapperSerializer : KSerializer> { override val descriptor: SerialDescriptor = diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt index 574d36b96..0f3d6b845 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.jws +import at.asitplus.wallet.lib.cbor.CoseEllipticCurve import io.matthewnelson.component.base64.decodeBase64ToArray import io.matthewnelson.component.base64.encodeBase64 @@ -21,6 +22,19 @@ object MultibaseHelper { return "$PREFIX_DID_KEY:${multibaseWrapBase64(multicodecWrapP256(encodeP256Key(x, y)))}" } + /** + * Returns something like `did:key:mEpA...` with the [x] and [y] values appended in Base64. + * This translates to `Base64(0x12, 0x90, EC-P-256-Key)`. + * Note that `0x1290` is not an official Multicodec prefix, but there seems to be none for + * uncompressed P-256 key. We can't use the compressed format, because decoding that would + * require some EC Point math... + */ + fun calcKeyId(curve: CoseEllipticCurve, x: ByteArray, y: ByteArray): String? { + if (curve != CoseEllipticCurve.P256) + return null + return "$PREFIX_DID_KEY:${multibaseWrapBase64(multicodecWrapP256(encodeP256Key(x, y)))}" + } + fun calcPublicKey(keyId: String): Pair? { if (!keyId.startsWith("$PREFIX_DID_KEY:")) return null val stripped = keyId.removePrefix("$PREFIX_DID_KEY:") diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt new file mode 100644 index 000000000..43f3fa980 --- /dev/null +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt @@ -0,0 +1,44 @@ +package at.asitplus.wallet.lib.cbor + +import at.asitplus.wallet.lib.agent.CryptoService +import at.asitplus.wallet.lib.agent.DefaultCryptoService +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.matthewnelson.component.encoding.base16.encodeBase16 +import kotlin.random.Random + +class CoseServiceTest : FreeSpec({ + + lateinit var cryptoService: CryptoService + lateinit var coseService: CoseService + lateinit var verifierCoseService: VerifierCoseService + lateinit var randomPayload: ByteArray + + beforeEach { + cryptoService = DefaultCryptoService() + coseService = DefaultCoseService(cryptoService) + verifierCoseService = DefaultVerifierCoseService() + randomPayload = Random.nextBytes(32) + } + + "signed object with bytes can be verified" { + val signed = + coseService.createSignedCose(CoseHeader(algorithm = CoseAlgorithm.ES256), CoseHeader(), randomPayload, true) + .getOrThrow() + signed.shouldNotBeNull() + println(signed.serialize().encodeBase16()) + + signed.payload shouldBe randomPayload + signed.signature.shouldNotBeNull() + + // TODO activate once serialization works + //val parsed = CoseSigned.deserialize(signed.serialize()) + //parsed.shouldNotBeNull() + val parsed = signed + + val result = verifierCoseService.verifyCose(parsed, cryptoService.toCoseKey()).getOrThrow() + result shouldBe true + } + +}) diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index d2cf5e5ca..2cc285426 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -1,6 +1,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.cbor.CoseAlgorithm +import at.asitplus.wallet.lib.cbor.CoseEllipticCurve +import at.asitplus.wallet.lib.cbor.CoseKey +import at.asitplus.wallet.lib.cbor.CoseKeyType import io.matthewnelson.component.base64.encodeBase64 import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey @@ -52,9 +56,11 @@ import platform.CoreFoundation.CFDictionaryAddValue as CFDictionaryAddValue1 actual class DefaultCryptoService : CryptoService { override val jwsAlgorithm = JwsAlgorithm.ES256 + override val coseAlgorithm = CoseAlgorithm.ES256 private val privateKey: SecKeyRef private val publicKey: SecKeyRef private val jsonWebKey: JsonWebKey + private val coseKey: CoseKey actual constructor() { val query = CFDictionaryCreateMutable(null, 2, null, null).apply { @@ -66,6 +72,7 @@ actual class DefaultCryptoService : CryptoService { val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData this.jsonWebKey = JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray())!! + this.coseKey = CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray())!! } override suspend fun sign(input: ByteArray): KmmResult { @@ -137,6 +144,7 @@ actual class DefaultCryptoService : CryptoService { override fun toJsonWebKey() = jsonWebKey + override fun toCoseKey() = coseKey } @Suppress("UNCHECKED_CAST") @@ -173,6 +181,37 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { } } + + override fun verify( + input: ByteArray, + signature: ByteArray, + algorithm: CoseAlgorithm, + publicKey: CoseKey + ): KmmResult { + memScoped { + val ansix962 = publicKey.toAnsiX963ByteArray().getOrElse { + return KmmResult.failure(it) + } + val keyData = CFBridgingRetain(toData(ansix962)) as CFDataRef + val attributes = CFDictionaryCreateMutable(null, 3, null, null).apply { + CFDictionaryAddValue1(this, kSecAttrKeyClass, kSecAttrKeyClassPublic) + CFDictionaryAddValue1(this, kSecAttrKeyType, kSecAttrKeyTypeEC) + CFDictionaryAddValue1(this, kSecAttrKeySizeInBits, CFBridgingRetain(NSNumber(256))) + } + val secKey = SecKeyCreateWithData(keyData, attributes, null) + ?: return KmmResult.failure(IllegalArgumentException()) + val inputData = CFBridgingRetain(toData(input)) as CFDataRef + val signatureData = CFBridgingRetain(toData(signature.convertToAsn1Signature(32))) as CFDataRef + val verified = SecKeyVerifySignature( + secKey, + kSecKeyAlgorithmECDSASignatureMessageX962SHA256, + inputData, + signatureData, + null + ) + return KmmResult.success(verified) + } + } } @Suppress("UNCHECKED_CAST") diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 6832c8f3d..e091d990b 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -1,6 +1,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.cbor.CoseAlgorithm +import at.asitplus.wallet.lib.cbor.CoseEllipticCurve +import at.asitplus.wallet.lib.cbor.CoseKey +import at.asitplus.wallet.lib.cbor.CoseKeyType import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm @@ -34,31 +38,50 @@ actual open class DefaultCryptoService : CryptoService { private val ecCurve: EcCurve = EcCurve.SECP_256_R_1 private val keyPair: KeyPair private val jsonWebKey: JsonWebKey + private val coseKey: CoseKey actual constructor() { this.keyPair = KeyPairGenerator.getInstance("EC").also { it.initialize(ecCurve.keyLengthBits) }.genKeyPair() + val ecPublicKey = keyPair.public as ECPublicKey this.jsonWebKey = JsonWebKey.fromCoordinates( - JwkType.EC, - EcCurve.SECP_256_R_1, - (keyPair.public as ECPublicKey).w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - (keyPair.public as ECPublicKey).w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + type = JwkType.EC, + curve = EcCurve.SECP_256_R_1, + x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), + y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + )!! + this.coseKey = CoseKey.fromCoordinates( + type = CoseKeyType.EC2, + curve = CoseEllipticCurve.P256, + x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), + y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) )!! } constructor(keyPair: KeyPair) { this.keyPair = keyPair + val ecPublicKey = keyPair.public as ECPublicKey this.jsonWebKey = JsonWebKey.fromCoordinates( - JwkType.EC, - EcCurve.SECP_256_R_1, - (keyPair.public as ECPublicKey).w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - (keyPair.public as ECPublicKey).w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + type = JwkType.EC, + curve = EcCurve.SECP_256_R_1, + x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), + y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + )!! + this.coseKey = CoseKey.fromCoordinates( + type = CoseKeyType.EC2, + curve = CoseEllipticCurve.P256, + x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), + y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) )!! } override val jwsAlgorithm = JwsAlgorithm.ES256 + override val coseAlgorithm = CoseAlgorithm.ES256 + override fun toJsonWebKey() = jsonWebKey + override fun toCoseKey() = coseKey + override suspend fun sign(input: ByteArray): KmmResult = try { val signed = Signature.getInstance(jwsAlgorithm.jcaName).apply { @@ -175,6 +198,23 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { } } + override fun verify( + input: ByteArray, + signature: ByteArray, + algorithm: CoseAlgorithm, + publicKey: CoseKey + ): KmmResult { + return try { + val asn1Signature = signature.convertToAsn1Signature(publicKey.curve?.signatureLengthBytes ?: 32) + val result = Signature.getInstance(algorithm.jcaName).apply { + initVerify(publicKey.getPublicKey()) + update(input) + }.verify(asn1Signature) + KmmResult.success(result) + } catch (e: Throwable) { + KmmResult.failure(e) + } + } } actual object CryptoUtils { @@ -193,6 +233,13 @@ val JwsAlgorithm.jcaName JwsAlgorithm.ES256 -> "SHA256withECDSA" } +val CoseAlgorithm.jcaName + get() = when (this) { + CoseAlgorithm.ES256 -> "SHA256withECDSA" + CoseAlgorithm.ES384 -> "SHA384withECDSA" + CoseAlgorithm.ES512 -> "SHA512withECDSA" + } + val Digest.jcaName get() = when (this) { Digest.SHA256 -> "SHA-256" @@ -218,8 +265,15 @@ val EcCurve.jcaName EcCurve.SECP_256_R_1 -> "secp256r1" } +val CoseEllipticCurve.jcaName + get() = when (this) { + CoseEllipticCurve.P256 -> "P-256" + CoseEllipticCurve.P384 -> "P-384" + CoseEllipticCurve.P521 -> "P-521" + } + fun JsonWebKey.getPublicKey(): PublicKey { - val parameterSpec = ECNamedCurveTable.getParameterSpec("P-256") + val parameterSpec = ECNamedCurveTable.getParameterSpec(curve?.jcaName ?: "P-256") val x = BigInteger(1, x) val y = BigInteger(1, y) val ecPoint = parameterSpec.curve.createPoint(x, y) @@ -227,6 +281,14 @@ fun JsonWebKey.getPublicKey(): PublicKey { return JCEECPublicKey("EC", ecPublicKeySpec) } +fun CoseKey.getPublicKey(): PublicKey { + val parameterSpec = ECNamedCurveTable.getParameterSpec(curve?.jcaName ?: "P-256") + val x = BigInteger(1, x) + val y = BigInteger(1, y) + val ecPoint = parameterSpec.curve.createPoint(x, y) + val ecPublicKeySpec = ECPublicKeySpec(ecPoint, parameterSpec) + return JCEECPublicKey("EC", ecPublicKeySpec) +} fun JsonWebKey.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: EcCurve) = fromCoordinates(