Skip to content

Commit

Permalink
Merge branch 'feature/cose-typed-2' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Dec 4, 2024
2 parents 6ce7fe6 + 5c40e62 commit 665f983
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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


Expand Down Expand Up @@ -316,18 +317,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")
}

Expand All @@ -336,13 +333,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")
}

Expand Down Expand Up @@ -497,7 +495,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ 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
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.
Expand All @@ -35,69 +31,48 @@ interface CoseService {
* @param addKeyId whether to set [CoseHeader.kid] in [protectedHeader]
* @param addCertificate whether to set [CoseHeader.certificateChain] in [unprotectedHeader]
*/
suspend fun <P : Any?> createSignedCose(
suspend fun <P : Any> createSignedCose(
protectedHeader: CoseHeader? = null,
unprotectedHeader: CoseHeader? = null,
payload: P? = null,
serializer: KSerializer<P>,
addKeyId: Boolean = true,
addCertificate: Boolean = false,
): KmmResult<CoseSigned<P>>
}

interface VerifierCoseService {

fun verifyCose(coseSigned: CoseSigned<*>, signer: CoseKey): KmmResult<Verifier.Success>
fun <P : Any> verifyCose(
coseSigned: CoseSigned<P>,
signer: CoseKey,
serializer: KSerializer<P>,
): KmmResult<Verifier.Success>

}

class DefaultCoseService(private val cryptoService: CryptoService) : CoseService {

override val algorithm: CoseAlgorithm = cryptoService.keyMaterial.signatureAlgorithm.toCoseAlgorithm().getOrThrow()

override suspend fun <P : Any?> createSignedCose(
override suspend fun <P : Any> createSignedCose(
protectedHeader: CoseHeader?,
unprotectedHeader: CoseHeader?,
payload: P?,
serializer: KSerializer<P>,
addKeyId: Boolean,
addCertificate: Boolean,
): KmmResult<CoseSigned<P>> = catching {
protectedHeader.withAlgorithmAndKeyId(addKeyId).let { coseHeader ->
payload.asCosePayload().let { cosePayload ->
CoseSigned(
protectedHeader = coseHeader,
unprotectedHeader = unprotectedHeader.withCertificateIfExists(addCertificate),
payload = cosePayload,
signature = calcSignature(coseHeader, cosePayload)
)
}
CoseSigned<P>(
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 : Any?> P.asCosePayload(): ByteArray? = when (this) {
is ByteArray -> this
is ByteStringWrapper<*> -> vckCborSerializer
.encodeToByteArray(this)
.wrapInCborTag(24)

is MobileSecurityObject -> vckCborSerializer
.encodeToByteArray(ByteStringWrapper(this) as ByteStringWrapper<MobileSecurityObject>)
.wrapInCborTag(24)

is Nothing? -> null
else -> throw NotImplementedError()
}

private suspend fun CoseHeader?.withCertificateIfExists(addCertificate: Boolean): CoseHeader? =
if (addCertificate) {
withCertificate(cryptoService.keyMaterial.getCertificate())
Expand All @@ -122,16 +97,18 @@ class DefaultCoseService(private val cryptoService: CryptoService) : CoseService
this?.copy(algorithm = coseAlgorithm)
?: CoseHeader(algorithm = coseAlgorithm)

private suspend fun calcSignature(
@Throws(Throwable::class)
private suspend fun <P : Any> calcSignature(
protectedHeader: CoseHeader,
payload: ByteArray?,
payload: P?,
serializer: KSerializer<P>,
): CryptoSignature.RawByteEncodable =
cryptoService.sign(CoseSigned.prepareCoseSignatureInput(protectedHeader, payload))
.asKmmResult().getOrElse {
CoseSigned.prepareCoseSignatureInput<P>(protectedHeader, payload, serializer).let { signatureInput ->
cryptoService.sign(signatureInput).asKmmResult().getOrElse {
Napier.w("No signature from native code", it)
throw it
}

}
}

class DefaultVerifierCoseService(
Expand All @@ -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 <P : Any> verifyCose(
coseSigned: CoseSigned<P>,
signer: CoseKey,
serializer: KSerializer<P>,
) = 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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class SdJwtVerificationTest : FreeSpec({
}
}
""".trimIndent()
println(reconstructed)

reconstructed shouldBe vckJsonSerializer.parseToJsonElement(expected)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -47,9 +47,9 @@ class DeviceSignedItemSerializationTest : FreeSpec({
key = elementId,
value = Random.nextBytes(32),
)
val protectedHeader = ByteStringWrapper(CoseHeader(), CoseHeader().serialize())
val issuerAuth = CoseSigned<MobileSecurityObject>(protectedHeader, null, null, byteArrayOf())
val deviceAuth = CoseSigned<ByteArray>(protectedHeader, null, null, byteArrayOf())
val protectedHeader = CoseHeader()
val issuerAuth = CoseSigned<MobileSecurityObject>(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()))
val deviceAuth = CoseSigned<ByteArray>(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()))

val doc = Document(
docType = uuid4().toString(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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
Expand Down Expand Up @@ -45,8 +46,8 @@ class IssuerSignedItemSerializationTest : FreeSpec({
elementIdentifier = elementId,
elementValue = Random.nextBytes(32),
)
val protectedHeader = ByteStringWrapper(CoseHeader(), CoseHeader().serialize())
val issuerAuth = CoseSigned<MobileSecurityObject>(protectedHeader, null, null, byteArrayOf())
val protectedHeader = CoseHeader()
val issuerAuth = CoseSigned<MobileSecurityObject>(protectedHeader, null, null, CryptoSignature.RSAorHMAC(byteArrayOf()))
val doc = Document(
docType = uuid4().toString(),
issuerSigned = IssuerSigned.fromIssuerSignedItems(
Expand Down
Loading

0 comments on commit 665f983

Please sign in to comment.