Skip to content

Commit c0bd6b1

Browse files
authored
Merge pull request #92 from vincentkelleher/feature/signing-algorithms
Open the signing algorithm list in the credential issuance process
2 parents 6e76f57 + d9b17af commit c0bd6b1

3 files changed

Lines changed: 157 additions & 43 deletions

File tree

packages/common/lib/types/OpenID4VCIErrors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Alg } from './CredentialIssuance.types';
1+
import { Alg } from './CredentialIssuance.types'
22

33
export const BAD_PARAMS = 'Wrong parameters provided';
44
export const URL_NOT_VALID = 'Request url is not valid';
55
export const JWS_NOT_VALID = 'JWS is not valid';
66
export const PROOF_CANT_BE_CONSTRUCTED = "Proof can't be constructed.";
77
export const NO_JWT_PROVIDED = 'No JWT provided';
88
export const TYP_ERROR = 'Typ must be "openid4vci-proof+jwt"';
9-
export const ALG_ERROR = `Algorithm is a required field and must be one of: ${Object.keys(Alg).join(', ')}`;
9+
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(', ')}`;
1010
export const KID_JWK_X5C_ERROR = 'Only one must be present: kid, jwk or x5c';
1111
export const KID_DID_NO_DID_ERROR = 'A DID value needs to be returned when kid is present';
1212
export const DID_NO_DIDDOC_ERROR = 'A DID Document needs to be resolved when a DID is encountered';

packages/issuer/lib/VcIssuer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
Alg,
32
ALG_ERROR,
43
AUD_ERROR,
54
CNonceState,
@@ -33,14 +32,19 @@ import {
3332
toUniformCredentialOfferRequest,
3433
TYP_ERROR,
3534
UniformCredentialRequest,
36-
URIState,
35+
URIState
3736
} from '@sphereon/oid4vci-common'
3837
import { CompactSdJwtVc, CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types'
3938
import { v4 } from 'uuid'
4039

4140
import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions'
4241
import { LookupStateManager } from './state-manager'
43-
import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types'
42+
import {
43+
CredentialDataSupplier,
44+
CredentialDataSupplierArgs,
45+
CredentialIssuanceInput,
46+
CredentialSignerCallback
47+
} from './types'
4448

4549
const SECOND = 1000
4650

@@ -463,7 +467,7 @@ export class VcIssuer<DIDDoc extends object> {
463467

464468
if (typ !== 'openid4vci-proof+jwt') {
465469
throw Error(TYP_ERROR)
466-
} else if (!alg || !(alg in Alg)) {
470+
} else if (!alg) {
467471
throw Error(ALG_ERROR)
468472
} else if (!([kid, jwk, x5c].filter((x) => !!x).length === 1)) {
469473
// only 1 is allowed, but need to look into whether jwk and x5c are allowed together

packages/issuer/lib/__tests__/VcIssuer.spec.ts

Lines changed: 147 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
22
import {
33
Alg,
4+
ALG_ERROR,
45
CredentialOfferSession,
56
CredentialSupported,
67
IssuerCredentialSubjectDisplay,
78
IssueStatus,
8-
STATE_MISSING_ERROR,
9+
STATE_MISSING_ERROR
910
} from '@sphereon/oid4vci-common'
1011
import { IProofPurpose, IProofType } from '@sphereon/ssi-types'
1112
import { DIDDocument } from 'did-resolver'
@@ -16,13 +17,36 @@ import { MemoryStates } from '../state-manager'
1617

1718
const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io'
1819

20+
const verifiableCredential = {
21+
'@context': [
22+
'https://www.w3.org/2018/credentials/v1',
23+
'https://w3id.org/security/suites/jws-2020/v1'
24+
],
25+
id: 'http://university.example/credentials/1872',
26+
type: [
27+
'VerifiableCredential',
28+
'ExampleAlumniCredential'
29+
],
30+
issuer: 'https://university.example/issuers/565049',
31+
issuanceDate: new Date().toISOString(),
32+
credentialSubject: {
33+
id: 'did:example:ebfeb1f712ebc6f1c276e12ec21',
34+
alumniOf: {
35+
id: 'did:example:c276e12ec21ebfeb1f712ebc6f1',
36+
name: 'Example University'
37+
}
38+
}
39+
}
40+
1941
describe('VcIssuer', () => {
2042
let vcIssuer: VcIssuer<DIDDocument>
2143
const issuerState = 'previously-created-state'
2244
const clientId = 'sphereon:wallet'
2345
const preAuthorizedCode = 'test_code'
2446

25-
beforeAll(async () => {
47+
const jwtVerifyCallback: jest.Mock = jest.fn()
48+
49+
beforeEach(async () => {
2650
jest.clearAllMocks()
2751
const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11()
2852
.withCryptographicSuitesSupported('ES256K')
@@ -105,29 +129,7 @@ describe('VcIssuer', () => {
105129
},
106130
}),
107131
)
108-
.withJWTVerifyCallback(() =>
109-
Promise.resolve({
110-
did: 'did:example:1234',
111-
kid: 'did:example:1234#auth',
112-
alg: 'ES256k',
113-
didDocument: {
114-
'@context': 'https://www.w3.org/ns/did/v1',
115-
id: 'did:example:1234',
116-
},
117-
jwt: {
118-
header: {
119-
typ: 'openid4vci-proof+jwt',
120-
alg: Alg.ES256K,
121-
kid: 'test-kid',
122-
},
123-
payload: {
124-
aud: 'https://credential-issuer',
125-
iat: +new Date(),
126-
nonce: 'test-nonce',
127-
},
128-
},
129-
}),
130-
)
132+
.withJWTVerifyCallback(jwtVerifyCallback)
131133
.build()
132134
})
133135

@@ -272,6 +274,29 @@ describe('VcIssuer', () => {
272274

273275
// Of course this doesn't work. The state is part of the proof to begin with
274276
it('should fail issuing credential if an invalid state is used', async () => {
277+
jwtVerifyCallback.mockResolvedValue({
278+
did: 'did:example:1234',
279+
kid: 'did:example:1234#auth',
280+
alg: Alg.ES256K,
281+
didDocument: {
282+
'@context': 'https://www.w3.org/ns/did/v1',
283+
id: 'did:example:1234',
284+
},
285+
jwt: {
286+
header: {
287+
typ: 'openid4vci-proof+jwt',
288+
alg: Alg.ES256K,
289+
kid: 'test-kid',
290+
},
291+
payload: {
292+
aud: IDENTIPROOF_ISSUER_URL,
293+
iat: +new Date(),
294+
nonce: 'test-nonce',
295+
},
296+
}
297+
}
298+
)
299+
275300
await expect(
276301
vcIssuer.issueCredential({
277302
credentialRequest: {
@@ -287,23 +312,65 @@ describe('VcIssuer', () => {
287312
).rejects.toThrow(Error(STATE_MISSING_ERROR + ' (test-nonce)'))
288313
})
289314

290-
// Of course this doesn't work. The state is part of the proof to begin with
291-
xit('should issue credential if a valid state is passed in', async () => {
292-
await expect(
315+
it.each([...Object.values<string>(Alg), 'CUSTOM'])('should issue %s signed credential if a valid state is passed in', async (alg: string) => {
316+
jwtVerifyCallback.mockResolvedValue({
317+
did: 'did:example:1234',
318+
kid: 'did:example:1234#auth',
319+
alg: alg,
320+
didDocument: {
321+
'@context': 'https://www.w3.org/ns/did/v1',
322+
id: 'did:example:1234',
323+
},
324+
jwt: {
325+
header: {
326+
typ: 'openid4vci-proof+jwt',
327+
alg: alg,
328+
kid: 'test-kid',
329+
},
330+
payload: {
331+
aud: IDENTIPROOF_ISSUER_URL,
332+
iat: +new Date(),
333+
nonce: 'test-nonce',
334+
},
335+
}
336+
}
337+
)
338+
339+
let createdAt = +new Date()
340+
await vcIssuer.cNonces.set('test-nonce', {
341+
cNonce: 'test-nonce',
342+
preAuthorizedCode: 'test-pre-authorized-code',
343+
createdAt: createdAt
344+
})
345+
await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', {
346+
createdAt: createdAt,
347+
preAuthorizedCode: 'test-pre-authorized-code',
348+
credentialOffer: {
349+
credential_offer: {
350+
credential_issuer: 'did:key:test',
351+
credentials: []
352+
}
353+
},
354+
lastUpdatedAt: createdAt,
355+
status: IssueStatus.ACCESS_TOKEN_CREATED
356+
})
357+
358+
expect(
293359
vcIssuer.issueCredential({
360+
credential: verifiableCredential,
294361
credentialRequest: {
295362
types: ['VerifiableCredential'],
296363
format: 'jwt_vc_json',
297364
proof: {
298365
proof_type: 'jwt',
299-
jwt: 'ye.ye.ye',
300-
},
366+
jwt: 'ye.ye.ye'
367+
}
301368
},
302-
// issuerState,
303-
}),
369+
newCNonce: 'new-test-nonce'
370+
})
304371
).resolves.toEqual({
305-
c_nonce: expect.any(String),
306-
c_nonce_expires_in: 90000,
372+
c_nonce: 'new-test-nonce',
373+
c_nonce_expires_in: 300000,
307374
credential: {
308375
'@context': ['https://www.w3.org/2018/credentials/v1'],
309376
credentialSubject: {},
@@ -314,11 +381,54 @@ describe('VcIssuer', () => {
314381
jwt: 'ye.ye.ye',
315382
proofPurpose: 'assertionMethod',
316383
type: 'JwtProof2020',
317-
verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf',
384+
verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf'
318385
},
319-
type: ['VerifiableCredential'],
386+
type: ['VerifiableCredential']
320387
},
321-
format: 'jwt_vc_json',
388+
format: 'jwt_vc_json'
389+
})
390+
})
391+
392+
it('should fail issuing credential if the signing algorithm is missing', async () => {
393+
let createdAt = +new Date()
394+
await vcIssuer.cNonces.set('test-nonce', {
395+
cNonce: 'test-nonce',
396+
preAuthorizedCode: 'test-pre-authorized-code',
397+
createdAt: createdAt
322398
})
399+
400+
jwtVerifyCallback.mockResolvedValue({
401+
did: 'did:example:1234',
402+
kid: 'did:example:1234#auth',
403+
alg: undefined,
404+
didDocument: {
405+
'@context': 'https://www.w3.org/ns/did/v1',
406+
id: 'did:example:1234',
407+
},
408+
jwt: {
409+
header: {
410+
typ: 'openid4vci-proof+jwt',
411+
alg: undefined,
412+
kid: 'test-kid',
413+
},
414+
payload: {
415+
aud: IDENTIPROOF_ISSUER_URL,
416+
iat: +new Date(),
417+
nonce: 'test-nonce',
418+
},
419+
}
420+
}
421+
)
422+
423+
expect(vcIssuer.issueCredential({
424+
credentialRequest: {
425+
types: ['VerifiableCredential'],
426+
format: 'jwt_vc_json',
427+
proof: {
428+
proof_type: 'jwt',
429+
jwt: 'ye.ye.ye',
430+
},
431+
},
432+
})).rejects.toThrow(Error(ALG_ERROR))
323433
})
324434
})

0 commit comments

Comments
 (0)