@pagopa/io-react-native-wallet 2.0.0-next.5 → 2.0.0-next.7
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/lib/commonjs/credential/issuance/02-evaluate-issuer-trust.js +6 -1
- package/lib/commonjs/credential/issuance/02-evaluate-issuer-trust.js.map +1 -1
- package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
- package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +186 -9
- package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/commonjs/credential/issuance/README.md +7 -2
- package/lib/commonjs/mdoc/const.js +9 -0
- package/lib/commonjs/mdoc/const.js.map +1 -0
- package/lib/commonjs/mdoc/converter.js +26 -0
- package/lib/commonjs/mdoc/converter.js.map +1 -0
- package/lib/commonjs/mdoc/index.js +74 -0
- package/lib/commonjs/mdoc/index.js.map +1 -0
- package/lib/commonjs/mdoc/utils.js +14 -0
- package/lib/commonjs/mdoc/utils.js.map +1 -0
- package/lib/commonjs/trust/types.js +2 -1
- package/lib/commonjs/trust/types.js.map +1 -1
- package/lib/commonjs/utils/crypto.js +35 -1
- package/lib/commonjs/utils/crypto.js.map +1 -1
- package/lib/module/credential/issuance/02-evaluate-issuer-trust.js +6 -1
- package/lib/module/credential/issuance/02-evaluate-issuer-trust.js.map +1 -1
- package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js +187 -10
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/module/credential/issuance/README.md +7 -2
- package/lib/module/mdoc/const.js +2 -0
- package/lib/module/mdoc/const.js.map +1 -0
- package/lib/module/mdoc/converter.js +20 -0
- package/lib/module/mdoc/converter.js.map +1 -0
- package/lib/module/mdoc/index.js +67 -0
- package/lib/module/mdoc/index.js.map +1 -0
- package/lib/module/mdoc/utils.js +7 -0
- package/lib/module/mdoc/utils.js.map +1 -0
- package/lib/module/trust/types.js +2 -1
- package/lib/module/trust/types.js.map +1 -1
- package/lib/module/utils/crypto.js +32 -0
- package/lib/module/utils/crypto.js.map +1 -1
- package/lib/typescript/credential/issuance/02-evaluate-issuer-trust.d.ts.map +1 -1
- package/lib/typescript/credential/issuance/06-obtain-credential.d.ts +2 -1
- package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +8 -9
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
- package/lib/typescript/mdoc/const.d.ts +2 -0
- package/lib/typescript/mdoc/const.d.ts.map +1 -0
- package/lib/typescript/mdoc/converter.d.ts +8 -0
- package/lib/typescript/mdoc/converter.d.ts.map +1 -0
- package/lib/typescript/mdoc/index.d.ts +5 -0
- package/lib/typescript/mdoc/index.d.ts.map +1 -0
- package/lib/typescript/mdoc/utils.d.ts +7 -0
- package/lib/typescript/mdoc/utils.d.ts.map +1 -0
- package/lib/typescript/trust/build-chain.d.ts +2 -2
- package/lib/typescript/trust/types.d.ts +161 -26
- package/lib/typescript/trust/types.d.ts.map +1 -1
- package/lib/typescript/utils/crypto.d.ts +16 -0
- package/lib/typescript/utils/crypto.d.ts.map +1 -1
- package/package.json +13 -11
- package/src/credential/issuance/02-evaluate-issuer-trust.ts +2 -1
- package/src/credential/issuance/06-obtain-credential.ts +2 -1
- package/src/credential/issuance/07-verify-and-parse-credential.ts +257 -22
- package/src/credential/issuance/README.md +7 -2
- package/src/mdoc/const.ts +1 -0
- package/src/mdoc/converter.ts +26 -0
- package/src/mdoc/index.ts +93 -0
- package/src/mdoc/utils.ts +7 -0
- package/src/trust/types.ts +5 -1
- package/src/utils/crypto.ts +39 -1
- package/lib/commonjs/credential/issuance/const.js +0 -14
- package/lib/commonjs/credential/issuance/const.js.map +0 -1
- package/lib/module/credential/issuance/const.js +0 -4
- package/lib/module/credential/issuance/const.js.map +0 -1
- package/lib/typescript/credential/issuance/const.d.ts +0 -5
- package/lib/typescript/credential/issuance/const.d.ts.map +0 -1
- package/src/credential/issuance/const.ts +0 -11
@@ -1,4 +1,5 @@
|
|
1
1
|
import { type CryptoContext } from "@pagopa/io-react-native-jwt";
|
2
|
+
import { JWK } from "./jwk";
|
2
3
|
/**
|
3
4
|
* Create a CryptoContext bound to a key pair.
|
4
5
|
* Key pair is supposed to exist already in the device's keychain.
|
@@ -16,4 +17,19 @@ export declare const createCryptoContextFor: (keytag: string) => CryptoContext;
|
|
16
17
|
* @returns The returned value of the input procedure.
|
17
18
|
*/
|
18
19
|
export declare const withEphemeralKey: <R>(fn: (ephemeralContext: CryptoContext) => Promise<R>) => Promise<R>;
|
20
|
+
/**
|
21
|
+
* Converts a base64-encoded DER certificate to PEM format.
|
22
|
+
*
|
23
|
+
* @param certificate - The base64-encoded DER certificate.
|
24
|
+
* @returns The PEM-formatted certificate.
|
25
|
+
*/
|
26
|
+
export declare const convertBase64DerToPem: (certificate: string) => string;
|
27
|
+
/**
|
28
|
+
* Retrieves the signing JWK from a PEM-formatted certificate.
|
29
|
+
*
|
30
|
+
* @param pemCert - The PEM-formatted certificate.
|
31
|
+
* @returns The signing JWK.
|
32
|
+
* @throws Will throw an error if the public key is unsupported.
|
33
|
+
*/
|
34
|
+
export declare const getSigninJwkFromCert: (pemCert: string) => JWK;
|
19
35
|
//# sourceMappingURL=crypto.d.ts.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAOA,OAAO,
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAOA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAI5B;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WAAY,MAAM,KAAG,aAsBvD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,6BACJ,aAAa,8BAOrC,CAAC;AACF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,gBAAiB,MAAM,KAAG,MACc,CAAC;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAa,MAAM,KAAG,GAkBtD,CAAC"}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@pagopa/io-react-native-wallet",
|
3
|
-
"version": "2.0.0-next.
|
3
|
+
"version": "2.0.0-next.7",
|
4
4
|
"description": "Provide data structures, helpers and API for IO Wallet",
|
5
5
|
"main": "lib/commonjs/index",
|
6
6
|
"module": "lib/module/index",
|
@@ -30,7 +30,7 @@
|
|
30
30
|
"test": "jest",
|
31
31
|
"tsc": "tsc --noEmit",
|
32
32
|
"lint": "eslint . -c .eslintrc.js --ext .ts,.tsx",
|
33
|
-
"prepack": "bob build",
|
33
|
+
"prepack": "yarn generate && bob build",
|
34
34
|
"release": "release-it",
|
35
35
|
"example": "yarn --cwd example",
|
36
36
|
"bootstrap": "yarn example && yarn install",
|
@@ -53,12 +53,15 @@
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
54
54
|
},
|
55
55
|
"devDependencies": {
|
56
|
-
"@pagopa/io-react-native-crypto": "^1.2.
|
56
|
+
"@pagopa/io-react-native-crypto": "^1.2.3",
|
57
|
+
"@pagopa/io-react-native-iso18013": "^0.3.0",
|
57
58
|
"@pagopa/io-react-native-jwt": "^2.1.0",
|
59
|
+
"@react-native/babel-preset": "0.78.3",
|
58
60
|
"@react-native/eslint-config": "^0.75.5",
|
59
61
|
"@rushstack/eslint-patch": "^1.3.2",
|
60
|
-
"@types/jest": "^
|
61
|
-
"@types/
|
62
|
+
"@types/jest": "^29.5.13",
|
63
|
+
"@types/jsrsasign": "^10.5.15",
|
64
|
+
"@types/react": "^19.0.0",
|
62
65
|
"@types/react-native": "0.70.0",
|
63
66
|
"@types/url-parse": "^1.4.11",
|
64
67
|
"del-cli": "^5.0.0",
|
@@ -67,16 +70,14 @@
|
|
67
70
|
"jest": "^28.1.1",
|
68
71
|
"pod-install": "^0.1.0",
|
69
72
|
"prettier": "^3.5.3",
|
70
|
-
"react": "
|
71
|
-
"react-native": "0.
|
73
|
+
"react": "19.0.0",
|
74
|
+
"react-native": "0.78.3",
|
72
75
|
"react-native-builder-bob": "^0.20.0",
|
73
76
|
"typed-openapi": "^0.4.1",
|
74
77
|
"typescript": "5.0.4"
|
75
78
|
},
|
76
|
-
"resolutions": {
|
77
|
-
"@types/react": "^18.2.6"
|
78
|
-
},
|
79
79
|
"peerDependencies": {
|
80
|
+
"@pagopa/io-react-native-iso18013": "*",
|
80
81
|
"@pagopa/io-react-native-crypto": "*",
|
81
82
|
"@pagopa/io-react-native-jwt": "*",
|
82
83
|
"react": "*",
|
@@ -93,7 +94,7 @@
|
|
93
94
|
"<rootDir>/lib/"
|
94
95
|
],
|
95
96
|
"transformIgnorePatterns": [
|
96
|
-
"node_modules/(?!(jest-)?@react-native|react-native|uuid)"
|
97
|
+
"node_modules/(?!(jest-)?@react-native|react-native|uuid|@pagopa/io-react-native-iso18013)"
|
97
98
|
],
|
98
99
|
"setupFiles": [
|
99
100
|
"<rootDir>/jestSetup.js"
|
@@ -118,6 +119,7 @@
|
|
118
119
|
"js-base64": "^3.7.7",
|
119
120
|
"js-sha256": "^0.9.0",
|
120
121
|
"jsonpath-plus": "^10.2.0",
|
122
|
+
"jsrsasign": "^11.1.0",
|
121
123
|
"parse-url": "^9.2.0",
|
122
124
|
"react-native-url-polyfill": "^2.0.0",
|
123
125
|
"react-native-uuid": "^2.0.1",
|
@@ -27,6 +27,7 @@ export const evaluateIssuerTrust: EvaluateIssuerTrust = async (
|
|
27
27
|
) => {
|
28
28
|
const issuerConf = await getCredentialIssuerEntityConfiguration(issuerUrl, {
|
29
29
|
appFetch: context.appFetch,
|
30
|
-
}).then((
|
30
|
+
}).then(({ payload }) => payload.metadata);
|
31
|
+
|
31
32
|
return { issuerConf };
|
32
33
|
};
|
@@ -18,6 +18,7 @@ import { CredentialResponse, NonceResponse } from "./types";
|
|
18
18
|
import { createDPopToken } from "../../utils/dpop";
|
19
19
|
import { v4 as uuidv4 } from "uuid";
|
20
20
|
import { LogLevel, Logger } from "../../utils/logging";
|
21
|
+
import type { SupportedCredentialFormat } from "../../trust/types";
|
21
22
|
|
22
23
|
export type ObtainCredential = (
|
23
24
|
issuerConf: Out<EvaluateIssuerTrust>["issuerConf"],
|
@@ -35,7 +36,7 @@ export type ObtainCredential = (
|
|
35
36
|
operationType?: "reissuing"
|
36
37
|
) => Promise<{
|
37
38
|
credential: string;
|
38
|
-
format:
|
39
|
+
format: SupportedCredentialFormat;
|
39
40
|
}>;
|
40
41
|
|
41
42
|
export const createNonceProof = async (
|
@@ -6,12 +6,22 @@ import { SdJwt4VC, verify as verifySdJwt } from "../../sd-jwt";
|
|
6
6
|
import { getValueFromDisclosures } from "../../sd-jwt/converters";
|
7
7
|
import { isSameThumbprint, type JWK } from "../../utils/jwk";
|
8
8
|
import type { ObtainCredential } from "./06-obtain-credential";
|
9
|
-
import {
|
9
|
+
import { verify as verifyMdoc } from "../../mdoc";
|
10
|
+
import { MDOC_DEFAULT_NAMESPACE } from "../../mdoc/const";
|
11
|
+
import { getParsedCredentialClaimKey } from "../../mdoc/utils";
|
12
|
+
import { LogLevel, Logger } from "../../utils/logging";
|
13
|
+
import { extractElementValueAsDate } from "../../mdoc/converter";
|
14
|
+
import type { CBOR } from "@pagopa/io-react-native-iso18013";
|
15
|
+
import type { PublicKey } from "@pagopa/io-react-native-crypto";
|
10
16
|
|
11
17
|
type IssuerConf = Out<EvaluateIssuerTrust>["issuerConf"];
|
12
18
|
type CredentialConf =
|
13
19
|
IssuerConf["openid_credential_issuer"]["credential_configurations_supported"][string];
|
14
20
|
|
21
|
+
type DecodedMDocCredential = Out<typeof verifyMdoc> & {
|
22
|
+
issuerSigned: CBOR.IssuerSigned;
|
23
|
+
};
|
24
|
+
|
15
25
|
export type VerifyAndParseCredential = (
|
16
26
|
issuerConf: IssuerConf,
|
17
27
|
credential: Out<ObtainCredential>["credential"],
|
@@ -26,7 +36,8 @@ export type VerifyAndParseCredential = (
|
|
26
36
|
* Include attributes that are not explicitly mapped in the issuer configuration.
|
27
37
|
*/
|
28
38
|
includeUndefinedAttributes?: boolean;
|
29
|
-
}
|
39
|
+
},
|
40
|
+
x509CertRoot?: string
|
30
41
|
) => Promise<{
|
31
42
|
parsedCredential: ParsedCredential;
|
32
43
|
expiration: Date;
|
@@ -34,11 +45,9 @@ export type VerifyAndParseCredential = (
|
|
34
45
|
}>;
|
35
46
|
|
36
47
|
// The credential as a collection of attributes in plain value
|
37
|
-
type ParsedCredential =
|
48
|
+
type ParsedCredential = {
|
38
49
|
/** Attribute key */
|
39
|
-
string
|
40
|
-
{
|
41
|
-
/** Human-readable name of the attribute */
|
50
|
+
[claim: string]: {
|
42
51
|
name:
|
43
52
|
| /* if i18n is provided */ Record<
|
44
53
|
string /* locale */,
|
@@ -46,10 +55,9 @@ type ParsedCredential = Record<
|
|
46
55
|
>
|
47
56
|
| /* if no i18n is provided */ string
|
48
57
|
| undefined; // Add undefined as a possible value for the name property
|
49
|
-
/** The actual value of the attribute */
|
50
58
|
value: unknown;
|
51
|
-
}
|
52
|
-
|
59
|
+
};
|
60
|
+
};
|
53
61
|
|
54
62
|
// handy alias
|
55
63
|
type DecodedSdJwtCredential = Out<typeof verifySdJwt> & {
|
@@ -139,7 +147,123 @@ const parseCredentialSdJwt = (
|
|
139
147
|
|
140
148
|
return definedValues;
|
141
149
|
};
|
150
|
+
const parseCredentialMDoc = (
|
151
|
+
// the list of supported credentials, as defined in the issuer configuration
|
152
|
+
credentialConfig: CredentialConf,
|
153
|
+
// credential_type: string,
|
154
|
+
{ issuerSigned }: DecodedMDocCredential,
|
155
|
+
ignoreMissingAttributes: boolean = false,
|
156
|
+
includeUndefinedAttributes: boolean = false
|
157
|
+
): ParsedCredential => {
|
158
|
+
if (!credentialConfig) {
|
159
|
+
throw new IoWalletError("Credential type not supported by the issuer");
|
160
|
+
}
|
161
|
+
|
162
|
+
if (!credentialConfig.claims) {
|
163
|
+
throw new IoWalletError("Missing claims in the credential subject");
|
164
|
+
}
|
165
|
+
|
166
|
+
const attrDefinitions = credentialConfig.claims.map<
|
167
|
+
[string, string, { name: string; locale: string }[]]
|
168
|
+
>(({ path: [namespace, attribute], display }) => [
|
169
|
+
namespace as string,
|
170
|
+
attribute as string,
|
171
|
+
display,
|
172
|
+
]);
|
173
|
+
|
174
|
+
if (!issuerSigned.nameSpaces) {
|
175
|
+
throw new IoWalletError("Missing claims in the credential");
|
176
|
+
}
|
177
|
+
|
178
|
+
const flatNamespaces = Object.entries(issuerSigned.nameSpaces).flatMap(
|
179
|
+
([namespace, values]) =>
|
180
|
+
values.map<[string, string, string]>((v) => [
|
181
|
+
namespace,
|
182
|
+
v.elementIdentifier,
|
183
|
+
v.elementValue,
|
184
|
+
])
|
185
|
+
);
|
186
|
+
|
187
|
+
// Check that all mandatory attributes defined in the issuer configuration are present in the disclosure set
|
188
|
+
// and filter the non present ones
|
189
|
+
const attrsNotInDisclosures = attrDefinitions.filter(
|
190
|
+
([attrDefNamespace, attrKey]) =>
|
191
|
+
flatNamespaces.some(
|
192
|
+
([namespace, claim]) =>
|
193
|
+
attrDefNamespace === namespace && attrKey === claim
|
194
|
+
)
|
195
|
+
);
|
196
|
+
if (attrsNotInDisclosures.length > 0) {
|
197
|
+
const missing = attrsNotInDisclosures
|
198
|
+
.map(([, attrKey]) => attrKey)
|
199
|
+
.join(", ");
|
200
|
+
const received = Object.keys(Object.values(flatNamespaces));
|
201
|
+
|
202
|
+
if (!ignoreMissingAttributes) {
|
203
|
+
throw new IoWalletError(
|
204
|
+
`Some attributes are missing in the credential. Missing: [${missing}], received: [${received}]`
|
205
|
+
);
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
// Attributes defined in the issuer configuration and present in the disclosure set
|
210
|
+
const definedValues = attrDefinitions
|
211
|
+
// Retrieve the value from the corresponding disclosure
|
212
|
+
.map(
|
213
|
+
([attrDefNamespace, attrKey, display]) =>
|
214
|
+
[
|
215
|
+
attrDefNamespace,
|
216
|
+
attrKey,
|
217
|
+
{
|
218
|
+
display,
|
219
|
+
value: flatNamespaces.find(
|
220
|
+
([namespace, name]) =>
|
221
|
+
attrDefNamespace === namespace && name === attrKey
|
222
|
+
)?.[2],
|
223
|
+
},
|
224
|
+
] as const
|
225
|
+
)
|
226
|
+
//filter the not found elements
|
227
|
+
.filter(([_, __, definition]) => definition.value !== undefined)
|
228
|
+
// Add a human-readable attribute name, with i18n, in the form { locale: name }
|
229
|
+
// Example: { "it-IT": "Nome", "en-EN": "Name", "es-ES": "Nombre" }
|
230
|
+
.reduce<ParsedCredential>(
|
231
|
+
(acc, [attrDefNamespace, attrKey, { display, value }]) => ({
|
232
|
+
...acc,
|
233
|
+
[getParsedCredentialClaimKey(attrDefNamespace, attrKey)]: {
|
234
|
+
value,
|
235
|
+
name: display.reduce(
|
236
|
+
(names, { locale, name }) => ({
|
237
|
+
...names,
|
238
|
+
[locale]: name,
|
239
|
+
}),
|
240
|
+
{}
|
241
|
+
),
|
242
|
+
},
|
243
|
+
}),
|
244
|
+
{}
|
245
|
+
);
|
246
|
+
|
247
|
+
if (includeUndefinedAttributes) {
|
248
|
+
const undefinedValues: ParsedCredential = Object.fromEntries(
|
249
|
+
Object.values(flatNamespaces)
|
250
|
+
.filter(
|
251
|
+
([namespace, key]) =>
|
252
|
+
!definedValues[getParsedCredentialClaimKey(namespace, key)]
|
253
|
+
)
|
254
|
+
.map(([namespace, key, value]) => [
|
255
|
+
getParsedCredentialClaimKey(namespace, key),
|
256
|
+
{ value, name: key },
|
257
|
+
])
|
258
|
+
);
|
259
|
+
return {
|
260
|
+
...definedValues,
|
261
|
+
...undefinedValues,
|
262
|
+
};
|
263
|
+
}
|
142
264
|
|
265
|
+
return definedValues;
|
266
|
+
};
|
143
267
|
/**
|
144
268
|
* Given a credential, verify it's in the supported format
|
145
269
|
* and the credential is correctly signed
|
@@ -176,6 +300,48 @@ async function verifyCredentialSdJwt(
|
|
176
300
|
|
177
301
|
return decodedCredential;
|
178
302
|
}
|
303
|
+
/**
|
304
|
+
* Given a credential, verify it's in the supported format
|
305
|
+
* and the credential is correctly signed
|
306
|
+
* and it's bound to the given key
|
307
|
+
*
|
308
|
+
* @param rawCredential The received credential
|
309
|
+
* @param issuerKeys The set of public keys of the issuer,
|
310
|
+
* which will be used to verify the signature
|
311
|
+
* @param holderBindingContext The access to the holder's key
|
312
|
+
*
|
313
|
+
* @throws If the signature verification fails
|
314
|
+
* @throws If the credential is not in the SdJwt4VC format
|
315
|
+
* @throws If the holder binding is not properly configured
|
316
|
+
*
|
317
|
+
*/
|
318
|
+
async function verifyCredentialMDoc(
|
319
|
+
rawCredential: string,
|
320
|
+
x509CertRoot: string,
|
321
|
+
holderBindingContext: CryptoContext
|
322
|
+
): Promise<DecodedMDocCredential> {
|
323
|
+
const [decodedCredential, holderBindingKey] =
|
324
|
+
// parallel for optimization
|
325
|
+
await Promise.all([
|
326
|
+
verifyMdoc(rawCredential, x509CertRoot),
|
327
|
+
holderBindingContext.getPublicKey(),
|
328
|
+
]);
|
329
|
+
|
330
|
+
if (!decodedCredential) {
|
331
|
+
throw new IoWalletError("No MDOC credentials found!");
|
332
|
+
}
|
333
|
+
|
334
|
+
const key =
|
335
|
+
decodedCredential.issuerSigned.issuerAuth.payload.deviceKeyInfo.deviceKey;
|
336
|
+
|
337
|
+
if (!(await isSameThumbprint(key, holderBindingKey as PublicKey))) {
|
338
|
+
throw new IoWalletError(
|
339
|
+
`Failed to verify holder binding, holder binding key and mDoc deviceKey don't match`
|
340
|
+
);
|
341
|
+
}
|
342
|
+
|
343
|
+
return decodedCredential;
|
344
|
+
}
|
179
345
|
|
180
346
|
const verifyAndParseCredentialSdJwt: VerifyAndParseCredential = async (
|
181
347
|
issuerConf,
|
@@ -231,6 +397,60 @@ const verifyAndParseCredentialSdJwt: VerifyAndParseCredential = async (
|
|
231
397
|
};
|
232
398
|
};
|
233
399
|
|
400
|
+
const verifyAndParseCredentialMDoc: VerifyAndParseCredential = async (
|
401
|
+
issuerConf,
|
402
|
+
credential,
|
403
|
+
credentialConfigurationId,
|
404
|
+
{ credentialCryptoContext, ignoreMissingAttributes },
|
405
|
+
x509CertRoot
|
406
|
+
) => {
|
407
|
+
if (!x509CertRoot) {
|
408
|
+
throw new IoWalletError("Missing x509CertRoot");
|
409
|
+
}
|
410
|
+
|
411
|
+
const decoded = await verifyCredentialMDoc(
|
412
|
+
credential,
|
413
|
+
x509CertRoot,
|
414
|
+
credentialCryptoContext
|
415
|
+
);
|
416
|
+
|
417
|
+
const credentialConfig =
|
418
|
+
issuerConf.openid_credential_issuer.credential_configurations_supported[
|
419
|
+
credentialConfigurationId
|
420
|
+
]!;
|
421
|
+
const parsedCredential = parseCredentialMDoc(
|
422
|
+
credentialConfig,
|
423
|
+
decoded,
|
424
|
+
ignoreMissingAttributes,
|
425
|
+
ignoreMissingAttributes
|
426
|
+
);
|
427
|
+
|
428
|
+
const expirationDate = extractElementValueAsDate(
|
429
|
+
parsedCredential?.[
|
430
|
+
getParsedCredentialClaimKey(MDOC_DEFAULT_NAMESPACE, "expiry_date")
|
431
|
+
]?.value as string
|
432
|
+
);
|
433
|
+
if (!expirationDate) {
|
434
|
+
throw new IoWalletError(`expirationDate must be present!!`);
|
435
|
+
}
|
436
|
+
expirationDate.setDate(expirationDate.getDate() + 1);
|
437
|
+
|
438
|
+
const maybeIssuedAt = extractElementValueAsDate(
|
439
|
+
parsedCredential?.[
|
440
|
+
getParsedCredentialClaimKey(MDOC_DEFAULT_NAMESPACE, "issue_date")
|
441
|
+
]?.value as string
|
442
|
+
);
|
443
|
+
maybeIssuedAt?.setDate(maybeIssuedAt.getDate() + 1);
|
444
|
+
|
445
|
+
return {
|
446
|
+
parsedCredential,
|
447
|
+
credential,
|
448
|
+
credentialConfigurationId,
|
449
|
+
expiration: expirationDate,
|
450
|
+
issuedAt: maybeIssuedAt ?? undefined,
|
451
|
+
};
|
452
|
+
};
|
453
|
+
|
234
454
|
/**
|
235
455
|
* Verify and parse an encoded credential.
|
236
456
|
* @param issuerConf The Issuer configuration returned by {@link evaluateIssuerTrust}
|
@@ -248,24 +468,39 @@ export const verifyAndParseCredential: VerifyAndParseCredential = async (
|
|
248
468
|
issuerConf,
|
249
469
|
credential,
|
250
470
|
credentialConfigurationId,
|
251
|
-
context
|
471
|
+
context,
|
472
|
+
x509CertRoot
|
252
473
|
) => {
|
253
474
|
const format =
|
254
475
|
issuerConf.openid_credential_issuer.credential_configurations_supported[
|
255
476
|
credentialConfigurationId
|
256
477
|
]?.format;
|
257
478
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
479
|
+
switch (format) {
|
480
|
+
case "dc+sd-jwt": {
|
481
|
+
Logger.log(LogLevel.DEBUG, "Parsing credential in dc+sd-jwt format");
|
482
|
+
return verifyAndParseCredentialSdJwt(
|
483
|
+
issuerConf,
|
484
|
+
credential,
|
485
|
+
credentialConfigurationId,
|
486
|
+
context
|
487
|
+
);
|
488
|
+
}
|
489
|
+
case "mso_mdoc": {
|
490
|
+
Logger.log(LogLevel.DEBUG, "Parsing credential in mso_mdoc format");
|
491
|
+
return verifyAndParseCredentialMDoc(
|
492
|
+
issuerConf,
|
493
|
+
credential,
|
494
|
+
credentialConfigurationId,
|
495
|
+
context,
|
496
|
+
x509CertRoot
|
497
|
+
);
|
498
|
+
}
|
267
499
|
|
268
|
-
|
269
|
-
|
270
|
-
|
500
|
+
default: {
|
501
|
+
const message = `Unsupported credential format: ${format}`;
|
502
|
+
Logger.log(LogLevel.ERROR, message);
|
503
|
+
throw new IoWalletError(message);
|
504
|
+
}
|
505
|
+
}
|
271
506
|
};
|
@@ -171,7 +171,7 @@ const { credential_configuration_id, credential_identifiers } =
|
|
171
171
|
accessToken.authorization_details[0]!;
|
172
172
|
|
173
173
|
// Obtain the credential
|
174
|
-
const { credential } = await Credential.Issuance.obtainCredential(
|
174
|
+
const { credential, format } = await Credential.Issuance.obtainCredential(
|
175
175
|
issuerConf,
|
176
176
|
accessToken,
|
177
177
|
clientId,
|
@@ -186,6 +186,10 @@ const { credential } = await Credential.Issuance.obtainCredential(
|
|
186
186
|
}
|
187
187
|
);
|
188
188
|
|
189
|
+
// The certificate below is required to perform the `x5chain` validation of credentials in `mdoc` format.
|
190
|
+
// In a real-world scenario, it must be obtained from the appropriate endpoint exposed by the Trust Anchor
|
191
|
+
const mockX509CertRoot = format === "mso_mdoc" ? "base64encodedX509CertRoot" : undefined
|
192
|
+
|
189
193
|
/*
|
190
194
|
* Parse and verify the credential. The ignoreMissingAttributes flag must be set to false or omitted in production.
|
191
195
|
* WARNING: includeUndefinedAttributes should not be set to true in production in order to get only claims explicitly declared by the issuer.
|
@@ -199,7 +203,8 @@ const { parsedCredential } =
|
|
199
203
|
credentialCryptoContext,
|
200
204
|
ignoreMissingAttributes: true,
|
201
205
|
includeUndefinedAttributes: false
|
202
|
-
}
|
206
|
+
},
|
207
|
+
mockX509CertRoot
|
203
208
|
);
|
204
209
|
|
205
210
|
const credentialType =
|
@@ -0,0 +1 @@
|
|
1
|
+
export const MDOC_DEFAULT_NAMESPACE = "org.iso.18013.5.1";
|
@@ -0,0 +1,26 @@
|
|
1
|
+
/**
|
2
|
+
* Extracts the date value of a given elementIdentifier from an MDOC object.
|
3
|
+
* Searches through the issuerSigned namespaces and attempts to parse the value as a Date.
|
4
|
+
* The expected date format is "YYYY-MM-DD".
|
5
|
+
* Returns the Date object if found, otherwise returns null.
|
6
|
+
*/
|
7
|
+
export function extractElementValueAsDate(elementValue: string): Date | null {
|
8
|
+
if (typeof elementValue === "string") {
|
9
|
+
const dateParts = elementValue.split("-");
|
10
|
+
if (dateParts.length === 3) {
|
11
|
+
const [year, month, day] = dateParts.map(Number);
|
12
|
+
if (
|
13
|
+
day !== undefined &&
|
14
|
+
month !== undefined &&
|
15
|
+
year !== undefined &&
|
16
|
+
!isNaN(day) &&
|
17
|
+
!isNaN(month) &&
|
18
|
+
!isNaN(year)
|
19
|
+
) {
|
20
|
+
return new Date(year, month - 1, day); // Month is zero-based in JS Date
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
return null; // Return null if no matching element is found or it's not a valid date
|
26
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { CBOR, COSE } from "@pagopa/io-react-native-iso18013";
|
2
|
+
import { b64utob64 } from "jsrsasign";
|
3
|
+
import {
|
4
|
+
verifyCertificateChain,
|
5
|
+
type CertificateValidationResult,
|
6
|
+
type PublicKey,
|
7
|
+
type X509CertificateOptions,
|
8
|
+
} from "@pagopa/io-react-native-crypto";
|
9
|
+
import { MissingX509CertsError, X509ValidationError } from "../trust/errors";
|
10
|
+
import { IoWalletError } from "../utils/errors";
|
11
|
+
import { convertBase64DerToPem, getSigninJwkFromCert } from "../utils/crypto";
|
12
|
+
|
13
|
+
export const verify = async (
|
14
|
+
token: string,
|
15
|
+
x509CertRoot: string
|
16
|
+
): Promise<{ issuerSigned: CBOR.IssuerSigned }> => {
|
17
|
+
// get decoded data
|
18
|
+
const issuerSigned = await CBOR.decodeIssuerSigned(token);
|
19
|
+
|
20
|
+
if (!issuerSigned) {
|
21
|
+
throw new IoWalletError("Invalid mDoc");
|
22
|
+
}
|
23
|
+
|
24
|
+
if (
|
25
|
+
!issuerSigned.issuerAuth.unprotectedHeader?.x5chain &&
|
26
|
+
(!Array.isArray(issuerSigned.issuerAuth.unprotectedHeader.x5chain) ||
|
27
|
+
issuerSigned.issuerAuth.unprotectedHeader.x5chain.length === 0)
|
28
|
+
) {
|
29
|
+
throw new MissingX509CertsError("Missing x509 certificates");
|
30
|
+
}
|
31
|
+
const x5chain =
|
32
|
+
issuerSigned.issuerAuth.unprotectedHeader.x5chain.map(b64utob64);
|
33
|
+
// Verify the x5chain
|
34
|
+
await verifyX5chain(x5chain, x509CertRoot);
|
35
|
+
|
36
|
+
const coseSign1 = issuerSigned.issuerAuth.rawValue;
|
37
|
+
|
38
|
+
if (!coseSign1) {
|
39
|
+
throw new IoWalletError("Missing coseSign1");
|
40
|
+
}
|
41
|
+
// Once the x5chain is verified, the signatures verification can be performed
|
42
|
+
await verifyMdocSignature(coseSign1, x5chain[0]!);
|
43
|
+
|
44
|
+
return { issuerSigned };
|
45
|
+
};
|
46
|
+
|
47
|
+
/**
|
48
|
+
* This function checks whether the x509 certificate chain is valid against a specified Certificate Authority (CA)
|
49
|
+
*
|
50
|
+
* @param x5chain The mdoc's x509 certificate chain
|
51
|
+
* @param x509CertRoot The Trust Anchor CA
|
52
|
+
* @param options Options for certificate validation
|
53
|
+
*/
|
54
|
+
const verifyX5chain = async (
|
55
|
+
x5chain: string[],
|
56
|
+
x509CertRoot: string,
|
57
|
+
options: X509CertificateOptions = {
|
58
|
+
connectTimeout: 10000,
|
59
|
+
readTimeout: 10000,
|
60
|
+
requireCrl: true,
|
61
|
+
}
|
62
|
+
) => {
|
63
|
+
const x509ValidationResult: CertificateValidationResult =
|
64
|
+
await verifyCertificateChain(x5chain, x509CertRoot, options);
|
65
|
+
|
66
|
+
if (!x509ValidationResult.isValid) {
|
67
|
+
throw new X509ValidationError(
|
68
|
+
`X.509 certificate chain validation failed. Status: ${x509ValidationResult.validationStatus}. Error: ${x509ValidationResult.errorMessage}`,
|
69
|
+
{
|
70
|
+
x509ValidationStatus: x509ValidationResult.validationStatus,
|
71
|
+
x509ErrorMessage: x509ValidationResult.errorMessage,
|
72
|
+
}
|
73
|
+
);
|
74
|
+
}
|
75
|
+
};
|
76
|
+
/**
|
77
|
+
* This function verifies that the signature is valid for the given certificate.
|
78
|
+
* If not, it throws an error
|
79
|
+
*
|
80
|
+
* @param coseSign1 The COSE-Sign1 object encoded in base64 or base64url
|
81
|
+
* @param cert The `x5chain`'s leaf certificate
|
82
|
+
*/
|
83
|
+
const verifyMdocSignature = async (coseSign1: string, cert: string) => {
|
84
|
+
const pemcert = convertBase64DerToPem(cert);
|
85
|
+
const jwk = getSigninJwkFromCert(pemcert);
|
86
|
+
|
87
|
+
jwk.x = b64utob64(jwk.x!);
|
88
|
+
jwk.y = b64utob64(jwk.y!);
|
89
|
+
|
90
|
+
const signatureCorrect = await COSE.verify(coseSign1, jwk as PublicKey);
|
91
|
+
|
92
|
+
if (!signatureCorrect) throw new Error("Invalid mDoc signature");
|
93
|
+
};
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
* @param namespace The mdoc credential `namespace`
|
3
|
+
* @param key The claim attribute key
|
4
|
+
* @returns A string consisting of the concatenation of the namespace and the claim key, separated by a colon
|
5
|
+
*/
|
6
|
+
export const getParsedCredentialClaimKey = (namespace: string, key: string) =>
|
7
|
+
`${namespace}:${key}`;
|
package/src/trust/types.ts
CHANGED
@@ -38,7 +38,7 @@ const CredentialIssuerDisplayMetadata = z.object({
|
|
38
38
|
|
39
39
|
type ClaimsMetadata = z.infer<typeof ClaimsMetadata>;
|
40
40
|
const ClaimsMetadata = z.object({
|
41
|
-
path: z.array(z.string()),
|
41
|
+
path: z.array(z.union([z.string(), z.number(), z.null()])), // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-claims-path-pointer
|
42
42
|
display: z.array(CredentialDisplayMetadata),
|
43
43
|
});
|
44
44
|
|
@@ -71,6 +71,10 @@ const SupportedCredentialMetadata = z.intersection(
|
|
71
71
|
})
|
72
72
|
);
|
73
73
|
|
74
|
+
export type SupportedCredentialFormat = z.infer<
|
75
|
+
typeof SupportedCredentialMetadata
|
76
|
+
>["format"];
|
77
|
+
|
74
78
|
export type EntityStatement = z.infer<typeof EntityStatement>;
|
75
79
|
export const EntityStatement = z.object({
|
76
80
|
header: z.object({
|