Skip to content

Commit

Permalink
feat: decoupling JWT from Pollux and adding KID header to JWTs (#271)
Browse files Browse the repository at this point in the history
Signed-off-by: Curtish <ch@curtish.me>
  • Loading branch information
curtis-h authored Aug 23, 2024
1 parent da27890 commit 8a1ed3f
Show file tree
Hide file tree
Showing 14 changed files with 533 additions and 339 deletions.
1 change: 1 addition & 0 deletions src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./buildingBlocks/Castor";
export * from "./buildingBlocks/Mercury";
export * from "./buildingBlocks/Pluto";
export * from "./buildingBlocks/Pollux";
export * from "./utils/JWT";
9 changes: 9 additions & 0 deletions src/domain/models/DIDDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ export class DIDDocument {
return serviceArray;
}, [] as Service[]);
}

get verificationMethods(): VerificationMethod[] {
return this.coreProperties.reduce((serviceArray, coreProperty) => {
if (coreProperty instanceof VerificationMethods) {
return [...serviceArray, ...coreProperty.values];
}
return serviceArray;
}, [] as VerificationMethod[]);
}
}

export interface PublicKeyJWK {
Expand Down
83 changes: 83 additions & 0 deletions src/domain/utils/JWT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { JWTPayload, Signer, createJWT } from "did-jwt";
import { base64url } from "multiformats/bases/base64";
import { DID, PrivateKey } from "..";
import { asJsonObj, isNil } from "../../utils/guards";

export namespace JWT {
export interface Header {
typ: string;
alg: string;
[key: string]: any;
}

export type Payload = JWTPayload;

export interface DecodedObj {
header: Header;
payload: Payload;
signature: string;
data: string;
}


/**
* Creates a signed JWT
*
* @param issuer
* @param privateKey
* @param payload
* @returns
*/
export const sign = async (
issuer: DID,
privateKey: PrivateKey,
payload: Partial<Payload>,
header?: Partial<Header>
): Promise<string> => {
if (!privateKey.isSignable()) {
throw new Error("Key is not signable");
}

const signer: Signer = async (data: any) => {
const signature = privateKey.sign(Buffer.from(data));
const encoded = base64url.baseEncode(signature);
return encoded;
};

const jwt = await createJWT(
payload,
{ issuer: issuer.toString(), signer },
{ alg: privateKey.alg, ...asJsonObj(header) }
);

return jwt;
};

/**
* decode a JWT into its parts
*
* @param jws
* @returns
*/
export const decode = (jws: string): DecodedObj => {
const parts = jws.split(".");
const headersEnc = parts.at(0);
const payloadEnc = parts.at(1);

if (parts.length != 3 || isNil(headersEnc) || isNil(payloadEnc)) {
// TODO error
// throw new InvalidJWTString();
throw new Error();
}

const headers = base64url.baseDecode(headersEnc);
const payload = base64url.baseDecode(payloadEnc);

return {
header: JSON.parse(Buffer.from(headers).toString()),
payload: JSON.parse(Buffer.from(payload).toString()),
signature: parts[2],
data: `${headersEnc}.${payloadEnc}`,
};
};
}
51 changes: 44 additions & 7 deletions src/pollux/Pollux.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { uuid } from "@stablelib/uuid";
import { base58btc } from "multiformats/bases/base58";
import type * as Anoncreds from "anoncreds-browser";
import * as jsonld from 'jsonld';
import { Castor } from "../domain/buildingBlocks/Castor";
Expand Down Expand Up @@ -356,7 +357,6 @@ export default class Pollux implements IPollux {
}
});

const subject = credential.subject;
if (!privateKey.isSignable()) {
throw new CastorError.InvalidKeyError("Cannot sign the proof challenge with this key.")
}
Expand All @@ -365,6 +365,10 @@ export default class Pollux implements IPollux {
throw new PolluxError.InvalidCredentialError("Cannot create proofs with this type of credential.")
}

const subject = credential.subject;
const issuerDID = DID.fromString(subject);
const kid = await this.getSigningKid(issuerDID, privateKey);

