@animo-id/eudi-wallet-functionality 0.0.0-alpha-20250603091506

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.
@@ -0,0 +1,14 @@
1
+ import { AgentContext } from '@credo-ts/core';
2
+ import { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc';
3
+
4
+ type VerifyAuthorizationRequestOptions = {
5
+ resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest;
6
+ trustedCertificates?: Array<string>;
7
+ allowUntrustedSigned?: boolean;
8
+ };
9
+ declare const verifyOpenid4VpAuthorizationRequest: (agentContext: AgentContext, { resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql }, trustedCertificates, allowUntrustedSigned, }: VerifyAuthorizationRequestOptions) => Promise<{
10
+ isValidButUntrusted: boolean;
11
+ isValidAndTrusted: boolean;
12
+ }[] | undefined>;
13
+
14
+ export { verifyOpenid4VpAuthorizationRequest };
@@ -0,0 +1,14 @@
1
+ import { AgentContext } from '@credo-ts/core';
2
+ import { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc';
3
+
4
+ type VerifyAuthorizationRequestOptions = {
5
+ resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest;
6
+ trustedCertificates?: Array<string>;
7
+ allowUntrustedSigned?: boolean;
8
+ };
9
+ declare const verifyOpenid4VpAuthorizationRequest: (agentContext: AgentContext, { resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql }, trustedCertificates, allowUntrustedSigned, }: VerifyAuthorizationRequestOptions) => Promise<{
10
+ isValidButUntrusted: boolean;
11
+ isValidAndTrusted: boolean;
12
+ }[] | undefined>;
13
+
14
+ export { verifyOpenid4VpAuthorizationRequest };
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ verifyOpenid4VpAuthorizationRequest: () => verifyOpenid4VpAuthorizationRequest
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/verifyOpenid4VpAuthorizationRequest.ts
38
+ var import_core2 = require("@credo-ts/core");
39
+ var import_zod = __toESM(require("zod"));
40
+
41
+ // src/isDcqlQueryEqualOrSubset.ts
42
+ var import_core = require("@credo-ts/core");
43
+ function isDcqlQueryEqualOrSubset(arq, rcq) {
44
+ if (rcq.credential_sets) {
45
+ return false;
46
+ }
47
+ if (rcq.credentials.some((c) => c.id)) {
48
+ return false;
49
+ }
50
+ if (arq.credentials.some((c) => c.format !== "mso_mdoc" && c.format !== "vc+sd-jwt" && c.format !== "dc+sd-jwt")) {
51
+ return false;
52
+ }
53
+ credentialQueryLoop: for (const credentialQuery of arq.credentials) {
54
+ const matchingRcqCredentialQueriesBasedOnFormat = rcq.credentials.filter((c) => c.format === credentialQuery.format);
55
+ if (matchingRcqCredentialQueriesBasedOnFormat.length === 0) return false;
56
+ switch (credentialQuery.format) {
57
+ case "mso_mdoc": {
58
+ const doctypeValue = credentialQuery.meta?.doctype_value;
59
+ if (!doctypeValue) return false;
60
+ if (typeof credentialQuery.meta?.doctype_value !== "string") return false;
61
+ const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(
62
+ (c) => !!(c.format === "mso_mdoc" && c.meta && c.meta.doctype_value === doctypeValue)
63
+ );
64
+ if (foundMatchingRequests.length === 0) return false;
65
+ let foundFullyMatching = false;
66
+ for (const matchedRequest of foundMatchingRequests) {
67
+ if (!matchedRequest.claims) continue credentialQueryLoop;
68
+ if (!credentialQuery.claims) continue credentialQueryLoop;
69
+ const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(
70
+ (c) => "path" in c && matchedRequest.claims?.some(
71
+ (mrc) => "path" in mrc && c.path[0] === mrc.path[0] && c.path[1] === mrc.path[1]
72
+ )
73
+ );
74
+ if (isEveryClaimAllowedToBeRequested) {
75
+ foundFullyMatching = true;
76
+ }
77
+ }
78
+ if (!foundFullyMatching) return false;
79
+ break;
80
+ }
81
+ case "dc+sd-jwt":
82
+ case "vc+sd-jwt": {
83
+ const vctValues = credentialQuery.meta?.vct_values;
84
+ if (!vctValues) return false;
85
+ if (credentialQuery.meta?.vct_values?.length === 0) return false;
86
+ const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(
87
+ (c) => !!((c.format === "dc+sd-jwt" || c.format === "vc+sd-jwt") && c.meta?.vct_values && (0, import_core.equalsIgnoreOrder)(c.meta.vct_values, vctValues))
88
+ );
89
+ if (foundMatchingRequests.length === 0) return false;
90
+ let foundFullyMatching = false;
91
+ for (const matchedRequest of foundMatchingRequests) {
92
+ if (!matchedRequest.claims) continue credentialQueryLoop;
93
+ if (!credentialQuery.claims) continue credentialQueryLoop;
94
+ const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(
95
+ (c) => "path" in c && matchedRequest.claims?.some((mrc) => "path" in mrc && (0, import_core.equalsWithOrder)(c.path, mrc.path))
96
+ );
97
+ if (isEveryClaimAllowedToBeRequested) {
98
+ foundFullyMatching = true;
99
+ }
100
+ }
101
+ if (!foundFullyMatching) return false;
102
+ break;
103
+ }
104
+ default:
105
+ return false;
106
+ }
107
+ }
108
+ return true;
109
+ }
110
+
111
+ // src/verifyOpenid4VpAuthorizationRequest.ts
112
+ var verifyOpenid4VpAuthorizationRequest = async (agentContext, {
113
+ resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql },
114
+ trustedCertificates,
115
+ allowUntrustedSigned
116
+ }) => {
117
+ const results = [];
118
+ if (!authorizationRequestPayload.verifier_attestations) return;
119
+ for (const va of authorizationRequestPayload.verifier_attestations) {
120
+ if (va.format === "jwt") {
121
+ if (typeof va.data !== "string") {
122
+ throw new Error("Only inline JWTs are supported");
123
+ }
124
+ const jwsService = agentContext.resolve(import_core2.JwsService);
125
+ let isValidButUntrusted = false;
126
+ let isValidAndTrusted = false;
127
+ const jwt = import_core2.Jwt.fromSerializedJwt(va.data);
128
+ try {
129
+ const { isValid } = await jwsService.verifyJws(agentContext, {
130
+ jws: va.data,
131
+ trustedCertificates
132
+ });
133
+ isValidAndTrusted = isValid;
134
+ } catch {
135
+ if (allowUntrustedSigned) {
136
+ const { isValid } = await jwsService.verifyJws(agentContext, {
137
+ jws: va.data,
138
+ trustedCertificates: jwt.header.x5c ?? []
139
+ });
140
+ isValidButUntrusted = isValid;
141
+ }
142
+ }
143
+ if (jwt.header.typ !== "rc-rp+jwt") {
144
+ throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`);
145
+ }
146
+ if (!signedAuthorizationRequest) {
147
+ throw new Error("Request must be signed for the registration certificate");
148
+ }
149
+ if (signedAuthorizationRequest.signer.method !== "x5c") {
150
+ throw new Error("x5c is only supported for registration certificate");
151
+ }
152
+ const registrationCertificateHeaderSchema = import_zod.default.object({
153
+ typ: import_zod.default.literal("rc-rp+jwt"),
154
+ alg: import_zod.default.string(),
155
+ // sprin-d did not define this
156
+ x5u: import_zod.default.string().url().optional(),
157
+ // sprin-d did not define this
158
+ "x5t#s256": import_zod.default.string().optional()
159
+ }).passthrough();
160
+ const registrationCertificatePayloadSchema = import_zod.default.object({
161
+ credentials: import_zod.default.array(
162
+ import_zod.default.object({
163
+ format: import_zod.default.string(),
164
+ multiple: import_zod.default.boolean().default(false),
165
+ meta: import_zod.default.object({
166
+ vct_values: import_zod.default.array(import_zod.default.string()).optional(),
167
+ doctype_value: import_zod.default.string().optional()
168
+ }).optional(),
169
+ trusted_authorities: import_zod.default.array(import_zod.default.object({ type: import_zod.default.string(), values: import_zod.default.array(import_zod.default.string()) })).nonempty().optional(),
170
+ require_cryptographic_holder_binding: import_zod.default.boolean().default(true),
171
+ claims: import_zod.default.array(
172
+ import_zod.default.object({
173
+ id: import_zod.default.string().optional(),
174
+ path: import_zod.default.array(import_zod.default.string()).nonempty().nonempty(),
175
+ values: import_zod.default.array(import_zod.default.number().or(import_zod.default.boolean())).optional()
176
+ })
177
+ ).nonempty().optional(),
178
+ claim_sets: import_zod.default.array(import_zod.default.array(import_zod.default.string())).nonempty().optional()
179
+ })
180
+ ),
181
+ contact: import_zod.default.object({
182
+ website: import_zod.default.string().url(),
183
+ "e-mail": import_zod.default.string().email(),
184
+ phone: import_zod.default.string()
185
+ }),
186
+ sub: import_zod.default.string(),
187
+ // Should be service
188
+ services: import_zod.default.array(import_zod.default.object({ lang: import_zod.default.string(), name: import_zod.default.string() })),
189
+ public_body: import_zod.default.boolean().default(false),
190
+ entitlements: import_zod.default.array(import_zod.default.any()),
191
+ provided_attestations: import_zod.default.array(
192
+ import_zod.default.object({
193
+ format: import_zod.default.string(),
194
+ meta: import_zod.default.any()
195
+ })
196
+ ).optional(),
197
+ privacy_policy: import_zod.default.string().url(),
198
+ iat: import_zod.default.number().optional(),
199
+ exp: import_zod.default.number().optional(),
200
+ purpose: import_zod.default.array(
201
+ import_zod.default.object({
202
+ locale: import_zod.default.string().optional(),
203
+ lang: import_zod.default.string().optional(),
204
+ name: import_zod.default.string()
205
+ })
206
+ ).optional(),
207
+ status: import_zod.default.any()
208
+ }).passthrough();
209
+ registrationCertificateHeaderSchema.parse(jwt.header);
210
+ const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson());
211
+ const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c;
212
+ const rpCert = import_core2.X509Certificate.fromEncodedCertificate(rpCertEncoded);
213
+ if (rpCert.subject !== parsedPayload.sub) {
214
+ throw new Error(
215
+ `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'`
216
+ );
217
+ }
218
+ if (parsedPayload.iat && (/* @__PURE__ */ new Date()).getTime() / 1e3 <= parsedPayload.iat) {
219
+ throw new Error("Issued at timestamp of the registration certificate is in the future");
220
+ }
221
+ if (!dcql) {
222
+ throw new Error("DCQL must be used when working registration certificates");
223
+ }
224
+ if (authorizationRequestPayload.presentation_definition || authorizationRequestPayload.presentation_definition_uri) {
225
+ throw new Error("Presentation Exchange is not supported for the registration certificate");
226
+ }
227
+ const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload);
228
+ if (!isValidDcqlQuery) {
229
+ throw new Error(
230
+ "DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate"
231
+ );
232
+ }
233
+ results.push({ isValidButUntrusted, isValidAndTrusted });
234
+ } else {
235
+ throw new Error(`only format of 'jwt' is supported`);
236
+ }
237
+ }
238
+ return results;
239
+ };
240
+ // Annotate the CommonJS export names for ESM import in node:
241
+ 0 && (module.exports = {
242
+ verifyOpenid4VpAuthorizationRequest
243
+ });
244
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/verifyOpenid4VpAuthorizationRequest.ts","../src/isDcqlQueryEqualOrSubset.ts"],"sourcesContent":["export { verifyOpenid4VpAuthorizationRequest } from './verifyOpenid4VpAuthorizationRequest'\n","import { allowedNodeEnvironmentFlags } from 'node:process'\nimport { type AgentContext, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core'\nimport type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc'\nimport z from 'zod'\nimport { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset'\n\nexport type VerifyAuthorizationRequestOptions = {\n resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest\n trustedCertificates?: Array<string>\n allowUntrustedSigned?: boolean\n}\n\nexport const verifyOpenid4VpAuthorizationRequest = async (\n agentContext: AgentContext,\n {\n resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql },\n trustedCertificates,\n allowUntrustedSigned,\n }: VerifyAuthorizationRequestOptions\n) => {\n const results = []\n if (!authorizationRequestPayload.verifier_attestations) return\n for (const va of authorizationRequestPayload.verifier_attestations) {\n // Here we verify it as a registration certificate according to\n // https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/Wallet-Relying-Party-Authentication/#registration-certificate\n if (va.format === 'jwt') {\n if (typeof va.data !== 'string') {\n throw new Error('Only inline JWTs are supported')\n }\n\n const jwsService = agentContext.resolve(JwsService)\n\n let isValidButUntrusted = false\n let isValidAndTrusted = false\n\n const jwt = Jwt.fromSerializedJwt(va.data)\n\n try {\n const { isValid } = await jwsService.verifyJws(agentContext, {\n jws: va.data,\n trustedCertificates,\n })\n isValidAndTrusted = isValid\n } catch {\n if (allowUntrustedSigned) {\n const { isValid } = await jwsService.verifyJws(agentContext, {\n jws: va.data,\n trustedCertificates: jwt.header.x5c ?? [],\n })\n isValidButUntrusted = isValid\n }\n }\n\n if (jwt.header.typ !== 'rc-rp+jwt') {\n throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`)\n }\n\n if (!signedAuthorizationRequest) {\n throw new Error('Request must be signed for the registration certificate')\n }\n\n if (signedAuthorizationRequest.signer.method !== 'x5c') {\n throw new Error('x5c is only supported for registration certificate')\n }\n\n const registrationCertificateHeaderSchema = z\n .object({\n typ: z.literal('rc-rp+jwt'),\n alg: z.string(),\n // sprin-d did not define this\n x5u: z.string().url().optional(),\n // sprin-d did not define this\n 'x5t#s256': z.string().optional(),\n })\n .passthrough()\n\n // TODO: does not support intermediaries\n const registrationCertificatePayloadSchema = z\n .object({\n credentials: z.array(\n z.object({\n format: z.string(),\n multiple: z.boolean().default(false),\n meta: z\n .object({\n vct_values: z.array(z.string()).optional(),\n doctype_value: z.string().optional(),\n })\n .optional(),\n trusted_authorities: z\n .array(z.object({ type: z.string(), values: z.array(z.string()) }))\n .nonempty()\n .optional(),\n require_cryptographic_holder_binding: z.boolean().default(true),\n claims: z\n .array(\n z.object({\n id: z.string().optional(),\n path: z.array(z.string()).nonempty().nonempty(),\n values: z.array(z.number().or(z.boolean())).optional(),\n })\n )\n .nonempty()\n .optional(),\n claim_sets: z.array(z.array(z.string())).nonempty().optional(),\n })\n ),\n contact: z.object({\n website: z.string().url(),\n 'e-mail': z.string().email(),\n phone: z.string(),\n }),\n sub: z.string(),\n // Should be service\n services: z.array(z.object({ lang: z.string(), name: z.string() })),\n public_body: z.boolean().default(false),\n entitlements: z.array(z.any()),\n provided_attestations: z\n .array(\n z.object({\n format: z.string(),\n meta: z.any(),\n })\n )\n .optional(),\n privacy_policy: z.string().url(),\n iat: z.number().optional(),\n exp: z.number().optional(),\n purpose: z\n .array(\n z.object({\n locale: z.string().optional(),\n lang: z.string().optional(),\n name: z.string(),\n })\n )\n .optional(),\n status: z.any(),\n })\n .passthrough()\n\n registrationCertificateHeaderSchema.parse(jwt.header)\n const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson())\n\n const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c\n const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded)\n\n if (rpCert.subject !== parsedPayload.sub) {\n throw new Error(\n `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'`\n )\n }\n\n if (parsedPayload.iat && new Date().getTime() / 1000 <= parsedPayload.iat) {\n throw new Error('Issued at timestamp of the registration certificate is in the future')\n }\n\n // TODO: check the status of the registration certificate\n\n if (!dcql) {\n throw new Error('DCQL must be used when working registration certificates')\n }\n\n if (\n authorizationRequestPayload.presentation_definition ||\n authorizationRequestPayload.presentation_definition_uri\n ) {\n throw new Error('Presentation Exchange is not supported for the registration certificate')\n }\n\n const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery)\n\n if (!isValidDcqlQuery) {\n throw new Error(\n 'DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate'\n )\n }\n\n results.push({ isValidButUntrusted, isValidAndTrusted })\n } else {\n throw new Error(`only format of 'jwt' is supported`)\n }\n }\n return results\n}\n","import { type DcqlQuery, equalsIgnoreOrder, equalsWithOrder } from '@credo-ts/core'\n\nexport function isDcqlQueryEqualOrSubset(arq: DcqlQuery, rcq: DcqlQuery): boolean {\n if (rcq.credential_sets) {\n return false\n }\n\n if (rcq.credentials.some((c) => c.id)) {\n return false\n }\n\n // only sd-jwt and mdoc are supported\n if (arq.credentials.some((c) => c.format !== 'mso_mdoc' && c.format !== 'vc+sd-jwt' && c.format !== 'dc+sd-jwt')) {\n return false\n }\n\n credentialQueryLoop: for (const credentialQuery of arq.credentials) {\n const matchingRcqCredentialQueriesBasedOnFormat = rcq.credentials.filter((c) => c.format === credentialQuery.format)\n\n if (matchingRcqCredentialQueriesBasedOnFormat.length === 0) return false\n\n switch (credentialQuery.format) {\n case 'mso_mdoc': {\n const doctypeValue = credentialQuery.meta?.doctype_value\n if (!doctypeValue) return false\n if (typeof credentialQuery.meta?.doctype_value !== 'string') return false\n\n const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(\n (c): c is typeof c & { format: 'mso_mdoc' } =>\n !!(c.format === 'mso_mdoc' && c.meta && c.meta.doctype_value === doctypeValue)\n )\n\n // We do not know which one we have to pick based on the meta+format\n if (foundMatchingRequests.length === 0) return false\n\n let foundFullyMatching = false\n for (const matchedRequest of foundMatchingRequests) {\n // credentialQuery.claims must match or be subset of matchedRequest\n\n // If the claims is empty, everything within the specific format+meta is allowed\n if (!matchedRequest.claims) continue credentialQueryLoop\n\n // If no specific claims are request, we allow it as the format+meta is allowed to be requested\n // but this requests no additional claims\n if (!credentialQuery.claims) continue credentialQueryLoop\n\n // Every claim request in the authorization request must be found in the registration certificate\n // for mdoc, this means matching the `path[0]` (namespace) and `path[1]` (value name)\n const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(\n (c) =>\n 'path' in c &&\n matchedRequest.claims?.some(\n (mrc) => 'path' in mrc && c.path[0] === mrc.path[0] && c.path[1] === mrc.path[1]\n )\n )\n if (isEveryClaimAllowedToBeRequested) {\n foundFullyMatching = true\n }\n }\n\n if (!foundFullyMatching) return false\n\n break\n }\n case 'dc+sd-jwt':\n case 'vc+sd-jwt': {\n const vctValues = credentialQuery.meta?.vct_values\n if (!vctValues) return false\n if (credentialQuery.meta?.vct_values?.length === 0) return false\n\n const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(\n (c): c is typeof c & ({ format: 'dc+sd-jwt' } | { format: 'vc+sd-jwt' }) =>\n !!(\n (c.format === 'dc+sd-jwt' || c.format === 'vc+sd-jwt') &&\n c.meta?.vct_values &&\n equalsIgnoreOrder(c.meta.vct_values, vctValues)\n )\n )\n\n // We do not know which one we have to pick based on the meta+format\n if (foundMatchingRequests.length === 0) return false\n\n let foundFullyMatching = false\n for (const matchedRequest of foundMatchingRequests) {\n // credentialQuery.claims must match or be subset of matchedRequest\n\n // If the claims is empty, everything within the specific format+meta is allowed\n if (!matchedRequest.claims) continue credentialQueryLoop\n\n // If no specific claims are request, we allow it as the format+meta is allowed to be requested\n // but this requests no additional claims\n if (!credentialQuery.claims) continue credentialQueryLoop\n\n // Every claim request in the authorization request must be found in the registration certificate\n // for sd-jwt, this means making sure that every `path[n]` is in the registration certificate\n const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(\n (c) =>\n 'path' in c && matchedRequest.claims?.some((mrc) => 'path' in mrc && equalsWithOrder(c.path, mrc.path))\n )\n if (isEveryClaimAllowedToBeRequested) {\n foundFullyMatching = true\n }\n }\n\n if (!foundFullyMatching) return false\n\n break\n }\n default:\n return false\n }\n }\n\n return true\n}\n"],"mappings":"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","names":["import_core","z"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,207 @@
1
+ // src/verifyOpenid4VpAuthorizationRequest.ts
2
+ import { JwsService, Jwt, X509Certificate } from "@credo-ts/core";
3
+ import z from "zod";
4
+
5
+ // src/isDcqlQueryEqualOrSubset.ts
6
+ import { equalsIgnoreOrder, equalsWithOrder } from "@credo-ts/core";
7
+ function isDcqlQueryEqualOrSubset(arq, rcq) {
8
+ if (rcq.credential_sets) {
9
+ return false;
10
+ }
11
+ if (rcq.credentials.some((c) => c.id)) {
12
+ return false;
13
+ }
14
+ if (arq.credentials.some((c) => c.format !== "mso_mdoc" && c.format !== "vc+sd-jwt" && c.format !== "dc+sd-jwt")) {
15
+ return false;
16
+ }
17
+ credentialQueryLoop: for (const credentialQuery of arq.credentials) {
18
+ const matchingRcqCredentialQueriesBasedOnFormat = rcq.credentials.filter((c) => c.format === credentialQuery.format);
19
+ if (matchingRcqCredentialQueriesBasedOnFormat.length === 0) return false;
20
+ switch (credentialQuery.format) {
21
+ case "mso_mdoc": {
22
+ const doctypeValue = credentialQuery.meta?.doctype_value;
23
+ if (!doctypeValue) return false;
24
+ if (typeof credentialQuery.meta?.doctype_value !== "string") return false;
25
+ const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(
26
+ (c) => !!(c.format === "mso_mdoc" && c.meta && c.meta.doctype_value === doctypeValue)
27
+ );
28
+ if (foundMatchingRequests.length === 0) return false;
29
+ let foundFullyMatching = false;
30
+ for (const matchedRequest of foundMatchingRequests) {
31
+ if (!matchedRequest.claims) continue credentialQueryLoop;
32
+ if (!credentialQuery.claims) continue credentialQueryLoop;
33
+ const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(
34
+ (c) => "path" in c && matchedRequest.claims?.some(
35
+ (mrc) => "path" in mrc && c.path[0] === mrc.path[0] && c.path[1] === mrc.path[1]
36
+ )
37
+ );
38
+ if (isEveryClaimAllowedToBeRequested) {
39
+ foundFullyMatching = true;
40
+ }
41
+ }
42
+ if (!foundFullyMatching) return false;
43
+ break;
44
+ }
45
+ case "dc+sd-jwt":
46
+ case "vc+sd-jwt": {
47
+ const vctValues = credentialQuery.meta?.vct_values;
48
+ if (!vctValues) return false;
49
+ if (credentialQuery.meta?.vct_values?.length === 0) return false;
50
+ const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(
51
+ (c) => !!((c.format === "dc+sd-jwt" || c.format === "vc+sd-jwt") && c.meta?.vct_values && equalsIgnoreOrder(c.meta.vct_values, vctValues))
52
+ );
53
+ if (foundMatchingRequests.length === 0) return false;
54
+ let foundFullyMatching = false;
55
+ for (const matchedRequest of foundMatchingRequests) {
56
+ if (!matchedRequest.claims) continue credentialQueryLoop;
57
+ if (!credentialQuery.claims) continue credentialQueryLoop;
58
+ const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(
59
+ (c) => "path" in c && matchedRequest.claims?.some((mrc) => "path" in mrc && equalsWithOrder(c.path, mrc.path))
60
+ );
61
+ if (isEveryClaimAllowedToBeRequested) {
62
+ foundFullyMatching = true;
63
+ }
64
+ }
65
+ if (!foundFullyMatching) return false;
66
+ break;
67
+ }
68
+ default:
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ // src/verifyOpenid4VpAuthorizationRequest.ts
76
+ var verifyOpenid4VpAuthorizationRequest = async (agentContext, {
77
+ resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql },
78
+ trustedCertificates,
79
+ allowUntrustedSigned
80
+ }) => {
81
+ const results = [];
82
+ if (!authorizationRequestPayload.verifier_attestations) return;
83
+ for (const va of authorizationRequestPayload.verifier_attestations) {
84
+ if (va.format === "jwt") {
85
+ if (typeof va.data !== "string") {
86
+ throw new Error("Only inline JWTs are supported");
87
+ }
88
+ const jwsService = agentContext.resolve(JwsService);
89
+ let isValidButUntrusted = false;
90
+ let isValidAndTrusted = false;
91
+ const jwt = Jwt.fromSerializedJwt(va.data);
92
+ try {
93
+ const { isValid } = await jwsService.verifyJws(agentContext, {
94
+ jws: va.data,
95
+ trustedCertificates
96
+ });
97
+ isValidAndTrusted = isValid;
98
+ } catch {
99
+ if (allowUntrustedSigned) {
100
+ const { isValid } = await jwsService.verifyJws(agentContext, {
101
+ jws: va.data,
102
+ trustedCertificates: jwt.header.x5c ?? []
103
+ });
104
+ isValidButUntrusted = isValid;
105
+ }
106
+ }
107
+ if (jwt.header.typ !== "rc-rp+jwt") {
108
+ throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`);
109
+ }
110
+ if (!signedAuthorizationRequest) {
111
+ throw new Error("Request must be signed for the registration certificate");
112
+ }
113
+ if (signedAuthorizationRequest.signer.method !== "x5c") {
114
+ throw new Error("x5c is only supported for registration certificate");
115
+ }
116
+ const registrationCertificateHeaderSchema = z.object({
117
+ typ: z.literal("rc-rp+jwt"),
118
+ alg: z.string(),
119
+ // sprin-d did not define this
120
+ x5u: z.string().url().optional(),
121
+ // sprin-d did not define this
122
+ "x5t#s256": z.string().optional()
123
+ }).passthrough();
124
+ const registrationCertificatePayloadSchema = z.object({
125
+ credentials: z.array(
126
+ z.object({
127
+ format: z.string(),
128
+ multiple: z.boolean().default(false),
129
+ meta: z.object({
130
+ vct_values: z.array(z.string()).optional(),
131
+ doctype_value: z.string().optional()
132
+ }).optional(),
133
+ trusted_authorities: z.array(z.object({ type: z.string(), values: z.array(z.string()) })).nonempty().optional(),
134
+ require_cryptographic_holder_binding: z.boolean().default(true),
135
+ claims: z.array(
136
+ z.object({
137
+ id: z.string().optional(),
138
+ path: z.array(z.string()).nonempty().nonempty(),
139
+ values: z.array(z.number().or(z.boolean())).optional()
140
+ })
141
+ ).nonempty().optional(),
142
+ claim_sets: z.array(z.array(z.string())).nonempty().optional()
143
+ })
144
+ ),
145
+ contact: z.object({
146
+ website: z.string().url(),
147
+ "e-mail": z.string().email(),
148
+ phone: z.string()
149
+ }),
150
+ sub: z.string(),
151
+ // Should be service
152
+ services: z.array(z.object({ lang: z.string(), name: z.string() })),
153
+ public_body: z.boolean().default(false),
154
+ entitlements: z.array(z.any()),
155
+ provided_attestations: z.array(
156
+ z.object({
157
+ format: z.string(),
158
+ meta: z.any()
159
+ })
160
+ ).optional(),
161
+ privacy_policy: z.string().url(),
162
+ iat: z.number().optional(),
163
+ exp: z.number().optional(),
164
+ purpose: z.array(
165
+ z.object({
166
+ locale: z.string().optional(),
167
+ lang: z.string().optional(),
168
+ name: z.string()
169
+ })
170
+ ).optional(),
171
+ status: z.any()
172
+ }).passthrough();
173
+ registrationCertificateHeaderSchema.parse(jwt.header);
174
+ const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson());
175
+ const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c;
176
+ const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded);
177
+ if (rpCert.subject !== parsedPayload.sub) {
178
+ throw new Error(
179
+ `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'`
180
+ );
181
+ }
182
+ if (parsedPayload.iat && (/* @__PURE__ */ new Date()).getTime() / 1e3 <= parsedPayload.iat) {
183
+ throw new Error("Issued at timestamp of the registration certificate is in the future");
184
+ }
185
+ if (!dcql) {
186
+ throw new Error("DCQL must be used when working registration certificates");
187
+ }
188
+ if (authorizationRequestPayload.presentation_definition || authorizationRequestPayload.presentation_definition_uri) {
189
+ throw new Error("Presentation Exchange is not supported for the registration certificate");
190
+ }
191
+ const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload);
192
+ if (!isValidDcqlQuery) {
193
+ throw new Error(
194
+ "DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate"
195
+ );
196
+ }
197
+ results.push({ isValidButUntrusted, isValidAndTrusted });
198
+ } else {
199
+ throw new Error(`only format of 'jwt' is supported`);
200
+ }
201
+ }
202
+ return results;
203
+ };
204
+ export {
205
+ verifyOpenid4VpAuthorizationRequest
206
+ };
207
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/verifyOpenid4VpAuthorizationRequest.ts","../src/isDcqlQueryEqualOrSubset.ts"],"sourcesContent":["import { allowedNodeEnvironmentFlags } from 'node:process'\nimport { type AgentContext, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core'\nimport type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc'\nimport z from 'zod'\nimport { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset'\n\nexport type VerifyAuthorizationRequestOptions = {\n resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest\n trustedCertificates?: Array<string>\n allowUntrustedSigned?: boolean\n}\n\nexport const verifyOpenid4VpAuthorizationRequest = async (\n agentContext: AgentContext,\n {\n resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql },\n trustedCertificates,\n allowUntrustedSigned,\n }: VerifyAuthorizationRequestOptions\n) => {\n const results = []\n if (!authorizationRequestPayload.verifier_attestations) return\n for (const va of authorizationRequestPayload.verifier_attestations) {\n // Here we verify it as a registration certificate according to\n // https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/Wallet-Relying-Party-Authentication/#registration-certificate\n if (va.format === 'jwt') {\n if (typeof va.data !== 'string') {\n throw new Error('Only inline JWTs are supported')\n }\n\n const jwsService = agentContext.resolve(JwsService)\n\n let isValidButUntrusted = false\n let isValidAndTrusted = false\n\n const jwt = Jwt.fromSerializedJwt(va.data)\n\n try {\n const { isValid } = await jwsService.verifyJws(agentContext, {\n jws: va.data,\n trustedCertificates,\n })\n isValidAndTrusted = isValid\n } catch {\n if (allowUntrustedSigned) {\n const { isValid } = await jwsService.verifyJws(agentContext, {\n jws: va.data,\n trustedCertificates: jwt.header.x5c ?? [],\n })\n isValidButUntrusted = isValid\n }\n }\n\n if (jwt.header.typ !== 'rc-rp+jwt') {\n throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`)\n }\n\n if (!signedAuthorizationRequest) {\n throw new Error('Request must be signed for the registration certificate')\n }\n\n if (signedAuthorizationRequest.signer.method !== 'x5c') {\n throw new Error('x5c is only supported for registration certificate')\n }\n\n const registrationCertificateHeaderSchema = z\n .object({\n typ: z.literal('rc-rp+jwt'),\n alg: z.string(),\n // sprin-d did not define this\n x5u: z.string().url().optional(),\n // sprin-d did not define this\n 'x5t#s256': z.string().optional(),\n })\n .passthrough()\n\n // TODO: does not support intermediaries\n const registrationCertificatePayloadSchema = z\n .object({\n credentials: z.array(\n z.object({\n format: z.string(),\n multiple: z.boolean().default(false),\n meta: z\n .object({\n vct_values: z.array(z.string()).optional(),\n doctype_value: z.string().optional(),\n })\n .optional(),\n trusted_authorities: z\n .array(z.object({ type: z.string(), values: z.array(z.string()) }))\n .nonempty()\n .optional(),\n require_cryptographic_holder_binding: z.boolean().default(true),\n claims: z\n .array(\n z.object({\n id: z.string().optional(),\n path: z.array(z.string()).nonempty().nonempty(),\n values: z.array(z.number().or(z.boolean())).optional(),\n })\n )\n .nonempty()\n .optional(),\n claim_sets: z.array(z.array(z.string())).nonempty().optional(),\n })\n ),\n contact: z.object({\n website: z.string().url(),\n 'e-mail': z.string().email(),\n phone: z.string(),\n }),\n sub: z.string(),\n // Should be service\n services: z.array(z.object({ lang: z.string(), name: z.string() })),\n public_body: z.boolean().default(false),\n entitlements: z.array(z.any()),\n provided_attestations: z\n .array(\n z.object({\n format: z.string(),\n meta: z.any(),\n })\n )\n .optional(),\n privacy_policy: z.string().url(),\n iat: z.number().optional(),\n exp: z.number().optional(),\n purpose: z\n .array(\n z.object({\n locale: z.string().optional(),\n lang: z.string().optional(),\n name: z.string(),\n })\n )\n .optional(),\n status: z.any(),\n })\n .passthrough()\n\n registrationCertificateHeaderSchema.parse(jwt.header)\n const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson())\n\n const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c\n const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded)\n\n if (rpCert.subject !== parsedPayload.sub) {\n throw new Error(\n `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'`\n )\n }\n\n if (parsedPayload.iat && new Date().getTime() / 1000 <= parsedPayload.iat) {\n throw new Error('Issued at timestamp of the registration certificate is in the future')\n }\n\n // TODO: check the status of the registration certificate\n\n if (!dcql) {\n throw new Error('DCQL must be used when working registration certificates')\n }\n\n if (\n authorizationRequestPayload.presentation_definition ||\n authorizationRequestPayload.presentation_definition_uri\n ) {\n throw new Error('Presentation Exchange is not supported for the registration certificate')\n }\n\n const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery)\n\n if (!isValidDcqlQuery) {\n throw new Error(\n 'DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate'\n )\n }\n\n results.push({ isValidButUntrusted, isValidAndTrusted })\n } else {\n throw new Error(`only format of 'jwt' is supported`)\n }\n }\n return results\n}\n","import { type DcqlQuery, equalsIgnoreOrder, equalsWithOrder } from '@credo-ts/core'\n\nexport function isDcqlQueryEqualOrSubset(arq: DcqlQuery, rcq: DcqlQuery): boolean {\n if (rcq.credential_sets) {\n return false\n }\n\n if (rcq.credentials.some((c) => c.id)) {\n return false\n }\n\n // only sd-jwt and mdoc are supported\n if (arq.credentials.some((c) => c.format !== 'mso_mdoc' && c.format !== 'vc+sd-jwt' && c.format !== 'dc+sd-jwt')) {\n return false\n }\n\n credentialQueryLoop: for (const credentialQuery of arq.credentials) {\n const matchingRcqCredentialQueriesBasedOnFormat = rcq.credentials.filter((c) => c.format === credentialQuery.format)\n\n if (matchingRcqCredentialQueriesBasedOnFormat.length === 0) return false\n\n switch (credentialQuery.format) {\n case 'mso_mdoc': {\n const doctypeValue = credentialQuery.meta?.doctype_value\n if (!doctypeValue) return false\n if (typeof credentialQuery.meta?.doctype_value !== 'string') return false\n\n const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(\n (c): c is typeof c & { format: 'mso_mdoc' } =>\n !!(c.format === 'mso_mdoc' && c.meta && c.meta.doctype_value === doctypeValue)\n )\n\n // We do not know which one we have to pick based on the meta+format\n if (foundMatchingRequests.length === 0) return false\n\n let foundFullyMatching = false\n for (const matchedRequest of foundMatchingRequests) {\n // credentialQuery.claims must match or be subset of matchedRequest\n\n // If the claims is empty, everything within the specific format+meta is allowed\n if (!matchedRequest.claims) continue credentialQueryLoop\n\n // If no specific claims are request, we allow it as the format+meta is allowed to be requested\n // but this requests no additional claims\n if (!credentialQuery.claims) continue credentialQueryLoop\n\n // Every claim request in the authorization request must be found in the registration certificate\n // for mdoc, this means matching the `path[0]` (namespace) and `path[1]` (value name)\n const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(\n (c) =>\n 'path' in c &&\n matchedRequest.claims?.some(\n (mrc) => 'path' in mrc && c.path[0] === mrc.path[0] && c.path[1] === mrc.path[1]\n )\n )\n if (isEveryClaimAllowedToBeRequested) {\n foundFullyMatching = true\n }\n }\n\n if (!foundFullyMatching) return false\n\n break\n }\n case 'dc+sd-jwt':\n case 'vc+sd-jwt': {\n const vctValues = credentialQuery.meta?.vct_values\n if (!vctValues) return false\n if (credentialQuery.meta?.vct_values?.length === 0) return false\n\n const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter(\n (c): c is typeof c & ({ format: 'dc+sd-jwt' } | { format: 'vc+sd-jwt' }) =>\n !!(\n (c.format === 'dc+sd-jwt' || c.format === 'vc+sd-jwt') &&\n c.meta?.vct_values &&\n equalsIgnoreOrder(c.meta.vct_values, vctValues)\n )\n )\n\n // We do not know which one we have to pick based on the meta+format\n if (foundMatchingRequests.length === 0) return false\n\n let foundFullyMatching = false\n for (const matchedRequest of foundMatchingRequests) {\n // credentialQuery.claims must match or be subset of matchedRequest\n\n // If the claims is empty, everything within the specific format+meta is allowed\n if (!matchedRequest.claims) continue credentialQueryLoop\n\n // If no specific claims are request, we allow it as the format+meta is allowed to be requested\n // but this requests no additional claims\n if (!credentialQuery.claims) continue credentialQueryLoop\n\n // Every claim request in the authorization request must be found in the registration certificate\n // for sd-jwt, this means making sure that every `path[n]` is in the registration certificate\n const isEveryClaimAllowedToBeRequested = credentialQuery.claims.every(\n (c) =>\n 'path' in c && matchedRequest.claims?.some((mrc) => 'path' in mrc && equalsWithOrder(c.path, mrc.path))\n )\n if (isEveryClaimAllowedToBeRequested) {\n foundFullyMatching = true\n }\n }\n\n if (!foundFullyMatching) return false\n\n break\n }\n default:\n return false\n }\n }\n\n return true\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@animo-id/eudi-wallet-functionality",
3
+ "description": "EUDI Wallet Functionality",
4
+ "version": "0.0.0-alpha-20250603091506",
5
+ "license": "Apache-2.0",
6
+ "author": "Animo Solutions",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/animo/eudi-wallet-functionality"
24
+ },
25
+ "peerDependencies": {
26
+ "@credo-ts/core": "*",
27
+ "@credo-ts/openid4vc": "*"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "^1.9.4",
31
+ "@changesets/cli": "^2.29.4",
32
+ "@credo-ts/askar": "0.6.0-alpha-20250602122957",
33
+ "@credo-ts/core": "0.6.0-alpha-20250602122957",
34
+ "@credo-ts/node": "0.6.0-alpha-20250602122957",
35
+ "@credo-ts/openid4vc": "0.6.0-alpha-20250602122957",
36
+ "@openwallet-foundation/askar-nodejs": "^0.3.2",
37
+ "@types/node": "^22.15.29",
38
+ "tsup": "^8.5.0",
39
+ "tsx": "^4.19.4",
40
+ "typescript": "~5.8.3"
41
+ },
42
+ "dependencies": {
43
+ "zod": "^3.25.42"
44
+ },
45
+ "scripts": {
46
+ "types:check": "tsc --noEmit",
47
+ "style:check": "biome check --unsafe",
48
+ "style:fix": "biome check --write --unsafe",
49
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
50
+ "test": "node --import tsx --test tests/*.test.ts",
51
+ "release": "pnpm build && pnpm changeset publish --no-git-tag",
52
+ "changeset-version": "pnpm changeset version && pnpm style:fix"
53
+ },
54
+ "main": "./dist/index.js",
55
+ "module": "./dist/index.mjs",
56
+ "types": "./dist/index.d.ts"
57
+ }