UNPKG

@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

307 lines (306 loc) • 15.6 kB
import { CredoError } from "../../../error/CredoError.mjs"; import "../../../error/index.mjs"; import { asArray } from "../../../utils/array.mjs"; import { isDid } from "../../../utils/did.mjs"; import { MessageValidator } from "../../../utils/MessageValidator.mjs"; import "../../../utils/index.mjs"; import { injectable } from "../../../plugins/index.mjs"; import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.mjs"; import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.mjs"; import { PublicJwk } from "../../kms/jwk/PublicJwk.mjs"; import "../../kms/index.mjs"; import { JwsService } from "../../../crypto/JwsService.mjs"; import { parseDid } from "../../dids/domain/parse.mjs"; import { getPublicJwkFromVerificationMethod, getSupportedVerificationMethodTypesForPublicJwk } from "../../dids/domain/key-type/keyDidMapping.mjs"; import { DidResolverService } from "../../dids/services/DidResolverService.mjs"; import { DidsApi } from "../../dids/DidsApi.mjs"; import "../../dids/index.mjs"; import { TrustedIssuerContext } from "../../../agent/TrustedIssuerContext.mjs"; import { W3cJsonLdVerifiableCredential } from "../linked-data-proofs/models/W3cJsonLdVerifiableCredential.mjs"; import { validateCredentialSubjectAuthentication } from "../util.mjs"; import { getJwtPayloadFromCredential } from "./credentialTransformer.mjs"; import { W3cJwtVerifiableCredential } from "./W3cJwtVerifiableCredential.mjs"; import { getJwtPayloadFromPresentation } from "./presentationTransformer.mjs"; import { W3cJwtVerifiablePresentation } from "./W3cJwtVerifiablePresentation.mjs"; //#region src/modules/vc/jwt-vc/W3cJwtCredentialService.ts let W3cJwtCredentialService = class W3cJwtCredentialService { constructor(jwsService) { this.jwsService = jwsService; } /** * Signs a credential */ async signCredential(agentContext, options) { MessageValidator.validateSync(options.credential); const jwtPayload = getJwtPayloadFromCredential(options.credential); if (!isDid(options.verificationMethod)) throw new CredoError("Only did identifiers are supported as verification method"); const publicJwk = await this.resolveVerificationMethod(agentContext, options.verificationMethod, ["assertionMethod"]); const jwt = await this.jwsService.createJwsCompact(agentContext, { payload: jwtPayload, keyId: publicJwk.keyId, protectedHeaderOptions: { typ: "JWT", alg: options.alg, kid: options.verificationMethod } }); return W3cJwtVerifiableCredential.fromSerializedJwt(jwt); } /** * Verifies the signature(s) of a credential * * @param credential the credential to be verified * @returns the verification result */ async verifyCredential(agentContext, options) { const verifyCredentialStatus = options.verifyCredentialStatus ?? true; const validationResults = { isValid: false, validations: {} }; try { let credential; try { if (options.credential instanceof W3cJwtVerifiableCredential) MessageValidator.validateSync(options.credential.credential); credential = options.credential instanceof W3cJwtVerifiableCredential ? options.credential : W3cJwtVerifiableCredential.fromSerializedJwt(options.credential); credential.jwt.payload.validate({ skewSeconds: agentContext.config.validitySkewSeconds }); validationResults.validations.dataModel = { isValid: true }; } catch (error) { validationResults.validations.dataModel = { isValid: false, error }; return validationResults; } const issuerVerificationMethod = await this.getVerificationMethodForJwtCredential(agentContext, { credential, purpose: ["assertionMethod"] }); const issuerPublicKey = getPublicJwkFromVerificationMethod(issuerVerificationMethod); await TrustedIssuerContext.ensureTrustedSigner(agentContext, { signer: { method: "did", didUrl: issuerVerificationMethod.id }, verification: { type: "credential", credential } }, options.trustedIssuers); let signatureResult; try { signatureResult = await this.jwsService.verifyJws(agentContext, { jws: credential.jwt.serializedJwt, jwsSigner: { method: "did", jwk: issuerPublicKey, didUrl: issuerVerificationMethod.id } }); if (!signatureResult.isValid) validationResults.validations.signature = { isValid: false, error: new CredoError("Invalid JWS signature") }; else validationResults.validations.signature = { isValid: true }; } catch (error) { validationResults.validations.signature = { isValid: false, error }; } if (credential.issuerId !== issuerVerificationMethod.controller) validationResults.validations.issuerIsSigner = { isValid: false, error: new CredoError(`Credential is signed using verification method ${issuerVerificationMethod.id}, while the issuer of the credential is '${credential.issuerId}'`) }; else validationResults.validations.issuerIsSigner = { isValid: true }; if (!signatureResult?.jwsSigners.some((jwsSigner) => jwsSigner.jwk.fingerprint === issuerPublicKey.fingerprint)) validationResults.validations.issuerIsSigner = { isValid: false, error: new CredoError("Credential is not signed by the issuer of the credential") }; else validationResults.validations.issuerIsSigner = { isValid: true }; if (verifyCredentialStatus && !credential.credentialStatus) validationResults.validations.credentialStatus = { isValid: true }; else if (verifyCredentialStatus && credential.credentialStatus) validationResults.validations.credentialStatus = { isValid: false, error: new CredoError("Verifying credential status is not supported for JWT VCs") }; validationResults.isValid = Object.values(validationResults.validations).every((v) => v.isValid); return validationResults; } catch (error) { validationResults.error = error; return validationResults; } } /** * Signs a presentation including the credentials it includes * * @param presentation the presentation to be signed * @returns the signed presentation */ async signPresentation(agentContext, options) { MessageValidator.validateSync(options.presentation); const jwtPayload = getJwtPayloadFromPresentation(options.presentation); jwtPayload.additionalClaims.nonce = options.challenge; jwtPayload.aud = options.domain; const publicJwk = await this.resolveVerificationMethod(agentContext, options.verificationMethod, ["authentication"]); const jwt = await this.jwsService.createJwsCompact(agentContext, { payload: jwtPayload, keyId: publicJwk.keyId, protectedHeaderOptions: { typ: "JWT", alg: options.alg, kid: options.verificationMethod } }); return W3cJwtVerifiablePresentation.fromSerializedJwt(jwt); } /** * Verifies a presentation including the credentials it includes * * @param presentation the presentation to be verified * @returns the verification result */ async verifyPresentation(agentContext, options) { const validationResults = { isValid: false, validations: {} }; try { let presentation; try { if (options.presentation instanceof W3cJwtVerifiablePresentation) MessageValidator.validateSync(options.presentation.presentation); presentation = options.presentation instanceof W3cJwtVerifiablePresentation ? options.presentation : W3cJwtVerifiablePresentation.fromSerializedJwt(options.presentation); presentation.jwt.payload.validate({ skewSeconds: agentContext.config.validitySkewSeconds }); if (options.challenge !== presentation.jwt.payload.additionalClaims.nonce) throw new CredoError(`JWT payload 'nonce' does not match challenge '${options.challenge}'`); const audArray = asArray(presentation.jwt.payload.aud); if (options.domain && !audArray.includes(options.domain)) throw new CredoError(`JWT payload 'aud' does not include domain '${options.domain}'`); validationResults.validations.dataModel = { isValid: true }; } catch (error) { validationResults.validations.dataModel = { isValid: false, error }; return validationResults; } const proverVerificationMethod = await this.getVerificationMethodForJwtCredential(agentContext, { credential: presentation, purpose: ["authentication"] }); const proverPublicKey = getPublicJwkFromVerificationMethod(proverVerificationMethod); let signatureResult; try { signatureResult = await this.jwsService.verifyJws(agentContext, { jws: presentation.jwt.serializedJwt, allowedJwsSignerMethods: ["did"], jwsSigner: { method: "did", didUrl: proverVerificationMethod.id, jwk: proverPublicKey }, trustedCertificates: [] }); if (!signatureResult.isValid) validationResults.validations.presentationSignature = { isValid: false, error: new CredoError("Invalid JWS signature on presentation") }; else validationResults.validations.presentationSignature = { isValid: true }; } catch (error) { validationResults.validations.presentationSignature = { isValid: false, error }; } if (presentation.holderId && proverVerificationMethod.controller !== presentation.holderId) validationResults.validations.holderIsSigner = { isValid: false, error: new CredoError(`Presentation is signed using verification method ${proverVerificationMethod.id}, while the holder of the presentation is '${presentation.holderId}'`) }; else validationResults.validations.holderIsSigner = { isValid: true }; const credentials = asArray(presentation.presentation.verifiableCredential); validationResults.validations.credentials = await Promise.all(credentials.map(async (credential) => { if (credential instanceof W3cJsonLdVerifiableCredential) return { isValid: false, error: new CredoError("Credential is of format ldp_vc. presentations in jwt_vp format can only contain credentials in jwt_vc format"), validations: {} }; const credentialResult = await this.verifyCredential(agentContext, { credential, verifyCredentialStatus: options.verifyCredentialStatus, trustedIssuers: options.trustedIssuers }); const credentialSubjectAuthentication = validateCredentialSubjectAuthentication(credential.credentialSubjectIds, proverVerificationMethod.controller); return { ...credentialResult, isValid: credentialResult.isValid && credentialSubjectAuthentication.isValid, validations: { ...credentialResult.validations, credentialSubjectAuthentication } }; })); validationResults.isValid = Object.values(validationResults.validations).every((v) => Array.isArray(v) ? v.every((vv) => vv.isValid) : v.isValid); return validationResults; } catch (error) { validationResults.error = error; return validationResults; } } async resolveVerificationMethod(agentContext, verificationMethod, allowsPurposes) { const dids = agentContext.resolve(DidsApi); const parsedDid = parseDid(verificationMethod); const { didDocument, keys } = await dids.resolveCreatedDidDocumentWithKeys(parsedDid.did); const verificationMethodObject = didDocument.dereferenceKey(verificationMethod, allowsPurposes); const publicJwk = getPublicJwkFromVerificationMethod(verificationMethodObject); publicJwk.keyId = keys?.find(({ didDocumentRelativeKeyId }) => verificationMethodObject.id.endsWith(didDocumentRelativeKeyId))?.kmsKeyId ?? publicJwk.legacyKeyId; return publicJwk; } /** * This method tries to find the verification method associated with the JWT credential or presentation. * This verification method can then be used to verify the credential or presentation signature. * * The following methods are used to extract the verification method: * - verification method is resolved based on the `kid` in the protected header * - either as absolute reference (e.g. `did:example:123#key-1`) * - or as relative reference to the `iss` of the JWT (e.g. `iss` is `did:example:123` and `kid` is `#key-1`) * - the did document is resolved based on the `iss` field, after which the verification method is extracted based on the `alg` * used to sign the JWT and the specified `purpose`. Only a single verification method may be present, and in all other cases, * an error is thrown. * * The signer (`iss`) of the JWT is verified against the `controller` of the verificationMethod resolved in the did * document. This means if the `iss` of a credential is `did:example:123` and the controller of the verificationMethod * is `did:example:456`, an error is thrown to prevent the JWT from successfully being verified. * * In addition the JWT must conform to one of the following rules: * - MUST be a credential and have an `iss` field and MAY have an absolute or relative `kid` * - MUST not be a credential AND ONE of the following: * - have an `iss` field and MAY have an absolute or relative `kid` * - does not have an `iss` field and MUST have an absolute `kid` */ async getVerificationMethodForJwtCredential(agentContext, options) { const { credential, purpose } = options; const kid = credential.jwt.header.kid; const didResolver = agentContext.dependencyManager.resolve(DidResolverService); const signerId = credential.jwt.payload.iss; let verificationMethod; if (kid?.startsWith("#")) { if (!signerId) throw new CredoError(`JWT 'kid' MUST be absolute when when no 'iss' is present in JWT payload`); verificationMethod = (await didResolver.resolveDidDocument(agentContext, signerId)).dereferenceKey(`${signerId}${kid}`, purpose); } else if (kid && isDid(kid)) { const didDocument = await didResolver.resolveDidDocument(agentContext, kid); verificationMethod = didDocument.dereferenceKey(kid, purpose); if (signerId && didDocument.id !== signerId) throw new CredoError(`kid '${kid}' does not match id of signer (holder/issuer) '${signerId}'`); } else { if (!signerId) throw new CredoError(`JWT 'iss' MUST be present in payload when no 'kid' is specified`); const jwkClass = PublicJwk.supportedPublicJwkClassForSignatureAlgorithm(credential.jwt.header.alg); const supportedVerificationMethodTypes = getSupportedVerificationMethodTypesForPublicJwk(jwkClass); const verificationMethods = (await didResolver.resolveDidDocument(agentContext, signerId)).findVerificationMethodsByTypeAndPurpose(supportedVerificationMethodTypes, ["assertionMethod"]); if (verificationMethods.length === 0) throw new CredoError(`No verification methods found for signer '${signerId}' and key type '${jwkClass.name}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.`); if (verificationMethods.length > 1) throw new CredoError(`Multiple verification methods found for signer '${signerId}' and key type '${jwkClass.name}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.`); verificationMethod = verificationMethods[0]; } if (signerId && verificationMethod.controller !== signerId) throw new CredoError(`Verification method controller '${verificationMethod.controller}' does not match the signer '${signerId}'`); return verificationMethod; } }; W3cJwtCredentialService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof JwsService === "undefined" ? Object : JwsService])], W3cJwtCredentialService); //#endregion export { W3cJwtCredentialService }; //# sourceMappingURL=W3cJwtCredentialService.mjs.map