Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix tagging ISO structures #136

Merged
merged 13 commits into from
Sep 26, 2024
5 changes: 4 additions & 1 deletion conventions-vclib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ dependencies {
}

repositories {
maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot
maven {
url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
name = "aspConventions"
} //KOTEST snapshot
mavenCentral()
google()
gradlePluginPortal()
Expand Down
2 changes: 1 addition & 1 deletion conventions-vclib/gradle-conventions-plugin
2 changes: 1 addition & 1 deletion eu-pid-credential
9 changes: 6 additions & 3 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import java.util.*
pluginManagement {
includeBuild("conventions-vclib")
repositories {

maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
google()
gradlePluginPortal()
mavenCentral()
maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot
maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
maven {
url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
name = "aspConventions"
}
}
}

Expand Down Expand Up @@ -43,6 +45,7 @@ include(":openid-data-classes")
include(":vck")
include(":vck-aries")
include(":vck-openid")
include(":mobiledrivinglicence")

dependencyResolutionManagement {
repositories {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class IssuerAgent(
digestAlgorithm = "SHA-256",
valueDigests = mapOf(
scheme.isoNamespace!! to ValueDigestList(credential.issuerSignedItems.map {
ValueDigest.fromIssuerSigned(scheme.isoNamespace!!, it)
ValueDigest.fromIssuerSignedItem(it, scheme.isoNamespace!!)
})
),
deviceKeyInfo = deviceKeyInfo,
Expand All @@ -145,14 +145,13 @@ class IssuerAgent(
validUntil = expirationDate,
)
)
val issuerSigned = IssuerSigned(
val issuerSigned = IssuerSigned.fromIssuerSignedItems(
namespacedItems = mapOf(scheme.isoNamespace!! to credential.issuerSignedItems),
issuerAuth = coseService.createSignedCose(
payload = mso.serializeForIssuerAuth(),
addKeyId = false,
addCertificate = true,
).getOrThrow(),
24 // TODO verify serialization of this
)
return Issuer.IssuedCredential.Iso(issuerSigned, scheme)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package at.asitplus.wallet.lib.agent

import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.cosef.CoseKey
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import at.asitplus.signum.indispensable.cosef.toCoseKey
import at.asitplus.signum.indispensable.equalsCryptographically
import at.asitplus.signum.indispensable.io.Base64Strict
Expand All @@ -23,7 +24,6 @@ import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base16.Base16
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper


/**
Expand Down Expand Up @@ -262,7 +262,7 @@ class Validator(
.also { Napier.w("IssuerAuth not verified: $issuerAuth") }
}

val mso = issuerSigned.getIssuerAuthPayloadAsMso()
val mso = issuerSigned.getIssuerAuthPayloadAsMso().getOrNull()
?: return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
.also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") }
if (mso.docType != doc.docType) {
Expand Down Expand Up @@ -303,13 +303,16 @@ class Validator(
)
}

/**
* Verify that calculated digests equal the corresponding digest values in the MSO.
*
* See ISO/IEC 18013-5:2021, 9.3.1 Inspection procedure for issuer data authentication
*/
private fun ByteStringWrapper<IssuerSignedItem>.verify(mdlItems: ValueDigestList?): Boolean {
val issuerHash = mdlItems?.entries?.firstOrNull { it.key == value.digestId } ?: return false
// TODO analyze usages of tag wrapping
val issuerHash = mdlItems?.entries?.firstOrNull { it.key == value.digestId }
?: return false
val verifierHash = serialized.wrapInCborTag(24).sha256()
JesusMcCloud marked this conversation as resolved.
Show resolved Hide resolved
if (!verifierHash.encodeToString(Base16(strict = true))
.contentEquals(issuerHash.value.encodeToString(Base16(strict = true)))
) {
if (!verifierHash.contentEquals(issuerHash.value)) {
Napier.w("Could not verify hash of value for ${value.elementIdentifier}")
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import at.asitplus.signum.indispensable.josef.JwsHeader
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.wallet.lib.cbor.CoseService
Expand Down Expand Up @@ -101,10 +102,14 @@ class VerifiablePresentationFactory(

return Holder.CreatePresentationResult.Document(
Document(
docType = credential.scheme.isoDocType!!, issuerSigned = IssuerSigned(
namespacedItems = disclosedItems, issuerAuth = credential.issuerSigned.issuerAuth
), deviceSigned = DeviceSigned(
namespaces = byteArrayOf(), deviceAuth = DeviceAuth(
docType = credential.scheme.isoDocType!!,
issuerSigned = IssuerSigned.fromIssuerSignedItems(
namespacedItems = disclosedItems,
issuerAuth = credential.issuerSigned.issuerAuth
),
deviceSigned = DeviceSigned(
namespaces = ByteStringWrapper(DeviceNameSpaces(mapOf())),
deviceAuth = DeviceAuth(
deviceSignature = deviceSignature
)
)
Expand Down Expand Up @@ -194,4 +199,4 @@ class VerifiablePresentationFactory(
throw PresentationException(it)
}.serialize()
)
}
}
16 changes: 16 additions & 0 deletions vck/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceAuth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package at.asitplus.wallet.lib.iso

import at.asitplus.signum.indispensable.cosef.CoseSigned
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Part of the ISO/IEC 18013-5:2021 standard: Data structure for mdoc request (8.3.2.1.2.1)
*/
@Serializable
data class DeviceAuth(
@SerialName("deviceSignature")
val deviceSignature: CoseSigned? = null,
@SerialName("deviceMac")
val deviceMac: CoseSigned? = null, // TODO is COSE_Mac0
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package at.asitplus.wallet.lib.iso

import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.signum.indispensable.cosef.CoseKey
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray

/**
* Part of the ISO/IEC 18013-5:2021 standard: Data structure for MSO (9.1.2.4)
*/
@Serializable
data class DeviceKeyInfo(
@SerialName("deviceKey")
val deviceKey: CoseKey,
@SerialName("keyAuthorizations")
val keyAuthorizations: KeyAuthorization? = null,
@SerialName("keyInfo")
val keyInfo: Map<Int, String>? = null,
) {

fun serialize() = vckCborSerializer.encodeToByteArray(this)

companion object {
fun deserialize(it: ByteArray) = kotlin.runCatching {
vckCborSerializer.decodeFromByteArray<DeviceKeyInfo>(it)
}.wrap()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package at.asitplus.wallet.lib.iso

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

/**
* Convenience class with a custom serializer ([DeviceNameSpacesSerializer]) to prevent
* usage of the type `ByteStringWrapper<Map<String, Map<String, Any>>>` in [DeviceSigned.namespaces].
*/
@Serializable(with = DeviceNameSpacesSerializer::class)
data class DeviceNameSpaces(
val entries: Map<String, DeviceSignedItemList>
)

/**
* Serializes [DeviceNameSpaces.entries] as a map with an "inline list",
* having the usual key as key,
* but serialized instances of [DeviceSignedItemList] as the values.
*/
object DeviceNameSpacesSerializer : KSerializer<DeviceNameSpaces> {

override val descriptor: SerialDescriptor = mapSerialDescriptor(
PrimitiveSerialDescriptor("key", PrimitiveKind.STRING),
DeviceSignedItemListSerializer.descriptor,
)

override fun serialize(encoder: Encoder, value: DeviceNameSpaces) {
encoder.encodeStructure(descriptor) {
var index = 0
value.entries.forEach {
encodeStringElement(descriptor, index++, it.key)
encodeSerializableElement(descriptor, index++, DeviceSignedItemList.serializer(), it.value)
}
}
}

override fun deserialize(decoder: Decoder): DeviceNameSpaces {
val entries = mutableMapOf<String, DeviceSignedItemList>()
decoder.decodeStructure(descriptor) {
lateinit var key: String
var value: DeviceSignedItemList
while (true) {
val index = decodeElementIndex(descriptor)
if (index == CompositeDecoder.DECODE_DONE) {
break
} else if (index % 2 == 0) {
key = decodeStringElement(descriptor, index)
} else if (index % 2 == 1) {
value = decodeSerializableElement(descriptor, index, DeviceSignedItemList.serializer())
entries[key] = value
}
}
}
return DeviceNameSpaces(entries)
}
}


/**
* Convenience class with a custom serializer ([DeviceSignedItemListSerializer]) to prevent
* usage of the type `Map<String, Map<String, Any>>` in [DeviceNameSpaces.entries].
*/
@Serializable(with = DeviceSignedItemListSerializer::class)
data class DeviceSignedItemList(
val entries: List<DeviceSignedItem>
)

/**
* Serializes [DeviceSignedItemList.entries] as an "inline list",
* having serialized instances of [DeviceSignedItem] as the values.
*/
object DeviceSignedItemListSerializer : KSerializer<DeviceSignedItemList> {

override val descriptor: SerialDescriptor = mapSerialDescriptor(
PrimitiveSerialDescriptor("key", PrimitiveKind.STRING),
PrimitiveSerialDescriptor("value", PrimitiveKind.STRING) // TODO Change to `Any`
)

override fun serialize(encoder: Encoder, value: DeviceSignedItemList) {
encoder.encodeStructure(descriptor) {
var index = 0
value.entries.forEach {
this.encodeStringElement(descriptor, index++, it.key)
this.encodeStringElement(descriptor, index++, it.value)
}
}
}

override fun deserialize(decoder: Decoder): DeviceSignedItemList {
val entries = mutableListOf<DeviceSignedItem>()
decoder.decodeStructure(descriptor) {
lateinit var key: String
var value: String
while (true) {
val index = decodeElementIndex(descriptor)
if (index == CompositeDecoder.DECODE_DONE) {
break
} else if (index % 2 == 0) {
key = decodeStringElement(descriptor, index)
} else if (index % 2 == 1) {
value = decodeStringElement(descriptor, index)
entries += DeviceSignedItem(key, value)
}
}
}
return DeviceSignedItemList(entries)
}
}


/**
* Convenience class (getting serialized in [DeviceSignedItemListSerializer]) to prevent
* usage of the type `List<Map<String, Any>>` in [DeviceSignedItemList.entries].
*/
data class DeviceSignedItem(
val key: String,
// TODO Make this `Any`, but based on the credential serializer
val value: String,
)
Loading
Loading