@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
222 lines (221 loc) • 9.8 kB
JavaScript
import { CredoError } from "../error/CredoError.mjs";
import "../error/index.mjs";
import { TypedArrayEncoder } from "../utils/TypedArrayEncoder.mjs";
import { JsonEncoder } from "../utils/JsonEncoder.mjs";
import "../utils/index.mjs";
import { JwtPayload } from "./jose/jwt/JwtPayload.mjs";
import { injectable } from "../plugins/index.mjs";
import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.mjs";
import { KeyManagementError } from "../modules/kms/error/KeyManagementError.mjs";
import { isKnownJwaSignatureAlgorithm } from "../modules/kms/jwk/jwa.mjs";
import { getJwkHumanDescription } from "../modules/kms/jwk/humanDescription.mjs";
import { asymmetricPublicJwkMatches } from "../modules/kms/jwk/equals.mjs";
import { assertJwkAsymmetric } from "../modules/kms/jwk/knownJwk.mjs";
import { PublicJwk } from "../modules/kms/jwk/PublicJwk.mjs";
import { KeyManagementApi } from "../modules/kms/KeyManagementApi.mjs";
import "../modules/kms/index.mjs";
import { convertLegacyTrustedCertificates } from "../modules/x509/utils/convertLegacyTrustedCertificates.mjs";
import { X509ModuleConfig } from "../modules/x509/X509ModuleConfig.mjs";
import { X509Service } from "../modules/x509/X509Service.mjs";
import "../modules/x509/X509ServiceOptions.mjs";
import { isJsonObject } from "../types.mjs";
import { JWS_COMPACT_FORMAT_MATCHER } from "./JwsTypes.mjs";
import "./jose/jwt/index.mjs";
//#region src/crypto/JwsService.ts
let JwsService = class JwsService {
async createJwsBase(agentContext, options) {
const { jwk, alg, x5c } = options.protectedHeaderOptions;
const kms = agentContext.dependencyManager.resolve(KeyManagementApi);
const key = await kms.getPublicKey({ keyId: options.keyId });
assertJwkAsymmetric(key);
const publicJwk = PublicJwk.fromPublicJwk(key);
if (x5c) {
const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: x5c });
if (!asymmetricPublicJwkMatches(certificate.publicJwk.toJson(), key)) throw new CredoError("Protected header x5c does not match key for signing.");
}
const jwkInstance = jwk instanceof PublicJwk ? jwk : jwk ? PublicJwk.fromUnknown(jwk) : void 0;
if (jwkInstance && !asymmetricPublicJwkMatches(jwkInstance.toJson(), key)) throw new CredoError("Protected header JWK does not match key for signing.");
if (!publicJwk.supportedSignatureAlgorithms.includes(alg)) throw new CredoError(`alg '${alg}' is not a valid JWA signature algorithm for this jwk with ${publicJwk.jwkTypeHumanDescription}. Supported algorithms are ${publicJwk.supportedSignatureAlgorithms.join(", ")}`);
const payload = options.payload instanceof JwtPayload ? JsonEncoder.toUint8Array(options.payload.toJson()) : options.payload;
const base64Payload = TypedArrayEncoder.toBase64Url(payload);
const base64UrlProtectedHeader = JsonEncoder.toBase64Url(this.buildProtected(options.protectedHeaderOptions));
const signResult = await kms.sign({
algorithm: alg,
data: TypedArrayEncoder.fromUtf8String(`${base64UrlProtectedHeader}.${base64Payload}`),
keyId: options.keyId
});
return {
base64Payload,
base64UrlProtectedHeader,
signature: TypedArrayEncoder.toBase64Url(signResult.signature)
};
}
async createJws(agentContext, { payload, keyId, header, protectedHeaderOptions }) {
const { base64UrlProtectedHeader, signature, base64Payload } = await this.createJwsBase(agentContext, {
payload,
keyId,
protectedHeaderOptions
});
return {
protected: base64UrlProtectedHeader,
signature,
header,
payload: base64Payload
};
}
/**
* @see {@link https://www.rfc-editor.org/rfc/rfc7515#section-3.1}
* */
async createJwsCompact(agentContext, { payload, keyId, protectedHeaderOptions }) {
const { base64Payload, base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, {
payload,
keyId,
protectedHeaderOptions
});
return `${base64UrlProtectedHeader}.${base64Payload}.${signature}`;
}
/**
* Verify a JWS
*/
async verifyJws(agentContext, { jws, resolveJwsSigner, trustedCertificates, jwsSigner: expectedJwsSigner, allowedJwsSignerMethods = [
"did",
"jwk",
"x5c"
] }) {
let signatures = [];
let payload;
if (expectedJwsSigner && !allowedJwsSignerMethods.includes(expectedJwsSigner.method)) throw new CredoError(`jwsSigner provided with method '${expectedJwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(", ")}.`);
if (typeof jws === "string") {
if (!JWS_COMPACT_FORMAT_MATCHER.test(jws)) throw new CredoError(`Invalid JWS compact format for value '${jws}'.`);
const [protectedHeader, _payload, signature] = jws.split(".");
payload = _payload;
signatures.push({
header: {},
protected: protectedHeader,
signature
});
} else if ("signatures" in jws) {
signatures = jws.signatures;
payload = jws.payload;
} else {
signatures.push(jws);
payload = jws.payload;
}
if (signatures.length === 0) throw new CredoError("Unable to verify JWS, no signatures present in JWS.");
const jwsFlattened = {
signatures,
payload
};
const jwsSigners = [];
for (const jws of signatures) {
const protectedJson = JsonEncoder.fromBase64Url(jws.protected);
if (!isJsonObject(protectedJson)) throw new CredoError("Unable to verify JWS, protected header is not a valid JSON object.");
if (!protectedJson.alg || typeof protectedJson.alg !== "string") throw new CredoError("Unable to verify JWS, protected header alg is not provided or not a string.");
const jwsSigner = expectedJwsSigner ?? await this.jwsSignerFromJws(agentContext, {
jws,
payload,
protectedHeader: {
...protectedJson,
alg: protectedJson.alg
},
allowedJwsSignerMethods,
resolveJwsSigner
});
await this.verifyJwsSigner(agentContext, {
jwsSigner,
trustedCertificates
});
if (!jwsSigner.jwk.supportedSignatureAlgorithms.includes(protectedJson.alg)) throw new CredoError(`alg '${protectedJson.alg}' is not a valid JWA signature algorithm for this jwk ${getJwkHumanDescription(jwsSigner.jwk.toJson())}. Supported algorithms are ${jwsSigner.jwk.supportedSignatureAlgorithms.join(", ")}`);
const data = TypedArrayEncoder.fromUtf8String(`${jws.protected}.${payload}`);
const signature = TypedArrayEncoder.fromBase64Url(jws.signature);
jwsSigners.push(jwsSigner);
const kms = agentContext.dependencyManager.resolve(KeyManagementApi);
try {
const { verified } = await kms.verify({
key: { publicJwk: jwsSigner.jwk.toJson() },
data,
signature,
algorithm: protectedJson.alg
});
if (!verified) return {
isValid: false,
jwsSigners: [],
jws: jwsFlattened
};
} catch (error) {
if (error instanceof KeyManagementError) return {
isValid: false,
jwsSigners: [],
jws: jwsFlattened
};
throw error;
}
}
return {
isValid: true,
jwsSigners,
jws: jwsFlattened
};
}
buildProtected(options) {
return {
...options,
alg: options.alg,
jwk: options.jwk instanceof PublicJwk ? options.jwk.toJson() : options.jwk,
kid: options.kid
};
}
async verifyJwsSigner(agentContext, options) {
const { jwsSigner } = options;
const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig);
if (jwsSigner.method === "x5c") {
const trustedCertificatesFromConfig = x509ModuleConfig.trustedCertificates ?? [];
const trustedCertificates = options.trustedCertificates ?? trustedCertificatesFromConfig;
if (trustedCertificates.length === 0) throw new CredoError(`trustedCertificates is required when the JWS protected header contains an 'x5c' property.`);
const convertedTrustedCertificates = convertLegacyTrustedCertificates(trustedCertificates);
await X509Service.validateCertificateChain(agentContext, {
certificateChain: jwsSigner.x5c,
trustedCertificates: convertedTrustedCertificates.flatMap(({ issuance }) => issuance)
});
}
}
async jwsSignerFromJws(agentContext, options) {
const { protectedHeader, resolveJwsSigner, jws, payload, allowedJwsSignerMethods } = options;
const alg = protectedHeader.alg;
if (!isKnownJwaSignatureAlgorithm(alg)) throw new CredoError(`Unsupported JWA signature algorithm '${protectedHeader.alg}'`);
if (protectedHeader.x5c && allowedJwsSignerMethods.includes("x5c")) {
if (!Array.isArray(protectedHeader.x5c) || protectedHeader.x5c.some((certificate) => typeof certificate !== "string")) throw new CredoError("x5c header is not a valid JSON array of strings.");
return {
method: "x5c",
jwk: X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c }).publicJwk,
x5c: protectedHeader.x5c
};
}
if (protectedHeader.jwk && allowedJwsSignerMethods.includes("jwk")) {
if (!isJsonObject(protectedHeader.jwk)) throw new CredoError("JWK is not a valid JSON object.");
return {
method: "jwk",
jwk: PublicJwk.fromUnknown(protectedHeader.jwk)
};
}
if (!resolveJwsSigner) throw new CredoError(`resolveJwsSigner is required for resolving jws signers other than 'jwk' and 'x5c'.`);
try {
const jwsSigner = await resolveJwsSigner({
jws,
protectedHeader: {
...protectedHeader,
alg
},
payload
});
if (!allowedJwsSignerMethods.includes(jwsSigner.method)) throw new CredoError(`resolveJwsSigner returned jws signer with method '${jwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(", ")}.`);
return jwsSigner;
} catch (error) {
throw new CredoError(`Error when resolving jws signer for jws in resolveJwsSigner. ${error.message}`, { cause: error });
}
}
};
JwsService = __decorate([injectable()], JwsService);
//#endregion
export { JwsService };
//# sourceMappingURL=JwsService.mjs.map