From 951bf2cb20d0a2a085a8a346d1ed519c71e31a07 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Fri, 17 Nov 2023 17:10:06 +0700 Subject: [PATCH 1/4] feat: add sd-jwt issuer support and e2e test Signed-off-by: Timo Glastra --- packages/client/lib/OpenID4VCIClient.ts | 26 ++- .../client/lib/ProofOfPossessionBuilder.ts | 8 + packages/client/lib/__tests__/SdJwt.spec.ts | 157 ++++++++++++++++++ packages/client/lib/functions/ProofUtil.ts | 22 ++- packages/issuer/lib/VcIssuer.ts | 2 +- 5 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 packages/client/lib/__tests__/SdJwt.spec.ts diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 227eb835..f0548293 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -9,6 +9,8 @@ import { CredentialSupported, EndpointMetadataResult, JsonURIMode, + JWK, + KID_JWK_X5C_ERROR, OID4VCICredentialFormat, OpenId4VCIVersion, ProofOfPossessionCallbacks, @@ -49,6 +51,7 @@ export class OpenID4VCIClient { private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl; private _clientId?: string; private _kid: string | undefined; + private _jwk: JWK | undefined; private _alg: Alg | string | undefined; private _endpointMetadata: EndpointMetadataResult | undefined; private _accessTokenResponse: AccessTokenResponse | undefined; @@ -281,6 +284,7 @@ export class OpenID4VCIClient { proofCallbacks, format, kid, + jwk, alg, jti, }: { @@ -288,16 +292,18 @@ export class OpenID4VCIClient { proofCallbacks: ProofOfPossessionCallbacks; format?: CredentialFormat | OID4VCICredentialFormat; kid?: string; + jwk?: JWK; alg?: Alg | string; jti?: string; }): Promise { - if (alg) { - this._alg = alg; - } - if (kid) { - this._kid = kid; + if ([jwk, kid].filter((v) => v !== undefined).length > 1) { + throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`); } + if (alg) this._alg = alg; + if (jwk) this._jwk = jwk; + if (kid) this._kid = kid; + const requestBuilder = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, @@ -339,8 +345,14 @@ export class OpenID4VCIClient { version: this.version(), }) .withIssuer(this.getIssuer()) - .withAlg(this.alg) - .withKid(this.kid); + .withAlg(this.alg); + + if (this._jwk) { + proofBuilder.withJWK(this._jwk); + } + if (this._kid) { + proofBuilder.withKid(this._kid); + } if (this.clientId) { proofBuilder.withClientId(this.clientId); diff --git a/packages/client/lib/ProofOfPossessionBuilder.ts b/packages/client/lib/ProofOfPossessionBuilder.ts index ac402cf0..8aef307f 100644 --- a/packages/client/lib/ProofOfPossessionBuilder.ts +++ b/packages/client/lib/ProofOfPossessionBuilder.ts @@ -2,6 +2,7 @@ import { AccessTokenResponse, Alg, EndpointMetadata, + JWK, Jwt, NO_JWT_PROVIDED, OpenId4VCIVersion, @@ -19,6 +20,7 @@ export class ProofOfPossessionBuilder { private readonly version: OpenId4VCIVersion; private kid?: string; + private jwk?: JWK; private clientId?: string; private issuer?: string; private jwt?: Jwt; @@ -91,6 +93,11 @@ export class ProofOfPossessionBuilder { return this; } + withJWK(jwk: JWK): this { + this.jwk = jwk; + return this; + } + withIssuer(issuer: string): this { this.issuer = issuer; return this; @@ -182,6 +189,7 @@ export class ProofOfPossessionBuilder { { typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'), kid: this.kid, + jwk: this.jwk, jti: this.jti, alg: this.alg, issuer: this.issuer, diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts new file mode 100644 index 00000000..81eebecf --- /dev/null +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -0,0 +1,157 @@ +import { AccessTokenRequest, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common'; +import nock from 'nock'; + +import { OpenID4VCIClient } from '..'; +import { createAccessTokenResponse, IssuerMetadataBuilderV1_11, VcIssuerBuilder } from '../../../issuer'; + +export const UNIT_TEST_TIMEOUT = 30000; + +const alg = 'ES256'; +const jwk = { kty: 'EC', crv: 'P-256', x: 'zQOowIC1gWJtdddB5GAt4lau6Lt8Ihy771iAfam-1pc', y: 'cjD_7o3gdQ1vgiQy3_sMGs7WrwCMU9FQYimA3HxnMlw' }; + +const issuerMetadata = new IssuerMetadataBuilderV1_11() + .withCredentialIssuer('https://example.com') + .withCredentialEndpoint('https://credenital-endpoint.example.com') + .withTokenEndpoint('https://token-endpoint.example.com') + .addSupportedCredential({ + format: 'vc+sd-jwt', + credential_definition: { + vct: 'SdJwtCredential', + }, + id: 'SdJwtCredentialId', + }) + .build(); + +const vcIssuer = new VcIssuerBuilder() + .withIssuerMetadata(issuerMetadata) + .withInMemoryCNonceState() + .withInMemoryCredentialOfferState() + .withInMemoryCredentialOfferURIState() + // TODO: see if we can construct an sd-jwt vc based on the input + .withCredentialSignerCallback(async () => { + return 'sd-jwt'; + }) + .withJWTVerifyCallback(() => + Promise.resolve({ + alg, + jwk, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg, + jwk, + }, + payload: { + aud: issuerMetadata.credential_issuer, + iat: +new Date(), + nonce: 'a-c-nonce', + }, + }, + }), + ) + .build(); + +describe('sd-jwt vc', () => { + beforeEach(() => { + nock.cleanAll(); + }); + afterEach(() => { + nock.cleanAll(); + }); + + it( + 'succeed with a full flow', + async () => { + const offerUri = await vcIssuer.createCredentialOfferURI({ + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '123', + user_pin_required: false, + }, + }, + credentials: ['SdJwtCredentialId'], + }); + + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(issuerMetadata)); + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-configuration').reply(404); + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/oauth-authorization-server').reply(404); + + expect(offerUri.uri).toEqual( + 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22SdJwtCredentialId%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%7D', + ); + + const client = await OpenID4VCIClient.fromURI({ + uri: offerUri.uri, + }); + + expect(client.credentialOffer.credential_offer).toEqual({ + credential_issuer: 'https://example.com', + credentials: ['SdJwtCredentialId'], + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '123', + user_pin_required: false, + }, + }, + }); + + const supported = client.getCredentialsSupported(true, 'vc+sd-jwt'); + expect(supported).toEqual([ + { + credential_definition: { + vct: 'SdJwtCredential', + }, + format: 'vc+sd-jwt', + id: 'SdJwtCredentialId', + }, + ]); + + const offered = supported[0] as CredentialSupportedSdJwtVc; + + nock(issuerMetadata.token_endpoint as string) + .post('/') + .reply(200, async (_, body: string) => { + const parsedBody = Object.fromEntries(body.split('&').map((x) => x.split('='))); + return createAccessTokenResponse(parsedBody as AccessTokenRequest, { + credentialOfferSessions: vcIssuer.credentialOfferSessions, + accessTokenIssuer: 'https://issuer.example.com', + cNonces: vcIssuer.cNonces, + cNonce: 'a-c-nonce', + accessTokenSignerCallback: async () => 'ey.val.ue', + tokenExpiresIn: 500, + }); + }); + + await client.acquireAccessToken({}); + + nock(issuerMetadata.credential_endpoint as string) + .post('/') + .reply(200, async (_, body) => + vcIssuer.issueCredential({ + credentialRequest: body as CredentialRequestV1_0_11, + credential: {} as any, // TODO: define the interface for credential when using sd-jwt + newCNonce: 'new-c-nonce', + }), + ); + + const credentials = await client.acquireCredentials({ + credentialTypes: [offered.credential_definition.vct], + format: 'vc+sd-jwt', + alg, + jwk, + proofCallbacks: { + // When using sd-jwt for real, this jwt should include a jwk + signCallback: async () => 'ey.ja.ja', + }, + }); + + expect(credentials).toEqual({ + c_nonce: 'new-c-nonce', + c_nonce_expires_in: 300000, + credential: 'sd-jwt', // TODO: make this a real sd-jwt vc + format: 'vc+sd-jwt', + }); + }, + UNIT_TEST_TIMEOUT, + ); +}); diff --git a/packages/client/lib/functions/ProofUtil.ts b/packages/client/lib/functions/ProofUtil.ts index 53fe90b6..cbfed4be 100644 --- a/packages/client/lib/functions/ProofUtil.ts +++ b/packages/client/lib/functions/ProofUtil.ts @@ -1,4 +1,15 @@ -import { BAD_PARAMS, JWS_NOT_VALID, Jwt, JWTHeader, JWTPayload, ProofOfPossession, ProofOfPossessionCallbacks, Typ } from '@sphereon/oid4vci-common'; +import { + BAD_PARAMS, + BaseJWK, + JWK, + JWS_NOT_VALID, + Jwt, + JWTHeader, + JWTPayload, + ProofOfPossession, + ProofOfPossessionCallbacks, + Typ, +} from '@sphereon/oid4vci-common'; import Debug from 'debug'; const debug = Debug('sphereon:openid4vci:token'); @@ -61,6 +72,7 @@ const partiallyValidateJWS = (jws: string): void => { export interface JwtProps { typ?: Typ; kid?: string; + jwk?: JWK; issuer?: string; clientId?: string; alg?: string; @@ -76,7 +88,8 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => { const nonce = getJwtProperty('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 kid = getJwtProperty('kid', false, jwtProps?.kid, existingJwt?.header?.kid); + const jwk = getJwtProperty('jwk', false, jwtProps?.jwk, existingJwt?.header?.jwk); const jwt: Partial = existingJwt ? existingJwt : {}; const now = +new Date(); const jwtPayload: Partial = { @@ -92,6 +105,7 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => { typ, alg, kid, + jwk, }; return { payload: { ...jwt.payload, ...jwtPayload }, @@ -99,8 +113,8 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => { }; }; -const getJwtProperty = (propertyName: string, required: boolean, option?: string, jwtProperty?: T, defaultValue?: T): T | undefined => { - if (option && jwtProperty && option !== jwtProperty) { +const getJwtProperty = (propertyName: string, required: boolean, option?: string | JWK, jwtProperty?: T, defaultValue?: T): T | undefined => { + if (typeof option === 'string' && 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) as T | undefined; diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index b8411f07..5fa1679d 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -404,7 +404,7 @@ export class VcIssuer { let preAuthorizedCode: string | undefined let issuerState: string | undefined try { - if (credentialRequest.format !== 'jwt_vc_json' && credentialRequest.format !== 'jwt_vc_json-ld') { + if (credentialRequest.format !== 'jwt_vc_json' && credentialRequest.format !== 'jwt_vc_json-ld' && credentialRequest.format !== 'vc+sd-jwt') { throw Error(`Format ${credentialRequest.format} not supported yet`) } else if (typeof this._jwtVerifyCallback !== 'function' && typeof jwtVerifyCallback !== 'function') { throw new Error(JWT_VERIFY_CONFIG_ERROR) From 0d928ccc1b7c35564e9a7e225e31a282c04eba15 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 7 Dec 2023 16:50:51 +0700 Subject: [PATCH 2/4] update with correct typing Signed-off-by: Timo Glastra --- .../callback-example/lib/IssuerCallback.ts | 7 +++-- .../lib/__tests__/issuerCallback.spec.ts | 2 +- packages/client/lib/__tests__/SdJwt.spec.ts | 10 ++++++- packages/issuer/lib/VcIssuer.ts | 30 +++++++++++-------- packages/issuer/lib/types/index.ts | 8 +++-- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/callback-example/lib/IssuerCallback.ts b/packages/callback-example/lib/IssuerCallback.ts index 4319f3e0..6e634b38 100644 --- a/packages/callback-example/lib/IssuerCallback.ts +++ b/packages/callback-example/lib/IssuerCallback.ts @@ -4,7 +4,8 @@ import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verifica import { securityLoader } from '@digitalcredentials/security-document-loader' import vc from '@digitalcredentials/vc' import { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' -import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' +import { CredentialIssuanceInput } from '@sphereon/oid4vci-issuer' +import { W3CVerifiableCredential } from '@sphereon/ssi-types' // Example on how to generate a did:key to issue a verifiable credential export const generateDid = async () => { @@ -14,12 +15,12 @@ export const generateDid = async () => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getIssuerCallback = (credential: ICredential, keyPair: any, verificationMethod: string) => { +export const getIssuerCallback = (credential: CredentialIssuanceInput, keyPair: any, verificationMethod: string) => { if (!credential) { throw new Error('A credential needs to be provided') } // eslint-disable-next-line @typescript-eslint/no-unused-vars - return async (_opts: { credentialRequest?: CredentialRequestV1_0_11; credential?: ICredential }): Promise => { + return async (_opts: { credentialRequest?: CredentialRequestV1_0_11; credential?: CredentialIssuanceInput }): Promise => { const documentLoader = securityLoader().build() // eslint-disable-next-line @typescript-eslint/no-explicit-any const verificationKey: any = Array.from(keyPair.values())[0] diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 16ae66d2..9ec8f93a 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -167,7 +167,7 @@ describe('issuerCallback', () => { ) .withCredentialSignerCallback((opts) => Promise.resolve({ - ...opts.credential, + ...(opts.credential as ICredential), proof: { type: IProofType.JwtProof2020, jwt: 'ye.ye.ye', diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index 81eebecf..3a9c8804 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -129,7 +129,15 @@ describe('sd-jwt vc', () => { .reply(200, async (_, body) => vcIssuer.issueCredential({ credentialRequest: body as CredentialRequestV1_0_11, - credential: {} as any, // TODO: define the interface for credential when using sd-jwt + credential: { + vct: 'Hello', + iss: 'did:example:123', + iat: 123, + // Defines what can be disclosed (optional) + __disclosureFrame: { + name: true, + }, + }, newCNonce: 'new-c-nonce', }), ); diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 5fa1679d..5eb92133 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -36,12 +36,12 @@ import { UniformCredentialRequest, URIState, } from '@sphereon/oid4vci-common' -import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' +import { CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' import { LookupStateManager } from './state-manager' -import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialSignerCallback } from './types' +import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' const SECOND = 1000 @@ -219,7 +219,7 @@ export class VcIssuer { */ public async issueCredential(opts: { credentialRequest: CredentialRequestV1_0_11 - credential?: ICredential + credential?: CredentialIssuanceInput credentialDataSupplier?: CredentialDataSupplier credentialDataSupplierInput?: CredentialDataSupplierInput newCNonce?: string @@ -255,7 +255,7 @@ export class VcIssuer { if (!opts.credential && this._credentialDataSupplier === undefined && opts.credentialDataSupplier === undefined) { throw Error(`Either a credential needs to be supplied or a credentialDataSupplier`) } - let credential: ICredential | undefined + let credential: CredentialIssuanceInput | undefined let format: OID4VCICredentialFormat = opts.credentialRequest.format let signerCallback: CredentialSignerCallback | undefined = opts.credentialSignerCallback if (opts.credential) { @@ -295,14 +295,18 @@ export class VcIssuer { throw Error('A credential needs to be supplied at this point') } if (did) { - const credentialSubjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [credential.credentialSubject] - credentialSubjects.map((subject) => { - if (!subject.id) { - subject.id = did - } - return subject - }) - credential.credentialSubject = Array.isArray(credential.credentialSubject) ? credentialSubjects : credentialSubjects[0] + if (CredentialMapper.isSdJwtDecodedCredentialPayload(credential)) { + credential.sub = did + } else { + const credentialSubjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [credential.credentialSubject] + credentialSubjects.map((subject) => { + if (!subject.id) { + subject.id = did + } + return subject + }) + credential.credentialSubject = Array.isArray(credential.credentialSubject) ? credentialSubjects : credentialSubjects[0] + } } const verifiableCredential = await this.issueCredentialImpl( @@ -526,7 +530,7 @@ export class VcIssuer { private async issueCredentialImpl( opts: { credentialRequest: UniformCredentialRequest - credential: ICredential + credential: CredentialIssuanceInput jwtVerifyResult: JwtVerifyResult format?: OID4VCICredentialFormat }, diff --git a/packages/issuer/lib/types/index.ts b/packages/issuer/lib/types/index.ts index 71fc1ba3..438797ca 100644 --- a/packages/issuer/lib/types/index.ts +++ b/packages/issuer/lib/types/index.ts @@ -7,11 +7,11 @@ import { OID4VCICredentialFormat, UniformCredentialRequest, } from '@sphereon/oid4vci-common' -import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' +import { ICredential, SdJwtDecodedVerifiableCredentialPayload, SdJwtDisclosureFrame, W3CVerifiableCredential } from '@sphereon/ssi-types' export type CredentialSignerCallback = (opts: { credentialRequest: UniformCredentialRequest - credential: ICredential + credential: CredentialIssuanceInput format?: OID4VCICredentialFormat /** * We use object since we don't want to expose the DID Document TS type to too many interfaces. @@ -28,8 +28,10 @@ export interface CredentialDataSupplierArgs extends CNonceState { credentialDataSupplierInput?: CredentialDataSupplierInput } +export type CredentialIssuanceInput = ICredential | (SdJwtDecodedVerifiableCredentialPayload & { __disclosureFrame?: SdJwtDisclosureFrame }) + export interface CredentialDataSupplierResult { - credential: ICredential + credential: CredentialIssuanceInput format?: OID4VCICredentialFormat signCallback?: CredentialSignerCallback // If the data supplier wants to actually sign directly } From 74d2d92498298d122fce14fcbdbc8b63c042b86d Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 18 Dec 2023 17:14:39 +0700 Subject: [PATCH 3/4] update sd-jwt profile from oid4vci Signed-off-by: Timo Glastra --- .../client/lib/CredentialRequestClient.ts | 4 +--- packages/client/lib/OpenID4VCIClient.ts | 4 ++-- packages/client/lib/__tests__/SdJwt.spec.ts | 12 ++++------- .../lib/functions/CredentialOfferUtil.ts | 2 +- .../lib/functions/CredentialRequestUtil.ts | 2 +- .../lib/functions/IssuerMetadataUtils.ts | 2 +- .../common/lib/types/Authorization.types.ts | 4 ++-- packages/common/lib/types/Generic.types.ts | 20 ++++++++++--------- .../CredentialSupportedBuilderV1_11.ts | 4 +--- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 8bcdfb89..f4f3508d 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -123,9 +123,7 @@ export class CredentialRequestClient { return { format, proof, - credential_definition: { - vct: types[0], - }, + vct: types[0], }; } diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index f0548293..377b638a 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -411,8 +411,8 @@ export class OpenID4VCIClient { return [c]; } else if ('types' in c) { return c.types; - } else if ('vct' in c.credential_definition) { - return [c.credential_definition.vct]; + } else if ('vct' in c) { + return [c.vct]; } else { return c.credential_definition.types; } diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index 3a9c8804..e3f8311d 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -15,9 +15,7 @@ const issuerMetadata = new IssuerMetadataBuilderV1_11() .withTokenEndpoint('https://token-endpoint.example.com') .addSupportedCredential({ format: 'vc+sd-jwt', - credential_definition: { - vct: 'SdJwtCredential', - }, + vct: 'SdJwtCredential', id: 'SdJwtCredentialId', }) .build(); @@ -98,9 +96,7 @@ describe('sd-jwt vc', () => { const supported = client.getCredentialsSupported(true, 'vc+sd-jwt'); expect(supported).toEqual([ { - credential_definition: { - vct: 'SdJwtCredential', - }, + vct: 'SdJwtCredential', format: 'vc+sd-jwt', id: 'SdJwtCredentialId', }, @@ -143,7 +139,7 @@ describe('sd-jwt vc', () => { ); const credentials = await client.acquireCredentials({ - credentialTypes: [offered.credential_definition.vct], + credentialTypes: [offered.vct], format: 'vc+sd-jwt', alg, jwk, @@ -156,7 +152,7 @@ describe('sd-jwt vc', () => { expect(credentials).toEqual({ c_nonce: 'new-c-nonce', c_nonce_expires_in: 300000, - credential: 'sd-jwt', // TODO: make this a real sd-jwt vc + credential: 'sd-jwt', format: 'vc+sd-jwt', }); }, diff --git a/packages/common/lib/functions/CredentialOfferUtil.ts b/packages/common/lib/functions/CredentialOfferUtil.ts index 7f2428f1..df56e116 100644 --- a/packages/common/lib/functions/CredentialOfferUtil.ts +++ b/packages/common/lib/functions/CredentialOfferUtil.ts @@ -353,7 +353,7 @@ export function getTypesFromOffer(credentialOffer: UniformCredentialOfferPayload } else if (curr.format === 'jwt_vc_json') { return [...prev, ...curr.types]; } else if (curr.format === 'vc+sd-jwt') { - return [...prev, curr.credential_definition.vct]; + return [...prev, curr.vct]; } return prev; diff --git a/packages/common/lib/functions/CredentialRequestUtil.ts b/packages/common/lib/functions/CredentialRequestUtil.ts index 66ee9fe6..854ca3b6 100644 --- a/packages/common/lib/functions/CredentialRequestUtil.ts +++ b/packages/common/lib/functions/CredentialRequestUtil.ts @@ -9,7 +9,7 @@ export function getTypesFromRequest(credentialRequest: UniformCredentialRequest, } else if (credentialRequest.format === 'jwt_vc_json-ld' || credentialRequest.format === 'ldp_vc') { types = credentialRequest.credential_definition.types; } else if (credentialRequest.format === 'vc+sd-jwt') { - types = [credentialRequest.credential_definition.vct]; + types = [credentialRequest.vct]; } if (!types || types.length === 0) { diff --git a/packages/common/lib/functions/IssuerMetadataUtils.ts b/packages/common/lib/functions/IssuerMetadataUtils.ts index 40eb85d6..d031974d 100644 --- a/packages/common/lib/functions/IssuerMetadataUtils.ts +++ b/packages/common/lib/functions/IssuerMetadataUtils.ts @@ -104,7 +104,7 @@ export function getTypesFromCredentialSupported(credentialSupported: CredentialS if (credentialSupported.format === 'jwt_vc_json' || credentialSupported.format === 'jwt_vc_json-ld' || credentialSupported.format === 'ldp_vc') { types = credentialSupported.types; } else if (credentialSupported.format === 'vc+sd-jwt') { - types = [credentialSupported.credential_definition.vct]; + types = [credentialSupported.vct]; } if (!types || types.length === 0) { diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index 0276d510..895eef20 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -5,7 +5,6 @@ import { JsonLdIssuerCredentialDefinition, OID4VCICredentialFormat, PRE_AUTH_CODE_LITERAL, - SdJwtVcCredentialDefinition, } from './Generic.types'; import { EndpointMetadata } from './ServerMetadata'; @@ -140,7 +139,8 @@ export interface AuthorizationDetailsJwtVcJsonLdAndLdpVc extends CommonAuthoriza export interface AuthorizationDetailsSdJwtVc extends CommonAuthorizationDetails { format: 'vc+sd-jwt'; - credential_definition: SdJwtVcCredentialDefinition; + vct: string; + claims?: IssuerCredentialSubject; } export enum GrantTypes { diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index 16e042df..28a75909 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -90,16 +90,12 @@ export interface CredentialSupportedJwtVcJson extends CommonCredentialSupported format: 'jwt_vc_json'; } -export interface SdJwtVcCredentialDefinition { - vct: string; // REQUIRED. JSON string designating the type of an SD-JWT vc - claims?: IssuerCredentialSubject; -} - export interface CredentialSupportedSdJwtVc extends CommonCredentialSupported { format: 'vc+sd-jwt'; - // REQUIRED. JSON object containing the detailed description of the credential type - credential_definition: SdJwtVcCredentialDefinition; + vct: string; + claims?: IssuerCredentialSubject; + order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. } @@ -121,9 +117,14 @@ export interface CredentialOfferFormatJwtVcJson extends CommonCredentialOfferFor types: string[]; // REQUIRED. JSON array as defined in Appendix E.1.1.2. This claim contains the type values the Wallet shall request in the subsequent Credential Request. } +// NOTE: the sd-jwt format is added to oid4vci in a later draft version than currently +// supported, so there's no defined offer format. However, based on the request structure +// we support sd-jwt for older drafts of oid4vci as well export interface CredentialOfferFormatSdJwtVc extends CommonCredentialOfferFormat { format: 'vc+sd-jwt'; - credential_definition: SdJwtVcCredentialDefinition; + + vct: string; + claims?: IssuerCredentialSubject; } export type CredentialOfferFormat = CommonCredentialOfferFormat & @@ -176,7 +177,8 @@ export interface CredentialRequestJwtVcJsonLdAndLdpVc extends CommonCredentialRe export interface CredentialRequestSdJwtVc extends CommonCredentialRequest { format: 'vc+sd-jwt'; - credential_definition: SdJwtVcCredentialDefinition; + vct: string; + claims?: IssuerCredentialSubject; } export interface CommonCredentialResponse { diff --git a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts index 1ba45598..ab2ecafa 100644 --- a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts +++ b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts @@ -125,9 +125,7 @@ export class CredentialSupportedBuilderV1_11 { if (this.types.length > 1) { throw new Error('Only one type is allowed for vc+sd-jwt') } - credentialSupported.credential_definition = { - vct: this.types[0], - } + credentialSupported.vct = this.types[0] } // And else would work here, but this way we get the correct typing else if (isNotFormat(credentialSupported, 'vc+sd-jwt')) { From 40c2908cd1ed2f1099d918faba72be2a3fefbe96 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 23 Dec 2023 13:32:52 +0700 Subject: [PATCH 4/4] update to released ssi-types Signed-off-by: Timo Glastra --- packages/callback-example/package.json | 2 +- packages/client/package.json | 2 +- packages/common/lib/types/QRCode.types.ts | 4 +-- packages/common/package.json | 2 +- packages/issuer-rest/package.json | 4 +-- packages/issuer/package.json | 2 +- pnpm-lock.yaml | 43 +++++++++++++---------- 7 files changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/callback-example/package.json b/packages/callback-example/package.json index 7373224e..8c814fb8 100644 --- a/packages/callback-example/package.json +++ b/packages/callback-example/package.json @@ -18,7 +18,7 @@ "@sphereon/oid4vci-client": "workspace:*", "@sphereon/oid4vci-common": "workspace:*", "@sphereon/oid4vci-issuer": "workspace:*", - "@sphereon/ssi-types": "0.17.2", + "@sphereon/ssi-types": "0.17.6-unstable.69", "jose": "^4.10.0" }, "devDependencies": { diff --git a/packages/client/package.json b/packages/client/package.json index 1d1c5a01..8b339c6b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@sphereon/oid4vci-common": "workspace:*", - "@sphereon/ssi-types": "0.17.2", + "@sphereon/ssi-types": "0.17.6-unstable.69", "cross-fetch": "^3.1.8", "debug": "^4.3.4" }, diff --git a/packages/common/lib/types/QRCode.types.ts b/packages/common/lib/types/QRCode.types.ts index 99deb63b..16280dcb 100644 --- a/packages/common/lib/types/QRCode.types.ts +++ b/packages/common/lib/types/QRCode.types.ts @@ -60,7 +60,7 @@ export interface ComponentOptions { */ protectors?: boolean; }; -}; +} export interface QRCodeOpts { /** @@ -224,4 +224,4 @@ export interface QRCodeOpts { * @deafultValue 0.4 */ dotScale?: number; -}; +} diff --git a/packages/common/package.json b/packages/common/package.json index ff5ff6bd..a08104fd 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -10,7 +10,7 @@ "build:clean": "tsc --build --clean && tsc --build" }, "dependencies": { - "@sphereon/ssi-types": "0.17.2", + "@sphereon/ssi-types": "0.17.6-unstable.69", "cross-fetch": "^3.1.8", "jwt-decode": "^3.1.2" }, diff --git a/packages/issuer-rest/package.json b/packages/issuer-rest/package.json index 3f1e5ede..02270a8a 100644 --- a/packages/issuer-rest/package.json +++ b/packages/issuer-rest/package.json @@ -13,8 +13,8 @@ "dependencies": { "@sphereon/oid4vci-common": "workspace:*", "@sphereon/oid4vci-issuer": "workspace:*", - "@sphereon/ssi-express-support": "0.17.2", - "@sphereon/ssi-types": "0.17.2", + "@sphereon/ssi-express-support": "0.17.6-unstable.69", + "@sphereon/ssi-types": "0.17.6-unstable.69", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", diff --git a/packages/issuer/package.json b/packages/issuer/package.json index 5a68335c..61fae484 100644 --- a/packages/issuer/package.json +++ b/packages/issuer/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@sphereon/oid4vci-common": "workspace:*", - "@sphereon/ssi-types": "0.17.2", + "@sphereon/ssi-types": "0.17.6-unstable.69", "uuid": "^9.0.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbe61181..ffc1e70b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: workspace:* version: link:../issuer '@sphereon/ssi-types': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 jose: specifier: ^4.10.0 version: 4.14.6 @@ -118,8 +118,8 @@ importers: specifier: workspace:* version: link:../common '@sphereon/ssi-types': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 cross-fetch: specifier: ^3.1.8 version: 3.1.8 @@ -200,8 +200,8 @@ importers: packages/common: dependencies: '@sphereon/ssi-types': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 cross-fetch: specifier: ^3.1.8 version: 3.1.8 @@ -222,8 +222,8 @@ importers: specifier: workspace:* version: link:../common '@sphereon/ssi-types': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 awesome-qr: specifier: ^2.1.5-rc.0 version: 2.1.5-rc.0 @@ -256,11 +256,11 @@ importers: specifier: workspace:* version: link:../issuer '@sphereon/ssi-express-support': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 '@sphereon/ssi-types': - specifier: 0.17.2 - version: 0.17.2 + specifier: 0.17.6-unstable.69 + version: 0.17.6-unstable.69 body-parser: specifier: ^1.20.2 version: 1.20.2 @@ -3349,6 +3349,12 @@ packages: /@react-native/polyfills@2.0.0: resolution: {integrity: sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==} + /@sd-jwt/core@0.1.2-alpha.0: + resolution: {integrity: sha512-x4MVXar6WmPauZDRJ3aHwaY8o/bHzN77Ts7o43JKuuqIBFjPgAcSlRtd/Xk1rWhazFai4MCIwJDSQ1OQRJtNug==} + dependencies: + buffer: 6.0.3 + dev: false + /@segment/loosely-validate-event@2.0.0: resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} dependencies: @@ -3412,8 +3418,8 @@ packages: dependencies: '@sinonjs/commons': 3.0.0 - /@sphereon/ssi-express-support@0.17.2: - resolution: {integrity: sha512-OrLC7YAelpUmCIzPRgHM97HBNFqDoSdJNNssstS6Ho0ZXswq4fsPDm+h49+//ogp1ERbuOl9Ywqhp+3DdLZCPA==} + /@sphereon/ssi-express-support@0.17.6-unstable.69: + resolution: {integrity: sha512-IpiiW6KPv5zjvlCCxyw4S093WL4na6zvJjoWI26te04Y4T1yYF3FfaGdcA+BAQl4URvvcp09mzay2Z8RwmDLSA==} peerDependencies: '@noble/hashes': 1.2.0 passport-azure-ad: ^4.3.5 @@ -3443,9 +3449,10 @@ packages: - supports-color dev: false - /@sphereon/ssi-types@0.17.2: - resolution: {integrity: sha512-Qo1dkISavtPIe1WKZXZGyHvquoUvdUlDI0GLzb21clKFPuxbawXdlxpCqOh6NCNRfX7ohEeCUQdEA1PNBlnKYA==} + /@sphereon/ssi-types@0.17.6-unstable.69: + resolution: {integrity: sha512-VwjVd6XhoV5QecWcRh0RpBf5N324tfhYcQrVk9se3brqMNMauZTBbhUhk+QLdwFd+u/1+IfEWnS28HyIU7lXHQ==} dependencies: + '@sd-jwt/core': 0.1.2-alpha.0 jwt-decode: 3.1.2 dev: false @@ -4964,7 +4971,7 @@ packages: minipass-pipeline: 1.2.4 p-map: 4.0.0 ssri: 10.0.5 - tar: 6.2.0 + tar: 6.1.11 unique-filename: 3.0.0 dev: true @@ -10044,7 +10051,7 @@ packages: npmlog: 6.0.2 rimraf: 3.0.2 semver: 7.5.4 - tar: 6.2.0 + tar: 6.1.11 which: 2.0.2 transitivePeerDependencies: - supports-color