Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Updating to OID4VP_1_0_20 #78

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vscode/*
.idea/*
.tsimp/*
*.iml
.nyc_output
build
Expand Down
10 changes: 5 additions & 5 deletions generator/schemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,20 @@ const authorizationRequestPayloadVD11 = {
skipTypeCheck: true
};

const authorizationRequestPayloadVD12OID4VPD18 = {
const authorizationRequestPayloadVD13OID4VPD20 = {
path: '../src/types/SIOP.types.ts',
tsconfig: 'tsconfig.json',
type: 'AuthorizationRequestPayloadVD12OID4VPD18', // Or <type-name> if you want to generate schema for that one type only
schemaId: 'AuthorizationRequestPayloadVD12OID4VPD18Schema',
outputPath: 'src/schemas/AuthorizationRequestPayloadVD12OID4VPD18.schema.ts',
type: 'AuthorizationRequestPayloadVD13OID4VPD20', // Or <type-name> if you want to generate schema for that one type only
schemaId: 'AuthorizationRequestPayloadVD13OID4VPD20Schema',
outputPath: 'src/schemas/AuthorizationRequestPayloadVD13OID4VPD20.schema.ts',
// outputConstName: 'AuthorizationRequestPayloadSchemaVD11',
skipTypeCheck: true
};

let schemas: Schema[] = [
writeSchema(authorizationRequestPayloadVID1),
writeSchema(authorizationRequestPayloadVD11),
writeSchema(authorizationRequestPayloadVD12OID4VPD18),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing v18?

writeSchema(authorizationRequestPayloadVD13OID4VPD20),
// writeSchema(requestOptsConf),
writeSchema(responseOptsConf),
writeSchema(rPRegistrationMetadataPayload),
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"events": "^3.3.0",
"language-tags": "^1.0.9",
"multiformats": "^12.1.3",
"node-forge": "^1.3.1",
"qs": "^6.11.2",
"sha.js": "^2.4.11",
"uint8arrays": "^3.1.1",
Expand All @@ -64,6 +65,7 @@
"@transmute/ed25519-signature-2018": "^0.7.0-unstable.82",
"@types/jest": "^29.5.11",
"@types/language-tags": "^1.0.4",
"@types/node-forge": "^1.3.11",
"@types/qs": "^6.9.11",
"@types/sha.js": "^2.4.4",
"@types/uuid": "^9.0.7",
Expand Down
104 changes: 101 additions & 3 deletions src/authorization-request/AuthorizationRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { JWTVerifyOptions } from 'did-jwt';
import { decodeJWT } from 'did-jwt';
import { JWTDecoded } from 'did-jwt/lib/JWT';
import forge from 'node-forge';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cannot work. This library is used in browser, RN and Node. The lib is node only


import { PresentationDefinitionWithLocation } from '../authorization-response';
import { PresentationExchange } from '../authorization-response/PresentationExchange';
Expand Down Expand Up @@ -186,11 +189,15 @@
throw new Error(`${SIOPErrors.INVALID_REQUEST}, redirect_uri or response_uri is needed`);
}

if (mergedPayload.client_id_scheme === 'verifier_attestation') {
verifiedJwt = await AuthorizationRequest.verifyAttestationJWT(jwt, mergedPayload.client_id);
} else if (mergedPayload.client_id_scheme === 'x509_san_dns') {
await this.checkX509SanDNSScheme(jwt, mergedPayload.client_id);
} else if (mergedPayload.client_id_scheme === 'x509_san_uri') {
throw new Error(SIOPErrors.VERIFICATION_X509_SAN_URI_SCHEME_NOT_IMPLEMENTED_ERROR);
}
await checkWellknownDIDFromRequest(mergedPayload, opts);

// TODO: we need to verify somewhere that if response_mode is direct_post, that the response_uri may be present,
// BUT not both redirect_uri and response_uri. What is the best place to do this?

const presentationDefinitions = await PresentationExchange.findValidPresentationDefinitions(mergedPayload, await this.getSupportedVersion());
return {
...verifiedJwt,
Expand Down Expand Up @@ -248,6 +255,97 @@
};
}

/**
* Verifies a JWT according to the 'verifier_attestation' client_id_scheme, where the JWT must be
* signed with a private key corresponding to the public key specified within the JWT itself. This method
* ensures that the JWT's 'sub' claim matches the provided clientId, and it extracts and validates the
* public key from the JWT's 'cnf' (confirmation) claim, which must contain a JWK.
*
* An example of such request would be:
* GET /authorize?
* response_type=vp_token
* &client_id=https%3A%2F%2Fverifier.example.org
* &client_id_scheme=verifier_attestation
* &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
* &presentation_definition=...
* &nonce=n-0S6_WzA2Mj
* &jwt=eyJ...abc
*
* @param jwt The JSON Web Token string to be verified. It is expected that this JWT is formatted correctly
* and includes a 'cnf' claim with a JWK representing the public key used for signing the JWT.
* @param clientId The client identifier expected to match the 'sub' claim in the JWT. This is used to
* validate that the JWT is intended for the correct recipient/client.
*/
private static async verifyAttestationJWT(jwt: string, clientId: string): Promise<VerifiedJWT> {
if (!jwt) {
throw new Error(SIOPErrors.NO_JWT);
}
const payload = decodeJWT(jwt);
AuthorizationRequest.checkPayloadClaims(payload, ['iss', 'sub', 'exp', 'cnf']);
const sub = payload['sub'];
const cnf = payload['cnf'];

if (sub !== clientId || !cnf || typeof cnf !== 'object' || !cnf['jwk'] || typeof cnf['jwk'] !== 'object') {
throw new Error(SIOPErrors.VERIFICATION_VERIFIER_ATTESTATION_SCHEME_ERROR);
}

return {
jwt,
payload: payload.payload,
issuer: payload['iss'],
jwk: cnf['jwk'],
};
}

