Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0761dcb
ISO: Add data classes for updated DeviceResponse
opsiv Dec 15, 2025
2935152
ISO: Data classes for updated Iso 18013-5 ItemsRequest
opsiv Dec 15, 2025
d289957
ISO: Implemented serializer registry for ZkSystemSpec
opsiv Dec 19, 2025
9da5e6b
ISO: ZkDocument equals simplification
opsiv Dec 29, 2025
88a36ae
ISO: Added simple identity testcase for ZkDocument serialization
opsiv Dec 29, 2025
90ee564
ISO: provided equals and hashcode overrides for data class
opsiv Jan 8, 2026
43e2181
ISO: Fix toString override
opsiv Jan 8, 2026
2df192b
Changelog: Add Iso data classes and serializer registry
opsiv Jan 8, 2026
3362566
OpendId DCQL: Add support for DCQLZkSystemType
opsiv Dec 15, 2025
855e0c6
Openid: Prepare verification of Zk IsoMdoc documents
opsiv Dec 16, 2025
c820024
OpenId DCQL: move validity check to metadata class
opsiv Dec 16, 2025
935b6bf
OpenId: fix link in comment
opsiv Dec 16, 2025
2e8b147
OpenId: Simplify ZkDocument validation and verification
opsiv Dec 16, 2025
49370cb
OpenId: Add wallet capabilitis for ZK IsoMdoc presentations
opsiv Dec 16, 2025
9c5e1d9
OpenId: rename zkSpec class
opsiv Dec 17, 2025
bcdb7bb
OpenId: accomodate for updated Iso ZkSystemSpec
opsiv Dec 19, 2025
dea06e6
OpenId: HolderAgent supports mapping DCQLZkSystemType to iso ZkSystem…
opsiv Dec 19, 2025
b541f3b
ISO: prevent re-registration of same ProofSystem
opsiv Dec 20, 2025
0394b34
ISO: move mDocZk code out of wallet package
opsiv Dec 27, 2025
c7d096b
renamed issuer- and deviceSigned Items in zk context for clarity
opsiv Dec 27, 2025
581926d
OpenId: simplify proof verification
opsiv Jan 8, 2026
70a7510
OpenId-MdocZk: Simplify creation of Zk and Plain Document creation
opsiv Jan 8, 2026
74818f4
OpenId-IsoMdoc: Increase readability of verifyDeviceResponse
opsiv Jan 8, 2026
d5291a0
OpenId: Revert default value for IsoDocumentParsed
opsiv Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Release 5.11.0 (unreleased):
- Add `VerifyStatusListTokenHAIP` and related resolver/tests to enforce HAIP d04
- Add `IdentifierList` and `IdentifierListInfo` and related classes
- Add data classes and serializers for ISO-18013-5 2nd edition
- Add `ZkSystemParamRegistry` to enable Zero-Knowledge backends to register serializers for their custom parameters
- OpenID for Verifiable Credential Issuance:
- In `SimpleAuthorizationService` add parameter `configurationIds` to method `credentialOfferWithAuthorizationCode`
- Support different supported credential formats having the same scope value (as this is covered by the spec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ data class DeviceResponse(
val version: String,
@SerialName("documents")
val documents: Array<Document>? = null,
@SerialName("zkDocuments")
val zkDocuments: Array<ZkDocument>? = null,
@SerialName("documentErrors")
val documentErrors: Array<Pair<String, UInt>>? = null,
@SerialName("status")
Expand All @@ -29,6 +31,10 @@ data class DeviceResponse(
if (other.documents == null) return false
if (!documents.contentEquals(other.documents)) return false
} else if (other.documents != null) return false
if (zkDocuments != null) {
if (other.zkDocuments == null) return false
if (!zkDocuments.contentEquals(other.zkDocuments)) return false
} else if (other.zkDocuments != null) return false
if (documentErrors != null) {
if (other.documentErrors == null) return false
if (!documentErrors.contentEquals(other.documentErrors)) return false
Expand All @@ -39,6 +45,7 @@ data class DeviceResponse(
override fun hashCode(): Int {
var result = version.hashCode()
result = 31 * result + (documents?.contentHashCode() ?: 0)
result = 31 * result + (zkDocuments?.contentHashCode() ?: 0)
result = 31 * result + (documentErrors?.contentHashCode() ?: 0)
result = 31 * result + status.hashCode()
return result
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package at.asitplus.iso

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.ByteString

@Serializable
data class DocRequestInfo(
@SerialName("issuerIdentifiers")
@ByteString
val issuerIdentifiers: List<ByteArray>? = null,
@SerialName("uniqueDocSetRequired")
val uniqueDocSetRequired: Boolean? = null,
@SerialName("maximumResponseSize")
val maximumResponseSize: UInt? = null,
@SerialName("zkRequest")
val zkRequest: ZkRequest? = null,
@SerialName("docResponseEncryption")
val docResponseEncryption: EncryptionParameters? = null,
// TODO: Implement alternativeDataElements
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as DocRequestInfo

if (uniqueDocSetRequired != other.uniqueDocSetRequired) return false
if (issuerIdentifiers != null && other.issuerIdentifiers != null) {
if (issuerIdentifiers.size != other.issuerIdentifiers.size) return false
if (!issuerIdentifiers.zip(other.issuerIdentifiers).all {
(a, b) -> a.contentEquals(b)
}) return false
} else if (issuerIdentifiers != other.issuerIdentifiers) return false

if (maximumResponseSize != other.maximumResponseSize) return false
if (zkRequest != other.zkRequest) return false
if (docResponseEncryption != other.docResponseEncryption) return false

return true
}

override fun hashCode(): Int {
var result = uniqueDocSetRequired?.hashCode() ?: 0
result = 31 * result + (issuerIdentifiers?.fold(1) { acc, arr -> 31 * acc + arr.contentHashCode() } ?: 0)
result = 31 * result + (maximumResponseSize?.hashCode() ?: 0)
result = 31 * result + (zkRequest?.hashCode() ?: 0)
result = 31 * result + (docResponseEncryption?.hashCode() ?: 0)
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ data class IssuerSignedItem(
@ByteString
val random: ByteArray,
@SerialName(PROP_ELEMENT_ID)
val elementIdentifier: String,
override val elementIdentifier: String,
@SerialName(PROP_ELEMENT_VALUE)
val elementValue: Any,
) {
override val elementValue: Any,
) : Item {

override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package at.asitplus.iso

interface Item {
val elementIdentifier: String
val elementValue: Any
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ data class ItemsRequest(
@SerialName("nameSpaces")
val namespaces: Map<String, ItemsRequestList>,
@SerialName("requestInfo")
val requestInfo: Map<String, String>? = null,
val requestInfo: DocRequestInfo? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package at.asitplus.iso

import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object NamespacedZkSignedListSerializer : KSerializer<Map<String, ZkSignedList>> {
private val mapSerializer = MapSerializer(String.serializer(), object :
ZkSignedListSerializer("") {})

override val descriptor = mapSerializer.descriptor
override fun deserialize(decoder: Decoder): Map<String, ZkSignedList> = NamespacedMapEntryDeserializer().let {
MapSerializer(it.namespaceSerializer, it.itemSerializer).deserialize(decoder)
}

class NamespacedMapEntryDeserializer {
lateinit var key: String
val namespaceSerializer = NamespaceSerializer()
val itemSerializer = ZkSignedListSerializer()

inner class NamespaceSerializer internal constructor() : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("ISO namespace", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): String = decoder.decodeString().apply { key = this }

override fun serialize(encoder: Encoder, value: String) {
encoder.encodeString(value).also { key = value }
}
}

inner class ZkSignedListSerializer internal constructor() : KSerializer<ZkSignedList> {
override val descriptor = mapSerializer.descriptor

override fun deserialize(decoder: Decoder): ZkSignedList =
decoder.decodeSerializableValue(ZkSignedListSerializer(key))

override fun serialize(encoder: Encoder, value: ZkSignedList) =
encoder.encodeSerializableValue(ZkSignedListSerializer(key), value)
}
}

override fun serialize(encoder: Encoder, value: Map<String, ZkSignedList>) =
NamespacedMapEntryDeserializer().let {
MapSerializer(it.namespaceSerializer, it.itemSerializer).serialize(encoder, value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package at.asitplus.iso

import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.ByteString
import kotlinx.serialization.cbor.ValueTags

/**
* Part of the ISO/IEC 18013-5:2021 standard: Data structure for mdoc request in ZK (10.3.4)
*/
@Serializable
data class ZkDocument (
@SerialName("zkDocumentDataBytes")
@ValueTags(24U)
val zkDocumentDataBytes: ByteStringWrapper<ZkDocumentData>,
@SerialName("proof")
@ByteString
val proof: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ZkDocument) return false

if (zkDocumentDataBytes != other.zkDocumentDataBytes) return false
if (!proof.contentEquals(other.proof)) return false

return true
}

override fun hashCode(): Int {
var result = zkDocumentDataBytes.hashCode()
result = 31 * result + proof.contentHashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package at.asitplus.iso

import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.ByteString
import kotlinx.serialization.cbor.CborLabel
import kotlin.time.Instant

@Serializable
data class ZkDocumentData (
@SerialName("docType")
val docType: String,
@SerialName("zkSystemId")
val zkSystemId: String,
@SerialName("timestamp")
val timestamp: Instant,
@SerialName("issuerSigned")
@Serializable(with = NamespacedZkSignedListSerializer::class)
val issuerSigned: Map<String, @Contextual ZkSignedList>? = null,
@SerialName("deviceSigned")
@Serializable(with = NamespacedZkSignedListSerializer::class)
val deviceSigned: Map<String, @Contextual ZkSignedList>? = null,
/**
* This header parameter contains an ordered array of X.509 certificates. The certificates are to be ordered
* starting with the certificate containing the end-entity key followed by the certificate that signed it, and so
* on. There is no requirement for the entire chain to be present in the element if there is reason to believe that
* the relying party already has, or can locate, the missing certificates. This means that the relying party is
* still required to do path building but that a candidate path is proposed in this header parameter.
*
* This header parameter allows for a single X.509 certificate or a chain of X.509 certificates to be carried in
* the message.
*
* See [RFC9360](https://www.rfc-editor.org/rfc/rfc9360.html)
*/
@CborLabel(33)
@SerialName("msoX5chain")
@ByteString
val certificateChain: List<ByteArray>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package at.asitplus.iso

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ZkRequest (
@SerialName("ZkRequired")
val zkRequired: Boolean,
@SerialName("systemSpecs")
val systemSpecs: List<ZkSystemSpec>,
) {
fun validate() {
require(!zkRequired || systemSpecs.isNotEmpty()) {
"systemSpecs list cannot be empty if Zero-Knowledge is enforced"
}
}
companion object {
val Default = ZkRequest(false, emptyList())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package at.asitplus.iso

import at.asitplus.signum.indispensable.cosef.io.Base16Strict
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.SerialName


data class ZkSignedItem(
@SerialName(PROP_ELEMENT_ID)
override val elementIdentifier: String,

@SerialName(PROP_ELEMENT_VALUE)
override val elementValue: Any,
) : Item {
override fun toString(): String = "ZkSignedItem(elementIdentifier='$elementIdentifier'," +
" elementValue=${elementValue.toCustomString()})"

companion object {
internal const val PROP_ELEMENT_ID = "elementIdentifier"
internal const val PROP_ELEMENT_VALUE = "elementValue"
}
}

private fun Any.toCustomString(): String = when (this) {
is ByteArray -> this.encodeToString(Base16Strict)
is Array<*> -> this.contentDeepToString()
else -> this.toString()
}
Loading