Skip to content

Commit

Permalink
ISO: Fix serialization of device signed items
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Nov 12, 2024
1 parent 51f199b commit 371155c
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 125 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Release 5.2.0:
- SD-JWT: Validate confirmation claims correctly
- ISO credentials: Serialize and deserialize device signed items correctly (i.e. considering the namespace of the element)

Release 5.1.0:
- Drop ARIES protocol implementation, and the `vck-aries` artifact
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package at.asitplus.wallet.lib.iso

import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.signum.indispensable.cosef.CoseSigned
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.ValueTags
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray

/**
* Part of the ISO/IEC 18013-5:2021 standard: Data structure for mdoc request (8.3.2.1.2.1)
Expand All @@ -18,6 +22,8 @@ data class DeviceSigned(
val deviceAuth: DeviceAuth,
) {

fun serialize() = vckCborSerializer.encodeToByteArray(this)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
Expand All @@ -35,4 +41,40 @@ data class DeviceSigned(
result = 31 * result + deviceAuth.hashCode()
return result
}

companion object {
fun deserialize(it: ByteArray) = kotlin.runCatching {
vckCborSerializer.decodeFromByteArray<DeviceSigned>(it)
}.wrap()


// Note: Can't be a secondary constructor, because it would have the same JVM signature as the primary one.
/**
* Ensures the serialization of this structure in [Document.deviceSigned]:
* ```
* DeviceSigned = {
* "nameSpaces" : DeviceNameSpacesBytes ; Returned data elements
* "deviceAuth" : DeviceAuth ; Contains the device authentication for mdoc authentication
* }
* DeviceNameSpaceBytes = #6.24(bstr .cbor DeviceNameSpaces)
* DeviceNameSpaces = {
* * NameSpace => DeviceSignedItems ; Returned data elements for each namespace
* }
* DeviceSignedItems = {
* + DataElementIdentifier => DataElementValue ; Returned data element identifier and value
* }
* ```
*
* See ISO/IEC 18013-5:2021, 8.3.2.1.2.2 Device retrieval mdoc response
*/
fun fromDeviceSignedItems(
namespacedItems: Map<String, List<DeviceSignedItem>>,
deviceAuth: CoseSigned,
): DeviceSigned = DeviceSigned(
namespaces = ByteStringWrapper(DeviceNameSpaces( namespacedItems.map { (namespace, value) ->
namespace to DeviceSignedItemList(value)
}.toMap())),
deviceAuth = DeviceAuth(deviceAuth),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package at.asitplus.wallet.lib.iso

import at.asitplus.wallet.lib.data.InstantStringSerializer
import io.github.aakira.napier.Napier
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.mapSerialDescriptor
import kotlinx.serialization.encoding.*

/**
* Serializes [DeviceSignedItemList.entries] as an "inline map",
* having serialized instances of [DeviceSignedItem] as the values.
*/
open class DeviceSignedItemListSerializer(private val namespace: String) :
KSerializer<DeviceSignedItemList> {

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

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


private fun CompositeEncoder.encodeAnything(value: DeviceSignedItem, index: Int) {
val elementValueSerializer = buildElementValueSerializer(namespace, value.value, value.key)
val descriptor = mapSerialDescriptor(
PrimitiveSerialDescriptor("key", PrimitiveKind.STRING),
elementValueSerializer.descriptor
)

when (val it = value.value) {
is String -> encodeStringElement(descriptor, index, it)
is Int -> encodeIntElement(descriptor, index, it)
is Long -> encodeLongElement(descriptor, index, it)
is LocalDate -> encodeSerializableElement(descriptor, index, LocalDate.serializer(), it)
is Instant -> encodeSerializableElement(descriptor, index, InstantStringSerializer(), it)
is Boolean -> encodeBooleanElement(descriptor, index, it)
is ByteArray -> encodeSerializableElement(descriptor, index, ByteArraySerializer(), it)
else -> CborCredentialSerializer.encode(namespace, value.key, descriptor, index, this, it)
}
}

private inline fun <reified T> buildElementValueSerializer(
namespace: String,
elementValue: T,
elementIdentifier: String
) = when (elementValue) {
is String -> String.serializer()
is Int -> Int.serializer()
is Long -> Long.serializer()
is LocalDate -> LocalDate.serializer()
is Instant -> InstantStringSerializer()
is Boolean -> Boolean.serializer()
is ByteArray -> ByteArraySerializer()
is Any -> CborCredentialSerializer.lookupSerializer(namespace, elementIdentifier)
?: error("serializer not found for $elementIdentifier, with value $elementValue")

else -> error("serializer not found for $elementIdentifier, with value $elementValue")
}

override fun deserialize(decoder: Decoder): DeviceSignedItemList {
val entries = mutableListOf<DeviceSignedItem>()
decoder.decodeStructure(descriptor) {
lateinit var key: String
var value: Any
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 = decodeAnything(index, key)
entries += DeviceSignedItem(key, value)
}
}
}
return DeviceSignedItemList(entries)
}

private fun CompositeDecoder.decodeAnything(index: Int, elementIdentifier: String?): Any {
if (namespace.isBlank())
Napier.w("This decoder is not namespace-aware! Unspeakable things may happen…")

// Tags are not read out here but skipped because `decodeElementIndex` is never called, so we cannot
// discriminate technically, this should be a good thing though, because otherwise we'd consume more from the
// input
elementIdentifier?.let {
CborCredentialSerializer.decode(descriptor, index, this, elementIdentifier, namespace)
?.let { return it }
?: Napier.v(
"Falling back to defaults for namespace $namespace and elementIdentifier $elementIdentifier"
)
}

// These are the ones that map to different CBOR data types, the rest don't, so if it is not registered, we'll
// lose type information. No others must be added here, as they could consume data from the underlying bytes
runCatching { return decodeStringElement(descriptor, index) }
runCatching { return decodeLongElement(descriptor, index) }
runCatching { return decodeDoubleElement(descriptor, index) }
runCatching { return decodeBooleanElement(descriptor, index) }

throw IllegalArgumentException("Could not decode value at $index")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package at.asitplus.wallet.lib.iso

import at.asitplus.KmmResult.Companion.wrap
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray

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

fun serialize() = vckCborSerializer.encodeToByteArray(this)

companion object {
fun deserialize(it: ByteArray) = kotlin.runCatching {
vckCborSerializer.decodeFromByteArray<DeviceNameSpaces>(it)
}.wrap()
}
}


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

/**
* 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,
val value: Any,
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as DeviceSignedItem

if (key != other.key) return false
if (value is ByteArray && other.value is ByteArray) return value.contentEquals(other.value)
if (value is IntArray && other.value is IntArray) return value.contentEquals(other.value)
if (value is BooleanArray && other.value is BooleanArray) return value.contentEquals(other.value)
if (value is CharArray && other.value is CharArray) return value.contentEquals(other.value)
if (value is ShortArray && other.value is ShortArray) return value.contentEquals(other.value)
if (value is LongArray && other.value is LongArray) return value.contentEquals(other.value)
if (value is FloatArray && other.value is FloatArray) return value.contentEquals(other.value)
if (value is DoubleArray && other.value is DoubleArray) return value.contentEquals(other.value)
return if (value is Array<*> && other.value is Array<*>) value.contentDeepEquals(other.value)
else value == other.value
}

override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + value.hashCode()
return result
}
}
Loading

0 comments on commit 371155c

Please sign in to comment.