diff --git a/signum b/signum index 2ae952abc..67298aeeb 160000 --- a/signum +++ b/signum @@ -1 +1 @@ -Subproject commit 2ae952abc6cf3a9dda1d21ff0bb405558618c408 +Subproject commit 67298aeeb7be70680eab15bc21fbc4ee2fa89260 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 2260f4086..a6241bb61 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 @@ -24,6 +24,7 @@ import at.asitplus.wallet.lib.data.vckJsonSerializer import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import kotlinx.serialization.builtins.ByteArraySerializer /** * Server implementation to issue credentials using OID4VCI. @@ -98,8 +99,7 @@ class CredentialIssuer( credentialIssuer = publicContext, configurationIds = credentialSchemes.flatMap { it.toCredentialIdentifier() }, grants = CredentialOfferGrants( - authorizationCode = - CredentialOfferGrantsAuthCode( + authorizationCode = CredentialOfferGrantsAuthCode( // TODO remember this state, for subsequent requests from the Wallet issuerState = uuid4().toString(), authorizationServer = authorizationService.publicContext @@ -219,7 +219,7 @@ class CredentialIssuer( * Removed in OID4VCI Draft 14, kept here for a bit of backwards-compatibility */ private suspend fun String.validateCwtProof(): CryptoPublicKey { - val coseSigned = CoseSigned.deserialize(decodeToByteArray(Base64UrlStrict)).getOrNull() + val coseSigned = CoseSigned.deserialize(ByteArraySerializer(), decodeToByteArray(Base64UrlStrict)).getOrNull() ?: throw OAuth2Exception(Errors.INVALID_PROOF) .also { Napier.w("client did provide invalid proof: $this") } val cwt = coseSigned.payload?.let { CborWebToken.deserialize(it).getOrNull() } diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt index 2591eb2dc..8c6d0c5d0 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt @@ -119,6 +119,7 @@ class IssuerAgent( namespacedItems = mapOf(credential.scheme.isoNamespace!! to credential.issuerSignedItems), issuerAuth = coseService.createSignedCose( payload = mso, + serializer = MobileSecurityObject.serializer(), addKeyId = false, addCertificate = true, ).getOrThrow(), diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt index 1bcee5e74..23ea33d78 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt @@ -24,6 +24,7 @@ import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.json.buildJsonObject @@ -295,17 +296,14 @@ class Validator( throw IllegalArgumentException("issuerKey") } - if (verifierCoseService.verifyCose(issuerAuth, issuerKey).isFailure) { + if (verifierCoseService.verifyCose(issuerAuth, issuerKey, MobileSecurityObject.serializer()).isFailure) { Napier.w("IssuerAuth not verified: $issuerAuth") throw IllegalArgumentException("issuerAuth") } - val mso: MobileSecurityObject? = issuerSigned.issuerAuth.getTypedPayload(MobileSecurityObject.serializer()).onFailure { - throw IllegalArgumentException("mso", it) - Napier.w("MSO could not be decoded", it) - }.getOrNull()?.value + val mso: MobileSecurityObject? = issuerSigned.issuerAuth.payload if (mso == null) { - Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") + Napier.w("MSO is null: $issuerAuth") throw IllegalArgumentException("mso") } @@ -314,13 +312,14 @@ class Validator( throw IllegalArgumentException("mso.docType") } val walletKey = mso.deviceKeyInfo.deviceKey + val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature ?: run { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") throw IllegalArgumentException("deviceSignature") } - if (verifierCoseService.verifyCose(deviceSignature, walletKey).isFailure) { - Napier.w("DeviceSignature not verified") + if (verifierCoseService.verifyCose(deviceSignature, walletKey, ByteArraySerializer()).isFailure) { + Napier.w("DeviceSignature not verified: ${doc.deviceSigned.deviceAuth}") throw IllegalArgumentException("deviceSignature") } @@ -471,7 +470,7 @@ class Validator( it.serialize().encodeToString(Base16(strict = true)) ) } - val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey) + val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey, MobileSecurityObject.serializer()) if (result.isFailure) { Napier.w("ISO: Could not verify credential", result.exceptionOrNull()) return Verifier.VerifyCredentialResult.InvalidStructure( diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifiablePresentationFactory.kt b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifiablePresentationFactory.kt index 3bbe04e37..ba051b3be 100644 --- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifiablePresentationFactory.kt +++ b/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifiablePresentationFactory.kt @@ -15,6 +15,7 @@ import at.asitplus.wallet.lib.jws.JwsService import at.asitplus.wallet.lib.jws.SdJwtSigned import io.github.aakira.napier.Napier import kotlinx.datetime.Clock +import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.json.JsonElement class VerifiablePresentationFactory( @@ -57,6 +58,7 @@ class VerifiablePresentationFactory( ): Holder.CreatePresentationResult.DeviceResponse { val deviceSignature = coseService.createSignedCose( payload = challenge.encodeToByteArray(), + serializer = ByteArraySerializer(), addKeyId = false ).getOrElse { Napier.w("Could not create DeviceAuth for presentation", it) 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 2aa3eac26..6e9d4b522 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 @@ -4,7 +4,6 @@ import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.CryptoSignature import at.asitplus.signum.indispensable.cosef.* -import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.toX509SignatureAlgorithm import at.asitplus.signum.supreme.asKmmResult @@ -12,11 +11,8 @@ import at.asitplus.signum.supreme.sign.Verifier 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.iso.MobileSecurityObject -import at.asitplus.wallet.lib.iso.vckCborSerializer -import at.asitplus.wallet.lib.iso.wrapInCborTag import io.github.aakira.napier.Napier -import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.KSerializer /** * Creates and parses COSE objects. @@ -35,10 +31,11 @@ interface CoseService { * @param addKeyId whether to set [CoseHeader.kid] in [protectedHeader] * @param addCertificate whether to set [CoseHeader.certificateChain] in [unprotectedHeader] */ - suspend fun
createSignedCose( + suspend fun
createSignedCose( protectedHeader: CoseHeader? = null, unprotectedHeader: CoseHeader? = null, payload: P? = null, + serializer: KSerializer
,
addKeyId: Boolean = true,
addCertificate: Boolean = false,
): KmmResult verifyCose(
+ coseSigned: CoseSigned ,
+ signer: CoseKey,
+ serializer: KSerializer ,
+ ): KmmResult createSignedCose(
+ override suspend fun createSignedCose(
protectedHeader: CoseHeader?,
unprotectedHeader: CoseHeader?,
payload: P?,
+ serializer: KSerializer ,
addKeyId: Boolean,
addCertificate: Boolean,
): KmmResult (
+ protectedHeader = coseHeader,
+ unprotectedHeader = unprotectedHeader.withCertificateIfExists(addCertificate),
+ payload = payload,
+ signature = calcSignature(coseHeader, payload, serializer)
+ )
}
}
- /**
- * Encodes [this] as payload for [createSignedCose], i.e. encodes it into a byte array.
- * + [ByteArray] is processed as it is
- * + [ByteStringWrapper] is wrapped in Tag(24)
- * + [MobileSecurityObject] is wrapped as [ByteStringWrapper] and wrapped in Tag(24)
- * + null is processed as it is
- *
- * If other complex data classes need to be serialized (other than [MobileSecurityObject]),
- * extend this method in the same fashion
- */
- @Throws(NotImplementedError::class)
- private fun P.asCosePayload(): ByteArray? = when (this) {
- is ByteArray -> this
- is ByteStringWrapper<*> -> vckCborSerializer
- .encodeToByteArray(this)
- .wrapInCborTag(24)
-
- is MobileSecurityObject -> vckCborSerializer
- .encodeToByteArray(ByteStringWrapper(this) as ByteStringWrapper calcSignature(
protectedHeader: CoseHeader,
- payload: ByteArray?,
+ payload: P?,
+ serializer: KSerializer ,
): CryptoSignature.RawByteEncodable =
- cryptoService.sign(CoseSigned.prepareCoseSignatureInput(protectedHeader, payload))
- .asKmmResult().getOrElse {
+ CoseSigned.prepareCoseSignatureInput (protectedHeader, payload, serializer).let { signatureInput ->
+ cryptoService.sign(signatureInput).asKmmResult().getOrElse {
Napier.w("No signature from native code", it)
throw it
}
-
+ }
}
class DefaultVerifierCoseService(
@@ -141,9 +118,16 @@ class DefaultVerifierCoseService(
/**
* Verifiers the signature of [coseSigned] by using [signer].
*/
- override fun verifyCose(coseSigned: CoseSigned<*>, signer: CoseKey) = catching {
- val signatureInput = CoseSigned.prepareCoseSignatureInput(coseSigned.protectedHeader.value, coseSigned.payload)
-
+ override fun verifyCose(
+ coseSigned: CoseSigned ,
+ signer: CoseKey,
+ serializer: KSerializer ,
+ ) = catching {
+ val signatureInput = CoseSigned.prepareCoseSignatureInput(
+ protectedHeader = coseSigned.protectedHeader.value,
+ payload = coseSigned.payload,
+ serializer = serializer
+ )
val algorithm = coseSigned.protectedHeader.value.algorithm
?: throw IllegalArgumentException("Algorithm not specified")
val publicKey = signer.toCryptoPublicKey().getOrElse { ex ->
diff --git a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/SdJwtVerificationTest.kt b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/SdJwtVerificationTest.kt
index b7d34b5f0..217b8e86d 100644
--- a/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/SdJwtVerificationTest.kt
+++ b/vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/SdJwtVerificationTest.kt
@@ -129,7 +129,7 @@ class SdJwtVerificationTest : FreeSpec({
}
}
""".trimIndent()
- println(reconstructed)
+
reconstructed shouldBe vckJsonSerializer.parseToJsonElement(expected)
}
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 50a6f6239..3078d4c56 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
@@ -9,8 +9,12 @@ import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import kotlinx.datetime.Clock
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.builtins.ByteArraySerializer
+import kotlinx.serialization.builtins.NothingSerializer
import kotlin.random.Random
+@OptIn(ExperimentalSerializationApi::class)
class CoseServiceTest : FreeSpec({
lateinit var cryptoService: CryptoService
@@ -29,21 +33,25 @@ class CoseServiceTest : FreeSpec({
}
"signed object with bytes can be verified" {
+ val parameterSerializer = ByteArraySerializer()
val signed = coseService.createSignedCose(
unprotectedHeader = CoseHeader(algorithm = CoseAlgorithm.ES256),
payload = randomPayload,
+ serializer = parameterSerializer,
).getOrThrow()
signed.payload shouldBe randomPayload
signed.signature.shouldNotBeNull()
- val parsed = CoseSigned.deserialize(signed.serialize()).getOrThrow()
+ val parsed = CoseSigned.deserialize(parameterSerializer, signed.serialize(parameterSerializer)).getOrThrow()
+ parsed shouldBe signed
- val result = verifierCoseService.verifyCose(parsed, coseKey)
+ val result = verifierCoseService.verifyCose(parsed, coseKey, parameterSerializer)
result.isSuccess shouldBe true
}
"signed object with MSO payload can be verified" {
+ val parameterSerializer = MobileSecurityObject.serializer()
val mso = MobileSecurityObject(
version = "1.0",
digestAlgorithm = "SHA-256",
@@ -62,29 +70,34 @@ class CoseServiceTest : FreeSpec({
val signed = coseService.createSignedCose(
protectedHeader = CoseHeader(algorithm = CoseAlgorithm.ES256),
payload = mso,
+ serializer = parameterSerializer
).getOrThrow()
- signed.getTypedPayload(MobileSecurityObject.serializer()).getOrThrow().shouldNotBeNull().value shouldBe mso
+ signed.payload shouldBe mso
signed.signature.shouldNotBeNull()
- val parsed = CoseSigned.deserialize(signed.serialize()).getOrThrow()
+ val parsed = CoseSigned.deserialize(parameterSerializer,signed.serialize(parameterSerializer)).getOrThrow()
+ parsed shouldBe signed
- val result = verifierCoseService.verifyCose(parsed, coseKey)
+ val result = verifierCoseService.verifyCose(parsed, coseKey, parameterSerializer)
result.isSuccess shouldBe true
}
"signed object without payload can be verified" {
+ val parameterSerializer = NothingSerializer()
val signed = coseService.createSignedCose(
unprotectedHeader = null,
payload = null,
+ serializer = parameterSerializer
).getOrThrow()
signed.payload shouldBe null
signed.signature.shouldNotBeNull()
- val parsed = CoseSigned.deserialize(signed.serialize()).getOrThrow()
+ val parsed = CoseSigned.deserialize(parameterSerializer,signed.serialize(parameterSerializer)).getOrThrow()
+ parsed shouldBe signed
- val result = verifierCoseService.verifyCose(parsed, coseKey)
+ val result = verifierCoseService.verifyCose(parsed, coseKey, parameterSerializer)
result.isSuccess shouldBe true
}
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 d062acffc..66e95e261 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,8 +1,8 @@
package at.asitplus.wallet.lib.cbor
+import at.asitplus.signum.indispensable.CryptoSignature
import at.asitplus.signum.indispensable.cosef.CoseHeader
import at.asitplus.signum.indispensable.cosef.CoseSigned
-import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import at.asitplus.wallet.lib.iso.*
import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
@@ -47,9 +47,9 @@ class DeviceSignedItemSerializationTest : FreeSpec({
key = elementId,
value = Random.nextBytes(32),
)
- val protectedHeader = ByteStringWrapper(CoseHeader(), CoseHeader().serialize())
- val issuerAuth = CoseSigned