Skip to content

Commit 45889bf

Browse files
committed
OID4VP: Refactor ISO mDoc presentation nonce handling
1 parent aa5cf3b commit 45889bf

File tree

3 files changed

+47
-57
lines changed

3 files changed

+47
-57
lines changed

vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/openid/PresentationFactory.kt

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import at.asitplus.signum.indispensable.CryptoPublicKey
1212
import at.asitplus.signum.indispensable.cosef.CoseSigned
1313
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
1414
import at.asitplus.signum.indispensable.cosef.io.coseCompliantSerializer
15-
import at.asitplus.signum.indispensable.io.Base64Strict
1615
import at.asitplus.signum.indispensable.io.Base64UrlStrict
1716
import at.asitplus.signum.indispensable.josef.JsonWebKey
1817
import at.asitplus.signum.indispensable.josef.JwsSigned
@@ -24,13 +23,16 @@ import at.asitplus.wallet.lib.data.DeprecatedBase64URLTransactionDataSerializer
2423
import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator
2524
import at.asitplus.wallet.lib.data.vckJsonSerializer
2625
import at.asitplus.wallet.lib.iso.*
26+
import at.asitplus.wallet.lib.iso.DeviceSignedItemList
27+
import at.asitplus.wallet.lib.iso.wrapInCborTag
2728
import at.asitplus.wallet.lib.jws.JwsService
2829
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
2930
import at.asitplus.wallet.lib.oidvci.OAuth2Exception.*
3031
import io.github.aakira.napier.Napier
3132
import io.matthewnelson.encoding.base16.Base16
3233
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
3334
import kotlinx.datetime.Clock
35+
import kotlinx.serialization.Contextual
3436
import kotlinx.serialization.PolymorphicSerializer
3537
import kotlinx.serialization.builtins.ByteArraySerializer
3638
import kotlinx.serialization.encodeToByteArray
@@ -64,7 +66,12 @@ internal class PresentationFactory(
6466
audience = audience,
6567
transactionData = transactionData,
6668
calcIsoDeviceSignature = { docType, mdocGenNonce ->
67-
reuseMdocGeneratedNonce(mdocGenNonce, clientId, responseUrl, nonce, docType, responseWillBeEncrypted)
69+
calcDeviceSignature(mdocGenNonce, clientId, responseUrl, nonce, docType)
70+
},
71+
provideMdocGeneratedNonce = {
72+
if (clientId != null && responseUrl != null) {
73+
if (responseWillBeEncrypted) Random.nextBytes(16).encodeToString(Base64UrlStrict) else ""
74+
} else null
6875
}
6976
)
7077

@@ -78,26 +85,13 @@ internal class PresentationFactory(
7885
clientMetadata?.vpFormats?.let {
7986
when (presentation) {
8087
is PresentationResponseParameters.DCQLParameters -> presentation.verifyFormatSupport(it)
81-
82-
is PresentationResponseParameters.PresentationExchangeParameters -> {
88+
is PresentationResponseParameters.PresentationExchangeParameters ->
8389
presentation.verifyFormatSupport(it)
84-
}
8590
}
8691
}
8792
}
8893
}
8994

