@sd-jwt/sd-jwt-vc
Version:
sd-jwt draft 7 implementation in typescript
323 lines (321 loc) • 11.2 kB
JavaScript
var __getProtoOf = Object.getPrototypeOf;
var __reflectGet = Reflect.get;
var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/sd-jwt-vc-instance.ts
import { Jwt, SDJwt, SDJwtInstance } from "@sd-jwt/core";
import { base64urlDecode, SDJWTException } from "@sd-jwt/utils";
import {
getListFromStatusListJWT
} from "@sd-jwt/jwt-status-list";
import Ajv from "ajv";
import addFormats from "ajv-formats";
var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
constructor(userConfig) {
super(userConfig);
/**
* The type of the SD-JWT-VC set in the header.typ field.
*/
this.type = "dc+sd-jwt";
this.userConfig = {};
if (userConfig) {
this.userConfig = userConfig;
}
}
/**
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
* @param disclosureFrame
*/
validateReservedFields(disclosureFrame) {
if ((disclosureFrame == null ? void 0 : disclosureFrame._sd) && Array.isArray(disclosureFrame._sd) && disclosureFrame._sd.length > 0) {
const reservedNames = ["iss", "nbf", "exp", "cnf", "vct", "status"];
const reservedNamesInDisclosureFrame = disclosureFrame._sd.filter((key) => reservedNames.includes(key));
if (reservedNamesInDisclosureFrame.length > 0) {
throw new SDJWTException("Cannot disclose protected field");
}
}
}
/**
* Fetches the status list from the uri with a timeout of 10 seconds.
* @param uri The URI to fetch from.
* @returns A promise that resolves to a compact JWT.
*/
statusListFetcher(uri) {
return __async(this, null, function* () {
var _a;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1e4);
try {
const response = yield fetch(uri, {
signal: controller.signal,
headers: { Accept: "application/statuslist+jwt" }
});
if (!response.ok) {
throw new Error(
`Error fetching status list: ${response.status} ${yield response.text()}`
);
}
if (!((_a = response.headers.get("content-type")) == null ? void 0 : _a.includes("application/statuslist+jwt"))) {
throw new Error("Invalid content type");
}
return response.text();
} finally {
clearTimeout(timeoutId);
}
});
}
/**
* Validates the status, throws an error if the status is not 0.
* @param status
* @returns
*/
statusValidator(status) {
return __async(this, null, function* () {
if (status !== 0) throw new SDJWTException("Status is not valid");
return Promise.resolve();
});
}
/**
* Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
* @param currentDate current time in seconds
*/
verify(encodedSDJwt, options) {
return __async(this, null, function* () {
const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, options).then((res) => {
return {
payload: res.payload,
header: res.header,
kb: res.kb
};
});
yield this.verifyStatus(result, options);
if (this.userConfig.loadTypeMetadataFormat) {
yield this.verifyVct(result);
}
return result;
});
}
/**
* Gets VCT Metadata of the raw SD-JWT-VC. Returns the type metadata format. If the SD-JWT-VC is invalid or does not contain a vct claim, an error is thrown.
* @param encodedSDJwt
* @returns
*/
getVct(encodedSDJwt) {
return __async(this, null, function* () {
const { payload, header } = yield SDJwt.extractJwt(encodedSDJwt);
if (!payload) {
throw new SDJWTException("JWT payload is missing");
}
const result = {
payload,
header,
kb: void 0
};
return this.fetchVct(result);
});
}
/**
* Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
* @param integrity
* @param response
*/
validateIntegrity(response, url, integrity) {
return __async(this, null, function* () {
if (integrity) {
const arrayBuffer = yield response.arrayBuffer();
const alg = integrity.split("-")[0];
const hashBuffer = yield this.userConfig.hasher(
arrayBuffer,
alg
);
const integrityHash = integrity.split("-")[1];
const hash = Array.from(new Uint8Array(hashBuffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
if (hash !== integrityHash) {
throw new Error(
`Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`
);
}
}
});
}
/**
* Fetches the content from the url with a timeout of 10 seconds.
* @param url
* @returns
*/
fetch(url, integrity) {
return __async(this, null, function* () {
var _a;
try {
const response = yield fetch(url, {
signal: AbortSignal.timeout((_a = this.userConfig.timeout) != null ? _a : 1e4)
});
if (!response.ok) {
const errorText = yield response.text();
throw new Error(
`Error fetching ${url}: ${response.status} ${response.statusText} - ${errorText}`
);
}
yield this.validateIntegrity(response.clone(), url, integrity);
return response.json();
} catch (error) {
if (error.name === "TimeoutError") {
throw new Error(`Request to ${url} timed out`);
}
throw error;
}
});
}
/**
* Loads the schema either from the object or as fallback from the uri.
* @param typeMetadataFormat
* @returns
*/
loadSchema(typeMetadataFormat) {
return __async(this, null, function* () {
if (typeMetadataFormat.schema) return typeMetadataFormat.schema;
if (typeMetadataFormat.schema_uri) {
const schema = yield this.fetch(
typeMetadataFormat.schema_uri,
typeMetadataFormat["schema_uri#Integrity"]
);
return schema;
}
throw new Error("No schema or schema_uri found");
});
}
/**
* Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
* @param result
* @returns
*/
verifyVct(result) {
return __async(this, null, function* () {
const typeMetadataFormat = yield this.fetchVct(result);
if (typeMetadataFormat.extends) {
}
const schema = yield this.loadSchema(typeMetadataFormat);
const loadedSchemas = /* @__PURE__ */ new Set();
const ajv = new Ajv({
loadSchema: (uri) => __async(this, null, function* () {
if (loadedSchemas.has(uri)) {
return {};
}
const response = yield fetch(uri);
if (!response.ok) {
throw new Error(
`Error fetching schema: ${response.status} ${yield response.text()}`
);
}
loadedSchemas.add(uri);
return response.json();
})
});
addFormats(ajv);
const validate = yield ajv.compileAsync(schema);
const valid = validate(result.payload);
if (!valid) {
throw new SDJWTException(
`Payload does not match the schema: ${JSON.stringify(validate.errors)}`
);
}
return typeMetadataFormat;
});
}
/**
* Fetches VCT Metadata of the SD-JWT-VC. Returns the type metadata format. If the SD-JWT-VC does not contain a vct claim, an error is thrown.
* @param result
* @returns
*/
fetchVct(result) {
return __async(this, null, function* () {
var _a, _b;
if (!result.payload.vct) {
throw new SDJWTException("vct claim is required");
}
if ((_a = result.header) == null ? void 0 : _a.vctm) {
return this.fetchVctFromHeader(result.payload.vct, result);
}
const fetcher = (_b = this.userConfig.vctFetcher) != null ? _b : (uri, integrity) => this.fetch(uri, integrity);
return fetcher(result.payload.vct, result.payload["vct#Integrity"]);
});
}
/**
* Fetches VCT Metadata from the header of the SD-JWT-VC. Returns the type metadata format. If the SD-JWT-VC does not contain a vct claim, an error is thrown.
* @param result
* @param
*/
fetchVctFromHeader(vct, result) {
return __async(this, null, function* () {
var _a;
const vctmHeader = (_a = result.header) == null ? void 0 : _a.vctm;
if (!vctmHeader || !Array.isArray(vctmHeader)) {
throw new Error("vctm claim in SD JWT header is invalid");
}
const typeMetadataFormat = vctmHeader.map((vctm) => {
if (!(typeof vctm === "string")) {
throw new Error("vctm claim in SD JWT header is invalid");
}
return JSON.parse(base64urlDecode(vctm));
}).find((typeMetadataFormat2) => {
return typeMetadataFormat2.vct === vct;
});
if (!typeMetadataFormat) {
throw new Error("could not find VCT Metadata in JWT header");
}
return typeMetadataFormat;
});
}
/**
* Verifies the status of the SD-JWT-VC.
* @param result
* @param options
*/
verifyStatus(result, options) {
return __async(this, null, function* () {
var _a, _b, _c, _d;
if (result.payload.status) {
if (result.payload.status.status_list) {
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
const statusListJWT = yield fetcher(
result.payload.status.status_list.uri
);
const slJWT = Jwt.fromEncode(statusListJWT);
yield slJWT.verify(this.userConfig.verifier, options);
const currentDate = (_b = options == null ? void 0 : options.currentDate) != null ? _b : Math.floor(Date.now() / 1e3);
if (((_c = slJWT.payload) == null ? void 0 : _c.exp) && slJWT.payload.exp < currentDate) {
throw new SDJWTException("Status list is expired");
}
const statusList = getListFromStatusListJWT(statusListJWT);
const status = statusList.getStatus(
result.payload.status.status_list.idx
);
const statusValidator = (_d = this.userConfig.statusValidator) != null ? _d : this.statusValidator.bind(this);
yield statusValidator(status);
}
}
});
}
};
export {
SDJwtVcInstance
};