@@ -8,6 +8,8 @@ import id.walt.credentials.examples.SdJwtExamples
88import id.walt.wallet2.data.Wallet
99import id.walt.wallet2.data.StoredCredential
1010import id.walt.wallet2.data.WalletCredentialStore
11+ import id.walt.wallet2.data.WalletDidEntry
12+ import id.walt.wallet2.stores.inmemory.InMemoryDidStore
1113import io.ktor.client.HttpClient
1214import io.ktor.client.engine.mock.MockEngine
1315import io.ktor.client.engine.mock.MockRequestHandleScope
@@ -235,6 +237,99 @@ class WalletIssuanceSessionServiceTest {
235237 assertEquals(1 , credentialCalls)
236238 }
237239
240+ @Test
241+ fun proofPrefersHolderDidBoundToSelectedKey () = runTest {
242+ val key = JWKKey .generate(KeyType .secp256r1)
243+ val holderDid = " did:jwk:holder"
244+ val holderDidKeyId = " $holderDid #0"
245+ val didStore = InMemoryDidStore ().also { store ->
246+ store.addDid(
247+ WalletDidEntry (
248+ did = holderDid,
249+ document = didDocument(holderDid, holderDidKeyId, key),
250+ )
251+ )
252+ }
253+ val client = client { request ->
254+ when (request.url.toString()) {
255+ ISSUER_METADATA -> jsonResponse(
256+ issuerMetadata(proofRequired = true )
257+ .replace(
258+ " \" cryptographic_binding_methods_supported\" :[\" jwk\" ]" ,
259+ " \" cryptographic_binding_methods_supported\" :[\" jwk\" ,\" did:jwk\" ]" ,
260+ )
261+ )
262+ AS_METADATA -> jsonResponse(authorizationServerMetadata(authorizationCode = false ))
263+ TOKEN_ENDPOINT -> jsonResponse(""" {"access_token":"access","token_type":"Bearer"}""" )
264+ NONCE_ENDPOINT -> jsonResponse(""" {"c_nonce":"endpoint-nonce"}""" )
265+ CREDENTIAL_ENDPOINT -> {
266+ val body = Json .parseToJsonElement(request.bodyText()).jsonObject
267+ val proof = body[" proofs" ]!! .jsonObject[" jwt" ]!! .jsonArray.single().jsonPrimitive.content
268+ val proofHeader = jwtPart(proof, 0 )
269+ assertEquals(holderDidKeyId, proofHeader[" kid" ]?.jsonPrimitive?.content)
270+ assertEquals(null , proofHeader[" jwk" ])
271+ jsonResponse(""" {"transaction_id":"transaction-1"}""" , HttpStatusCode .Accepted )
272+ }
273+ else -> respondError(HttpStatusCode .NotFound )
274+ }
275+ }
276+ val service = WalletIssuanceSessionService (
277+ wallet = Wallet (" test" , staticKey = key, didStore = didStore),
278+ httpClient = client,
279+ )
280+
281+ val session = service.start(preAuthorizedRequest().copy(did = holderDid))
282+ assertIs<WalletIssuanceOutcome .Deferred >(service.continuePreAuthorized(session.id))
283+ }
284+
285+ @Test
286+ fun proofNeverUsesHolderDidBoundToAnotherKey () = runTest {
287+ val selectedKey = JWKKey .generate(KeyType .secp256r1)
288+ val otherKey = JWKKey .generate(KeyType .secp256r1)
289+ val holderDid = " did:jwk:other-holder"
290+ val didStore = InMemoryDidStore ().also { store ->
291+ store.addDid(
292+ WalletDidEntry (
293+ did = holderDid,
294+ document = didDocument(holderDid, " $holderDid #0" , otherKey),
295+ )
296+ )
297+ }
298+ val client = client { request ->
299+ when (request.url.toString()) {
300+ ISSUER_METADATA -> jsonResponse(
301+ issuerMetadata(proofRequired = true )
302+ .replace(
303+ " \" cryptographic_binding_methods_supported\" :[\" jwk\" ]" ,
304+ " \" cryptographic_binding_methods_supported\" :[\" jwk\" ,\" did:jwk\" ]" ,
305+ )
306+ )
307+ AS_METADATA -> jsonResponse(authorizationServerMetadata(authorizationCode = false ))
308+ TOKEN_ENDPOINT -> jsonResponse(""" {"access_token":"access","token_type":"Bearer"}""" )
309+ NONCE_ENDPOINT -> jsonResponse(""" {"c_nonce":"endpoint-nonce"}""" )
310+ CREDENTIAL_ENDPOINT -> {
311+ val body = Json .parseToJsonElement(request.bodyText()).jsonObject
312+ val proof = body[" proofs" ]!! .jsonObject[" jwt" ]!! .jsonArray.single().jsonPrimitive.content
313+ val proofHeader = jwtPart(proof, 0 )
314+ assertEquals(null , proofHeader[" kid" ])
315+ assertEquals(
316+ Json .parseToJsonElement(selectedKey.getPublicKey().exportJWK()).jsonObject,
317+ proofHeader[" jwk" ],
318+ )
319+ jsonResponse(""" {"transaction_id":"transaction-1"}""" , HttpStatusCode .Accepted )
320+ }
321+ else -> respondError(HttpStatusCode .NotFound )
322+ }
323+ }
324+ val service = WalletIssuanceSessionService (
325+ wallet = Wallet (" test" , staticKey = selectedKey, didStore = didStore),
326+ httpClient = client,
327+ )
328+
329+ val session = service.start(preAuthorizedRequest().copy(did = holderDid))
330+ assertIs<WalletIssuanceOutcome .Deferred >(service.continuePreAuthorized(session.id))
331+ }
332+
238333 @Test
239334 fun insecureNonceEndpointRequiresExplicitTestOptIn () = runTest {
240335 val key = JWKKey .generate(KeyType .secp256r1)
@@ -586,6 +681,18 @@ class WalletIssuanceSessionServiceTest {
586681 private fun HttpRequestData.bodyText (): String =
587682 (body as OutgoingContent .ByteArrayContent ).bytes().decodeToString()
588683
684+ private suspend fun didDocument (did : String , keyId : String , key : JWKKey ) = buildJsonObject {
685+ put(" id" , did)
686+ put(" verificationMethod" , buildJsonArray {
687+ add(buildJsonObject {
688+ put(" id" , keyId)
689+ put(" controller" , did)
690+ put(" type" , " JsonWebKey2020" )
691+ put(" publicKeyJwk" , Json .parseToJsonElement(key.getPublicKey().exportJWK()))
692+ })
693+ })
694+ }
695+
589696 private fun jwtPart (jwt : String , index : Int ) =
590697 Json .parseToJsonElement(jwt.split(' .' )[index].decodeFromBase64Url().decodeToString()).jsonObject
591698
0 commit comments