Skip to content

Commit

Permalink
feat: created special type for CredentialRequest v1_0_13 and fixed th…
Browse files Browse the repository at this point in the history
…e tests for it
  • Loading branch information
sksadjad committed May 23, 2024
1 parent cd8c11d commit 25a6051
Show file tree
Hide file tree
Showing 19 changed files with 179 additions and 132 deletions.
31 changes: 29 additions & 2 deletions packages/callback-example/lib/IssuerCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020
import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verification-key-2020'
import { securityLoader } from '@digitalcredentials/security-document-loader'
import vc from '@digitalcredentials/vc'
import { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common'
import { CredentialRequest, CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common'
import { CredentialIssuanceInput } from '@sphereon/oid4vci-issuer'
import { CompactSdJwtVc, W3CVerifiableCredential } from '@sphereon/ssi-types'

Expand All @@ -15,7 +15,7 @@ export const generateDid = async () => {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const getIssuerCallback = (credential: CredentialIssuanceInput, keyPair: any, verificationMethod: string) => {
export const getIssuerCallbackV1_0_11 = (credential: CredentialIssuanceInput, keyPair: any, verificationMethod: string) => {
if (!credential) {
throw new Error('A credential needs to be provided')
}
Expand All @@ -33,6 +33,33 @@ export const getIssuerCallback = (credential: CredentialIssuanceInput, keyPair:
return await vc.issue({ credential, suite, documentLoader })
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const getIssuerCallbackV1_0_13 = (
credential: CredentialIssuanceInput,
credentialRequest: CredentialRequest,
keyPair: any,
verificationMethod: string,
) => {
if (!credential) {
throw new Error('A credential needs to be provided')
}

return async (_opts: {
credentialRequest: CredentialRequest
credential: CredentialIssuanceInput
format?: string
jwtVerifyResult: any // Adjust type if necessary
}): Promise<W3CVerifiableCredential | CompactSdJwtVc> => {
const documentLoader = securityLoader().build()
const verificationKey: any = Array.from(keyPair.values())[0]
const keys = await Ed25519VerificationKey2020.from({ ...verificationKey })
const suite = new Ed25519Signature2020({ key: keys })
suite.verificationMethod = verificationMethod
return await vc.issue({ credential, suite, documentLoader })
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const verifyCredential = async (credential: W3CVerifiableCredential, keyPair: any, verificationMethod: string): Promise<any> => {
const documentLoader = securityLoader().build()
Expand Down
15 changes: 9 additions & 6 deletions packages/callback-example/lib/__tests__/issuerCallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CNonceState,
CredentialConfigurationSupportedV1_0_13,
CredentialIssuerMetadataV1_0_13,
CredentialRequest,
IssuerCredentialSubjectDisplay,
IssueStatus,
Jwt,
Expand All @@ -21,7 +22,7 @@ import { ICredential, IProofPurpose, IProofType, W3CVerifiableCredential } from
import { DIDDocument } from 'did-resolver'
import * as jose from 'jose'

import { generateDid, getIssuerCallback, verifyCredential } from '../IssuerCallback'
import { generateDid, getIssuerCallbackV1_0_11, getIssuerCallbackV1_0_13, verifyCredential } from '../IssuerCallback'

const INITIATION_TEST_URI =
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credential_configuration_ids%22:%5B%22UniversityDegreeCredential%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'
Expand Down Expand Up @@ -195,7 +196,7 @@ describe('issuerCallback', () => {
credentialSubject: {},
issuanceDate: new Date().toISOString(),
}
const vc = await getIssuerCallback(credential, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id)({})
const vc = await getIssuerCallbackV1_0_11(credential, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id)({})
expect(vc).toEqual({
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'],
credentialSubject: {},
Expand All @@ -214,6 +215,7 @@ describe('issuerCallback', () => {
expect.objectContaining({ verified: true }),
)
})

it('Should pass requesting a verifiable credential using the client', async () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
Expand Down Expand Up @@ -249,26 +251,27 @@ describe('issuerCallback', () => {
.build()

const credentialRequestClient = new CredentialRequestClient(credReqClient)
const credentialRequest = await credentialRequestClient.createCredentialRequest({
credentialTypes: ['VerifiableCredential'],
const credentialRequest: CredentialRequest = await credentialRequestClient.createCredentialRequest({
credentialType: 'VerifiableCredential',
format: 'jwt_vc_json',
proofInput: proof,
version: OpenId4VCIVersion.VER_1_0_13,
})

expect(credentialRequest).toEqual({
format: 'jwt_vc_json',
proof: {
jwt: expect.stringContaining('eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFj'),
proof_type: 'jwt',
},
types: ['VerifiableCredential'],
credential_identifier: 'VerifiableCredential',
})

const credentialResponse = await vcIssuer.issueCredential({
credentialRequest: credentialRequest,
credential,
responseCNonce: state,
credentialSignerCallback: getIssuerCallback(credential, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id),
credentialSignerCallback: getIssuerCallbackV1_0_13(credential, credentialRequest, didKey.keyPairs, didKey.didDocument.verificationMethod[0].id),
})

expect(credentialResponse).toEqual({
Expand Down
2 changes: 1 addition & 1 deletion packages/callback-example/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { getIssuerCallback, generateDid, verifyCredential } from './IssuerCallback'
export { getIssuerCallbackV1_0_11, generateDid, verifyCredential } from './IssuerCallback'
55 changes: 15 additions & 40 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
import { isValidURL, post } from './functions';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { isValidURL, post } from './functions';

const debug = Debug('sphereon:oid4vci:credential');

Expand All @@ -26,7 +26,7 @@ export interface CredentialRequestOpts {
deferredCredentialIntervalInMS?: number;
credentialEndpoint: string;
deferredCredentialEndpoint?: string;
credentialTypes: string[];
credentialType: string;
format?: CredentialFormat | OID4VCICredentialFormat;
proof: ProofOfPossession;
token: string;
Expand Down Expand Up @@ -78,13 +78,12 @@ export class CredentialRequestClient {

public async acquireCredentialsUsingProof<DIDDoc>(opts: {
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession;
credentialTypes?: string | string[];
context?: string[];
credentialType: string;
format?: CredentialFormat | OID4VCICredentialFormat;
}): Promise<OpenIDResponse<CredentialResponse>> {
const { credentialTypes, proofInput, format, context } = opts;
const { credentialType, proofInput, format } = opts;

const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version() });
const request = await this.createCredentialRequest({ proofInput, credentialType, format, version: this.version() });
return await this.acquireCredentialsUsingRequest(request);
}

Expand Down Expand Up @@ -137,66 +136,46 @@ export class CredentialRequestClient {

public async createCredentialRequest<DIDDoc>(opts: {
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession;
credentialTypes?: string | string[];
context?: string[];
credentialType: string;
format?: CredentialFormat | OID4VCICredentialFormat;
version: OpenId4VCIVersion;
}): Promise<UniformCredentialRequest> {
}): Promise<CredentialRequestV1_0_13> {
const { proofInput } = opts;
const formatSelection = opts.format ?? this.credentialRequestOpts.format;

if (!formatSelection) {
throw Error(`Format of credential to be issued is missing`);
}
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`);
}
// FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11
else if (!this.isV13OrHigher() && types.length !== 1) {
throw Error('Only a single credential type is supported for V8/V9');
const typeSelection = opts.credentialType ?? this.credentialRequestOpts.credentialType;
if (!typeSelection) {
throw Error(`Credential type needs to be provided`);
}
const proof = await buildProof(proofInput, opts);

// TODO: we should move format specific logic
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
return {
types,
credential_identifier: typeSelection,
format,
proof,
};
} else if (format === 'jwt_vc_json-ld' || format === 'ldp_vc') {
if (this.version() >= OpenId4VCIVersion.VER_1_0_12 && !opts.context) {
throw Error('No @context value present, but it is required');
}

return {
format,
proof,

// Ignored because v11 does not have the context value, but it is required in v12
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
credential_definition: {
types,
...(opts.context && { '@context': opts.context }),
},
credential_identifier: typeSelection,
};
} else if (format === 'vc+sd-jwt') {
if (types.length > 1) {
throw Error(`Only a single credential type is supported for ${format}`);
}

// fixme: this isn't up to the CredentialRequest that we see in the version v1_0_13
return {
format,
proof,
vct: types[0],
};
vct: typeSelection,
} as CredentialRequestV1_0_13;
}

throw new Error(`Unsupported format: ${format}`);
Expand All @@ -205,8 +184,4 @@ export class CredentialRequestClient {
private version(): OpenId4VCIVersion {
return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_13;
}

private isV13OrHigher(): boolean {
return this.version() >= OpenId4VCIVersion.VER_1_0_13;
}
}
31 changes: 14 additions & 17 deletions packages/client/lib/CredentialRequestClientBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
AccessTokenResponse,
CredentialIssuerMetadataV1_0_13,
CredentialOfferPayloadV1_0_08,
CredentialOfferPayloadV1_0_11,
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
determineSpecVersionFromOffer,
EndpointMetadata,
getIssuerFromCredentialOfferPayload,
getTypesFromOfferV1_0_11,
OID4VCICredentialFormat,
OpenId4VCIVersion,
UniformCredentialOfferRequest,
Expand All @@ -22,7 +20,7 @@ export class CredentialRequestClientBuilder {
deferredCredentialEndpoint?: string;
deferredCredentialAwait = false;
deferredCredentialIntervalInMS = 5000;
credentialTypes: string[] = [];
credentialType?: string;
format?: CredentialFormat | OID4VCICredentialFormat;
token?: string;
version?: OpenId4VCIVersion;
Expand All @@ -31,12 +29,12 @@ export class CredentialRequestClientBuilder {
credentialIssuer,
metadata,
version,
credentialTypes,
credentialType,
}: {
credentialIssuer: string;
metadata?: EndpointMetadata;
version?: OpenId4VCIVersion;
credentialTypes: string | string[];
credentialType: string;
}): CredentialRequestClientBuilder {
const issuer = credentialIssuer;
const builder = new CredentialRequestClientBuilder();
Expand All @@ -45,7 +43,7 @@ export class CredentialRequestClientBuilder {
if (metadata?.deferred_credential_endpoint) {
builder.withDeferredCredentialEndpoint(metadata.deferred_credential_endpoint);
}
builder.withCredentialType(credentialTypes);
builder.withCredentialType(credentialType);
return builder;
}

Expand All @@ -63,22 +61,21 @@ export class CredentialRequestClientBuilder {
}): CredentialRequestClientBuilder {
const { request, metadata } = opts;
const version = opts.version ?? request.version ?? determineSpecVersionFromOffer(request.original_credential_offer);
if (version < OpenId4VCIVersion.VER_1_0_13) {
throw new Error('Versions below v1.0.13 (draft 13) are not supported.');
}
const builder = new CredentialRequestClientBuilder();
const issuer = getIssuerFromCredentialOfferPayload(request.credential_offer) ?? (metadata?.issuer as string);
builder.withVersion(version);
builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`));
if (metadata?.deferred_credential_endpoint) {
builder.withDeferredCredentialEndpoint(metadata.deferred_credential_endpoint);
}

if (version <= OpenId4VCIVersion.VER_1_0_08) {
//todo: This basically sets all types available during initiation. Probably the user only wants a subset. So do we want to do this?
builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type);
} else if (version <= OpenId4VCIVersion.VER_1_0_11) {
// todo: look whether this is correct
builder.withCredentialType(getTypesFromOfferV1_0_11(request.credential_offer as CredentialOfferPayloadV1_0_11));
const types: string[] = (request.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids;
// if there's only one in the offer, we pre-select it. if not, you should provide the credentialType
if (types.length && types.length === 1) {
builder.withCredentialType(types[0]);
}

return builder;
}

Expand Down Expand Up @@ -122,8 +119,8 @@ export class CredentialRequestClientBuilder {
return this;
}

public withCredentialType(credentialTypes: string | string[]): this {
this.credentialTypes = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
public withCredentialType(credentialType: string): this {
this.credentialType = credentialType;
return this;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/client/lib/CredentialRequestClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getCredentialRequestForVersion,
getUniformFormat,
isDeferredCredentialResponse,
JsonLdIssuerCredentialDefinition,
OID4VCICredentialFormat,
OpenId4VCIVersion,
OpenIDResponse,
Expand Down Expand Up @@ -163,7 +164,7 @@ export class CredentialRequestClientV1_0_11 {
credential_definition: {
types,
...(opts.context && { '@context': opts.context }),
},
} as JsonLdIssuerCredentialDefinition,
};
} else if (format === 'vc+sd-jwt') {
if (types.length > 1) {
Expand Down
Loading

0 comments on commit 25a6051

Please sign in to comment.