Skip to content

Commit

Permalink
feat: Add support for alg, kid, did, did document to Jwt Verification…
Browse files Browse the repository at this point in the history
… callback so we can ensure to set proper values in the resulting VC.
  • Loading branch information
nklomp committed Jun 18, 2023
1 parent 224567c commit 62dd947
Show file tree
Hide file tree
Showing 20 changed files with 209 additions and 94 deletions.
27 changes: 21 additions & 6 deletions packages/callback-example/lib/__tests__/issuerCallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IssuerCredentialSubjectDisplay,
IssueStatus,
Jwt,
JwtVerifyResult,
OpenId4VCIVersion,
ProofOfPossession,
} from '@sphereon/oid4vci-common'
Expand All @@ -17,6 +18,7 @@ import { CredentialSupportedBuilderV1_11, VcIssuer, VcIssuerBuilder } from '@sph
import { MemoryStates } from '@sphereon/oid4vci-issuer'
import { CredentialDataSupplierResult } from '@sphereon/oid4vci-issuer/dist/types'
import { ICredential, IProofPurpose, IProofType, W3CVerifiableCredential } from '@sphereon/ssi-types'
import { DIDDocument } from 'did-resolver'
import * as jose from 'jose'

import { generateDid, getIssuerCallback, verifyCredential } from '../IssuerCallback'
Expand All @@ -43,12 +45,25 @@ async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promi
.sign(keypair.privateKey)
}

async function verifyCallbackFunction(args: { jwt: string; kid?: string }): Promise<Jwt> {
async function verifyCallbackFunction(args: { jwt: string; kid?: string }): Promise<JwtVerifyResult<DIDDocument>> {
const result = await jose.jwtVerify(args.jwt, keypair.publicKey)
const kid = result.protectedHeader.kid ?? args.kid
const did = kid!.split('#')[0]
const didDocument: DIDDocument = {
'@context': 'https://www.w3.org/ns/did/v1',
id: did,
}
const alg = result.protectedHeader.alg
return {
header: result.protectedHeader,
payload: result.payload,
} as Jwt
alg,
kid,
did,
didDocument,
jwt: {
header: result.protectedHeader,
payload: result.payload,
},
}
}

