Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [EUDIW-209] Add Remote presentation for Potential #174

Merged
merged 34 commits into from
Jan 24, 2025
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
25f8190
feat: enhance types adding PresentationDefinition
manuraf Jan 14, 2025
d08357d
feat: add fetch presentation definition
manuraf Jan 14, 2025
cbfe798
feat: add evaluate input descriptor for each credential
manuraf Jan 15, 2025
39147fd
feat: add send authorization response
manuraf Jan 15, 2025
51e2a24
fix: some improvements
manuraf Jan 15, 2025
1dab0a5
refactor: move signature to dedicated step
manuraf Jan 16, 2025
873c97d
feat: build wellKnownUrl to retrive rp jwks using iss of request object
manuraf Jan 16, 2025
d65a2f4
refactor: reorg step number name and enhance README
manuraf Jan 16, 2025
4c1984e
fix: SendAuthorizationResponse input object
manuraf Jan 16, 2025
9cff22d
fix: handle custom link or universal link on reading qrcode
manuraf Jan 20, 2025
1f37820
fix: request object gets a JAR rather than json
manuraf Jan 20, 2025
435cc45
fix: RelyingPartyEntityConfiguration type on retrieve rp jwks
manuraf Jan 20, 2025
ace631f
fix: README presentation
manuraf Jan 20, 2025
b30ac87
Merge branch 'eudiw-master' into feature/EUDIW-209-presentation-defin…
manuraf Jan 20, 2025
09534fc
fix: retrieve rp jwks test
manuraf Jan 20, 2025
17dc9bd
fix: reference to PresentationDefinition inside trust types
manuraf Jan 20, 2025
585356e
fix: add config allErrors true to Ajv
manuraf Jan 21, 2025
ab34a24
feat: build presente_submission using PresentationDefinition object
manuraf Jan 21, 2025
720e2a2
refactor: avoid replace original resource in case when evaluate qrcode
manuraf Jan 21, 2025
3eaebec
chore: remove unused import
manuraf Jan 21, 2025
824c06b
refactor: move logic to retrieve jwk from request object inside dedic…
manuraf Jan 22, 2025
77be819
feat: added specific error case
manuraf Jan 22, 2025
d0ba9af
fix: JWKS.parse when retrieve RP public key from endpoint
manuraf Jan 22, 2025
d75977b
amend me
manuraf Jan 22, 2025
1047af4
refactor: evaluate input descriptor
manuraf Jan 22, 2025
82408a5
feat: throw exception if exp is expired
manuraf Jan 22, 2025
94c60df
chore: improve README
manuraf Jan 22, 2025
7eb5a89
feat: handle support to other path pattern in evaluate description
manuraf Jan 23, 2025
1e24f86
amend me
manuraf Jan 23, 2025
18d1957
fix: aud and sd_hash with base64 output
manuraf Jan 23, 2025
2d62bec
feat: added redirect_uri on response
manuraf Jan 23, 2025
eb27b80
refactor: remove useless let
manuraf Jan 23, 2025
e2cd10c
refactor: improve immutability and readability when categorizeDisclos…
manuraf Jan 24, 2025
d69f8e4
Merge branch 'eudiw-master' into feature/EUDIW-209-presentation-defin…
LazyAfternoons Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -107,8 +107,10 @@
]
},
"dependencies": {
"ajv": "^8.17.1",
"js-base64": "^3.7.7",
"js-sha256": "^0.9.0",
"jsonpath-plus": "^10.2.0",
"parse-url": "^9.2.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.1",
16 changes: 10 additions & 6 deletions src/credential/presentation/01-start-flow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as z from "zod";
import { decodeBase64 } from "@pagopa/io-react-native-jwt";
import { AuthRequestDecodeError } from "./errors";
import { InvalidQRCodeError } from "./errors";

const QRCodePayload = z.object({
protocol: z.string(),
@@ -31,10 +30,15 @@ export type StartFlow<T extends Array<unknown> = []> = (...args: T) => {
export const startFlowFromQR: StartFlow<[string]> = (qrcode) => {
let decodedUrl: URL;
try {
const decoded = decodeBase64(qrcode);
decodedUrl = new URL(decoded);
// splitting qrcode to identify which is link format
const originalQrCode = qrcode.split("://");
const replacedQrcode = originalQrCode[1]?.startsWith("?")
? qrcode.replace(`${originalQrCode[0]}://`, "https://wallet.example/")
: qrcode;

decodedUrl = new URL(replacedQrcode);
} catch (error) {
throw new AuthRequestDecodeError("Failed to decode QR code: ", qrcode);
throw new InvalidQRCodeError(`Failed to decode QR code: ${qrcode}`);
}

const protocol = decodedUrl.protocol;
@@ -52,6 +56,6 @@ export const startFlowFromQR: StartFlow<[string]> = (qrcode) => {
if (result.success) {
return result.data;
} else {
throw new AuthRequestDecodeError(result.error.message, `${decodedUrl}`);
throw new InvalidQRCodeError(`${result.error.message}, ${decodedUrl}`);
}
};
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import uuid from "react-native-uuid";
import {
decode as decodeJwt,
sha256ToBase64,
verify,
type CryptoContext,
} from "@pagopa/io-react-native-jwt";

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

export type GetRequestObject = (
requestUri: Out<StartFlow>["requestURI"],
context: {
wiaCryptoContext: CryptoContext;
appFetch?: GlobalFetch["fetch"];
walletInstanceAttestation: string;
},
jwkKeys?: Out<FetchJwks>["keys"]
) => Promise<{ requestObject: RequestObject }>;
}
) => Promise<{ requestObjectEncodedJwt: string }>;

/**
* Obtain the Request Object for RP authentication
@@ -36,8 +30,7 @@ export type GetRequestObject = (
*/
export const getRequestObject: GetRequestObject = async (
requestUri,
{ wiaCryptoContext, appFetch = fetch, walletInstanceAttestation },
jwkKeys
{ wiaCryptoContext, appFetch = fetch, walletInstanceAttestation }
) => {
const signedWalletInstanceDPoP = await createDPopToken(
{
@@ -49,55 +42,17 @@ export const getRequestObject: GetRequestObject = async (
wiaCryptoContext
);

const responseEncodedJwt = await appFetch(requestUri, {
const requestObjectEncodedJwt = await appFetch(requestUri, {
method: "GET",
headers: {
Authorization: `DPoP ${walletInstanceAttestation}`,
DPoP: signedWalletInstanceDPoP,
},
})
.then(hasStatusOrThrow(200))
.then((res) => res.json())
.then((responseJson) => responseJson.response);

const responseJwt = decodeJwt(responseEncodedJwt);

await verifyTokenSignature(jwkKeys, responseJwt);

// Ensure that the request object conforms to the expected specification.
const requestObject = RequestObject.parse(responseJwt.payload);
.then((res) => res.text());

return {
requestObject,
requestObjectEncodedJwt,
};
};

const verifyTokenSignature = async (
jwkKeys?: Out<FetchJwks>["keys"],
responseJwt?: any
): Promise<void> => {
// verify token signature to ensure the request object is authentic
// 1. according to entity configuration if present
if (jwkKeys) {
const pubKey = jwkKeys.find(
({ kid }) => kid === responseJwt.protectedHeader.kid
);
if (!pubKey) {
throw new NoSuitableKeysFoundInEntityConfiguration(
"Request Object signature verification"
);
}
await verify(responseJwt, pubKey);
return;
}

// 2. If jwk is not retrieved from entity config, check if the token contains the 'jwk' attribute
if (responseJwt.protectedHeader?.jwk) {
const pubKey = responseJwt.protectedHeader.jwk;
await verify(responseJwt, pubKey);
return;
}

// No verification condition matched: skipping signature verification for now.
// TODO: [EUDIW-215] Remove skipping signature verification
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { JWKS, JWK } from "../../utils/jwk";
import { hasStatusOrThrow } from "../../utils/misc";
import { RelyingPartyEntityConfiguration } from "../../entity/trust/types";
import { decode as decodeJwt } from "@pagopa/io-react-native-jwt";
import { NoSuitableKeysFoundInEntityConfiguration } from "./errors";

/**
* 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<{

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

const wellKnownUrl = new URL(
"/.well-known/jar-issuer/jwk",
clientUrl
).toString();
// 1. check if request object jwt contains the 'jwk' attribute
if (requestObjectJwt.protectedHeader?.jwk) {
return {
keys: [JWK.parse(requestObjectJwt.protectedHeader.jwk)],
};
}

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

return {
keys: jwks.keys,
};
// Fetches the JWKS from a specific endpoint of the entity's well-known configuration
const jwks = await appFetch(wellKnownUrl, {
method: "GET",
})
.then(hasStatusOrThrow(200))
.then((raw) => raw.json())
.then((json) => JWKS.parse(json.jwks));

return {
keys: jwks.keys,
};
}

throw new NoSuitableKeysFoundInEntityConfiguration(
"Request Object signature verification"
);
};

/**
@@ -54,14 +74,9 @@ export const fetchJwksFromUri: FetchJwks<
* @throws Will throw an error if the configuration is invalid or if JWKS is not found.
*/
export const fetchJwksFromConfig: FetchJwks<
[RelyingPartyEntityConfiguration]
[RelyingPartyEntityConfiguration["payload"]["metadata"]]
> = async (rpConfig) => {
const parsedConfig = RelyingPartyEntityConfiguration.safeParse(rpConfig);
if (!parsedConfig.success) {
throw new Error("Invalid Relying Party configuration.");
}

const jwks = parsedConfig.data.payload.metadata.wallet_relying_party.jwks;
const jwks = rpConfig.wallet_relying_party.jwks;

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

Unchanged files with check annotations Beta

<IconButton
icon={expanded ? "chevronTop" : "chevronBottom"}
accessibilityLabel="expand"
onPress={() => setExpanded((_) => !_)}

Check warning on line 93 in example/src/components/debug/DebugPrettyPrint.tsx

GitHub Actions / code_review

'_' is already declared in the upper scope on line 15 column 8
color="contrast"
/>
)}