Skip to content

Commit 49ef84a

Browse files
HaoenvskiModacht-chrischang
authored andcommitted
fix: treat vc.issuer as optional per W3C VC-JWT encoding
In the W3C VC Data Model JWT encoding the `iss` claim represents the issuer and `vc.issuer` MAY be omitted. Treat `iss` as authoritative: fail only when `iss` is absent, or when `vc.issuer` is present and disagrees with `iss`. Previously a conformant VC-JWT omitting `vc.issuer` was wrongly rejected with "Each vc token issuer must match...".
1 parent 38881b6 commit 49ef84a

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

common/audit.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,18 @@ const _getVpTokenMetadataJwt = vpToken => {
4949
const issuerDidJwt = vcJwtPayload?.iss;
5050
const issuerDidVc = typeof vcJwtPayload?.vc?.issuer === 'string' ?
5151
vcJwtPayload.vc.issuer : vcJwtPayload?.vc?.issuer?.id;
52-
if(!issuerDidJwt || !issuerDidVc || issuerDidJwt !== issuerDidVc) {
52+
// Per the W3C VC Data Model JWT encoding, the `iss` claim represents the
53+
// issuer and `vc.issuer` MAY be omitted. Treat `iss` as authoritative;
54+
// only fail when `iss` is absent, or when `vc.issuer` is present and
55+
// disagrees with `iss`.
56+
if(!issuerDidJwt) {
57+
return {
58+
valid: false,
59+
error: 'Each vc token must have an issuer (JWT "iss")',
60+
issuerDids
61+
};
62+
}
63+
if(issuerDidVc && issuerDidVc !== issuerDidJwt) {
5364
return {
5465
valid: false,
5566
error: 'Each vc token issuer must match the issuer ' +
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* Copyright 2023 - 2026 California Department of Motor Vehicles
3+
* Copyright 2023 - 2026 Digital Bazaar, Inc.
4+
*
5+
* SPDX-License-Identifier: BSD-3-Clause
6+
*/
7+
8+
import {getVpTokenMetadata} from '../../common/audit.js';
9+
import expect from 'expect.js';
10+
11+
// Encode a payload as an (unsigned) JWT. getVpTokenMetadata uses decodeJwt,
12+
// which does not verify the signature, so a placeholder signature suffices.
13+
const b64u = obj => Buffer.from(JSON.stringify(obj)).toString('base64url');
14+
const jwt = payload => `${b64u({alg: 'ES256', typ: 'JWT'})}.${b64u(payload)}.AAAA`;
15+
// Build a VP-JWT whose `vp.verifiableCredential` holds the given VC payloads,
16+
// each itself encoded as a VC-JWT.
17+
const vpToken = vcPayloads =>
18+
jwt({vp: {verifiableCredential: vcPayloads.map(jwt)}});
19+
20+
const ISS = 'did:key:zDnaeyXNn6Xc8KZ9Yx1vJhRYV7Vvac7vZxbzb1234567890ab';
21+
const OTHER = 'did:key:zDnaOtherIssuer000000000000000000000000000000000';
22+
23+
describe('getVpTokenMetadata - vc.issuer per W3C VC-JWT encoding', () => {
24+
it('accepts a VC-JWT that omits vc.issuer (iss is authoritative)', () => {
25+
const res = getVpTokenMetadata(
26+
vpToken([{iss: ISS, vc: {type: ['VerifiableCredential']}}]));
27+
expect(res.valid).to.be(true);
28+
expect(res.issuerDids).to.eql([ISS]);
29+
});
30+
31+
it('accepts when vc.issuer (string) matches iss', () => {
32+
const res = getVpTokenMetadata(
33+
vpToken([{iss: ISS, vc: {issuer: ISS}}]));
34+
expect(res.valid).to.be(true);
35+
expect(res.issuerDids).to.eql([ISS]);
36+
});
37+
38+
it('accepts when vc.issuer object id matches iss', () => {
39+
const res = getVpTokenMetadata(
40+
vpToken([{iss: ISS, vc: {issuer: {id: ISS}}}]));
41+
expect(res.valid).to.be(true);
42+
expect(res.issuerDids).to.eql([ISS]);
43+
});
44+
45+
it('rejects when a present vc.issuer disagrees with iss', () => {
46+
const res = getVpTokenMetadata(
47+
vpToken([{iss: ISS, vc: {issuer: OTHER}}]));
48+
expect(res.valid).to.be(false);
49+
});
50+
51+
it('rejects when iss is absent', () => {
52+
const res = getVpTokenMetadata(
53+
vpToken([{vc: {type: ['VerifiableCredential']}}]));
54+
expect(res.valid).to.be(false);
55+
});
56+
});

0 commit comments

Comments
 (0)