const payload: JWTPresentationPayload = {
iss: subject,
aud: domain,
Expand All @@ -374,9 +378,10 @@ export default class Pollux implements IPollux {
}

const jws = await this.JWT.sign({
issuerDID: DID.fromString(subject),
issuerDID,
privateKey,
payload
payload,
header: { kid }
});

const presentationSubmission: PresentationSubmission = {
Expand Down Expand Up @@ -911,9 +916,11 @@ export default class Pollux implements IPollux {
if (!keyPair) {
throw new Error("Required keyPair ");
}

const kid = await this.getSigningKid(did, keyPair.privateKey);
const challenge = offer.options.challenge;
const domain = offer.options.domain;

const signedJWT = await this.JWT.sign({
issuerDID: did,
privateKey: keyPair.privateKey,
Expand All @@ -924,7 +931,8 @@ export default class Pollux implements IPollux {
"@context": ["https://www.w3.org/2018/presentations/v1"],
type: ["VerifiablePresentation"],
},
}
},
header: { kid }
});
return signedJWT as ProcessedCredentialOfferPayloads[Types];
}
Expand All @@ -936,8 +944,11 @@ export default class Pollux implements IPollux {
if (!keyPair) {
throw new Error("Required keyPair ");
}

const kid = await this.getSigningKid(did, keyPair.privateKey);
const challenge = offer.options.challenge;
const domain = offer.options.domain;

const signedJWT = await this.JWT.sign({
issuerDID: did,
privateKey: keyPair.privateKey,
Expand All @@ -948,8 +959,10 @@ export default class Pollux implements IPollux {
"@context": ["https://www.w3.org/2018/presentations/v1"],
type: ["VerifiablePresentation"],
},
}
},
header: { kid },
});

return signedJWT as ProcessedCredentialOfferPayloads[Types];
}

Expand Down Expand Up @@ -1094,6 +1107,8 @@ export default class Pollux implements IPollux {
const jwtPresentationRequest = presentationRequest
const presReqJson: JWTJson = jwtPresentationRequest.toJSON() as any;
const presReqOptions = presReqJson.options;
const kid = await this.getSigningKid(options.did, options.privateKey);

const signedJWT = await this.JWT.sign({
issuerDID: options.did,
privateKey: options.privateKey,
Expand All @@ -1102,7 +1117,8 @@ export default class Pollux implements IPollux {
aud: presReqOptions.domain,
nonce: presReqOptions.challenge,
vp: credential.presentation()
}
},
header: { kid }
});

return signedJWT;
Expand Down Expand Up @@ -1146,4 +1162,25 @@ export default class Pollux implements IPollux {

throw new PolluxError.InvalidPresentationProofArgs();
}


/**
* try to match the privateKey with a dids verificationMethod
* returning the relevant key id
*
* @param did
* @param privateKey
* @returns {string} kid (key identifier)
*/
private async getSigningKid(did: DID, privateKey: PrivateKey) {
const pubKey = privateKey.publicKey();
const encoded = base58btc.encode(pubKey.to.Buffer());
const document = await this.castor.resolveDID(did.toString());

const signingKey = document.verificationMethods.find(key => {
return key.publicKeyMultibase === encoded;
});

return signingKey?.id;
}
}
46 changes: 19 additions & 27 deletions src/pollux/utils/JWT.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import * as didJWT from "did-jwt";
import { JWTCredential } from "../../pollux/models/JWTVerifiableCredential";
import { JWTCore } from "./jwt/JWTCore";
import { JWTInstanceType, JWTSignOptions, JWTVerifyOptions } from "./jwt/types";
import { decodeJWS } from "./decodeJWS";
import { base64url } from "multiformats/bases/base64";
import { isNil } from "../../utils";

