Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@
"@tradetrust-tt/tradetrust": "^6.10.3",
"@tradetrust-tt/tt-verify": "^9.7.5",
"@trustvc/document-store": "^1.0.3",
"@trustvc/w3c": "^2.4.1",
"@trustvc/w3c": "^2.4.2",
"@trustvc/w3c-context": "^2.4.0",
"@trustvc/w3c-credential-status": "^2.4.0",
"@trustvc/w3c-issuer": "^2.3.0",
"@trustvc/w3c-vc": "^2.4.1",
"@trustvc/w3c-vc": "^2.4.2",
"ethers": "^5.8.0",
"ethersV6": "npm:ethers@^6.14.4",
"js-sha3": "^0.9.3",
Expand Down
78 changes: 78 additions & 0 deletions src/__tests__/w3c/presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,84 @@ describe('W3C Verifiable Presentation (via @trustvc/trustvc)', () => {
});

describe('credential policy', () => {
// A holder can omit any statement the issuer did not mark mandatory and the credential
// still verifies. These two prove the trustvc-level guarantee end-to-end: a revoked or
// expired credential cannot be presented even by a holder who deliberately derives the
// offending field away. It holds because @trustvc/w3c-vc makes `/credentialStatus` and
// the expiry mandatory at issuance — so this ALSO guards the dependency: downgrade or
// weaken that and these fail, rather than the hole silently reopening.
describe('fields a holder cannot strip to dodge a check', () => {
const bol = (extra: Record<string, unknown>) => ({
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://trustvc.io/context/bill-of-lading.json',
],
type: ['VerifiableCredential'],
issuer: HOLDER_DID,
credentialSubject: { id: HOLDER_DID, type: ['BillOfLading'], blNumber: 'BL-STRIP' },
...extra,
});

/**
* Signs, then derives revealing ONLY the subject — asking for nothing else.
* @param {object} raw - The raw credential to sign.
* @returns {Promise<object>} The derived credential, minus anything not mandatory.
*/
const signAndStrip = async (raw: object) => {
const signed = await signW3C(raw as never, holderKey as never, 'ecdsa-sd-2023');
if (signed.error) throw new Error(`sign failed: ${signed.error}`);
const derived = await deriveW3C(assertDefined(signed.signed, 'signed'), [
'/credentialSubject/id',
'/credentialSubject/blNumber',
]);
if (derived.error) throw new Error(`derive failed: ${derived.error}`);
return assertDefined(derived.derived, 'derived');
};

it('cannot present a REVOKED credential by stripping credentialStatus', async () => {
// Index 5 on the hosted status list is revoked.
const vc = await signAndStrip(
bol({
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://trustvc.io/context/bill-of-lading.json',
'https://w3id.org/vc/status-list/2021/v1',
],
validFrom: '2024-04-01T12:19:52Z',
credentialStatus: {
id: 'https://trustvc.github.io/did/credentials/statuslist/1#5',
type: 'StatusList2021Entry',
statusPurpose: 'revocation',
statusListIndex: '5',
statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1',
},
}),
);
expect(vc.credentialStatus).toBeDefined(); // survived the strip attempt

const result = await signW3CPresentation(vc, holderKey as never, {
holder: HOLDER_DID,
expiresInSeconds: 600,
});
expect(result.signed).toBeUndefined();
expect(result.error).toMatch(/revocation/);
});

it('cannot present an EXPIRED credential by stripping validUntil', async () => {
const vc = await signAndStrip(
bol({ validFrom: '2020-01-01T00:00:00Z', validUntil: '2021-01-01T00:00:00Z' }),
);
expect(vc.validUntil).toBeDefined(); // survived the strip attempt

const result = await signW3CPresentation(vc, holderKey as never, {
holder: HOLDER_DID,
expiresInSeconds: 600,
});
expect(result.signed).toBeUndefined();
expect(result.error).toMatch(/has expired/);
});
});

it('blocks a credential with a TransferableRecords status', async () => {
const result = await signW3CPresentation(
W3C_TRANSFERABLE_RECORD as never,
Expand Down
Loading