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: add sd-jwt support #78

Merged
merged 2 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 11 additions & 7 deletions packages/callback-example/lib/__tests__/issuerCallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess
import {
Alg,
CNonceState,
CredentialOfferLdpVcV1_0_11,
CredentialSupported,
IssuerCredentialSubjectDisplay,
IssueStatus,
Expand Down Expand Up @@ -118,19 +117,24 @@ describe('issuerCallback', () => {
credentialOffer: {
credential_offer: {
credential_issuer: 'did:key:test',
credential_definition: {
types: ['VerifiableCredential'],
'@context': ['https://www.w3.org/2018/credentials/v1'],
credentialSubject: {},
},
credentials: [
{
format: 'ldp_vc',
credential_definition: {
types: ['VerifiableCredential'],
'@context': ['https://www.w3.org/2018/credentials/v1'],
credentialSubject: {},
},
},
],
grants: {
authorization_code: { issuer_state: 'test_code' },
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': 'test_code',
user_pin_required: true,
},
},
} as CredentialOfferLdpVcV1_0_11,
},
},
})

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/AuthorizationDetailsBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AuthorizationDetailsJwtVcJson, OID4VCICredentialFormat } from '@sphereon/oid4vci-common';
import { AuthorizationDetails, AuthorizationDetailsJwtVcJson, OID4VCICredentialFormat } from '@sphereon/oid4vci-common';

