Skip to content

Commit

Permalink
feat: add jws serialization feature (#253)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas.J.Han <[email protected]>
  • Loading branch information
lukasjhan authored Nov 18, 2024
1 parent 1aa3aea commit e83b494
Show file tree
Hide file tree
Showing 9 changed files with 903 additions and 8 deletions.
57 changes: 57 additions & 0 deletions examples/sd-jwt-example/flattenJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FlattenJSON, SDJwtInstance } from '@sd-jwt/core';
import type { DisclosureFrame } from '@sd-jwt/types';
import { createSignerVerifier, digest, generateSalt, ES256 } from './utils';

(async () => {
const { signer, verifier } = await createSignerVerifier();

// Create SDJwt instance for use
const sdjwt = new SDJwtInstance({
signer,
signAlg: ES256.alg,
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
kbSignAlg: ES256.alg,
kbVerifier: verifier,
});
const claims = {
firstname: 'John',
lastname: 'Doe',
ssn: '123-45-6789',
id: '1234',
};
const disclosureFrame: DisclosureFrame<typeof claims> = {
_sd: ['firstname', 'id'],
};

const kbPayload = {
iat: Math.floor(Date.now() / 1000),
aud: 'https://example.com',
nonce: '1234',
custom: 'data',
};

const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame);
console.log('encodedSdjwt:', encodedSdjwt);

const flattenJSON = FlattenJSON.fromEncode(encodedSdjwt);
console.log('flattenJSON(credential): ', flattenJSON.toJson());

const presentedSdJwt = await sdjwt.present<typeof claims>(
encodedSdjwt,
{ id: true },
{
kb: {
payload: kbPayload,
},
},
);

const flattenPresentationJSON = FlattenJSON.fromEncode(presentedSdJwt);
console.log('flattenJSON(presentation): ', flattenPresentationJSON.toJson());

const verified = await sdjwt.verify(presentedSdJwt, ['id', 'ssn'], true);
console.log(verified);
})();
71 changes: 71 additions & 0 deletions examples/sd-jwt-example/generalJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { GeneralJSON, SDJwtInstance } from '@sd-jwt/core';
import type { DisclosureFrame } from '@sd-jwt/types';
import { createSignerVerifier, digest, generateSalt, ES256 } from './utils';

(async () => {
const { signer, verifier } = await createSignerVerifier();

// Create SDJwt instance for use
const sdjwt = new SDJwtInstance({
signer,
signAlg: ES256.alg,
verifier,
hasher: digest,
saltGenerator: generateSalt,
kbSigner: signer,
kbSignAlg: ES256.alg,
kbVerifier: verifier,
});
const claims = {
firstname: 'John',
lastname: 'Doe',
ssn: '123-45-6789',
id: '1234',
};
const disclosureFrame: DisclosureFrame<typeof claims> = {
_sd: ['firstname', 'id'],
};

const kbPayload = {
iat: Math.floor(Date.now() / 1000),
aud: 'https://example.com',
nonce: '1234',
custom: 'data',
};

const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame);
console.log('encodedSdjwt:', encodedSdjwt);

const generalJSON = GeneralJSON.fromEncode(encodedSdjwt);
console.log('flattenJSON(credential): ', generalJSON.toJson());

const presentedSdJwt = await sdjwt.present<typeof claims>(
encodedSdjwt,
{ id: true },
{
kb: {
payload: kbPayload,
},
},
);

const generalPresentationJSON = GeneralJSON.fromEncode(presentedSdJwt);

await generalPresentationJSON.addSignature(
{
alg: 'ES256',
typ: 'sd+jwt',
kid: 'key-1',
},
signer,
'key-1',
);

console.log(
'flattenJSON(presentation): ',
JSON.stringify(generalPresentationJSON.toJson(), null, 2),
);

const verified = await sdjwt.verify(presentedSdJwt, ['id', 'ssn'], true);
console.log(verified);
})();
4 changes: 3 additions & 1 deletion examples/sd-jwt-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"decoy": "ts-node decoy.ts",
"custom_header": "ts-node custom_header.ts",
"kb": "ts-node kb.ts",
"decode": "ts-node decode.ts"
"decode": "ts-node decode.ts",
"flattenJSON": "ts-node flattenJSON.ts",
"generalJSON": "ts-node generalJSON.ts"
},
"keywords": [],
"author": "",
Expand Down
90 changes: 90 additions & 0 deletions packages/core/src/flattenJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { SDJWTException } from '@sd-jwt/utils';
import { splitSdJwt } from '@sd-jwt/decode';
import { SD_SEPARATOR } from '@sd-jwt/types';

export type FlattenJSONData = {
jwtData: {
protected: string;
payload: string;
signature: string;
};
disclosures: Array<string>;
kb_jwt?: string;
};

export type FlattenJSONSerialized = {
payload: string;
signature: string;
protected: string;
header: {
disclosures: Array<string>;
kb_jwt?: string;
};
};

