@credo-ts/core
Version:
<p align="center"> <br /> <img alt="Credo Logo" src="https://github.com/openwallet-foundation/credo-ts/blob/c7886cb8377ceb8ee4efe8d264211e561a75072d/images/credo-logo.png" height="250px" /> </p> <h1 align="center"><b>Credo - Core</b></h1
420 lines (416 loc) • 19.5 kB
JavaScript
import { CredoError } from "../../error/CredoError.mjs";
import "../../error/index.mjs";
import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs";
import { Hasher } from "../../crypto/hashes/Hasher.mjs";
import { IntegrityVerifier } from "../../utils/IntegrityVerifier.mjs";
import { dateToSeconds, nowInSeconds } from "../../utils/timestamp.mjs";
import "../../utils/index.mjs";
import { JwtPayload } from "../../crypto/jose/jwt/JwtPayload.mjs";
import { injectable } from "../../plugins/index.mjs";
import { __decorateMetadata } from "../../_virtual/_@oxc-project_runtime@0.112.0/helpers/decorateMetadata.mjs";
import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.112.0/helpers/decorate.mjs";
import "../../agent/index.mjs";
import { KeyManagementApi } from "../kms/KeyManagementApi.mjs";
import "../kms/index.mjs";
import { X509Certificate } from "../x509/X509Certificate.mjs";
import { X509ModuleConfig } from "../x509/X509ModuleConfig.mjs";
import { X509Service } from "../x509/X509Service.mjs";
import "../x509/index.mjs";
import { parseDid } from "../dids/domain/parse.mjs";
import { getPublicJwkFromVerificationMethod } from "../dids/domain/key-type/keyDidMapping.mjs";
import "../dids/index.mjs";
import { ClaimFormat } from "../vc/models/ClaimFormat.mjs";
import "../../crypto/index.mjs";
import { extractKeyFromHolderBinding, getSdJwtSigner, getSdJwtVerifier, parseHolderBindingFromCredential, resolveDidUrl, resolveSigningPublicJwkFromDidUrl } from "./utils.mjs";
import "../vc/index.mjs";
import { decodeSdJwtVc, sdJwtVcHasher } from "./decodeSdJwtVc.mjs";
import { SdJwtVcRepository } from "./repository/SdJwtVcRepository.mjs";
import "./repository/index.mjs";
import { getDomainFromUrl } from "../../utils/domain.mjs";
import { fetchWithTimeout } from "../../utils/fetch.mjs";
import { buildDisclosureFrameForPayload } from "./disclosureFrame.mjs";
import { SdJwtVcError } from "./SdJwtVcError.mjs";
import { SdJwtVcModuleConfig } from "./SdJwtVcModuleConfig.mjs";
import { decodeSdJwtSync } from "@sd-jwt/decode";
import { selectDisclosures } from "@sd-jwt/present";
import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc";
//#region src/modules/sd-jwt-vc/SdJwtVcService.ts
var _ref;
let SdJwtVcService = class SdJwtVcService {
constructor(sdJwtVcRepository) {
this.sdJwtVcRepository = sdJwtVcRepository;
}
async sign(agentContext, options) {
const { payload, disclosureFrame, hashingAlgorithm } = options;
if (hashingAlgorithm && hashingAlgorithm !== "sha-256") throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`);
const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer, true);
const holderBinding = options.holder ? await extractKeyFromHolderBinding(agentContext, options.holder) : void 0;
const header = {
alg: issuer.alg,
typ: options.headerType ?? "dc+sd-jwt",
kid: issuer.kid,
x5c: issuer.x5c?.map((cert) => cert.toString("base64"))
};
const sdJwt = new SDJwtVcInstance({
...this.getBaseSdJwtConfig(agentContext),
signer: getSdJwtSigner(agentContext, issuer.publicJwk),
hashAlg: "sha-256",
signAlg: issuer.alg
});
if (!payload.vct || typeof payload.vct !== "string") throw new SdJwtVcError("Missing required parameter 'vct'");
const compact = await sdJwt.issue({
...payload,
cnf: holderBinding?.cnf,
iss: issuer.iss,
iat: nowInSeconds(),
vct: payload.vct
}, disclosureFrame, { header });
const prettyClaims = await sdJwt.getClaims(compact);
const sdJwtPayload = (await sdJwt.decode(compact)).jwt?.payload;
if (!sdJwtPayload) throw new SdJwtVcError("Invalid sd-jwt-vc state.");
return {
compact,
prettyClaims,
header,
holder: options.holder,
payload: sdJwtPayload,
claimFormat: ClaimFormat.SdJwtDc,
encoded: compact
};
}
fromCompact(compactSdJwtVc, typeMetadata) {
return decodeSdJwtVc(compactSdJwtVc, typeMetadata);
}
applyDisclosuresForPayload(compactSdJwtVc, requestedPayload) {
const decoded = decodeSdJwtSync(compactSdJwtVc, Hasher.hash);
const presentationFrame = buildDisclosureFrameForPayload(requestedPayload) ?? {};
if (decoded.kbJwt) throw new SdJwtVcError("Cannot apply limit disclosure on an sd-jwt with key binding jwt");
const requiredDisclosures = selectDisclosures(decoded.jwt.payload, decoded.disclosures.map((d) => ({
digest: d.digestSync({
alg: "sha-256",
hasher: Hasher.hash
}),
encoded: d.encode(),
key: d.key,
salt: d.salt,
value: d.value
})), presentationFrame);
const [jwt] = compactSdJwtVc.split("~");
return decodeSdJwtVc(`${jwt}~${requiredDisclosures.length > 0 ? `${requiredDisclosures.map((d) => d.encoded).join("~")}~` : ""}`);
}
async present(agentContext, { sdJwtVc, presentationFrame, verifierMetadata, additionalPayload }) {
const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext));
const compactSdJwtVc = typeof sdJwtVc === "string" ? sdJwtVc : sdJwtVc.compact;
const holderBinding = parseHolderBindingFromCredential((await sdjwt.decode(compactSdJwtVc)).jwt?.payload);
if (!holderBinding && verifierMetadata) throw new SdJwtVcError("Verifier metadata provided, but credential has no 'cnf' claim to create a KB-JWT from");
const holder = holderBinding ? await extractKeyFromHolderBinding(agentContext, holderBinding, {
forSigning: true,
jwkKeyId: typeof sdJwtVc !== "string" ? sdJwtVc.kmsKeyId : void 0
}) : void 0;
sdjwt.config({
kbSigner: holder ? getSdJwtSigner(agentContext, holder.publicJwk) : void 0,
kbSignAlg: holder?.alg
});
return await sdjwt.present(compactSdJwtVc, presentationFrame, { kb: verifierMetadata ? { payload: {
iat: verifierMetadata.issuedAt,
nonce: verifierMetadata.nonce,
aud: verifierMetadata.audience,
...additionalPayload
} } : void 0 });
}
assertValidX5cJwtIssuer(agentContext, iss, leafCertificate) {
if (!iss) return;
if (!iss.startsWith("https://") && !(iss.startsWith("http://") && agentContext.config.allowInsecureHttpUrls)) throw new SdJwtVcError("The X509 certificate issuer must be a HTTPS URI.");
if (!leafCertificate.sanUriNames?.includes(iss) && !leafCertificate.sanDnsNames?.includes(getDomainFromUrl(iss))) throw new SdJwtVcError(`The 'iss' claim in the payload does not match a 'SAN-URI' name and the domain extracted from the HTTPS URI does not match a 'SAN-DNS' name in the x5c certificate. Either remove the 'iss' claim or make it match with at least one SAN-URI or DNS-URI entry`);
}
async verify(agentContext, { compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata, trustedCertificates, now }) {
const sdjwt = new SDJwtVcInstance({
...this.getBaseSdJwtConfig(agentContext),
loadTypeMetadataFormat: false
});
let sdJwtVc;
let holderBinding;
try {
sdJwtVc = await sdjwt.decode(compactSdJwtVc);
if (!sdJwtVc.jwt) throw new CredoError("Invalid sd-jwt-vc");
holderBinding = parseHolderBindingFromCredential(sdJwtVc.jwt.payload) ?? void 0;
} catch (error) {
return {
isValid: false,
error
};
}
const returnSdJwtVc = {
payload: sdJwtVc.jwt.payload,
header: sdJwtVc.jwt.header,
compact: compactSdJwtVc,
prettyClaims: await sdJwtVc.getClaims(sdJwtVcHasher),
holder: holderBinding,
kbJwt: sdJwtVc.kbJwt ? {
payload: sdJwtVc.kbJwt.payload,
header: sdJwtVc.kbJwt.header
} : void 0,
claimFormat: ClaimFormat.SdJwtDc,
encoded: compactSdJwtVc
};
let typeMetadataChain;
try {
const credentialIssuer = await this.parseIssuerFromCredential(agentContext, sdJwtVc, returnSdJwtVc, trustedCertificates);
const issuer = await this.extractKeyFromIssuer(agentContext, credentialIssuer);
const holder = returnSdJwtVc.holder ? await extractKeyFromHolderBinding(agentContext, returnSdJwtVc.holder) : void 0;
sdjwt.config({
verifier: getSdJwtVerifier(agentContext, issuer.publicJwk),
kbVerifier: holder ? getSdJwtVerifier(agentContext, holder.publicJwk) : void 0
});
try {
await sdjwt.verify(compactSdJwtVc, {
requiredClaimKeys: requiredClaimKeys ? [...requiredClaimKeys, "vct"] : ["vct"],
keyBindingNonce: keyBinding?.nonce,
currentDate: dateToSeconds(now ?? /* @__PURE__ */ new Date()),
skewSeconds: agentContext.config.validitySkewSeconds
});
} catch (error) {
return {
error,
isValid: false,
sdJwtVc: returnSdJwtVc
};
}
if (sdJwtVc.jwt.header?.typ !== "vc+sd-jwt" && sdJwtVc.jwt.header?.typ !== "dc+sd-jwt") return {
error: new SdJwtVcError(`SD-JWT VC header 'typ' must be 'dc+sd-jwt' or 'vc+sd-jwt'`),
isValid: false,
sdJwtVc: returnSdJwtVc
};
try {
JwtPayload.fromJson(returnSdJwtVc.payload).validate({
now: dateToSeconds(now ?? /* @__PURE__ */ new Date()),
skewSeconds: agentContext.config.validitySkewSeconds
});
} catch (error) {
return {
error,
isValid: false,
sdJwtVc: returnSdJwtVc
};
}
try {
if (keyBinding) {
if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) throw new SdJwtVcError("Keybinding is required for verification of the sd-jwt-vc");
if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) throw new SdJwtVcError("The key binding JWT does not contain the expected audience");
if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) throw new SdJwtVcError("The key binding JWT does not contain the expected nonce");
}
} catch (error) {
return {
error,
isValid: false,
sdJwtVc: returnSdJwtVc
};
}
if (fetchTypeMetadata) {
const resolvedTypeMetadata = await this.fetchTypeMetadata(agentContext, returnSdJwtVc, {
throwErrorOnFetchError: false,
throwErrorOnUnsupportedVctValue: false
});
returnSdJwtVc.typeMetadata = resolvedTypeMetadata?.mergedTypeMetadata;
typeMetadataChain = resolvedTypeMetadata?.typeMetadataChain;
}
} catch (error) {
return {
isValid: false,
error,
sdJwtVc: returnSdJwtVc
};
}
return {
isValid: true,
sdJwtVc: returnSdJwtVc,
typeMetadataChain
};
}
/**
* Resolve the type metadata for an SD-JWT instance.
*
* This method resolves the `vct` value, and any `extends` values.
*/
async fetchTypeMetadata(agentContext, sdJwtVc, { throwErrorOnFetchError = true, throwErrorOnUnsupportedVctValue = true } = {}) {
return await new SDJwtVcInstance({
...this.getBaseSdJwtConfig(agentContext),
vctFetcher: this.getVctFetcher(agentContext, {
baseVct: sdJwtVc.payload.vct,
throwErrorOnFetchError,
throwErrorOnUnsupportedVctValue
})
}).getVct(sdJwtVc.compact);
}
async store(agentContext, options) {
await this.sdJwtVcRepository.save(agentContext, options.record);
return options.record;
}
async getById(agentContext, id) {
return await this.sdJwtVcRepository.getById(agentContext, id);
}
async getAll(agentContext) {
return await this.sdJwtVcRepository.getAll(agentContext);
}
async findByQuery(agentContext, query, queryOptions) {
return await this.sdJwtVcRepository.findByQuery(agentContext, query, queryOptions);
}
async deleteById(agentContext, id) {
await this.sdJwtVcRepository.deleteById(agentContext, id);
}
async update(agentContext, sdJwtVcRecord) {
await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord);
}
async extractKeyFromIssuer(agentContext, issuer, forSigning = false) {
if (issuer.method === "did") {
const parsedDid = parseDid(issuer.didUrl);
if (!parsedDid.fragment) throw new SdJwtVcError(`didUrl '${issuer.didUrl}' does not contain a '#'. Unable to derive key from did document`);
let publicJwk;
if (forSigning) publicJwk = await resolveSigningPublicJwkFromDidUrl(agentContext, issuer.didUrl);
else {
const { verificationMethod } = await resolveDidUrl(agentContext, issuer.didUrl);
publicJwk = getPublicJwkFromVerificationMethod(verificationMethod);
}
const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms;
if (supportedSignatureAlgorithms.length === 0) throw new SdJwtVcError(`No supported JWA signature algorithms found for key ${publicJwk.jwkTypeHumanDescription}`);
return {
alg: supportedSignatureAlgorithms[0],
publicJwk,
iss: parsedDid.did,
kid: `#${parsedDid.fragment}`
};
}
if (issuer.method === "x5c") {
const leafCertificate = issuer.x5c[0];
if (!leafCertificate) throw new SdJwtVcError("Empty 'x5c' array provided");
if (forSigning && !leafCertificate.publicJwk.hasKeyId) throw new SdJwtVcError("Expected leaf certificate in 'x5c' array to have a key id configured.");
const publicJwk = leafCertificate.publicJwk;
const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms;
if (supportedSignatureAlgorithms.length === 0) throw new SdJwtVcError(`No supported JWA signature algorithms found for key ${publicJwk.jwkTypeHumanDescription}`);
const alg = supportedSignatureAlgorithms[0];
this.assertValidX5cJwtIssuer(agentContext, issuer.issuer, leafCertificate);
return {
publicJwk,
iss: issuer.issuer,
x5c: issuer.x5c,
alg
};
}
throw new SdJwtVcError("Unsupported credential issuer. Only 'did' and 'x5c' is supported at the moment.");
}
async parseIssuerFromCredential(agentContext, sdJwtVc, credoSdJwtVc, _trustedCertificates) {
const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig);
if (!sdJwtVc.jwt?.payload) throw new SdJwtVcError("Credential not exist");
const iss = sdJwtVc.jwt.payload.iss;
if (sdJwtVc.jwt.header?.x5c) {
if (!Array.isArray(sdJwtVc.jwt.header.x5c)) throw new SdJwtVcError("Invalid x5c header in credential. Not an array.");
if (sdJwtVc.jwt.header.x5c.length === 0) throw new SdJwtVcError("Invalid x5c header in credential. Empty array.");
if (sdJwtVc.jwt.header.x5c.some((x5c) => typeof x5c !== "string")) throw new SdJwtVcError("Invalid x5c header in credential. Not an array of strings.");
let trustedCertificates = _trustedCertificates;
const certificateChain = sdJwtVc.jwt.header.x5c.map((cert) => X509Certificate.fromEncodedCertificate(cert));
if (!trustedCertificates) trustedCertificates = await x509Config.getTrustedCertificatesForVerification?.(agentContext, {
certificateChain,
verification: {
type: "credential",
credential: credoSdJwtVc
}
}) ?? x509Config.trustedCertificates;
if (!trustedCertificates) throw new SdJwtVcError("No trusted certificates configured for X509 certificate chain validation. Issuer cannot be verified.");
await X509Service.validateCertificateChain(agentContext, {
certificateChain: sdJwtVc.jwt.header.x5c,
trustedCertificates
});
return {
method: "x5c",
x5c: certificateChain,
issuer: iss
};
}
if (iss?.startsWith("did:")) {
if (!sdJwtVc.jwt?.header) throw new SdJwtVcError("Credential does not contain a header");
if (!sdJwtVc.jwt.header.kid) throw new SdJwtVcError("Credential does not contain a kid in the header");
const issuerKid = sdJwtVc.jwt.header.kid;
let didUrl;
if (issuerKid.startsWith("#")) didUrl = `${iss}${issuerKid}`;
else if (issuerKid.startsWith("did:")) {
const didFromKid = parseDid(issuerKid);
if (didFromKid.did !== iss) throw new SdJwtVcError(`kid in header is an absolute DID URL, but the did (${didFromKid.did}) does not match with the 'iss' did (${iss})`);
didUrl = issuerKid;
} else throw new SdJwtVcError("Invalid issuer kid for did. Only absolute or relative (starting with #) did urls are supported.");
return {
method: "did",
didUrl
};
}
throw new SdJwtVcError("Unsupported signing method for SD-JWT VC. Only did and x5c are supported at the moment.");
}
getBaseSdJwtConfig(agentContext) {
const kms = agentContext.resolve(KeyManagementApi);
return {
hasher: sdJwtVcHasher,
statusListFetcher: this.getStatusListFetcher(agentContext),
saltGenerator: (length) => TypedArrayEncoder.toBase64Url(kms.randomBytes({ length })).slice(0, length)
};
}
getStatusListFetcher(agentContext) {
return async (uri) => {
const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri, { headers: { Accept: "application/statuslist+jwt" } });
if (!response.ok) throw new CredoError(`Received invalid response with status ${response.status} when fetching status list from ${uri}. ${await response.text()}`);
return await response.text();
};
}
getVctFetcher(agentContext, { baseVct, throwErrorOnFetchError = true, throwErrorOnUnsupportedVctValue = true }) {
const sdJwtVcConfig = agentContext.dependencyManager.resolve(SdJwtVcModuleConfig);
return async (uri, integrity) => {
if (sdJwtVcConfig.customTypeMetadataResolver) return await sdJwtVcConfig.customTypeMetadataResolver(uri, integrity, {
isExtendedVct: uri !== baseVct,
defaultResolver: (userOverrideOptions) => this.httpsTypeMetadataResolver(agentContext, uri, integrity, {
throwErrorOnFetchError: userOverrideOptions.throwErrorOnFetchError ?? throwErrorOnFetchError,
throwErrorOnUnsupportedVctValue: userOverrideOptions.throwErrorOnUnsupportedVctValue ?? throwErrorOnUnsupportedVctValue,
isExtendedVct: uri !== baseVct
})
});
if (!uri.startsWith("https://")) {
if (!throwErrorOnUnsupportedVctValue) return void 0;
throw new SdJwtVcError(`Unable to resolve type metadata for vct '${uri}'. Only https supported and no 'customTypeMetadataResolver' provided in agent config.`);
}
return await this.httpsTypeMetadataResolver(agentContext, uri, integrity, {
throwErrorOnFetchError,
throwErrorOnUnsupportedVctValue,
isExtendedVct: uri !== baseVct
});
};
}
async httpsTypeMetadataResolver(agentContext, vct, integrity, { throwErrorOnFetchError, throwErrorOnUnsupportedVctValue, isExtendedVct }) {
if (!vct.startsWith("https://")) {
if (!throwErrorOnUnsupportedVctValue) return void 0;
throw new SdJwtVcError(`Unable to resolve type metadata for vct '${vct}'. Only https supported for default resolver`);
}
let firstError;
const firstResponse = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, vct).catch((error) => {
firstError = error;
});
let response = firstResponse;
if (!response || !response?.ok) {
const vctElements = vct.split("/");
vctElements.splice(3, 0, ".well-known/vct");
const legacyVctUrl = vctElements.join("/");
response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, legacyVctUrl).catch(() => void 0);
}
if (!response?.ok) {
if (!throwErrorOnFetchError) return void 0;
if (firstResponse) throw new SdJwtVcError(`Unable to resolve type metadata vct '${vct}'. Fetch returned a non-successful ${firstResponse.status} response. ${await firstResponse.text()}.`, { cause: firstError });
else throw new SdJwtVcError(`Unable to resolve type metadata vct '${vct}'. Fetch returned a non-successful response.`, { cause: firstError });
}
const typeMetadata = await response.clone().json();
if (integrity) {
if (typeof integrity !== "string") throw new SdJwtVcError(`Found '${isExtendedVct ? "extends" : "vct"}#integrity' with value '${integrity}' but value was not of type 'string'.`);
IntegrityVerifier.verifyIntegrity(new Uint8Array(await response.arrayBuffer()), integrity);
}
return typeMetadata;
}
};
SdJwtVcService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof (_ref = typeof SdJwtVcRepository !== "undefined" && SdJwtVcRepository) === "function" ? _ref : Object])], SdJwtVcService);
//#endregion
export { SdJwtVcService };
//# sourceMappingURL=SdJwtVcService.mjs.map