@sd-jwt/core
Version:
sd-jwt draft 7 implementation in typescript
633 lines (583 loc) • 16.7 kB
text/typescript
import Crypto, { type KeyLike } from 'node:crypto';
import { digest, ES256, generateSalt } from '@sd-jwt/crypto-nodejs';
import type { JwtPayload, KbVerifier, Signer, Verifier } from '@sd-jwt/types';
import { exportJWK, importJWK, type JWK } from 'jose';
import { describe, expect, test } from 'vitest';
import { SDJwtInstance, type SdJwtPayload } from '../index';
// Extract the major version as a number
const nodeVersionMajor = Number.parseInt(
process.version.split('.')[0].substring(1),
10,
);
const createSignerVerifier = () => {
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
const signer: Signer = async (data: string) => {
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
return Buffer.from(sig).toString('base64url');
};
const verifier: Verifier = async (data: string, sig: string) => {
return Crypto.verify(
null,
Buffer.from(data),
publicKey,
Buffer.from(sig, 'base64url'),
);
};
return { signer, verifier };
};
describe('index', () => {
test('create', async () => {
const sdjwt = new SDJwtInstance<SdJwtPayload>();
expect(sdjwt).toBeDefined();
});
test('kbJwt', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
expect(credential).toBeDefined();
const presentation = await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
expect(presentation).toBeDefined();
});
test('issue', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier,
hasher: digest,
saltGenerator: generateSalt,
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
expect(credential).toBeDefined();
});
test('verify failed', async () => {
const { signer } = createSignerVerifier();
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
const failedverifier: Verifier = async (data: string, sig: string) => {
return Crypto.verify(
null,
Buffer.from(data),
publicKey,
Buffer.from(sig, 'base64url'),
);
};
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier: failedverifier,
hasher: digest,
saltGenerator: generateSalt,
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
try {
await sdjwt.verify(credential);
} catch (e) {
expect(e).toBeDefined();
}
});
test('verify failed with kbJwt', async () => {
const { signer, verifier } = createSignerVerifier();
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
const failedverifier: Verifier = async (data: string, sig: string) => {
return Crypto.verify(
null,
Buffer.from(data),
publicKey,
Buffer.from(sig, 'base64url'),
);
};
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
kbVerifier: failedverifier,
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const presentation = await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '',
iat: 1,
nonce: '342',
},
},
},
);
try {
await sdjwt.verify(presentation);
} catch (e) {
expect(e).toBeDefined();
}
});
test('verify with kbJwt', async () => {
const { signer, verifier } = createSignerVerifier();
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
//TODO: maybe we can pass a minial class of the jwt to pass the token
const kbVerifier: KbVerifier = async (
data: string,
sig: string,
payload: JwtPayload,
) => {
let publicKey: JsonWebKey;
if (payload.cnf) {
// use the key from the cnf
publicKey = payload.cnf.jwk;
} else {
throw Error('key binding not supported');
}
// get the key of the holder to verify the signature
return Crypto.verify(
null,
Buffer.from(data),
(await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
Buffer.from(sig, 'base64url'),
);
};
const kbSigner = (data: string) => {
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
return Buffer.from(sig).toString('base64url');
};
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: kbSigner,
kbVerifier: kbVerifier,
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iat: Math.floor(Date.now() / 1000),
cnf: {
jwk: await exportJWK(publicKey),
},
},
{
_sd: ['foo'],
},
);
const presentation = await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
const results = await sdjwt.verify(presentation, ['foo'], true);
expect(results).toBeDefined();
});
test('Hasher not found', async () => {
const sdjwt = new SDJwtInstance<SdJwtPayload>({});
try {
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
expect(credential).toBeDefined();
} catch (e) {
expect(e).toBeDefined();
}
});
test('SaltGenerator not found', async () => {
const sdjwt = new SDJwtInstance<SdJwtPayload>({
hasher: digest,
});
try {
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
expect(credential).toBeDefined();
} catch (e) {
expect(e).toBeDefined();
}
});
test('Signer not found', async () => {
const sdjwt = new SDJwtInstance<SdJwtPayload>({
hasher: digest,
saltGenerator: generateSalt,
});
try {
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
expect(credential).toBeDefined();
} catch (e) {
expect(e).toBeDefined();
}
});
test('Verifier not found', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
kbVerifier: verifier,
signAlg: 'EdDSA',
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const presentation = await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
try {
await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] });
} catch (e) {
expect(e).toBeDefined();
}
});
test('kbSigner not found', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbVerifier: verifier,
signAlg: 'EdDSA',
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
try {
await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
} catch (e) {
expect(e).toBeDefined();
}
});
test('kbVerifier not found', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
signAlg: 'EdDSA',
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const presentation = await sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
try {
await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] });
} catch (e) {
expect(e).toBeDefined();
}
});
test('kbSignAlg not found', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
signAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const presentation = sdjwt.present<typeof claims>(
credential,
{ foo: true },
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
expect(presentation).rejects.toThrow(
'Key Binding sign algorithm not specified',
);
});
test('hasher is not found', async () => {
const { signer } = createSignerVerifier();
const sdjwt_create = new SDJwtInstance<SdJwtPayload>({
signer,
hasher: digest,
saltGenerator: generateSalt,
signAlg: 'EdDSA',
});
const credential = await sdjwt_create.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const sdjwt = new SDJwtInstance<SdJwtPayload>({});
expect(sdjwt.keys('')).rejects.toThrow('Hasher not found');
expect(sdjwt.presentableKeys('')).rejects.toThrow('Hasher not found');
expect(sdjwt.getClaims('')).rejects.toThrow('Hasher not found');
expect(() => sdjwt.decode('')).toThrowError('Hasher not found');
expect(
sdjwt.present<typeof claims>(credential, { foo: true }),
).rejects.toThrow('Hasher not found');
});
test('presentableKeys', async () => {
const { signer } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
hasher: digest,
saltGenerator: generateSalt,
signAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const keys = await sdjwt.presentableKeys(credential);
expect(keys).toBeDefined();
expect(keys).toEqual(['foo']);
});
test('present all disclosures with kb jwt', async () => {
const { signer } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
kbSigner: signer,
hasher: digest,
saltGenerator: generateSalt,
signAlg: 'EdDSA',
kbSignAlg: 'EdDSA',
});
const credential = await sdjwt.issue(
{
foo: 'bar',
iss: 'Issuer',
iat: Math.floor(Date.now() / 1000),
vct: '',
},
{
_sd: ['foo'],
},
);
const presentation = await sdjwt.present<typeof claims>(
credential,
undefined,
{
kb: {
payload: {
aud: '1',
iat: 1,
nonce: '342',
},
},
},
);
const decoded = await sdjwt.decode(presentation);
expect(decoded.jwt).toBeDefined();
expect(decoded.disclosures).toBeDefined();
expect(decoded.kbJwt).toBeDefined();
});
(nodeVersionMajor < 20 ? test.skip : test)(
'validate sd-jwt that created in other implemenation',
async () => {
const publicKeyExampleJwt: JsonWebKey = {
kty: 'EC',
crv: 'P-256',
x: 'b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ',
y: 'Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8',
};
const kbPubkey: JsonWebKey = {
kty: 'EC',
crv: 'P-256',
x: 'TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc',
y: 'ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ',
};
const encodedJwt =
'eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCIsICJraWQiOiAiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiamRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.QXgzrePAdq_WZVGCwDxP-l8h0iyckrHBNidxVqGtKJ0LMzObqgaXUD1cgGEf7d9TexPkBcgQYqjuzlfbeCxxuA~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MDk5OTYxODUsICJzZF9oYXNoIjogIjc4cFFEazJOblNEM1dKQm5SN015aWpmeUVqcGJ5a01yRnlpb2ZYSjlsN0kifQ.7k4goAlxM4a3tHnvCBCe70j_I-BCwtzhBRXQNk9cWJnQWxxt2kIqCyzcwzzUc0gTwtbGWVQoeWCiL5K6y3a4VQ';
const sdjwt = new SDJwtInstance({
hasher: digest,
verifier: await ES256.getVerifier(publicKeyExampleJwt),
kbVerifier: await ES256.getVerifier(kbPubkey),
});
const decode = await sdjwt.verify(encodedJwt);
expect(decode).toBeDefined();
},
);
});