Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jws serialization feature #253

Merged
merged 19 commits into from
Nov 18, 2024
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
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