UNPKG

@sd-jwt/core

Version:

sd-jwt draft 7 implementation in typescript

151 lines (139 loc) 6.29 kB
import { describe, expect, test } from 'vitest'; import { unpackObj } from '../src/decode'; import { Disclosure } from '../src/utils'; /** * Tests for RFC 9901 validation checks added to unpackObj / unpackObjInternal. * * Section 7.1 step 4: Duplicate digest rejection * Section 7.1 step 5: Unreferenced disclosure rejection * Section 7.1 step 3c.ii.3: Claim name collision rejection */ const makeDisclosure = ( digest: string, key: string | undefined, value: unknown, ) => Disclosure.fromArray(key ? ['salt', key, value] : ['salt', value], { digest, encoded: `encoded-${digest}`, }); describe('RFC 9901 validation', () => { // ────────────────────────────────────────── // 7.1 step 4 — Duplicate digest in _sd array // ────────────────────────────────────────── test('rejects duplicate digest in _sd array', () => { const digest = 'abc123'; const payload = { _sd: [digest, digest] }; const map: Record<string, Disclosure> = { [digest]: makeDisclosure(digest, 'foo', 'bar'), }; expect(() => unpackObj(payload, map)).toThrow( 'Duplicate digest found in SD-JWT payload', ); }); // ────────────────────────────────────────── // 7.1 step 4 — Duplicate digest across nested _sd // ────────────────────────────────────────── test('rejects duplicate digest across nested _sd arrays', () => { const digest = 'dup1'; const payload = { _sd: [digest], nested: { _sd: [digest], }, }; const map: Record<string, Disclosure> = { [digest]: makeDisclosure(digest, 'x', 'y'), }; expect(() => unpackObj(payload, map)).toThrow( 'Duplicate digest found in SD-JWT payload', ); }); // ────────────────────────────────────────── // 7.1 step 4 — Duplicate digest in array items // ────────────────────────────────────────── test('rejects duplicate digest in array element disclosures', () => { const digest = 'arrdup'; const payload = { arr: [{ '...': digest }, { '...': digest }], }; const map: Record<string, Disclosure> = { [digest]: makeDisclosure(digest, undefined, 'val'), }; expect(() => unpackObj(payload, map)).toThrow( 'Duplicate digest found in SD-JWT payload', ); }); // ────────────────────────────────────────── // 7.1 step 5 — Unreferenced disclosure // ────────────────────────────────────────── test('rejects unreferenced disclosure', () => { const usedDigest = 'used1'; const unusedDigest = 'unused1'; const payload = { _sd: [usedDigest] }; const map: Record<string, Disclosure> = { [usedDigest]: makeDisclosure(usedDigest, 'a', 1), [unusedDigest]: makeDisclosure(unusedDigest, 'b', 2), }; expect(() => unpackObj(payload, map)).toThrow( 'Unreferenced disclosure(s) detected in SD-JWT', ); }); test('rejects when no digests exist in payload but disclosures provided', () => { const payload = { plain: 'value' }; const map: Record<string, Disclosure> = { orphan: makeDisclosure('orphan', 'k', 'v'), }; expect(() => unpackObj(payload, map)).toThrow( 'Unreferenced disclosure(s) detected in SD-JWT', ); }); // ────────────────────────────────────────── // 7.1 step 3c.ii.3 — Claim name collision // ────────────────────────────────────────── test('rejects disclosed claim name that conflicts with plaintext key', () => { const digest = 'col1'; // The payload has both a plaintext "name" and a disclosure that would add "name" const payload = { name: 'Alice', _sd: [digest] }; const map: Record<string, Disclosure> = { [digest]: makeDisclosure(digest, 'name', 'Mallory'), }; expect(() => unpackObj(payload, map)).toThrow( 'Disclosed claim name "name" conflicts with existing payload key', ); }); // ────────────────────────────────────────── // Positive: valid SD-JWT unpacks without error // ────────────────────────────────────────── test('unpacks valid SD-JWT without errors', () => { const d1 = 'digest1'; const d2 = 'digest2'; const payload = { _sd: [d1, d2], plain: 'hello' }; const map: Record<string, Disclosure> = { [d1]: makeDisclosure(d1, 'foo', 'bar'), [d2]: makeDisclosure(d2, 'baz', 42), }; const { unpackedObj, disclosureKeymap } = unpackObj(payload, map); expect(unpackedObj).toEqual({ plain: 'hello', foo: 'bar', baz: 42 }); expect(disclosureKeymap).toEqual({ foo: d1, baz: d2 }); }); test('unpacks valid SD-JWT with array disclosures', () => { const d1 = 'arrdig1'; const d2 = 'arrdig2'; const payload = { arr: [{ '...': d1 }, 'plainItem', { '...': d2 }], }; const map: Record<string, Disclosure> = { [d1]: makeDisclosure(d1, undefined, 'secret1'), [d2]: makeDisclosure(d2, undefined, 'secret2'), }; const { unpackedObj } = unpackObj(payload, map); expect(unpackedObj).toEqual({ arr: ['secret1', 'plainItem', 'secret2'] }); }); test('allows empty disclosure map with no _sd in payload', () => { const payload = { plain: 'value' }; const map: Record<string, Disclosure> = {}; const { unpackedObj } = unpackObj(payload, map); expect(unpackedObj).toEqual({ plain: 'value' }); }); });