export class JWT extends JWTCore<JWTInstanceType.JWT> {
public type = JWTInstanceType.JWT;
import { JsonObj, isNil } from "../../utils";
import * as Domain from "../../domain";

export class JWT extends JWTCore {
async decode(jws: string) {
return decodeJWS(jws);
return Domain.JWT.decode(jws);
}

async sign(options: {
issuerDID: Domain.DID,
privateKey: Domain.PrivateKey,
payload: Partial<Domain.JWT.Payload>,
header?: JsonObj,
}): Promise<string> {
const { issuerDID, privateKey, payload, header } = options;
return Domain.JWT.sign(issuerDID, privateKey, payload, header);
}

async verify(
options: JWTVerifyOptions<JWTInstanceType.JWT>
): Promise<boolean> {
async verify(options: {
jws: string;
issuerDID: Domain.DID,
holderDID?: Domain.DID,
}): Promise<boolean> {
try {
const { issuerDID, jws, holderDID } = options;
const resolved = await this.resolve(issuerDID.toString());
Expand Down Expand Up @@ -54,20 +62,4 @@ export class JWT extends JWTCore<JWTInstanceType.JWT> {
return false;
}
}

async sign(
options: JWTSignOptions<JWTInstanceType.JWT, any>
): Promise<string> {
const { issuerDID, privateKey, payload } = options;
if (!privateKey.isSignable()) {
throw new Error("Key is not signable");
}
const { signAlg, signer } = this.getSKConfig(privateKey);
const jwt = await didJWT.createJWT(
payload,
{ issuer: issuerDID.toString(), signer },
{ alg: signAlg }
);
return jwt;
}
}
53 changes: 27 additions & 26 deletions src/pollux/utils/SDJWT.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { SDJwtVcInstance, } from '@sd-jwt/sd-jwt-vc';
import { SDJwtVcInstance, SdJwtVcPayload, } from '@sd-jwt/sd-jwt-vc';
import type { DisclosureFrame, Extensible, PresentationFrame } from '@sd-jwt/types';
import { JWTCore } from "./jwt/JWTCore";
import { JWTInstanceType, JWTSignOptions, JWTVerifyOptions } from "./jwt/types";
import { JWTObject, PublicKey, PrivateKey } from '../../domain';
import { decodeJWS } from './decodeJWS';
import { SDJWTCredential } from '../models/SDJWTVerifiableCredential';
import * as Domain from '../../domain';


export class SDJWT extends JWTCore<JWTInstanceType.SDJWT> {
public type = JWTInstanceType.SDJWT;

async decode(jws: string): Promise<JWTObject> {
return decodeJWS(jws)
export class SDJWT extends JWTCore {
async decode(jws: string) {
return Domain.JWT.decode(jws);
}

public createDisclosureFrameFor<T extends Extensible>(config: DisclosureFrame<T>): DisclosureFrame<T> {
return config;
createDisclosureFrameFor<T extends Extensible>(config: DisclosureFrame<T>): DisclosureFrame<T> {
return config;
}

async verify(options: JWTVerifyOptions<JWTInstanceType.SDJWT>): Promise<boolean> {
async verify(options: {
issuerDID: Domain.DID,
jws: string,
requiredClaimKeys?: string[],
requiredKeyBindings?: boolean
}): Promise<boolean> {
const { issuerDID, jws } = options;
const resolved = await this.resolve(issuerDID.toString());
const verificationMethods = resolved.didDocument?.verificationMethod;
Expand All @@ -30,7 +30,7 @@ export class SDJWT extends JWTCore<JWTInstanceType.SDJWT> {
throw new Error("Invalid issuer");
}
for (const verificationMethod of verificationMethods) {
const pk: PublicKey | undefined = this.getPKInstance(verificationMethod)
const pk: Domain.PublicKey | undefined = this.getPKInstance(verificationMethod)
if (pk && pk.canVerify()) {
const sdjwt = new SDJwtVcInstance(this.getPKConfig(pk));
try {
Expand All @@ -49,22 +49,23 @@ export class SDJWT extends JWTCore<JWTInstanceType.SDJWT> {
return false;
}

async sign<E extends Extensible>(options: JWTSignOptions<JWTInstanceType.SDJWT, E>): Promise<string> {
async sign<E extends Extensible>(options: {
issuerDID: Domain.DID,
privateKey: Domain.PrivateKey,
payload: SdJwtVcPayload,
disclosureFrame: DisclosureFrame<E>
}): Promise<string> {
const sdjwt = new SDJwtVcInstance(this.getSKConfig(options.privateKey));
return sdjwt.issue(options.payload, options.disclosureFrame)
}

async createPresentationFor<E extends Extensible>(
options: {
jws: string,
privateKey: PrivateKey,
frame?: PresentationFrame<E> | undefined
}
) {
const sdjwt = new SDJwtVcInstance(
this.getSKConfig(options.privateKey)
);
return sdjwt.present<E>(options.jws, options.frame)
async createPresentationFor<E extends Extensible>(options: {
jws: string,
privateKey: Domain.PrivateKey,
frame?: PresentationFrame<E> | undefined
}) {
const sdjwt = new SDJwtVcInstance(this.getSKConfig(options.privateKey));
return sdjwt.present<E>(options.jws, options.frame)
}

}
Loading

0 comments on commit 8a1ed3f

Please sign in to comment.