Skip to content

Commit efb1e93

Browse files
feat: [EUDIW-209] Add Remote presentation for Potential (#174)
Co-authored-by: LazyAfternoons <[email protected]>
1 parent 0d81381 commit efb1e93

21 files changed

+1653
-497
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,10 @@
107107
]
108108
},
109109
"dependencies": {
110+
"ajv": "^8.17.1",
110111
"js-base64": "^3.7.7",
111112
"js-sha256": "^0.9.0",
113+
"jsonpath-plus": "^10.2.0",
112114
"parse-url": "^9.2.0",
113115
"react-native-url-polyfill": "^2.0.0",
114116
"react-native-uuid": "^2.0.1",

src/credential/presentation/01-start-flow.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as z from "zod";
2-
import { decodeBase64 } from "@pagopa/io-react-native-jwt";
3-
import { AuthRequestDecodeError } from "./errors";
2+
import { InvalidQRCodeError } from "./errors";
43

54
const QRCodePayload = z.object({
65
protocol: z.string(),
@@ -31,10 +30,15 @@ export type StartFlow<T extends Array<unknown> = []> = (...args: T) => {
3130
export const startFlowFromQR: StartFlow<[string]> = (qrcode) => {
3231
let decodedUrl: URL;
3332
try {
34-
const decoded = decodeBase64(qrcode);
35-
decodedUrl = new URL(decoded);
33+
// splitting qrcode to identify which is link format
34+
const originalQrCode = qrcode.split("://");
35+
const replacedQrcode = originalQrCode[1]?.startsWith("?")
36+
? qrcode.replace(`${originalQrCode[0]}://`, "https://wallet.example/")
37+
: qrcode;
38+
39+
decodedUrl = new URL(replacedQrcode);
3640
} catch (error) {
37-
throw new AuthRequestDecodeError("Failed to decode QR code: ", qrcode);
41+
throw new InvalidQRCodeError(`Failed to decode QR code: ${qrcode}`);
3842
}
3943

4044
const protocol = decodedUrl.protocol;
@@ -52,6 +56,6 @@ export const startFlowFromQR: StartFlow<[string]> = (qrcode) => {
5256
if (result.success) {
5357
return result.data;
5458
} else {
55-
throw new AuthRequestDecodeError(result.error.message, `${decodedUrl}`);
59+
throw new InvalidQRCodeError(`${result.error.message}, ${decodedUrl}`);
5660
}
5761
};
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
import uuid from "react-native-uuid";
22
import {
3-
decode as decodeJwt,
43
sha256ToBase64,
5-
verify,
64
type CryptoContext,
75
} from "@pagopa/io-react-native-jwt";
86

97
import { createDPopToken } from "../../utils/dpop";
10-
import { NoSuitableKeysFoundInEntityConfiguration } from "./errors";
11-
import type { FetchJwks } from "./03-retrieve-jwks";
128
import { hasStatusOrThrow, type Out } from "../../utils/misc";
139
import type { StartFlow } from "./01-start-flow";
14-
import { RequestObject } from "./types";
1510

1611
export type GetRequestObject = (
1712
requestUri: Out<StartFlow>["requestURI"],
1813
context: {
1914
wiaCryptoContext: CryptoContext;
2015
appFetch?: GlobalFetch["fetch"];
2116
walletInstanceAttestation: string;
22-
},
23-
jwkKeys?: Out<FetchJwks>["keys"]
24-
) => Promise<{ requestObject: RequestObject }>;
17+
}
18+
) => Promise<{ requestObjectEncodedJwt: string }>;
2519

2620
/**
2721
* Obtain the Request Object for RP authentication
@@ -36,8 +30,7 @@ export type GetRequestObject = (
3630
*/
3731
export const getRequestObject: GetRequestObject = async (
3832
requestUri,
39-
{ wiaCryptoContext, appFetch = fetch, walletInstanceAttestation },
40-
jwkKeys
33+
{ wiaCryptoContext, appFetch = fetch, walletInstanceAttestation }
4134
) => {
4235
const signedWalletInstanceDPoP = await createDPopToken(
4336
{
@@ -49,55 +42,17 @@ export const getRequestObject: GetRequestObject = async (
4942
wiaCryptoContext
5043
);
5144

52-
const responseEncodedJwt = await appFetch(requestUri, {
45+
const requestObjectEncodedJwt = await appFetch(requestUri, {
5346
method: "GET",
5447
headers: {
5548
Authorization: `DPoP ${walletInstanceAttestation}`,
5649
DPoP: signedWalletInstanceDPoP,
5750
},
5851
})
5952
.then(hasStatusOrThrow(200))
60-
.then((res) => res.json())
61-
.then((responseJson) => responseJson.response);
62-
63-
const responseJwt = decodeJwt(responseEncodedJwt);
64-
65-
await verifyTokenSignature(jwkKeys, responseJwt);
66-
67-
// Ensure that the request object conforms to the expected specification.
68-
const requestObject = RequestObject.parse(responseJwt.payload);
53+
.then((res) => res.text());
6954

7055
return {
71-
requestObject,
56+
requestObjectEncodedJwt,
7257
};
7358
};
74-
75-
const verifyTokenSignature = async (
76-
jwkKeys?: Out<FetchJwks>["keys"],
77-
responseJwt?: any
78-
): Promise<void> => {
79-
// verify token signature to ensure the request object is authentic
80-
// 1. according to entity configuration if present
81-
if (jwkKeys) {
82-
const pubKey = jwkKeys.find(
83-
({ kid }) => kid === responseJwt.protectedHeader.kid
84-
);
85-
if (!pubKey) {
86-
throw new NoSuitableKeysFoundInEntityConfiguration(
87-
"Request Object signature verification"
88-
);
89-
}
90-
await verify(responseJwt, pubKey);
91-
return;
92-
}
93-
94-
// 2. If jwk is not retrieved from entity config, check if the token contains the 'jwk' attribute
95-
if (responseJwt.protectedHeader?.jwk) {
96-
const pubKey = responseJwt.protectedHeader.jwk;
97-
await verify(responseJwt, pubKey);
98-
return;
99-
}
100-
101-
// No verification condition matched: skipping signature verification for now.
102-
// TODO: [EUDIW-215] Remove skipping signature verification
103-
};

src/credential/presentation/03-retrieve-jwks.ts renamed to src/credential/presentation/04-retrieve-rp-jwks.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { JWKS, JWK } from "../../utils/jwk";
22
import { hasStatusOrThrow } from "../../utils/misc";
33
import { RelyingPartyEntityConfiguration } from "../../entity/trust/types";
4+
import { decode as decodeJwt } from "@pagopa/io-react-native-jwt";
5+
import { NoSuitableKeysFoundInEntityConfiguration } from "./errors";
46

57
/**
68
* Defines the signature for a function that retrieves JSON Web Key Sets (JWKS) from a client.
@@ -15,35 +17,53 @@ export type FetchJwks<T extends Array<unknown> = []> = (...args: T) => Promise<{
1517

1618
/**
1719
* Retrieves the JSON Web Key Set (JWKS) from the specified client's well-known endpoint.
20+
* It is formed using `{issUrl.base}/.well-known/jar-issuer${issUrl.pah}` as explained in SD-JWT VC issuer metadata section
1821
*
19-
* @param clientUrl - The base URL of the client entity from which to retrieve the JWKS.
22+
* @param requestObjectEncodedJwt - Request Object in JWT format.
2023
* @param options - Optional context containing a custom fetch implementation.
2124
* @param options.context - Optional context object.
2225
* @param options.context.appFetch - Optional custom fetch function to use instead of the global `fetch`.
2326
* @returns A promise resolving to an object containing an array of JWKs.
2427
* @throws Will throw an error if the JWKS retrieval fails.
2528
*/
26-
export const fetchJwksFromUri: FetchJwks<
29+
export const fetchJwksFromRequestObject: FetchJwks<
2730
[string, { context?: { appFetch?: GlobalFetch["fetch"] } }]
28-
> = async (clientUrl, { context = {} } = {}) => {
31+
> = async (requestObjectEncodedJwt, { context = {} } = {}) => {
2932
const { appFetch = fetch } = context;
33+
const requestObjectJwt = decodeJwt(requestObjectEncodedJwt);
3034

31-
const wellKnownUrl = new URL(
32-
"/.well-known/jar-issuer/jwk",
33-
clientUrl
34-
).toString();
35+
// 1. check if request object jwt contains the 'jwk' attribute
36+
if (requestObjectJwt.protectedHeader?.jwk) {
37+
return {
38+
keys: [JWK.parse(requestObjectJwt.protectedHeader.jwk)],
39+
};
40+
}
3541

36-
// Fetches the JWKS from a specific endpoint of the entity's well-known configuration
37-
const jwks = await appFetch(wellKnownUrl, {
38-
method: "GET",
39-
})
40-
.then(hasStatusOrThrow(200))
41-
.then((raw) => raw.json())
42-
.then((json) => JWKS.parse(json));
42+
// 2. According to Potential profile, retrieve from RP endpoint using iss claim
43+
const issClaimValue = requestObjectJwt.payload?.iss as string;
44+
if (issClaimValue) {
45+
const issUrl = new URL(issClaimValue);
46+
const wellKnownUrl = new URL(
47+
`/.well-known/jar-issuer${issUrl.pathname}`,
48+
`${issUrl.protocol}//${issUrl.host}`
49+
).toString();
4350

44-
return {
45-
keys: jwks.keys,
46-
};
51+
// Fetches the JWKS from a specific endpoint of the entity's well-known configuration
52+
const jwks = await appFetch(wellKnownUrl, {
53+
method: "GET",
54+
})
55+
.then(hasStatusOrThrow(200))
56+
.then((raw) => raw.json())
57+
.then((json) => JWKS.parse(json.jwks));
58+
59+
return {
60+
keys: jwks.keys,
61+
};
62+
}
63+
64+
throw new NoSuitableKeysFoundInEntityConfiguration(
65+
"Request Object signature verification"
66+
);
4767
};
4868

4969
/**
@@ -54,14 +74,9 @@ export const fetchJwksFromUri: FetchJwks<
5474
* @throws Will throw an error if the configuration is invalid or if JWKS is not found.
5575
*/
5676
export const fetchJwksFromConfig: FetchJwks<
57-
[RelyingPartyEntityConfiguration]
77+
[RelyingPartyEntityConfiguration["payload"]["metadata"]]
5878
> = async (rpConfig) => {
59-
const parsedConfig = RelyingPartyEntityConfiguration.safeParse(rpConfig);
60-
if (!parsedConfig.success) {
61-
throw new Error("Invalid Relying Party configuration.");
62-
}
63-
64-
const jwks = parsedConfig.data.payload.metadata.wallet_relying_party.jwks;
79+
const jwks = rpConfig.wallet_relying_party.jwks;
6580

6681
if (!jwks || !Array.isArray(jwks.keys)) {
6782
throw new Error("JWKS not found in Relying Party configuration.");

0 commit comments

Comments
 (0)