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: decoupling JWT from Pollux and adding KID header to JWTs #271

Merged
merged 1 commit into from
Aug 23, 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
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 @@ -257,7 +258,7 @@
if (jwsArray.length !== 3) {
throw new PolluxError.InvalidJWTString("Credential status jwt is invalid")
}
const { proof, ...cleanedPayload } = revocation;

Check warning on line 261 in src/pollux/Pollux.ts

View workflow job for this annotation

GitHub Actions / Build and test

'proof' is assigned a value but never used. Allowed unused vars must match /^_/u
const payload = { ...cleanedPayload };
const encoded = await this.encode(payload)
const signature = Buffer.from(base64url.baseDecode(jwsArray[2]));
Expand Down Expand Up @@ -356,7 +357,6 @@
}
});

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 @@
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 @@
}

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 @@
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 @@
"@context": ["https://www.w3.org/2018/presentations/v1"],
type: ["VerifiablePresentation"],
},
}
},
header: { kid }
});
return signedJWT as ProcessedCredentialOfferPayloads[Types];
}
Expand All @@ -936,8 +944,11 @@
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 @@
"@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 @@
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 @@
aud: presReqOptions.domain,
nonce: presReqOptions.challenge,
vp: credential.presentation()
}
},
header: { kid }
});

return signedJWT;
Expand Down Expand Up @@ -1146,4 +1162,25 @@

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
Loading