90-
private suspend fun PresentationFactory.reuseMdocGeneratedNonce(
91-
mdocGeneratedNonce: String?,
92-
clientId: String?,
93-
responseUrl: String?,
94-
nonce: String,
95-
docType: String,
96-
responseWillBeEncrypted: Boolean
97-
) = mdocGeneratedNonce?.let {
98-
calcDeviceSignatureWithNonce(mdocGeneratedNonce, clientId!!, responseUrl!!, nonce, docType)
99-
} ?: calcDeviceSignature(responseWillBeEncrypted, clientId, responseUrl, nonce, docType)
100-
10195
/**
10296
* Parses all `transaction_data` fields from the request, with a JsonPath, because
10397
* ... for OpenID4VP Draft 23, that's encoded in the AuthnRequest
@@ -120,20 +114,38 @@ internal class PresentationFactory(
120114

121115
/**
122116
* Performs calculation of the [at.asitplus.wallet.lib.iso.SessionTranscript] and [at.asitplus.wallet.lib.iso.DeviceAuthentication],
123-
* acc. to ISO/IEC 18013-5:2021 and ISO/IEC 18013-7:2024, if required in [responseWillBeEncrypted] (i.e. it will be encrypted)
117+
* acc. to ISO/IEC 18013-5:2021 and ISO/IEC 18013-7:2024, with the [mdocGeneratedNonce] provided if set,
118+
* or a fallback mechanism used otherwise
124119
*/
125120
@Throws(PresentationException::class, CancellationException::class)
126121
private suspend fun calcDeviceSignature(
127-
responseWillBeEncrypted: Boolean,
122+
mdocGeneratedNonce: String?,
128123
clientId: String?,
129124
responseUrl: String?,
130125
nonce: String,
131126
docType: String,
132-
): Pair<CoseSigned<ByteArray>, String?> = if (clientId != null && responseUrl != null) {
133-
// if it's not encrypted, we have no way of transporting the mdocGeneratedNonce, so we'll use the empty string
134-
val mdocGeneratedNonce = if (responseWillBeEncrypted)
135-
Random.Default.nextBytes(16).encodeToString(Base64UrlStrict) else ""
136-
calcDeviceSignatureWithNonce(mdocGeneratedNonce, clientId, responseUrl, nonce, docType)
127+
): CoseSigned<ByteArray> = if (mdocGeneratedNonce != null && clientId != null && responseUrl != null) {
128+
run {
129+
val deviceAuthentication = DeviceAuthentication(
130+
type = "DeviceAuthentication",
131+
sessionTranscript = calcSessionTranscript(mdocGeneratedNonce, clientId, responseUrl, nonce),
132+
docType = docType,
133+
namespaces = ByteStringWrapper(DeviceNameSpaces(mapOf<String, @Contextual DeviceSignedItemList>()))
134+
)
135+
val deviceAuthenticationBytes = coseCompliantSerializer
136+
.encodeToByteArray(ByteStringWrapper(deviceAuthentication))
137+
.wrapInCborTag(24)
138+
.also { Napier.d("Device authentication signature input is ${it.encodeToString(Base16())}") }
139+
140+
coseService.createSignedCoseWithDetachedPayload(
141+
payload = deviceAuthenticationBytes,
142+
serializer = ByteArraySerializer(),
143+
addKeyId = false
144+
).getOrElse {
145+
Napier.w("Could not create DeviceAuth for presentation", it)
146+
throw PresentationException(it)
147+
}
148+
}
137149
} else {
138150
coseService.createSignedCose(
139151
payload = nonce.encodeToByteArray(),
@@ -142,23 +154,16 @@ internal class PresentationFactory(
142154
).getOrElse {
143155
Napier.w("Could not create DeviceAuth for presentation", it)
144156
throw PresentationException(it)
145-
} to null
157+
}
146158
}
147159

148160