/**
* verifying JWTs against X.509 certificates focusing on DNS SAN compliance, which is crucial for environments where certificate-based security is pivotal.
*
* An example of such request would be:
* GET /authorize?
* response_type=vp_token
* &client_id=client.example.org
* &client_id_scheme=x509_san_dns
* &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
* &presentation_definition=...
* &nonce=n-0S6_WzA2Mj
*
* @param jwt The encoded JWT from which the certificate needs to be extracted.
* @param clientId The DNS name to match against the certificate's SANs.
*/
private async checkX509SanDNSScheme(jwt: string, clientId: string): Promise<void> {
const jwtDecoded: JWTDecoded = decodeJWT(jwt);
const x5c = jwtDecoded.header['x5c'];

if (x5c == null || !Array.isArray(x5c) || x5c.length === 0) {
throw new Error(SIOPErrors.VERIFICATION_X509_SAN_DNS_SCHEME_ERROR);
}

const certificate = x5c[0];
if (!certificate) {
throw new Error(SIOPErrors.VERIFICATION_X509_SAN_DNS_SCHEME_NO_CERTIFICATE_ERROR);
}

const der = forge.util.decode64(certificate);
const asn1 = forge.asn1.fromDer(der);
const cert = forge.pki.certificateFromAsn1(asn1);

const subjectAltNames = cert.getExtension('subjectAltName');
if (!subjectAltNames || !Array.isArray(subjectAltNames['altNames'])) {
throw new Error(SIOPErrors.VERIFICATION_X509_SAN_DNS_ALT_NAMES_ERROR);
}
if (!subjectAltNames || !subjectAltNames['altNames'].some((name: any) => name.value === clientId)) {

Check warning on line 336 in src/authorization-request/AuthorizationRequest.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type

Check warning on line 336 in src/authorization-request/AuthorizationRequest.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
throw new Error(SIOPErrors.VERIFICATION_X509_SAN_DNS_SCHEME_DNS_NAME_MATCH);
}
}

private static checkPayloadClaims(payload: JWTDecoded, requiredClaims: string[]): void {
requiredClaims.forEach((claim) => {
if (payload[claim] === undefined) {
throw new Error(`Payload is missing ${claim}`);
}
});
}

public async containsResponseType(singleType: ResponseType | string): Promise<boolean> {
const responseType: string = await this.getMergedProperty('response_type');
return responseType?.includes(singleType) === true;
Expand Down
8 changes: 4 additions & 4 deletions src/helpers/SIOPSpecVersion.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuthorizationRequestPayloadVD11Schema, AuthorizationRequestPayloadVID1Schema } from '../schemas';
import { AuthorizationRequestPayloadVD12OID4VPD18Schema } from '../schemas/validation/schemaValidation';
import { AuthorizationRequestPayloadVD13OID4VPD20Schema } from '../schemas/validation/schemaValidation';
import { AuthorizationRequestPayload, ResponseMode, SupportedVersion } from '../types';
import errors from '../types/Errors';

