diff --git a/packages/common/lib/types/OpenID4VCIErrors.ts b/packages/common/lib/types/OpenID4VCIErrors.ts index bf7e8c55..48432911 100644 --- a/packages/common/lib/types/OpenID4VCIErrors.ts +++ b/packages/common/lib/types/OpenID4VCIErrors.ts @@ -1,4 +1,4 @@ -import { Alg } from './CredentialIssuance.types'; +import { Alg } from './CredentialIssuance.types' export const BAD_PARAMS = 'Wrong parameters provided'; export const URL_NOT_VALID = 'Request url is not valid'; @@ -6,7 +6,7 @@ export const JWS_NOT_VALID = 'JWS is not valid'; export const PROOF_CANT_BE_CONSTRUCTED = "Proof can't be constructed."; 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 ALG_ERROR = `Algorithm is a required field, you are free to use the signing algorithm of your choice or one of the following: ${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'; diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index e2819b59..676b0be6 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -1,5 +1,4 @@ import { - Alg, ALG_ERROR, AUD_ERROR, CNonceState, @@ -33,14 +32,19 @@ import { toUniformCredentialOfferRequest, TYP_ERROR, UniformCredentialRequest, - URIState, + URIState } from '@sphereon/oid4vci-common' import { CompactSdJwtVc, CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' import { LookupStateManager } from './state-manager' -import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' +import { + CredentialDataSupplier, + CredentialDataSupplierArgs, + CredentialIssuanceInput, + CredentialSignerCallback +} from './types' const SECOND = 1000 @@ -463,7 +467,7 @@ export class VcIssuer { if (typ !== 'openid4vci-proof+jwt') { throw Error(TYP_ERROR) - } else if (!alg || !(alg in Alg)) { + } else if (!alg) { throw Error(ALG_ERROR) } else if (!([kid, jwk, x5c].filter((x) => !!x).length === 1)) { // only 1 is allowed, but need to look into whether jwk and x5c are allowed together diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index 98af5fb4..26bcbe76 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -1,11 +1,12 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { Alg, + ALG_ERROR, CredentialOfferSession, CredentialSupported, IssuerCredentialSubjectDisplay, IssueStatus, - STATE_MISSING_ERROR, + STATE_MISSING_ERROR } from '@sphereon/oid4vci-common' import { IProofPurpose, IProofType } from '@sphereon/ssi-types' import { DIDDocument } from 'did-resolver' @@ -16,13 +17,36 @@ import { MemoryStates } from '../state-manager' const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io' +const verifiableCredential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/suites/jws-2020/v1' + ], + id: 'http://university.example/credentials/1872', + type: [ + 'VerifiableCredential', + 'ExampleAlumniCredential' + ], + issuer: 'https://university.example/issuers/565049', + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', + alumniOf: { + id: 'did:example:c276e12ec21ebfeb1f712ebc6f1', + name: 'Example University' + } + } +} + describe('VcIssuer', () => { let vcIssuer: VcIssuer const issuerState = 'previously-created-state' const clientId = 'sphereon:wallet' const preAuthorizedCode = 'test_code' - beforeAll(async () => { + const jwtVerifyCallback: jest.Mock = jest.fn() + + beforeEach(async () => { jest.clearAllMocks() const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() .withCryptographicSuitesSupported('ES256K') @@ -105,29 +129,7 @@ describe('VcIssuer', () => { }, }), ) - .withJWTVerifyCallback(() => - Promise.resolve({ - did: 'did:example:1234', - kid: 'did:example:1234#auth', - alg: 'ES256k', - didDocument: { - '@context': 'https://www.w3.org/ns/did/v1', - id: 'did:example:1234', - }, - jwt: { - header: { - typ: 'openid4vci-proof+jwt', - alg: Alg.ES256K, - kid: 'test-kid', - }, - payload: { - aud: 'https://credential-issuer', - iat: +new Date(), - nonce: 'test-nonce', - }, - }, - }), - ) + .withJWTVerifyCallback(jwtVerifyCallback) .build() }) @@ -272,6 +274,29 @@ describe('VcIssuer', () => { // Of course this doesn't work. The state is part of the proof to begin with it('should fail issuing credential if an invalid state is used', async () => { + jwtVerifyCallback.mockResolvedValue({ + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: Alg.ES256K, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: Alg.ES256K, + kid: 'test-kid', + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', + }, + } + } + ) + await expect( vcIssuer.issueCredential({ credentialRequest: { @@ -287,23 +312,65 @@ describe('VcIssuer', () => { ).rejects.toThrow(Error(STATE_MISSING_ERROR + ' (test-nonce)')) }) - // Of course this doesn't work. The state is part of the proof to begin with - xit('should issue credential if a valid state is passed in', async () => { - await expect( + it.each([...Object.values(Alg), 'CUSTOM'])('should issue %s signed credential if a valid state is passed in', async (alg: string) => { + jwtVerifyCallback.mockResolvedValue({ + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: alg, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: alg, + kid: 'test-kid', + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', + }, + } + } + ) + + let createdAt = +new Date() + await vcIssuer.cNonces.set('test-nonce', { + cNonce: 'test-nonce', + preAuthorizedCode: 'test-pre-authorized-code', + createdAt: createdAt + }) + await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', { + createdAt: createdAt, + preAuthorizedCode: 'test-pre-authorized-code', + credentialOffer: { + credential_offer: { + credential_issuer: 'did:key:test', + credentials: [] + } + }, + lastUpdatedAt: createdAt, + status: IssueStatus.ACCESS_TOKEN_CREATED + }) + + expect( vcIssuer.issueCredential({ + credential: verifiableCredential, credentialRequest: { types: ['VerifiableCredential'], format: 'jwt_vc_json', proof: { proof_type: 'jwt', - jwt: 'ye.ye.ye', - }, + jwt: 'ye.ye.ye' + } }, - // issuerState, - }), + newCNonce: 'new-test-nonce' + }) ).resolves.toEqual({ - c_nonce: expect.any(String), - c_nonce_expires_in: 90000, + c_nonce: 'new-test-nonce', + c_nonce_expires_in: 300000, credential: { '@context': ['https://www.w3.org/2018/credentials/v1'], credentialSubject: {}, @@ -314,11 +381,54 @@ describe('VcIssuer', () => { jwt: 'ye.ye.ye', proofPurpose: 'assertionMethod', type: 'JwtProof2020', - verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf', + verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf' }, - type: ['VerifiableCredential'], + type: ['VerifiableCredential'] }, - format: 'jwt_vc_json', + format: 'jwt_vc_json' + }) + }) + + it('should fail issuing credential if the signing algorithm is missing', async () => { + let createdAt = +new Date() + await vcIssuer.cNonces.set('test-nonce', { + cNonce: 'test-nonce', + preAuthorizedCode: 'test-pre-authorized-code', + createdAt: createdAt }) + + jwtVerifyCallback.mockResolvedValue({ + did: 'did:example:1234', + kid: 'did:example:1234#auth', + alg: undefined, + didDocument: { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:1234', + }, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: undefined, + kid: 'test-kid', + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date(), + nonce: 'test-nonce', + }, + } + } + ) + + expect(vcIssuer.issueCredential({ + credentialRequest: { + types: ['VerifiableCredential'], + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: 'ye.ye.ye', + }, + }, + })).rejects.toThrow(Error(ALG_ERROR)) }) })