149-
/**
150-
* Performs calculation of the [at.asitplus.wallet.lib.iso.SessionTranscript] and [at.asitplus.wallet.lib.iso.DeviceAuthentication],
151-
* acc. to ISO/IEC 18013-5:2021 and ISO/IEC 18013-7:2024, with the [mdocGeneratedNonce] provided.
152-
*/
153-
@Throws(PresentationException::class, CancellationException::class)
154-
private suspend fun calcDeviceSignatureWithNonce(
161+
private fun calcSessionTranscript(
155162
mdocGeneratedNonce: String,
156163
clientId: String,
157164
responseUrl: String,
158165
nonce: String,
159-
docType: String,
160-
): Pair<CoseSigned<ByteArray>, String?> = run {
161-
val deviceNameSpaceBytes = ByteStringWrapper(DeviceNameSpaces(mapOf()))
166+
): SessionTranscript {
162167
val clientIdToHash = ClientIdToHash(
163168
clientId = clientId,
164169
mdocGeneratedNonce = mdocGeneratedNonce
@@ -176,25 +181,7 @@ internal class PresentationFactory(
176181
nonce = nonce
177182
),
178183
)
179-
val deviceAuthentication = DeviceAuthentication(
180-
type = "DeviceAuthentication",
181-
sessionTranscript = sessionTranscript,
182-
docType = docType,
183-
namespaces = deviceNameSpaceBytes
184-
)
185-
val deviceAuthenticationBytes = coseCompliantSerializer
186-
.encodeToByteArray(ByteStringWrapper(deviceAuthentication))
187-
.wrapInCborTag(24)
188-
.also { Napier.d("Device authentication signature input is ${it.encodeToString(Base16())}") }
189-
190-
coseService.createSignedCoseWithDetachedPayload(
191-
payload = deviceAuthenticationBytes,
192-
serializer = ByteArraySerializer(),
193-
addKeyId = false
194-
).getOrElse {
195-
Napier.w("Could not create DeviceAuth for presentation", it)
196-
throw PresentationException(it)
197-
} to mdocGeneratedNonce
184+
return sessionTranscript
198185
}
199186

200187
suspend fun <T : RequestParameters> createSignedIdToken(

vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentationDataClasses.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@ data class PresentationRequestParameters(
3737
val transactionData: Collection<TransactionData>? = null,
3838
/**
3939
* Handle calculating device signature for ISO mDocs, as this depends on the transport protocol
40-
* (OpenId4VP with ISO/IEC 18013-7)
40+
* (OpenID4VP with ISO/IEC 18013-7)
4141
*/
42-
val calcIsoDeviceSignature: (suspend (docType: String, existingMdocGeneratedNonce: String?) -> Pair<CoseSigned<ByteArray>, String?>?) = { _, _ -> null },
42+
val calcIsoDeviceSignature: (suspend (docType: String, mdocGeneratedNonce: String?) -> CoseSigned<ByteArray>?) = { _, _ -> null },
43+
/**
44+
* Will be called once for the presentation, to create a fresh mdocGeneratedNonce (OpenID4VP with ISO/IEC 18013-7)
45+
*/
46+
val provideMdocGeneratedNonce: (suspend () -> String?) = { null },
4347
) {
4448
internal fun getTransactionDataHashes(): Set<ByteArray>? = transactionData?.map {
4549
(vckJsonSerializer.encodeToJsonElement(

vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifiablePresentationFactory.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class VerifiablePresentationFactory(
135135
): CreatePresentationResult.DeviceResponse {
136136
Napier.d("createIsoPresentation with $request and $credentialAndRequestedClaims")
137137

138-
var mDocGeneratedNonce: String? = null
138+
val mDocGeneratedNonce = request.provideMdocGeneratedNonce()
139139
val documents = credentialAndRequestedClaims.map { (credential, requestedClaims) ->
140140
// allows disclosure of attributes from different namespaces
141141
val namespaceToAttributesMap = requestedClaims.mapNotNull { normalizedJsonPath ->
@@ -176,9 +176,8 @@ class VerifiablePresentationFactory(
176176
val docType = credential.scheme?.isoDocType!!
177177
val deviceNameSpaceBytes = ByteStringWrapper(DeviceNameSpaces(mapOf()))
178178

179-
val (deviceSignature, newNonce) = request.calcIsoDeviceSignature.invoke(docType, mDocGeneratedNonce)
179+
val deviceSignature = request.calcIsoDeviceSignature(docType, mDocGeneratedNonce)
180180
?: throw PresentationException("calcIsoDeviceSignature not implemented")
181-
mDocGeneratedNonce = newNonce
182181

183182
Document(
184183
docType = docType,

0 commit comments

Comments
 (0)