@sphereon/ssi-sdk.mdl-mdoc 0.37.2-next.34 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +488 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +488 -36
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
- package/src/agent/mDLMdoc.ts +149 -21
- package/src/functions/index.ts +312 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sphereon/ssi-sdk.mdl-mdoc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"source": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"build": "tsup --config ../../tsup.config.ts --tsconfig ../../tsconfig.tsup.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sphereon/did-auth-siop-adapter": "0.
|
|
29
|
+
"@sphereon/did-auth-siop-adapter": "0.21.0",
|
|
30
30
|
"@sphereon/kmp-mdoc-core": "0.2.0-SNAPSHOT.26",
|
|
31
31
|
"@sphereon/pex": "5.0.0-unstable.28",
|
|
32
32
|
"@sphereon/pex-models": "^2.3.2",
|
|
33
|
-
"@sphereon/ssi-sdk-ext.did-utils": "0.
|
|
34
|
-
"@sphereon/ssi-sdk-ext.key-utils": "0.
|
|
35
|
-
"@sphereon/ssi-sdk-ext.x509-utils": "0.
|
|
36
|
-
"@sphereon/ssi-sdk.core": "0.
|
|
37
|
-
"@sphereon/ssi-types": "0.
|
|
33
|
+
"@sphereon/ssi-sdk-ext.did-utils": "0.38.0",
|
|
34
|
+
"@sphereon/ssi-sdk-ext.key-utils": "0.38.0",
|
|
35
|
+
"@sphereon/ssi-sdk-ext.x509-utils": "0.38.0",
|
|
36
|
+
"@sphereon/ssi-sdk.core": "0.38.0",
|
|
37
|
+
"@sphereon/ssi-types": "0.38.0",
|
|
38
38
|
"@veramo/core": "4.2.0",
|
|
39
39
|
"@veramo/did-manager": "4.2.0",
|
|
40
40
|
"@veramo/utils": "4.2.0",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"uuid": "^9.0.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sphereon/oid4vci-client": "0.
|
|
51
|
-
"@sphereon/oid4vci-common": "0.
|
|
52
|
-
"@sphereon/ssi-express-support": "0.
|
|
53
|
-
"@sphereon/ssi-sdk-ext.key-manager": "0.
|
|
54
|
-
"@sphereon/ssi-sdk-ext.kms-local": "0.
|
|
55
|
-
"@sphereon/ssi-sdk.agent-config": "0.
|
|
56
|
-
"@sphereon/ssi-sdk.public-key-hosting": "0.
|
|
50
|
+
"@sphereon/oid4vci-client": "0.21.0",
|
|
51
|
+
"@sphereon/oid4vci-common": "0.21.0",
|
|
52
|
+
"@sphereon/ssi-express-support": "0.38.0",
|
|
53
|
+
"@sphereon/ssi-sdk-ext.key-manager": "0.38.0",
|
|
54
|
+
"@sphereon/ssi-sdk-ext.kms-local": "0.38.0",
|
|
55
|
+
"@sphereon/ssi-sdk.agent-config": "0.38.0",
|
|
56
|
+
"@sphereon/ssi-sdk.public-key-hosting": "0.38.0",
|
|
57
57
|
"@transmute/json-web-signature": "0.7.0-unstable.81",
|
|
58
58
|
"@types/cors": "^2.8.17",
|
|
59
59
|
"@types/express": "^4.17.21",
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
"EBSI",
|
|
89
89
|
"EBSI Authorization Client"
|
|
90
90
|
],
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "a93cb5bf52d46acaf3b2b2d8eba83cc88aa5cda4"
|
|
92
92
|
}
|
package/src/agent/mDLMdoc.ts
CHANGED
|
@@ -52,7 +52,9 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
52
52
|
mdocOid4vpHolderPresent: this.mdocOid4vpHolderPresent.bind(this),
|
|
53
53
|
mdocOid4vpRPVerify: this.mdocOid4vpRPVerify.bind(this),
|
|
54
54
|
}
|
|
55
|
-
private readonly
|
|
55
|
+
private readonly staticTrustAnchors: string[]
|
|
56
|
+
private readonly trustAnchorProvider?: () => string[]
|
|
57
|
+
private readonly blindlyTrustedAnchorProvider?: () => string[]
|
|
56
58
|
private opts: {
|
|
57
59
|
trustRootWhenNoAnchors?: boolean
|
|
58
60
|
allowSingleNoCAChainElement?: boolean
|
|
@@ -61,6 +63,10 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
61
63
|
|
|
62
64
|
constructor(args?: {
|
|
63
65
|
trustAnchors?: string[]
|
|
66
|
+
// Provider returning runtime trust anchors, merged with the static (constructor) trustAnchors on every verification.
|
|
67
|
+
trustAnchorProvider?: () => string[]
|
|
68
|
+
// Provider returning runtime blindly-trusted anchors, merged with opts.blindlyTrustedAnchors on every verification.
|
|
69
|
+
blindlyTrustedAnchorProvider?: () => string[]
|
|
64
70
|
opts?: {
|
|
65
71
|
// Trust the supplied root from the chain, when no anchors are being passed in.
|
|
66
72
|
trustRootWhenNoAnchors?: boolean
|
|
@@ -71,10 +77,21 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
71
77
|
blindlyTrustedAnchors?: string[]
|
|
72
78
|
}
|
|
73
79
|
}) {
|
|
74
|
-
this.
|
|
80
|
+
this.staticTrustAnchors = args?.trustAnchors ?? []
|
|
81
|
+
this.trustAnchorProvider = args?.trustAnchorProvider
|
|
82
|
+
this.blindlyTrustedAnchorProvider = args?.blindlyTrustedAnchorProvider
|
|
75
83
|
this.opts = args?.opts ?? { trustRootWhenNoAnchors: true }
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
// Live-merged anchors: static (constructor) + provider (runtime/user). Read on every verification.
|
|
87
|
+
private get trustAnchors(): string[] {
|
|
88
|
+
return [...this.staticTrustAnchors, ...(this.trustAnchorProvider?.() ?? [])]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private get effectiveBlindlyTrustedAnchors(): string[] {
|
|
92
|
+
return [...(this.opts.blindlyTrustedAnchors ?? []), ...(this.blindlyTrustedAnchorProvider?.() ?? [])]
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
/**
|
|
79
96
|
* Processes and verifies the provided mdoc, generates device response and presentation submission tokens.
|
|
80
97
|
*
|
|
@@ -129,18 +146,46 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
129
146
|
if (!result.error || responseUri.includes('openid.net')) {
|
|
130
147
|
// TODO: We relax for the conformance suite, as the cert would be invalid
|
|
131
148
|
try {
|
|
132
|
-
|
|
149
|
+
// Source the DEVICE key from the matched document's deviceKeyInfo (the mdoc MSO device key). The validation
|
|
150
|
+
// result.keyInfo is the x5chain ISSUER key, which kmp-mdoc-core does not (yet) convert to a CborKey
|
|
151
|
+
// ("we do not convert all properties to a Cborkey yet"), so result.keyInfo.key is undefined. Without a
|
|
152
|
+
// complete device keyInfo the device-response assembly later fails ("Cannot read property 'toJson' of undefined").
|
|
153
|
+
const matchDeviceKeyInfo: any = (match as any).deviceKeyInfo
|
|
154
|
+
const deviceKeyInfoSource: any = matchDeviceKeyInfo ?? result.keyInfo
|
|
155
|
+
console.log(
|
|
156
|
+
`(mdl-mdoc:deviceKey) amend: match.deviceKeyInfo present=${!!matchDeviceKeyInfo}, match.deviceKeyInfo.key present=${!!matchDeviceKeyInfo?.key}, ` +
|
|
157
|
+
`result.keyInfo present=${!!result.keyInfo}, result.keyInfo.key present=${!!result.keyInfo?.key}, source=${matchDeviceKeyInfo ? 'match.deviceKeyInfo' : 'result.keyInfo'}`,
|
|
158
|
+
)
|
|
159
|
+
const cborKey = deviceKeyInfoSource?.key ? CoseKeyCbor.Static.fromDTO(deviceKeyInfoSource.key) : undefined
|
|
133
160
|
if (!cborKey) {
|
|
134
|
-
|
|
161
|
+
// Note: this is the PUBLIC device key only (the private key is hardware-backed in the KMS). We only need
|
|
162
|
+
// the public key here to look up the matching KMS key reference by thumbprint.
|
|
163
|
+
throw Error(
|
|
164
|
+
'No device (public) key found to amend: neither match.deviceKeyInfo.key nor result.keyInfo.key is populated. ' +
|
|
165
|
+
'The mdoc MSO device public key is required to resolve the hardware-backed KMS key for signing.',
|
|
166
|
+
)
|
|
135
167
|
}
|
|
136
168
|
let jwk = CoseJoseKeyMappingService.toJoseJwk(cborKey).toJsonDTO<JWK>()
|
|
137
|
-
if (!
|
|
138
|
-
const keyInfo =
|
|
139
|
-
const
|
|
169
|
+
if (!deviceKeyInfoSource?.kmsKeyRef) {
|
|
170
|
+
const keyInfo = deviceKeyInfoSource
|
|
171
|
+
const thumbprint = calculateJwkThumbprint({ jwk: jwk })
|
|
172
|
+
const kid = jwk.kid ?? thumbprint
|
|
173
|
+
console.log(`(mdl-mdoc:deviceKey) amend: resolved device public jwk kid=${jwk.kid}, thumbprint=${thumbprint}`)
|
|
140
174
|
|
|
141
|
-
|
|
175
|
+
// The device COSE key kid can be the (mangled) x-coordinate; the KMS resolves the key by its JWK
|
|
176
|
+
// thumbprint, so fall back to that when the kid lookup fails.
|
|
177
|
+
let key
|
|
178
|
+
try {
|
|
179
|
+
key = await _context.agent.keyManagerGet({ kid })
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.log(
|
|
182
|
+
`(mdl-mdoc:deviceKey) amend: keyManagerGet by kid '${kid}' failed (${(e as any)?.message}); retrying by thumbprint '${thumbprint}'`,
|
|
183
|
+
)
|
|
184
|
+
key = await _context.agent.keyManagerGet({ kid: thumbprint })
|
|
185
|
+
}
|
|
142
186
|
const kms = key.kms
|
|
143
|
-
const kmsKeyRef = key.meta?.kmsKeyRef
|
|
187
|
+
const kmsKeyRef = key.meta?.kmsKeyRef ?? key.kid
|
|
188
|
+
console.log(`(mdl-mdoc:deviceKey) amend: resolved hardware KMS key kms=${kms}, kmsKeyRef=${kmsKeyRef}`)
|
|
144
189
|
const updateCborKey = cborKey.copy(false, cborKey.kty, cborKey.kid ?? new CborByteString(decodeFrom(kid, Encoding.UTF8)))
|
|
145
190
|
const deviceKeyInfo = KeyInfo.Static.fromDTO(keyInfo).copy(
|
|
146
191
|
kid,
|
|
@@ -171,17 +216,100 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
171
216
|
}
|
|
172
217
|
return Promise.reject(Error('No matching documents found'))
|
|
173
218
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
// Log all createDeviceResponse arguments so we can reason about the post-sign 'toJson of undefined' crash.
|
|
220
|
+
try {
|
|
221
|
+
console.log(
|
|
222
|
+
`(mdl-mdoc:deviceResponse) args: clientId=${clientId}, responseUri=${responseUri}, authorizationRequestNonce=${authorizationRequestNonce}, docCount=${docsAndDescriptors.length}`,
|
|
223
|
+
)
|
|
224
|
+
try {
|
|
225
|
+
console.log(`(mdl-mdoc:deviceResponse) presentationDefinition=${JSON.stringify(presentationDefinition)}`)
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
console.log(`(mdl-mdoc:deviceResponse) presentationDefinition stringify failed: ${e?.message}`)
|
|
228
|
+
}
|
|
229
|
+
docsAndDescriptors.forEach((d: any, idx: number) => {
|
|
230
|
+
const dk: any = d?.deviceKeyInfo
|
|
231
|
+
let docType: any = undefined
|
|
232
|
+
try {
|
|
233
|
+
docType = d?.document?.docType?.value ?? d?.document?.MSO?.value?.docType?.value ?? d?.document?.getDocType?.()
|
|
234
|
+
} catch {
|
|
235
|
+
/* ignore */
|
|
236
|
+
}
|
|
237
|
+
console.log(
|
|
238
|
+
`(mdl-mdoc:deviceResponse) doc[${idx}]: inputDescriptor.id=${d?.inputDescriptor?.id?.value ?? d?.inputDescriptor?.id}, docType=${docType}, ` +
|
|
239
|
+
`document present=${!!d?.document}, documentError present=${!!d?.documentError}, ` +
|
|
240
|
+
`deviceKeyInfo present=${!!dk}, deviceKeyInfo.key present=${!!dk?.key}, deviceKeyInfo.kid=${dk?.kid}, kmsKeyRef=${dk?.kmsKeyRef}, kms=${dk?.kms}, ` +
|
|
241
|
+
`signatureAlgorithm=${dk?.signatureAlgorithm}, x5c present=${!!dk?.x5c}`,
|
|
242
|
+
)
|
|
243
|
+
try {
|
|
244
|
+
const dkJson = dk?.toJson ? dk.toJson() : dk
|
|
245
|
+
console.log(`(mdl-mdoc:deviceResponse) doc[${idx}] deviceKeyInfo=${JSON.stringify(dkJson)}`)
|
|
246
|
+
} catch (e: any) {
|
|
247
|
+
console.log(`(mdl-mdoc:deviceResponse) doc[${idx}] deviceKeyInfo serialize failed: ${e?.message}`)
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const keyJson = dk?.key?.toJson ? dk.key.toJson() : dk?.key
|
|
251
|
+
console.log(`(mdl-mdoc:deviceResponse) doc[${idx}] deviceKeyInfo.key=${JSON.stringify(keyJson)}`)
|
|
252
|
+
} catch (e: any) {
|
|
253
|
+
console.log(`(mdl-mdoc:deviceResponse) doc[${idx}] deviceKeyInfo.key serialize failed: ${e?.message}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
console.log(`(mdl-mdoc:deviceResponse) argument logging failed: ${e?.message}`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let deviceResponse
|
|
261
|
+
try {
|
|
262
|
+
deviceResponse = await oid4vpService.createDeviceResponse(
|
|
263
|
+
docsAndDescriptors,
|
|
264
|
+
presentationDefinition as IOid4VPPresentationDefinition,
|
|
265
|
+
clientId,
|
|
266
|
+
responseUri,
|
|
267
|
+
authorizationRequestNonce,
|
|
268
|
+
)
|
|
269
|
+
} catch (e: any) {
|
|
270
|
+
console.log(`(mdl-mdoc:deviceResponse) createDeviceResponse failed: ${e?.message}`)
|
|
271
|
+
console.log(`(mdl-mdoc:deviceResponse) STACK: ${e?.stack}`)
|
|
272
|
+
throw e
|
|
273
|
+
}
|
|
274
|
+
// NOTE: the 'Cannot read property toJson of undefined' crash happens HERE (after createDeviceResponse returns),
|
|
275
|
+
// during cborEncode() of the assembled DeviceResponse — NOT inside createDeviceResponse. Probe + stack-log it.
|
|
276
|
+
try {
|
|
277
|
+
console.log(`(mdl-mdoc:deviceResponse) createDeviceResponse returned: present=${!!deviceResponse}, type=${typeof deviceResponse}`)
|
|
278
|
+
const dr: any = deviceResponse as any
|
|
279
|
+
try {
|
|
280
|
+
const docs = dr?.documents ?? dr?.b3p_1 ?? undefined
|
|
281
|
+
console.log(
|
|
282
|
+
`(mdl-mdoc:deviceResponse) deviceResponse.version present=${!!dr?.version}, status present=${dr?.status != null}, ` +
|
|
283
|
+
`documents present=${!!docs}, documentsCount=${docs?.length ?? docs?.size ?? 'n/a'}`,
|
|
284
|
+
)
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
console.log(`(mdl-mdoc:deviceResponse) deviceResponse structure probe failed: ${e?.message}`)
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
/* ignore */
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let vp_token: string
|
|
293
|
+
try {
|
|
294
|
+
const encoded = deviceResponse.cborEncode()
|
|
295
|
+
console.log(`(mdl-mdoc:deviceResponse) cborEncode OK, byteLen=${encoded?.length}`)
|
|
296
|
+
vp_token = encodeTo(encoded, Encoding.BASE64URL)
|
|
297
|
+
} catch (e: any) {
|
|
298
|
+
console.log(`(mdl-mdoc:deviceResponse) cborEncode failed: ${e?.message}`)
|
|
299
|
+
console.log(`(mdl-mdoc:deviceResponse) cborEncode STACK: ${e?.stack}`)
|
|
300
|
+
throw e
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let presentation_submission
|
|
304
|
+
try {
|
|
305
|
+
presentation_submission = Oid4VPPresentationSubmission.Static.fromPresentationDefinition(
|
|
306
|
+
presentationDefinition as IOid4VPPresentationDefinition,
|
|
307
|
+
)
|
|
308
|
+
} catch (e: any) {
|
|
309
|
+
console.log(`(mdl-mdoc:deviceResponse) fromPresentationDefinition failed: ${e?.message}`)
|
|
310
|
+
console.log(`(mdl-mdoc:deviceResponse) fromPresentationDefinition STACK: ${e?.stack}`)
|
|
311
|
+
throw e
|
|
312
|
+
}
|
|
185
313
|
return { vp_token, presentation_submission }
|
|
186
314
|
}
|
|
187
315
|
|
|
@@ -274,7 +402,7 @@ export class MDLMdoc implements IAgentPlugin {
|
|
|
274
402
|
const validationResult = await new X509CallbackService(Array.from(mergedAnchors)).verifyCertificateChain({
|
|
275
403
|
...args,
|
|
276
404
|
trustAnchors: Array.from(trustAnchors),
|
|
277
|
-
opts: { ...args?.opts, ...this.opts },
|
|
405
|
+
opts: { ...args?.opts, ...this.opts, blindlyTrustedAnchors: this.effectiveBlindlyTrustedAnchors },
|
|
278
406
|
})
|
|
279
407
|
console.log(
|
|
280
408
|
`x509 validation for ${validationResult.error ? 'Error' : 'Success'}. message: ${validationResult.message}, details: ${validationResult.detailMessage}`,
|
package/src/functions/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ import * as crypto from 'crypto'
|
|
|
17
17
|
import { Certificate, CryptoEngine, setEngine } from 'pkijs'
|
|
18
18
|
// @ts-ignore
|
|
19
19
|
import { fromString } from 'uint8arrays/from-string'
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
import { toString as u8aToString } from 'uint8arrays/to-string'
|
|
20
22
|
import { IRequiredContext, VerifyCertificateChainArgs } from '../types/ImDLMdoc'
|
|
21
23
|
|
|
22
24
|
type CoseKeyCbor = mdocPkg.com.sphereon.crypto.cose.CoseKeyCbor
|
|
@@ -31,13 +33,238 @@ type Jwk = mdocPkg.com.sphereon.crypto.jose.Jwk
|
|
|
31
33
|
const KeyInfo = mdocPkg.com.sphereon.crypto.KeyInfo
|
|
32
34
|
type X509VerificationProfile = mdocPkg.com.sphereon.crypto.X509VerificationProfile
|
|
33
35
|
const DateTimeUtils = mdocPkg.com.sphereon.kmp.DateTimeUtils
|
|
34
|
-
const decodeFrom = mdocPkg.com.sphereon.kmp.decodeFrom
|
|
35
|
-
const encodeTo = mdocPkg.com.sphereon.kmp.encodeTo
|
|
36
|
-
const Encoding = mdocPkg.com.sphereon.kmp.Encoding
|
|
37
36
|
type LocalDateTimeKMP = mdocPkg.com.sphereon.kmp.LocalDateTimeKMP
|
|
38
37
|
const SignatureAlgorithm = mdocPkg.com.sphereon.crypto.generic.SignatureAlgorithm
|
|
39
38
|
const DefaultCallbacks = mdocPkg.com.sphereon.crypto.DefaultCallbacks
|
|
40
39
|
|
|
40
|
+
// ---------- Minimal CBOR helpers for the kmp-mdoc-core kid-mangling workaround ----------
|
|
41
|
+
function toU8(bytes: unknown): Uint8Array {
|
|
42
|
+
if (bytes instanceof Uint8Array) return bytes
|
|
43
|
+
if (ArrayBuffer.isView(bytes)) {
|
|
44
|
+
const v = bytes as ArrayBufferView
|
|
45
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(bytes)) return Uint8Array.from((bytes as number[]).map((b) => b & 0xff))
|
|
48
|
+
throw new Error('unsupported raw bytes type')
|
|
49
|
+
}
|
|
50
|
+
function extractIssuerAuthRawParts(rawInput: unknown): { protectedBytes: Uint8Array; payloadBytes: Uint8Array } | undefined {
|
|
51
|
+
const u8 = toU8(rawInput)
|
|
52
|
+
let pos = 0
|
|
53
|
+
const readHead = (): { mt: number; len: number } => {
|
|
54
|
+
const b = u8[pos++]
|
|
55
|
+
const mt = b >> 5
|
|
56
|
+
const info = b & 0x1f
|
|
57
|
+
let len: number
|
|
58
|
+
if (info < 24) len = info
|
|
59
|
+
else if (info === 24) {
|
|
60
|
+
len = u8[pos]
|
|
61
|
+
pos += 1
|
|
62
|
+
} else if (info === 25) {
|
|
63
|
+
len = (u8[pos] << 8) | u8[pos + 1]
|
|
64
|
+
pos += 2
|
|
65
|
+
} else if (info === 26) {
|
|
66
|
+
len = u8[pos] * 0x1000000 + (u8[pos + 1] << 16) + (u8[pos + 2] << 8) + u8[pos + 3]
|
|
67
|
+
pos += 4
|
|
68
|
+
} else throw new Error('unsupported cbor length info ' + info)
|
|
69
|
+
return { mt, len }
|
|
70
|
+
}
|
|
71
|
+
const skip = (): void => {
|
|
72
|
+
const h = readHead()
|
|
73
|
+
switch (h.mt) {
|
|
74
|
+
case 0:
|
|
75
|
+
case 1:
|
|
76
|
+
case 7:
|
|
77
|
+
return
|
|
78
|
+
case 2:
|
|
79
|
+
case 3:
|
|
80
|
+
pos += h.len
|
|
81
|
+
return
|
|
82
|
+
case 4:
|
|
83
|
+
for (let i = 0; i < h.len; i++) skip()
|
|
84
|
+
return
|
|
85
|
+
case 5:
|
|
86
|
+
for (let i = 0; i < h.len * 2; i++) skip()
|
|
87
|
+
return
|
|
88
|
+
case 6:
|
|
89
|
+
skip()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const readBstr = (): Uint8Array => {
|
|
94
|
+
const h = readHead()
|
|
95
|
+
if (h.mt !== 2) throw new Error('expected bstr, got mt ' + h.mt)
|
|
96
|
+
const out = u8.slice(pos, pos + h.len)
|
|
97
|
+
pos += h.len
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
const readTstr = (): string => {
|
|
101
|
+
const h = readHead()
|
|
102
|
+
if (h.mt !== 3) throw new Error('expected tstr, got mt ' + h.mt)
|
|
103
|
+
const out = new TextDecoder().decode(u8.slice(pos, pos + h.len))
|
|
104
|
+
pos += h.len
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
const outer = readHead()
|
|
108
|
+
if (outer.mt !== 5) return undefined
|
|
109
|
+
for (let i = 0; i < outer.len; i++) {
|
|
110
|
+
const key = readTstr()
|
|
111
|
+
if (key === 'issuerAuth') {
|
|
112
|
+
const arr = readHead()
|
|
113
|
+
if (arr.mt !== 4 || arr.len !== 4) throw new Error('issuerAuth is not a 4-element array')
|
|
114
|
+
const protectedBytes = readBstr()
|
|
115
|
+
skip()
|
|
116
|
+
const payloadBytes = readBstr()
|
|
117
|
+
return { protectedBytes, payloadBytes }
|
|
118
|
+
}
|
|
119
|
+
skip()
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
function encodeBstrHeader(len: number): Uint8Array {
|
|
124
|
+
if (len < 24) return new Uint8Array([0x40 | len])
|
|
125
|
+
if (len < 0x100) return new Uint8Array([0x58, len])
|
|
126
|
+
if (len < 0x10000) return new Uint8Array([0x59, (len >> 8) & 0xff, len & 0xff])
|
|
127
|
+
return new Uint8Array([0x5a, (len >>> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff])
|
|
128
|
+
}
|
|
129
|
+
function concatU8(parts: Uint8Array[]): Uint8Array {
|
|
130
|
+
let total = 0
|
|
131
|
+
for (const p of parts) total += p.length
|
|
132
|
+
const out = new Uint8Array(total)
|
|
133
|
+
let off = 0
|
|
134
|
+
for (const p of parts) {
|
|
135
|
+
out.set(p, off)
|
|
136
|
+
off += p.length
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
function buildSig1Structure(protectedBytes: Uint8Array, payloadBytes: Uint8Array): Uint8Array {
|
|
141
|
+
const sig1Label = new Uint8Array([0x6a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x31])
|
|
142
|
+
return concatU8([
|
|
143
|
+
new Uint8Array([0x84]),
|
|
144
|
+
sig1Label,
|
|
145
|
+
encodeBstrHeader(protectedBytes.length),
|
|
146
|
+
protectedBytes,
|
|
147
|
+
new Uint8Array([0x40]),
|
|
148
|
+
encodeBstrHeader(payloadBytes.length),
|
|
149
|
+
payloadBytes,
|
|
150
|
+
])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Convert a DER-encoded ECDSA signature (SEQUENCE { INTEGER r, INTEGER s }) to the raw fixed-width r||s
|
|
154
|
+
// form that COSE_Sign1 requires (each coordinate left-padded to `coordSize`, e.g. 32 bytes for ES256/P-256).
|
|
155
|
+
function derEcdsaToRaw(der: Uint8Array, coordSize: number): Uint8Array {
|
|
156
|
+
let offset = 0
|
|
157
|
+
if (der[offset++] !== 0x30) throw new Error('Invalid DER ECDSA signature: missing SEQUENCE tag')
|
|
158
|
+
let seqLen = der[offset++]
|
|
159
|
+
if (seqLen & 0x80) {
|
|
160
|
+
const numBytes = seqLen & 0x7f
|
|
161
|
+
seqLen = 0
|
|
162
|
+
for (let i = 0; i < numBytes; i++) seqLen = (seqLen << 8) | der[offset++]
|
|
163
|
+
}
|
|
164
|
+
const readInt = (): Uint8Array => {
|
|
165
|
+
if (der[offset++] !== 0x02) throw new Error('Invalid DER ECDSA signature: missing INTEGER tag')
|
|
166
|
+
const len = der[offset++]
|
|
167
|
+
let val = der.slice(offset, offset + len)
|
|
168
|
+
offset += len
|
|
169
|
+
let start = 0
|
|
170
|
+
while (start < val.length - 1 && val[start] === 0x00) start++ // strip DER sign/leading zero bytes
|
|
171
|
+
val = val.slice(start)
|
|
172
|
+
if (val.length > coordSize) throw new Error(`Invalid DER ECDSA signature: integer (${val.length}) exceeds ${coordSize}`)
|
|
173
|
+
const out = new Uint8Array(coordSize)
|
|
174
|
+
out.set(val, coordSize - val.length) // left-pad
|
|
175
|
+
return out
|
|
176
|
+
}
|
|
177
|
+
const r = readInt()
|
|
178
|
+
const s = readInt()
|
|
179
|
+
return concatU8([r, s])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The KMS/MUSAP bridge returns the signature as a base64 (or base64url) string. COSE needs raw r||s bytes.
|
|
183
|
+
// Normalize to url-safe unpadded base64, decode, and DER->raw-convert when the bytes are a DER ECDSA signature.
|
|
184
|
+
function decodeKmsSignatureToRaw(signature: string, coordSize: number): Uint8Array {
|
|
185
|
+
const normalized = signature.trim().replace(/\s+/g, '').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
186
|
+
const bytes = fromString(normalized, 'base64url') as Uint8Array
|
|
187
|
+
if (bytes.length > coordSize * 2 && bytes[0] === 0x30) {
|
|
188
|
+
return derEcdsaToRaw(bytes, coordSize)
|
|
189
|
+
}
|
|
190
|
+
return bytes
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------- Minimal CBOR length walker (definite-length items only) ----------
|
|
194
|
+
function cborHeader(buf: Uint8Array, off: number): { major: number; headerLen: number; argument: number } {
|
|
195
|
+
const ib = buf[off]
|
|
196
|
+
const major = ib >> 5
|
|
197
|
+
const ai = ib & 0x1f
|
|
198
|
+
if (ai < 24) return { major, headerLen: 1, argument: ai }
|
|
199
|
+
if (ai === 24) return { major, headerLen: 2, argument: buf[off + 1] }
|
|
200
|
+
if (ai === 25) return { major, headerLen: 3, argument: (buf[off + 1] << 8) | buf[off + 2] }
|
|
201
|
+
if (ai === 26) return { major, headerLen: 5, argument: buf[off + 1] * 0x1000000 + (buf[off + 2] << 16) + (buf[off + 3] << 8) + buf[off + 4] }
|
|
202
|
+
if (ai === 27) {
|
|
203
|
+
let v = 0
|
|
204
|
+
for (let i = 1; i <= 8; i++) v = v * 256 + buf[off + i]
|
|
205
|
+
return { major, headerLen: 9, argument: v }
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`unsupported CBOR additional-info ${ai} at offset ${off}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cborItemLen(buf: Uint8Array, off: number): number {
|
|
211
|
+
const h = cborHeader(buf, off)
|
|
212
|
+
let total = h.headerLen
|
|
213
|
+
switch (h.major) {
|
|
214
|
+
case 0: // uint
|
|
215
|
+
case 1: // negint
|
|
216
|
+
case 7: // simple/float (null/true/false/floats — argument captured in headerLen)
|
|
217
|
+
break
|
|
218
|
+
case 2: // bstr
|
|
219
|
+
case 3: // tstr
|
|
220
|
+
total += h.argument
|
|
221
|
+
break
|
|
222
|
+
case 4: // array
|
|
223
|
+
for (let i = 0; i < h.argument; i++) total += cborItemLen(buf, off + total)
|
|
224
|
+
break
|
|
225
|
+
case 5: // map
|
|
226
|
+
for (let i = 0; i < h.argument * 2; i++) total += cborItemLen(buf, off + total)
|
|
227
|
+
break
|
|
228
|
+
case 6: // tag (one following item)
|
|
229
|
+
total += cborItemLen(buf, off + total)
|
|
230
|
+
break
|
|
231
|
+
default:
|
|
232
|
+
throw new Error(`unsupported CBOR major type ${h.major}`)
|
|
233
|
+
}
|
|
234
|
+
return total
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The bundled kmp-mdoc-core builds the DeviceAuth COSE Sig_structure non-conformantly: per ISO 18013-5 §9.1.3 (and the
|
|
238
|
+
// reference impls auth0/mdl + IDK) the signed payload MUST be DeviceAuthenticationBytes = #6.24(bstr .cbor
|
|
239
|
+
// ["DeviceAuthentication", [null,null,OpenID4VPHandover], docType, #6.24(bstr .cbor DeviceNameSpaces)]), and the
|
|
240
|
+
// Sig_structure payload = bstr(DeviceAuthenticationBytes). kmp omits BOTH tag-24 wrappers (element 4 + the outer one).
|
|
241
|
+
// Re-wrap them here so the device signature matches a conformant verifier. (Element 2 = the SessionTranscript
|
|
242
|
+
// [null,null,handover] is already produced by the kmp-mdoc-core handover patch.) "Signature1" / protected / external_aad
|
|
243
|
+
// are left untouched. Returns the input unchanged if the structure isn't the expected DeviceAuth Sig_structure.
|
|
244
|
+
function reconstructMdocDeviceAuthSigStructure(sig: Uint8Array): Uint8Array {
|
|
245
|
+
if (sig[0] !== 0x84) return sig // Sig_structure must be a 4-element array
|
|
246
|
+
let off = 1
|
|
247
|
+
off += cborItemLen(sig, off) // "Signature1"
|
|
248
|
+
off += cborItemLen(sig, off) // protected (bstr)
|
|
249
|
+
off += cborItemLen(sig, off) // external_aad (bstr)
|
|
250
|
+
const payloadStart = off
|
|
251
|
+
const ph = cborHeader(sig, payloadStart)
|
|
252
|
+
if (ph.major !== 2) return sig // payload must be a bstr
|
|
253
|
+
const daStart = payloadStart + ph.headerLen
|
|
254
|
+
const da = sig.subarray(daStart, daStart + ph.argument) // cbor(DeviceAuthentication)
|
|
255
|
+
if (da[0] !== 0x84) return sig // DeviceAuthentication must be a 4-element array
|
|
256
|
+
let d = 1
|
|
257
|
+
d += cborItemLen(da, d) // "DeviceAuthentication"
|
|
258
|
+
d += cborItemLen(da, d) // SessionTranscript ([null,null,handover])
|
|
259
|
+
d += cborItemLen(da, d) // docType
|
|
260
|
+
const e4 = da.subarray(d, d + cborItemLen(da, d)) // deviceNameSpaces (currently un-tagged)
|
|
261
|
+
const e4Tagged = concatU8([new Uint8Array([0xd8, 0x18]), encodeBstrHeader(e4.length), e4]) // #6.24(bstr deviceNameSpaces)
|
|
262
|
+
const daCorrected = concatU8([da.subarray(0, d), e4Tagged])
|
|
263
|
+
const deviceAuthBytes = concatU8([new Uint8Array([0xd8, 0x18]), encodeBstrHeader(daCorrected.length), daCorrected]) // #6.24(bstr DeviceAuthentication)
|
|
264
|
+
const newPayload = concatU8([encodeBstrHeader(deviceAuthBytes.length), deviceAuthBytes]) // bstr(DeviceAuthenticationBytes)
|
|
265
|
+
return concatU8([sig.subarray(0, payloadStart), newPayload])
|
|
266
|
+
}
|
|
267
|
+
|
|
41
268
|
export class CoseCryptoService implements ICoseCryptoCallbackJS {
|
|
42
269
|
constructor(private context?: IRequiredContext) {}
|
|
43
270
|
|
|
@@ -50,7 +277,28 @@ export class CoseCryptoService implements ICoseCryptoCallbackJS {
|
|
|
50
277
|
throw Error('No context provided. Please provide a context with the setContext method or constructor')
|
|
51
278
|
}
|
|
52
279
|
const { keyInfo, alg, value } = input
|
|
280
|
+
// The kmp-mdoc-core Sig_structure omits the ISO 18013-5 §9.1.3 tag-24 wrappers around deviceNameSpaces and the
|
|
281
|
+
// whole DeviceAuthentication. Re-wrap them so the signed bytes match a conformant verifier (auth0/mdl, IDK).
|
|
282
|
+
let toBeSigned: Uint8Array = toU8(value as any)
|
|
283
|
+
try {
|
|
284
|
+
toBeSigned = reconstructMdocDeviceAuthSigStructure(toBeSigned)
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
console.log(`(mdl-mdoc:sign) Sig_structure tag-24 reconstruction failed, signing kmp original: ${e?.message}`)
|
|
287
|
+
}
|
|
288
|
+
// DIAGNOSTIC: dump the exact COSE Sig_structure (ToBeSigned) the holder signs, so it can be diffed
|
|
289
|
+
// byte-for-byte against the verifier's reconstructed DeviceAuthentication (handover/transcript debugging).
|
|
290
|
+
try {
|
|
291
|
+
let hex = ''
|
|
292
|
+
for (let i = 0; i < toBeSigned.length; i++) hex += (toBeSigned[i] & 0xff).toString(16).padStart(2, '0')
|
|
293
|
+
console.log(`(mdl-mdoc:sign) ToBeSigned len=${toBeSigned.length} hex=${hex}`)
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
console.log(`(mdl-mdoc:sign) ToBeSigned hex failed: ${e?.message}`)
|
|
296
|
+
}
|
|
53
297
|
let kmsKeyRef = keyInfo.kmsKeyRef ?? undefined
|
|
298
|
+
// Additional key references to try if the primary keyRef is not found in the KMS. The COSE key kid coming from
|
|
299
|
+
// an mdoc deviceKey can be the (mangled) x-coordinate rather than the KMS kid; the KMS can also resolve a key by
|
|
300
|
+
// its JWK thumbprint, so we fall back to that.
|
|
301
|
+
const fallbackKeyRefs: Array<string> = []
|
|
54
302
|
if (!kmsKeyRef) {
|
|
55
303
|
const key = keyInfo.key
|
|
56
304
|
if (key == null) {
|
|
@@ -59,19 +307,52 @@ export class CoseCryptoService implements ICoseCryptoCallbackJS {
|
|
|
59
307
|
const resolvedKeyInfo = com.sphereon.crypto.ResolvedKeyInfo.Static.fromKeyInfo(keyInfo, key)
|
|
60
308
|
const jwkKeyInfo: mdocPkg.com.sphereon.crypto.ResolvedKeyInfo<Jwk> = CoseJoseKeyMappingService.toResolvedJwkKeyInfo(resolvedKeyInfo)
|
|
61
309
|
|
|
62
|
-
const
|
|
310
|
+
const thumbprint = calculateJwkThumbprint({ jwk: jwkKeyInfo.key.toJsonDTO() })
|
|
311
|
+
const kid = jwkKeyInfo.kid ?? thumbprint ?? jwkKeyInfo.key.getKidAsString(true)
|
|
63
312
|
if (!kid) {
|
|
64
313
|
return Promise.reject(Error('No kid present and not kmsKeyRef provided'))
|
|
65
314
|
}
|
|
66
315
|
kmsKeyRef = kid
|
|
316
|
+
if (thumbprint && thumbprint !== kid) {
|
|
317
|
+
fallbackKeyRefs.push(thumbprint)
|
|
318
|
+
}
|
|
67
319
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
320
|
+
const doSign = (keyRef: string): Promise<string> =>
|
|
321
|
+
this.context!.agent.keyManagerSign({
|
|
322
|
+
algorithm: alg.jose!!.value,
|
|
323
|
+
// Pass the raw ToBeSigned (COSE Sig_structure) bytes as base64. The previous `encodeTo(value, UTF8)`
|
|
324
|
+
// interpreted the binary CBOR as UTF-8 text, corrupting every non-ASCII byte before the KMS even saw it
|
|
325
|
+
// (the MUSAP bridge then signed the mangled bytes -> verifier "Signature invalid"). base64 round-trips losslessly.
|
|
326
|
+
data: u8aToString(toBeSigned, 'base64'),
|
|
327
|
+
encoding: 'base64',
|
|
328
|
+
keyRef,
|
|
329
|
+
})
|
|
330
|
+
let result: string
|
|
331
|
+
try {
|
|
332
|
+
result = await doSign(kmsKeyRef!!)
|
|
333
|
+
} catch (error) {
|
|
334
|
+
let signed: string | undefined
|
|
335
|
+
for (const ref of fallbackKeyRefs) {
|
|
336
|
+
try {
|
|
337
|
+
signed = await doSign(ref)
|
|
338
|
+
break
|
|
339
|
+
} catch {
|
|
340
|
+
// try the next fallback key reference
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (signed === undefined) {
|
|
344
|
+
throw error
|
|
345
|
+
}
|
|
346
|
+
result = signed
|
|
347
|
+
}
|
|
348
|
+
// COSE_Sign1 needs the raw fixed-width r||s signature, not the base64(url)/DER form the KMS returns.
|
|
349
|
+
// (Previously this returned `decodeFrom(result, Encoding.UTF8)` — the UTF-8 bytes of the base64 string —
|
|
350
|
+
// which the verifier rejected with "Expected signature size 64, received: 86".)
|
|
351
|
+
const joseAlg = alg.jose?.value
|
|
352
|
+
const coordSize = joseAlg === 'ES512' ? 66 : joseAlg === 'ES384' ? 48 : 32
|
|
353
|
+
const raw = decodeKmsSignatureToRaw(result, coordSize)
|
|
354
|
+
console.log(`(mdl-mdoc:sign) signature decoded: alg=${joseAlg}, inputChars=${result.length}, rawLen=${raw.length} (expected ${coordSize * 2})`)
|
|
355
|
+
return Int8Array.from(raw)
|
|
75
356
|
}
|
|
76
357
|
|
|
77
358
|
async verify1Async<CborType>(
|
|
@@ -145,9 +426,27 @@ export class CoseCryptoService implements ICoseCryptoCallbackJS {
|
|
|
145
426
|
)
|
|
146
427
|
const recalculatedToBeSigned = input.toBeSignedJson(issuerCoseKeyInfo, SignatureAlgorithm.Static.fromCose(coseAlg))
|
|
147
428
|
const key = CoseJoseKeyMappingService.toJoseJwk(issuerCoseKeyInfo.key!).toJsonDTO<JWK>()
|
|
429
|
+
let data = fromString(recalculatedToBeSigned.base64UrlValue, 'base64url')
|
|
430
|
+
const signatureBytes = fromString(sign1Json.signature, 'base64url')
|
|
431
|
+
// Workaround: kmp-mdoc-core mangles binary protected-header values (e.g. a bstr kid)
|
|
432
|
+
// by round-tripping them through a UTF-8 String. When the caller stashed the raw mdoc
|
|
433
|
+
// bytes on globalThis (see @sphereon/ssi-sdk.credential-validation cvVerifyMdoc), reparse
|
|
434
|
+
// them here to build the Sig_structure from the untouched protected/payload bstrs, instead
|
|
435
|
+
// of relying on input.toBeSignedJson() which re-encodes the mangled protected header.
|
|
436
|
+
const rawMdocBytes = (globalThis as unknown as { __sphereon_mdoc_raw_bytes?: Uint8Array }).__sphereon_mdoc_raw_bytes
|
|
437
|
+
if (rawMdocBytes) {
|
|
438
|
+
try {
|
|
439
|
+
const extracted = extractIssuerAuthRawParts(rawMdocBytes)
|
|
440
|
+
if (extracted) {
|
|
441
|
+
data = buildSig1Structure(extracted.protectedBytes, extracted.payloadBytes)
|
|
442
|
+
}
|
|
443
|
+
} catch (e) {
|
|
444
|
+
console.warn('[mdl-mdoc verify] failed to reparse raw mdoc; falling back to kmp-computed Sig_structure:', (e as Error).message)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
148
447
|
const valid = await verifyRawSignature({
|
|
149
|
-
data
|
|
150
|
-
signature:
|
|
448
|
+
data,
|
|
449
|
+
signature: signatureBytes,
|
|
151
450
|
key,
|
|
152
451
|
})
|
|
153
452
|
|