Expand Down Expand Up @@ -34,15 +34,15 @@ export const authorizationRequestVersionDiscovery = (authorizationRequest: Autho
const versions = [];
const authorizationRequestCopy: AuthorizationRequestPayload = JSON.parse(JSON.stringify(authorizationRequest));
// todo: We could use v11 validation for v12 for now, as we do not differentiate in the schema at this point\
const vd12Validation = AuthorizationRequestPayloadVD12OID4VPD18Schema(authorizationRequestCopy);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't remove older versions

if (vd12Validation) {
const vd13Validation = AuthorizationRequestPayloadVD13OID4VPD20Schema(authorizationRequestCopy);
if (vd13Validation) {
if (
!authorizationRequestCopy.registration_uri &&
!authorizationRequestCopy.registration &&
!(authorizationRequestCopy.claims && 'vp_token' in authorizationRequestCopy.claims) &&
authorizationRequestCopy.response_mode !== ResponseMode.POST // Post has been replaced by direct post
) {
versions.push(SupportedVersion.SIOPv2_D12_OID4VP_D18);
versions.push(SupportedVersion.SIOPv2_D13_OID4VP_D20);
}
}
const vd11Validation = AuthorizationRequestPayloadVD11Schema(authorizationRequestCopy);
Expand Down
2 changes: 1 addition & 1 deletion src/id-token/Payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const createIDTokenPayload = async (
const rpSupportedVersions = authorizationRequestVersionDiscovery(payload);
const maxRPVersion = rpSupportedVersions.reduce(
(previous, current) => (current.valueOf() > previous.valueOf() ? current : previous),
SupportedVersion.SIOPv2_D12_OID4VP_D18,
SupportedVersion.SIOPv2_D13_OID4VP_D20,
);
if (responseOpts.version && rpSupportedVersions.length > 0 && !rpSupportedVersions.includes(responseOpts.version)) {
throw Error(`RP does not support spec version ${responseOpts.version}, supported versions: ${rpSupportedVersions.toString()}`);
Expand Down
2 changes: 1 addition & 1 deletion src/op/OP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class OP {
registration: { ...this._createResponseOptions?.registration, issuer },
responseURI,
responseURIType:
this._createResponseOptions.responseURIType ?? (version < SupportedVersion.SIOPv2_D12_OID4VP_D18 && responseURI ? 'redirect_uri' : undefined),
this._createResponseOptions.responseURIType ?? (version < SupportedVersion.SIOPv2_D13_OID4VP_D20 && responseURI ? 'redirect_uri' : undefined),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const AuthorizationRequestPayloadVD12OID4VPD18SchemaObj = {
"$id": "AuthorizationRequestPayloadVD12OID4VPD18Schema",
export const AuthorizationRequestPayloadVD13OID4VPD20SchemaObj = {
"$id": "AuthorizationRequestPayloadVD13OID4VPD20Schema",
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/AuthorizationRequestPayloadVD12OID4VPD18",
"$ref": "#/definitions/AuthorizationRequestPayloadVD13OID4VPD20",
"definitions": {
"AuthorizationRequestPayloadVD12OID4VPD18": {
"AuthorizationRequestPayloadVD13OID4VPD20": {
"type": "object",
"properties": {
"id_token_type": {
Expand Down Expand Up @@ -1052,7 +1052,10 @@ export const AuthorizationRequestPayloadVD12OID4VPD18SchemaObj = {
"pre-registered",
"redirect_uri",
"entity_id",
"did"
"did",
"verifier_attestation",
"x509_san_dns",
"x509_san_uri"
]
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/types/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ enum SIOPErrors {
NO_VERIFIABLE_PRESENTATION_NO_CREDENTIALS = 'Either no verifiable presentation or no credentials found in the verifiable presentation',
VERIFICATION_METHOD_NOT_SUPPORTED = 'Verification method not supported',
VERIFICATION_METHOD_NO_MATCH = "The verification method from the RP's DID Document does NOT match the kid of the SIOP Request",
VERIFICATION_VERIFIER_ATTESTATION_SCHEME_ERROR = 'Verification failed. verifier_attestation scheme error: Invalid payload data',
VERIFICATION_X509_SAN_DNS_SCHEME_DNS_NAME_MATCH = 'Verification of x509_san_dns scheme error: DNS name does not match',
VERIFICATION_X509_SAN_DNS_ALT_NAMES_ERROR = 'Verification of x509_san_dns scheme error: No SAN found or incorrect SAN format.',
VERIFICATION_X509_SAN_DNS_SCHEME_ERROR = 'Verification failed. x509_san_dns scheme error: Invalid x509 data',
VERIFICATION_X509_SAN_DNS_SCHEME_NO_CERTIFICATE_ERROR = 'Verification failed. x509_san_dns scheme error: No certificate found',
VERIFICATION_X509_SAN_URI_SCHEME_NOT_IMPLEMENTED_ERROR = 'Verification failed. x509_san_uri not implemented.',
VERIFY_BAD_PARAMS = 'Verify bad parameters',
VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID = 'The signature of the verifiable presentation is not valid',
VERIFIABLE_PRESENTATION_VERIFICATION_FUNCTION_MISSING = 'The verifiable presentation verification function is missing',
Expand Down
3 changes: 2 additions & 1 deletion src/types/JWT.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface JWTPayload {
exp?: number;
rexp?: number;
jti?: string;

cnf?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[x: string]: any;
}
Expand All @@ -30,6 +30,7 @@ export interface VerifiedJWT {
issuer: string; //The issuer (did) of the JWT
signer?: VerificationMethod; // The matching verification method from the DID that was used to sign
jwt: string; // The JWT
jwk?: string;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/types/SIOP.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
presentation_definition_uri?: string;
}

export interface AuthorizationRequestPayloadVD12OID4VPD18
export interface AuthorizationRequestPayloadVD13OID4VPD20
extends AuthorizationRequestCommonPayload,
RequestClientMetadataPayloadProperties,
RequestIdTokenPayloadProperties {
Expand All @@ -88,13 +88,13 @@
response_uri?: string; // New since OID4VP18 OPTIONAL. The Response URI to which the Wallet MUST send the Authorization Response using an HTTPS POST request as defined by the Response Mode direct_post. The Response URI receives all Authorization Response parameters as defined by the respective Response Type. When the response_uri parameter is present, the redirect_uri Authorization Request parameter MUST NOT be present. If the redirect_uri Authorization Request parameter is present when the Response Mode is direct_post, the Wallet MUST return an invalid_request Authorization Response error.
}

export type ClientIdScheme = 'pre-registered' | 'redirect_uri' | 'entity_id' | 'did';
export type ClientIdScheme = 'pre-registered' | 'redirect_uri' | 'entity_id' | 'did' | 'verifier_attestation' | 'x509_san_dns' | 'x509_san_uri';

// https://openid.bitbucket.io/connect/openid-connect-self-issued-v2-1_0.html#section-10
export type AuthorizationRequestPayload =
| AuthorizationRequestPayloadVID1
| AuthorizationRequestPayloadVD11
| AuthorizationRequestPayloadVD12OID4VPD18;
| AuthorizationRequestPayloadVD13OID4VPD20;

export type JWTVcPresentationProfileAuthenticationRequestPayload = RequestIdTokenPayloadProperties;

Expand Down Expand Up @@ -178,7 +178,7 @@
}

export interface ClaimPayloadCommon {
[x: string]: any;

Check warning on line 181 in src/types/SIOP.types.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type

Check warning on line 181 in src/types/SIOP.types.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
}

export interface ClaimPayloadVID1 extends ClaimPayloadCommon {
Expand Down Expand Up @@ -772,7 +772,7 @@
export enum SupportedVersion {
SIOPv2_ID1 = 70,
SIOPv2_D11 = 110,
SIOPv2_D12_OID4VP_D18 = 180,
SIOPv2_D13_OID4VP_D20 = 180,
JWT_VC_PRESENTATION_PROFILE_v1 = 71,
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/mattr.launchpad.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ describe('Mattr OID4VP v18 credential offer', () => {

console.log(JSON.stringify(verification));
expect(verification).toBeDefined();
expect(verification.versions).toEqual([SupportedVersion.SIOPv2_D12_OID4VP_D18]);
expect(verification.versions).toEqual([SupportedVersion.SIOPv2_D13_OID4VP_D20]);

/**
* pd value: {"id":"dae5d9b6-8145-4297-99b2-b8fcc5abb5ad","input_descriptors":[{"id":"OpenBadgeCredential","format":{"jwt_vc_json":{"alg":["EdDSA"]},"jwt_vc":{"alg":["EdDSA"]}},"constraints":{"fields":[{"path":["$.vc.type"],"filter":{"type":"array","items":{"type":"string"},"contains":{"const":"OpenBadgeCredential"}}}]}}]}
Expand Down
2 changes: 1 addition & 1 deletion test/interop/EBSI/EBSI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('EBSI', () => {
wellknownDIDVerifyCallback: async (_args: IVerifyCallbackArgs): Promise<IVerifyCredentialResult> => ({ verified: true }),
},
correlationId: '1234',
supportedVersions: [SupportedVersion.SIOPv2_D12_OID4VP_D18],
supportedVersions: [SupportedVersion.SIOPv2_D13_OID4VP_D20],
};
it(
'succeed from request opts when all params are set',
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3113,6 +3113,13 @@
resolved "https://registry.yarnpkg.com/@types/language-tags/-/language-tags-1.0.4.tgz#c622209605b919c41cbf5a78c2fb58dbc3d6f029"
integrity sha512-20PQbifv3v/djCT+KlXybv0KqO5ofoR1qD1wkinN59kfggTPVTWGmPFgL/1yWuDyRcsQP/POvkqK+fnl5nOwTg==

"@types/node-forge@^1.3.11":
version "1.3.11"
resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da"
integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==
dependencies:
"@types/node" "*"

"@types/node@*":
version "20.11.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
Expand Down Expand Up @@ -6530,6 +6537,11 @@ node-fetch@^3.2.10:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"

node-forge@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==

node-gyp-build@^4.2.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd"
Expand Down
Loading