@sd-jwt/sd-jwt-vc
Version: 
sd-jwt draft 7 implementation in typescript
326 lines (324 loc) • 11.3 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, _e;
      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(
            (_b = this.userConfig.statusVerifier) != null ? _b : this.userConfig.verifier,
            options
          );
          const currentDate = (_c = options == null ? void 0 : options.currentDate) != null ? _c : Math.floor(Date.now() / 1e3);
          if (((_d = slJWT.payload) == null ? void 0 : _d.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 = (_e = this.userConfig.statusValidator) != null ? _e : this.statusValidator.bind(this);
          yield statusValidator(status);
        }
      }
    });
  }
};
export {
  SDJwtVcInstance
};