interface KeyPair {
Expand All @@ -66,7 +81,7 @@ afterAll(async () => {
await new Promise((resolve) => setTimeout((v: void) => resolve(v), 500))
})
describe('issuerCallback', () => {
let vcIssuer: VcIssuer
let vcIssuer: VcIssuer<DIDDocument>
const state = 'existing-state'
const clientId = 'sphereon:wallet'

Expand Down Expand Up @@ -121,7 +136,7 @@ describe('issuerCallback', () => {

const nonces = new MemoryStates<CNonceState>()
nonces.set('test_value', { cNonce: 'test_value', createdAt: +new Date(), issuerState: 'existing-state' })
vcIssuer = new VcIssuerBuilder()
vcIssuer = new VcIssuerBuilder<DIDDocument>()
.withAuthorizationServer('https://authorization-server')
.withCredentialEndpoint('https://credential-endpoint')
.withCredentialIssuer(IDENTIPROOF_ISSUER_URL)
Expand Down
1 change: 1 addition & 0 deletions packages/callback-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"jose": "^4.10.0"
},
"devDependencies": {
"did-resolver": "^4.1.0",
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"@types/jest": "^29.5.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export class CredentialRequestClient {
this._credentialRequestOpts = { ...builder };
}

public async acquireCredentialsUsingProof(opts: {
proofInput: ProofOfPossessionBuilder | ProofOfPossession;
public async acquireCredentialsUsingProof<DIDDoc>(opts: {
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession;
credentialTypes?: string | string[];
format?: CredentialFormat | OID4VCICredentialFormat;
}): Promise<OpenIDResponse<CredentialResponse>> {
Expand Down Expand Up @@ -83,8 +83,8 @@ export class CredentialRequestClient {
return response;
}

public async createCredentialRequest(opts: {
proofInput: ProofOfPossessionBuilder | ProofOfPossession;
public async createCredentialRequest<DIDDoc>(opts: {
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession;
credentialTypes?: string | string[];
format?: CredentialFormat | OID4VCICredentialFormat;
version: OpenId4VCIVersion;
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export class OpenID4VCIClient {
jti,
}: {
credentialTypes: string | string[];
proofCallbacks: ProofOfPossessionCallbacks;
proofCallbacks: ProofOfPossessionCallbacks<never>;
format?: CredentialFormat | OID4VCICredentialFormat;
kid?: string;
alg?: Alg | string;
Expand Down
25 changes: 15 additions & 10 deletions packages/client/lib/ProofOfPossessionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {

import { createProofOfPossession } from './functions';

export class ProofOfPossessionBuilder {
export class ProofOfPossessionBuilder<DIDDoc> {
private readonly proof?: ProofOfPossession;
private readonly callbacks?: ProofOfPossessionCallbacks;
private readonly callbacks?: ProofOfPossessionCallbacks<DIDDoc>;
private readonly version: OpenId4VCIVersion;

private kid?: string;
Expand All @@ -35,7 +35,7 @@ export class ProofOfPossessionBuilder {
version,
}: {
proof?: ProofOfPossession;
callbacks?: ProofOfPossessionCallbacks;
callbacks?: ProofOfPossessionCallbacks<DIDDoc>;
accessTokenResponse?: AccessTokenResponse;
jwt?: Jwt;
version: OpenId4VCIVersion;
Expand All @@ -53,31 +53,31 @@ export class ProofOfPossessionBuilder {
}
}

static fromJwt({
static fromJwt<DIDDoc>({
jwt,
callbacks,
version,
}: {
jwt: Jwt;
callbacks: ProofOfPossessionCallbacks;
callbacks: ProofOfPossessionCallbacks<DIDDoc>;
version: OpenId4VCIVersion;
}): ProofOfPossessionBuilder {
}): ProofOfPossessionBuilder<DIDDoc> {
return new ProofOfPossessionBuilder({ callbacks, jwt, version });
}

static fromAccessTokenResponse({
static fromAccessTokenResponse<DIDDoc>({
accessTokenResponse,
callbacks,
version,
}: {
accessTokenResponse: AccessTokenResponse;
callbacks: ProofOfPossessionCallbacks;
callbacks: ProofOfPossessionCallbacks<DIDDoc>;
version: OpenId4VCIVersion;
}): ProofOfPossessionBuilder {
}): ProofOfPossessionBuilder<DIDDoc> {
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version });
}

static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder {
static fromProof<DIDDoc>(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder<DIDDoc> {
return new ProofOfPossessionBuilder({ proof, version });
}

Expand Down Expand Up @@ -159,6 +159,11 @@ export class ProofOfPossessionBuilder {
}
this.withAlg(jwt.header.alg);

if (Array.isArray(jwt.payload.aud)) {
// Rather do this than take the first value, as it might be very hard to figure out why something is failing
throw Error('We cannot handle multiple aud values currently');
}

if (jwt.payload) {
if (jwt.payload.iss) this.withClientId(jwt.payload.iss);
if (jwt.payload.aud) this.withIssuer(jwt.payload.aud);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Alg,
CredentialIssuerMetadata,
Jwt,
JWTPayload,
JwtVerifyResult,
OpenId4VCIVersion,
ProofOfPossession,
UniformCredentialRequest,
Expand Down Expand Up @@ -51,9 +51,19 @@ interface KeyPair {
privateKey: KeyObject;
}

async function proofOfPossessionVerifierCallbackFunction(args: { jwt: string; kid?: string }): Promise<Jwt> {
async function proofOfPossessionVerifierCallbackFunction(args: { jwt: string; kid?: string }): Promise<JwtVerifyResult<unknown>> {
const result = await jose.jwtVerify(args.jwt, keypair.publicKey);
return { header: result.protectedHeader, payload: result.payload as unknown as JWTPayload };
const kid = result.protectedHeader.kid ?? args.kid;
const did = kid!.split('#')[0];
const didDocument = {};
const alg = result.protectedHeader.alg;
return {
alg,
did,
kid,
didDocument,
jwt: { header: result.protectedHeader, payload: result.payload },
};
}

describe('Credential Request Client Builder', () => {
Expand Down
28 changes: 11 additions & 17 deletions packages/client/lib/functions/ProofUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const debug = Debug('sphereon:openid4vci:token');
* @param existingJwt
* - Optional, clientId of the party requesting the credential
*/
export const createProofOfPossession = async (
callbacks: ProofOfPossessionCallbacks,
export const createProofOfPossession = async <DIDDoc>(
callbacks: ProofOfPossessionCallbacks<DIDDoc>,
jwtProps?: JwtProps,
existingJwt?: Jwt
): Promise<ProofOfPossession> => {
Expand Down Expand Up @@ -69,14 +69,14 @@ export interface JwtProps {
}

const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
const aud = getJwtProperty('aud', true, jwtProps?.issuer, existingJwt?.payload?.aud);
const iss = getJwtProperty('iss', false, jwtProps?.clientId, existingJwt?.payload?.iss);
const jti = getJwtProperty('jti', false, jwtProps?.jti, existingJwt?.payload?.jti);
const typ = getJwtProperty('typ', true, jwtProps?.typ, existingJwt?.header?.typ, 'jwt');
const nonce = getJwtProperty('nonce', false, jwtProps?.nonce, existingJwt?.payload?.nonce); // Officially this is required, but some implementations don't have it
const aud = getJwtProperty<string | string[]>('aud', true, jwtProps?.issuer, existingJwt?.payload?.aud);
const iss = getJwtProperty<string>('iss', false, jwtProps?.clientId, existingJwt?.payload?.iss);
const jti = getJwtProperty<string>('jti', false, jwtProps?.jti, existingJwt?.payload?.jti);
const typ = getJwtProperty<string>('typ', true, jwtProps?.typ, existingJwt?.header?.typ, 'jwt');
const nonce = getJwtProperty<string>('nonce', false, jwtProps?.nonce, existingJwt?.payload?.nonce); // Officially this is required, but some implementations don't have it
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const alg = getJwtProperty('alg', false, jwtProps?.alg, existingJwt?.header?.alg, 'ES256')!;
const kid = getJwtProperty('kid', true, jwtProps?.kid, existingJwt?.header?.kid);
const alg = getJwtProperty<string>('alg', false, jwtProps?.alg, existingJwt?.header?.alg, 'ES256')!;
const kid = getJwtProperty<string>('kid', true, jwtProps?.kid, existingJwt?.header?.kid);
const jwt: Partial<Jwt> = existingJwt ? existingJwt : {};
const now = +new Date();
const jwtPayload: Partial<JWTPayload> = {
Expand All @@ -99,17 +99,11 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
};
};

const getJwtProperty = (
propertyName: string,
required: boolean,
option?: string,
jwtProperty?: string,
defaultValue?: string
): string | undefined => {
const getJwtProperty = <T>(propertyName: string, required: boolean, option?: string, jwtProperty?: T, defaultValue?: T): T | undefined => {
if (option && jwtProperty && option !== jwtProperty) {
throw Error(`Cannot have a property '${propertyName}' with value '${option}' and different JWT value '${jwtProperty}' at the same time`);
}
let result = jwtProperty ? jwtProperty : option;
let result = (jwtProperty ? jwtProperty : option) as T | undefined;
if (!result) {
if (required) {
throw Error(`No ${propertyName} property provided either in a JWT or as option`);
Expand Down
17 changes: 13 additions & 4 deletions packages/common/lib/types/CredentialIssuance.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ export interface Jwt {
payload: JWTPayload;
}

export interface ProofOfPossessionCallbacks {
export interface ProofOfPossessionCallbacks<DIDDoc> {
signCallback: JWTSignerCallback;
verifyCallback?: JWTVerifyCallback;
verifyCallback?: JWTVerifyCallback<DIDDoc>;
}

export enum Alg {
Expand Down Expand Up @@ -155,7 +155,7 @@ export type JWTHeader = JWTHeaderParameters;

export interface JWTPayload {
iss?: string; // REQUIRED (string). The value of this claim MUST be the client_id of the client making the credential request.
aud?: string; // REQUIRED (string). The value of this claim MUST be the issuer URL of credential issuer.
aud?: string | string[]; // REQUIRED (string). The value of this claim MUST be the issuer URL of credential issuer.
iat?: number; // REQUIRED (number). The value of this claim MUST be the time at which the proof was issued using the syntax defined in [RFC7519].
nonce?: string; // REQUIRED (string). The value type of this claim MUST be a string, where the value is a c_nonce provided by the credential issuer. //TODO: Marked as required not present in NGI flow
jti?: string; // A new nonce chosen by the wallet. Used to prevent replay
Expand All @@ -164,4 +164,13 @@ export interface JWTPayload {
}

export type JWTSignerCallback = (jwt: Jwt, kid?: string) => Promise<string>;
export type JWTVerifyCallback = (args: { jwt: string; kid?: string }) => Promise<Jwt>;
export type JWTVerifyCallback<DIDDoc> = (args: { jwt: string; kid?: string }) => Promise<JwtVerifyResult<DIDDoc>>;
export interface JwtVerifyResult<DIDDoc> {
jwt: Jwt;
kid?: string;
alg: string;
did?: string;
didDocument?: DIDDoc;
x5c?: string;
jwk?: BaseJWK;
}
2 changes: 2 additions & 0 deletions packages/common/lib/types/OpenID4VCIErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const NO_JWT_PROVIDED = 'No JWT provided';
export const TYP_ERROR = 'Typ must be "openid4vci-proof+jwt"';
export const ALG_ERROR = `Algorithm is a required field and must be one of: ${Object.keys(Alg).join(', ')}`;
export const KID_JWK_X5C_ERROR = 'Only one must be present: kid, jwk or x5c';
export const KID_DID_NO_DID_ERROR = 'A DID value needs to be returned when kid is present';
export const DID_NO_DIDDOC_ERROR = 'A DID Document needs to be resolved when a DID is encountered';
export const AUD_ERROR = 'aud must be the URL of the credential issuer';
export const IAT_ERROR = 'iat must be the time at which the proof was issued';
export const NONCE_ERROR = 'nonce must be c_nonce provided by the credential issuer';
Expand Down
4 changes: 2 additions & 2 deletions packages/issuer-rest/lib/IssuerTokenEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const handleTokenRequest = ({
issuer,
interval,
}: Required<Pick<ITokenEndpointOpts, 'accessTokenIssuer' | 'cNonceExpiresIn' | 'interval' | 'accessTokenSignerCallback' | 'tokenExpiresIn'>> & {
issuer: VcIssuer
issuer: VcIssuer<unknown>
}) => {
return async (request: Request, response: Response) => {
response.set({
Expand Down Expand Up @@ -66,7 +66,7 @@ export const handleTokenRequest = ({
export const verifyTokenRequest = ({
preAuthorizedCodeExpirationDuration,
issuer,
}: Required<Pick<ITokenEndpointOpts, 'preAuthorizedCodeExpirationDuration'> & { issuer: VcIssuer }>) => {
}: Required<Pick<ITokenEndpointOpts, 'preAuthorizedCodeExpirationDuration'> & { issuer: VcIssuer<unknown> }>) => {
return async (request: Request, response: Response, next: NextFunction) => {
try {
await assertValidAccessTokenRequest(request.body, {
Expand Down
8 changes: 4 additions & 4 deletions packages/issuer-rest/lib/OID4VCIServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function buildVCIFromEnvironment() {
} as IssuerCredentialSubjectDisplay // fixme: This is wrong (remove the cast and see it has no matches)
)
.build()
return new VcIssuerBuilder()
return new VcIssuerBuilder<never>()
.withUserPinRequired(process.env.user_pin_required as unknown as boolean)
.withAuthorizationServer(process.env.authorization_server as string)
.withCredentialEndpoint(process.env.credential_endpoint as string)
Expand Down Expand Up @@ -105,7 +105,7 @@ export interface IOID4VCIServerOpts {
}

export class OID4VCIServer {
private readonly _issuer: VcIssuer
private readonly _issuer: VcIssuer<unknown>
private authRequestsData: Map<string, AuthorizationRequest> = new Map()
private readonly _app: Express
private readonly _baseUrl: URL
Expand All @@ -127,7 +127,7 @@ export class OID4VCIServer {
}

constructor(
opts?: IOID4VCIServerOpts & { issuer?: VcIssuer } /*If not supplied as argument, it will be fully configured from environment variables*/
opts?: IOID4VCIServerOpts & { issuer?: VcIssuer<unknown> } /*If not supplied as argument, it will be fully configured from environment variables*/
) {
dotenv.config()

Expand Down Expand Up @@ -420,7 +420,7 @@ export class OID4VCIServer {
})
}

get issuer(): VcIssuer {
get issuer(): VcIssuer<unknown> {
return this._issuer
}

Expand Down
Loading

0 comments on commit 62dd947

Please sign in to comment.