//todo: refactor this builder to be able to create ldp details as well
export class AuthorizationDetailsBuilder {
private readonly authorizationDetails: Partial<AuthorizationDetailsJwtVcJson>;
private readonly authorizationDetails: Partial<Exclude<AuthorizationDetails, string>>;

constructor() {
this.authorizationDetails = {};
Expand Down
79 changes: 42 additions & 37 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CredentialRequestV1_0_08,
CredentialResponse,
getCredentialRequestForVersion,
getUniformFormat,
OID4VCICredentialFormat,
OpenId4VCIVersion,
OpenIDResponse,
Expand Down Expand Up @@ -53,24 +54,7 @@ export class CredentialRequestClient {
}

public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise<OpenIDResponse<CredentialResponse>> {
let request: CredentialRequestV1_0_08 | UniformCredentialRequest = uniformRequest;
if (!this.isV11OrHigher()) {
let format: string = uniformRequest.format;
if (format === 'jwt_vc_json') {
format = 'jwt_vc';
} else if (format === 'jwt_vc_json-ld') {
format = 'ldp_vc';
}

request = {
format,
proof: uniformRequest.proof,
type:
'types' in uniformRequest
? uniformRequest.types.filter((t) => t !== 'VerifiableCredential')[0]
: uniformRequest.credential_definition.types[0],
} as CredentialRequestV1_0_08;
}
const request = getCredentialRequestForVersion(uniformRequest, this.version());
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
if (!isValidURL(credentialEndpoint)) {
debug(`Invalid credential endpoint: ${credentialEndpoint}`);
Expand All @@ -92,45 +76,66 @@ export class CredentialRequestClient {
const { proofInput } = opts;
const formatSelection = opts.format ?? this.credentialRequestOpts.format;

let format: OID4VCICredentialFormat = formatSelection as OID4VCICredentialFormat;
if (opts.version < OpenId4VCIVersion.VER_1_0_11) {
if (formatSelection === 'jwt_vc' || formatSelection === 'jwt') {
format = 'jwt_vc_json';
} else if (formatSelection === 'ldp_vc' || formatSelection === 'ldp') {
format = 'jwt_vc_json-ld';
}
}

if (!format) {
if (!formatSelection) {
throw Error(`Format of credential to be issued is missing`);
} else if (format !== 'jwt_vc_json-ld' && format !== 'jwt_vc_json' && format !== 'ldp_vc') {
throw Error(`Invalid format of credential to be issued: ${format}`);
}
const format = getUniformFormat(formatSelection);
const typesSelection =
opts?.credentialTypes && (typeof opts.credentialTypes === 'string' || opts.credentialTypes.length > 0)
? opts.credentialTypes
: this.credentialRequestOpts.credentialTypes;
const types = Array.isArray(typesSelection) ? typesSelection : [typesSelection];
if (types.length === 0) {
throw Error(`Credential type(s) need to be provided`);
} else if (!this.isV11OrHigher() && types.length !== 1) {
}
// FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11
nklomp marked this conversation as resolved.
Show resolved Hide resolved
else if (!this.isV11OrHigher() && types.length !== 1) {
throw Error('Only a single credential type is supported for V8/V9');
}

const proof =
'proof_type' in proofInput
? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build()
: await proofInput.build();
return {
types,
format,
proof,
} as UniformCredentialRequest;

// TODO: we should move format specific logic
if (format === 'jwt_vc_json') {
return {
types,
format,
proof,
};
} else if (format === 'jwt_vc_json-ld' || format === 'ldp_vc') {
return {
format,
proof,
credential_definition: {
types,
// FIXME: this was not included in the original code, but it is required
'@context': [],
},
};
} else if (format === 'vc+sd-jwt') {
if (types.length > 1) {
throw Error(`Only a single credential type is supported for ${format}`);
}

return {
format,
proof,
credential_definition: {
vct: types[0],
},
};
}

throw new Error(`Unsupported format: ${format}`);
}

private version(): OpenId4VCIVersion {
return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11;
}

private isV11OrHigher(): boolean {
return this.version() >= OpenId4VCIVersion.VER_1_0_11;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/client/lib/CredentialRequestClientBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
determineSpecVersionFromOffer,
EndpointMetadata,
getIssuerFromCredentialOfferPayload,
getTypesFromOffer,
OID4VCICredentialFormat,
OpenId4VCIVersion,
UniformCredentialOfferRequest,
Expand Down Expand Up @@ -46,7 +47,7 @@ export class CredentialRequestClientBuilder {
builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type);
} else {
// todo: look whether this is correct
builder.withCredentialType(request.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types)));
builder.withCredentialType(getTypesFromOffer(request.credential_offer));
}

return builder;
Expand Down
20 changes: 13 additions & 7 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
PushedAuthorizationResponse,
ResponseType,
} from '@sphereon/oid4vci-common';
import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
import { getSupportedCredentials, getTypesFromCredentialSupported } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';
Expand Down Expand Up @@ -312,12 +312,10 @@ export class OpenID4VCIClient {
let typeSupported = false;

metadata.credentials_supported.forEach((supportedCredential) => {
if (!supportedCredential.types || supportedCredential.types.length === 0) {
throw Error('types is required in the credentials supported');
}
const subTypes = getTypesFromCredentialSupported(supportedCredential);
if (
supportedCredential.types.sort().every((t, i) => types[i] === t) ||
(types.length === 1 && (types[0] === supportedCredential.id || supportedCredential.types.includes(types[0])))
subTypes.sort().every((t, i) => types[i] === t) ||
(types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
) {
typeSupported = true;
}
Expand Down Expand Up @@ -397,7 +395,15 @@ export class OpenID4VCIClient {
return result;
} else {
return this.credentialOffer.credential_offer.credentials.map((c) => {
return typeof c === 'string' ? [c] : c.types;
if (typeof c === 'string') {
return [c];
} else if ('types' in c) {
return c.types;
} else if ('vct' in c.credential_definition) {
return [c.credential_definition.vct];
} else {
return c.credential_definition.types;
}
});
}
}
Expand Down
19 changes: 12 additions & 7 deletions packages/client/lib/__tests__/CredentialRequestClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { KeyObject } from 'crypto';
import {
Alg,
EndpointMetadata,
getCredentialRequestForVersion,
getIssuerFromCredentialOfferPayload,
Jwt,
OpenId4VCIVersion,
Expand Down Expand Up @@ -149,9 +150,7 @@ describe('Credential Request Client ', () => {
.withKid(kid)
.withClientId('sphereon:wallet')
.build();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json-ld', types: ['random'], proof })).rejects.toThrow(
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow(
Error(URL_NOT_VALID),
);
});
Expand Down Expand Up @@ -194,10 +193,11 @@ describe('Credential Request Client with different issuers ', () => {
jwt: getMockData('spruce')?.credential.request.proof.jwt as string,
},
credentialTypes: ['OpenBadgeCredential'],
format: 'jwt_vc_json-ld',
format: 'jwt_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialRequest).toEqual(getMockData('spruce')?.credential.request);
const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08);
expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request);
});

it('should create correct CredentialRequest for Walt', async () => {
Expand Down Expand Up @@ -264,7 +264,8 @@ describe('Credential Request Client with different issuers ', () => {
format: 'ldp_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request);
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);
expect(credentialRequest).toEqual(getMockData('mattr')?.credential.request);
});

it('should create correct CredentialRequest for diwala', async () => {
Expand All @@ -286,6 +287,10 @@ describe('Credential Request Client with different issuers ', () => {
format: 'ldp_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request);

// createCredentialRequest returns uniform format in draft 11
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);

expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request);
});
});
25 changes: 13 additions & 12 deletions packages/client/lib/__tests__/data/VciDataFixtures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CredentialSupportedBrief, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/oid4vci-common';
import { CredentialSupportedFormatV1_0_08, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/oid4vci-common';
import { ICredentialStatus, W3CVerifiableCredential } from '@sphereon/ssi-types';

export function getMockData(issuerName: string): IssuerMockData | null {
Expand Down Expand Up @@ -42,7 +42,8 @@ export interface IssuerMockData {
url: string;
deeplink: string;
request: {
types: [string];
types?: [string];
type?: string;
format: 'jwt_vc' | 'ldp_vc' | 'jwt_vc_json-ld' | string;
proof: {
proof_type: 'jwt' | string;
Expand Down Expand Up @@ -110,8 +111,8 @@ const mockData: VciMockDataStructure = {
deeplink:
'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false',
request: {
types: ['OpenBadgeCredential'],
format: 'jwt_vc_json-ld',
type: 'OpenBadgeCredential',
format: 'jwt_vc',
proof: {
proof_type: 'jwt',
jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lKclpuVmpTa0V0VEhKck9VWjBPRmx5TFVkMlQzSmpia3N3YjNkc2RqUlhNblUwU3pJeFNHZHZTVlIzSWl3aWVTSTZJalozY0ZCUE1rOUNRVXBTU0ZFMVRXdEtXVlJaV0dsQlJFUXdOMU5OTlV0amVXcDNYMkUzVUUxWmVGa2lmUSMwIn0.eyJhdWQiOiJodHRwczovL25naS1vaWRjNHZjaS10ZXN0LnNwcnVjZWlkLnh5eiIsImlhdCI6MTY4MTkxMTA2MC45NDIsImV4cCI6MTY4MTkxMTcyMC45NDIsImlzcyI6InNwaGVyZW9uOnNzaS13YWxsZXQiLCJqdGkiOiJhNjA4MzMxZi02ZmE0LTQ0ZjAtYWNkZWY5NmFjMjdmNmQ3MCJ9.NwF3_41gwnlIdd_6Uk9CczeQHzIQt6UcvTT5Cxv72j9S1vNwiY9annA2kLsjsTiR5-WMBdUhJCO7wYCtZ15mxw',
Expand Down Expand Up @@ -514,7 +515,7 @@ const mockData: VciMockDataStructure = {
types: ['PermanentResidentCard'],
binding_methods_supported: ['did'],
cryptographic_suites_supported: ['Ed25519Signature2018'],
} as CredentialSupportedBrief,
} as CredentialSupportedFormatV1_0_08,
},
},
AcademicAward: {
Expand All @@ -525,7 +526,7 @@ const mockData: VciMockDataStructure = {
types: ['AcademicAward'],
binding_methods_supported: ['did'],
cryptographic_suites_supported: ['Ed25519Signature2018'],
} as CredentialSupportedBrief,
} as CredentialSupportedFormatV1_0_08,
},
},
LearnerProfile: {
Expand All @@ -536,7 +537,7 @@ const mockData: VciMockDataStructure = {
types: ['LearnerProfile'],
binding_methods_supported: ['did'],
cryptographic_suites_supported: ['Ed25519Signature2018'],
} as CredentialSupportedBrief,
} as CredentialSupportedFormatV1_0_08,
},
},
OpenBadgeCredential: {
Expand All @@ -547,7 +548,7 @@ const mockData: VciMockDataStructure = {
types: ['OpenBadgeCredential'],
binding_methods_supported: ['did'],
cryptographic_suites_supported: ['Ed25519Signature2018'],
} as CredentialSupportedBrief,
} as CredentialSupportedFormatV1_0_08,
},
},
},
Expand All @@ -573,8 +574,8 @@ const mockData: VciMockDataStructure = {
'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO',
url: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential',
request: {
types: ['OpenBadgeCredential'],
format: 'jwt_vc_json-ld',
type: 'OpenBadgeCredential',
format: 'ldp_vc',
proof: {
proof_type: 'jwt',
jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3AxM3N6QUFMVFN0cDV1OGtMcnl5YW5vYWtrVWtFUGZXazdvOHY3dms0RW1KI3o2TWtwMTNzekFBTFRTdHA1dThrTHJ5eWFub2Fra1VrRVBmV2s3bzh2N3ZrNEVtSiJ9.eyJhdWQiOiJodHRwczovL2xhdW5jaHBhZC5tYXR0cmxhYnMuY29tIiwiaWF0IjoxNjgxOTE0NDgyLjUxOSwiZXhwIjoxNjgxOTE1MTQyLjUxOSwiaXNzIjoic3BoZXJlb246c3NpLXdhbGxldCIsImp0aSI6ImI5NDY1ZGE5LTY4OGYtNDdjNi04MjUwNDA0ZGNiOWI5Y2E5In0.uQ8ewOfIjy_1p_Gk6PjeEWccBJnjOca1pwbTWiCAFMQX9wlIsfeUdGtXUoHjH5_PQtpwytodx7WU456_CT9iBQ',
Expand Down Expand Up @@ -687,8 +688,8 @@ const mockData: VciMockDataStructure = {
'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&amp;credential_type=OpenBadgeCredential&amp;pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng',
url: 'https://oidc4vc.diwala.io/credential',
request: {
types: ['OpenBadgeCredential'],
format: 'jwt_vc_json-ld',
type: 'OpenBadgeCredential',
format: 'ldp_vc',
proof: {
proof_type: 'jwt',
jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3AxM3N6QUFMVFN0cDV1OGtMcnl5YW5vYWtrVWtFUGZXazdvOHY3dms0RW1KI3o2TWtwMTNzekFBTFRTdHA1dThrTHJ5eWFub2Fra1VrRVBmV2s3bzh2N3ZrNEVtSiJ9.eyJhdWQiOiJodHRwczovL29pZGM0dmMuZGl3YWxhLmlvIiwiaWF0IjoxNjgxOTE1MDk1LjIwMiwiZXhwIjoxNjgxOTE1NzU1LjIwMiwiaXNzIjoic3BoZXJlb246c3NpLXdhbGxldCIsImp0aSI6IjYxN2MwM2EzLTM3MTUtNGJlMy1hYjkxNzM4MTlmYzYxNTYzIn0.KA-cHjecaYp9FSaWHkz5cqtNyhBIVT_0I7cJnpHn03T4UWFvdhjhn8Hpe-BU247enFyWOWJ6v3NQZyZgle7xBA',
Expand Down
27 changes: 27 additions & 0 deletions packages/common/lib/functions/CredentialOfferUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,30 @@ function recordVersion(currentVersion: OpenId4VCIVersion, matchingVersion: OpenI
`Invalid param. Some keys have been used from version: ${currentVersion} version while '${key}' is used from version: ${matchingVersion}`,
);
}

export function getTypesFromOffer(credentialOffer: UniformCredentialOfferPayload, opts?: { filterVerifiableCredential: boolean }) {
const types = credentialOffer.credentials.reduce<string[]>((prev, curr) => {
// FIXME returning the string value is wrong (as it's an id), but just matching the current behavior of this library
// The credential_type (from draft 8) and the actual 'type' value in a VC (from draft 11) are mixed up
// Fix for this here: https://github.com/Sphereon-Opensource/OID4VCI/pull/54
if (typeof curr === 'string') {
return [...prev, curr];
} else if (curr.format === 'jwt_vc_json-ld' || curr.format === 'ldp_vc') {
return [...prev, ...curr.credential_definition.types];
} 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;
}, []);

if (!types || types.length === 0) {
throw Error('Could not deduce types from credential offer');
}
if (opts?.filterVerifiableCredential) {
return types.filter((type) => type !== 'VerifiableCredential');
}
return types;
}
Loading
Loading