export class FlattenJSON {
public disclosures: Array<string>;
public kb_jwt?: string;

public payload: string;
public signature: string;
public protected: string;

constructor(data: FlattenJSONData) {
this.disclosures = data.disclosures;
this.kb_jwt = data.kb_jwt;
this.payload = data.jwtData.payload;
this.signature = data.jwtData.signature;
this.protected = data.jwtData.protected;
}

public static fromEncode(encodedSdJwt: string) {
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);

const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
if (!protectedHeader || !payload || !signature) {
throw new SDJWTException('Invalid JWT');
}

return new FlattenJSON({
jwtData: {
protected: protectedHeader,
payload,
signature,
},
disclosures,
kb_jwt: kbJwt,
});
}

public static fromSerialized(json: FlattenJSONSerialized) {
return new FlattenJSON({
jwtData: {
protected: json.protected,
payload: json.payload,
signature: json.signature,
},
disclosures: json.header.disclosures,
kb_jwt: json.header.kb_jwt,
});
}

public toJson(): FlattenJSONSerialized {
return {
payload: this.payload,
signature: this.signature,
protected: this.protected,
header: {
disclosures: this.disclosures,
kb_jwt: this.kb_jwt,
},
};
}

public toEncoded() {
const jwt = `${this.protected}.${this.payload}.${this.signature}`;
const disclosures = this.disclosures.join(SD_SEPARATOR);
const kb_jwt = this.kb_jwt ?? '';
return [jwt, disclosures, kb_jwt].join(SD_SEPARATOR);
}
}
140 changes: 140 additions & 0 deletions packages/core/src/generalJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { base64urlEncode, SDJWTException } from '@sd-jwt/utils';
import { splitSdJwt } from '@sd-jwt/decode';
import { SD_SEPARATOR, type Signer } from '@sd-jwt/types';

export type GeneralJSONData = {
payload: string;
disclosures: Array<string>;
kb_jwt?: string;
signatures: Array<{
protected: string;
signature: string;
kid?: string;
}>;
};

export type GeneralJSONSerialized = {
payload: string;
signatures: Array<{
header: {
disclosures?: Array<string>;
kid?: string;
kb_jwt?: string;
};
protected: string;
signature: string;
}>;
};

export class GeneralJSON {
public payload: string;
public disclosures: Array<string>;
public kb_jwt?: string;
public signatures: Array<{
protected: string;
signature: string;
kid?: string;
}>;

constructor(data: GeneralJSONData) {
this.payload = data.payload;
this.disclosures = data.disclosures;
this.kb_jwt = data.kb_jwt;
this.signatures = data.signatures;
}

public static fromEncode(encodedSdJwt: string) {
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);

const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
if (!protectedHeader || !payload || !signature) {
throw new SDJWTException('Invalid JWT');
}

return new GeneralJSON({
payload,
disclosures,
kb_jwt: kbJwt,
signatures: [
{
protected: protectedHeader,
signature,
},
],
});
}

public static fromSerialized(json: GeneralJSONSerialized) {
if (!json.signatures[0]) {
throw new SDJWTException('Invalid JSON');
}
const disclosures = json.signatures[0].header?.disclosures ?? [];
const kb_jwt = json.signatures[0].header?.kb_jwt;
return new GeneralJSON({
payload: json.payload,
disclosures,
kb_jwt,
signatures: json.signatures.map((s) => {
return {
protected: s.protected,
signature: s.signature,
kid: s.header?.kid,
};
}),
});
}

public toJson() {
return {
payload: this.payload,
signatures: this.signatures.map((s, i) => {
if (i !== 0) {
// If present, disclosures and kb_jwt, MUST be included in the first unprotected header and
// MUST NOT be present in any following unprotected headers.
return {
header: {
kid: s.kid,
},
protected: s.protected,
signature: s.signature,
};
}
return {
header: {
disclosures: this.disclosures,
kid: s.kid,
kb_jwt: this.kb_jwt,
},
protected: s.protected,
signature: s.signature,
};
}),
};
}

public toEncoded(index: number) {
if (index < 0 || index >= this.signatures.length) {
throw new SDJWTException('Index out of bounds');
}

const { protected: protectedHeader, signature } = this.signatures[index];
const disclosures = this.disclosures.join(SD_SEPARATOR);
const kb_jwt = this.kb_jwt ?? '';
const jwt = `${protectedHeader}.${this.payload}.${signature}`;
return [jwt, disclosures, kb_jwt].join(SD_SEPARATOR);
}

public async addSignature(
protectedHeader: Record<string, unknown>,
signer: Signer,
kid?: string,
) {
const header = base64urlEncode(JSON.stringify(protectedHeader));
const signature = await signer(`${header}.${this.payload}`);
this.signatures.push({
protected: header,
signature,
kid,
});
}
}
Loading

0 comments on commit e83b494

Please sign in to comment.