import domain from './domain'; import type { ParsedCertificate } from './parsers'; import parseJSON from './parsers/index'; import type { IFinalVerificationStatus, IVerificationStepCallbackFn } from './verifier'; import Verifier from './verifier'; import { DEFAULT_OPTIONS } from './constants'; import { deepCopy } from './helpers/object'; import convertHashlink, { getHashlinksFrom } from './parsers/helpers/convertHashlink'; import type { HashlinkVerifier } from '@blockcerts/hashlink-verifier'; import type { ExplorerAPI, IBlockchainObject } from '@blockcerts/explorer-lookup'; import type { Blockcerts } from './models/Blockcerts'; import type { Issuer } from './models/Issuer'; import type { SignatureImage } from './models'; import type { BlockcertsV3, BlockcertsV3Display } from './models/BlockcertsV3'; import { isVerifiablePresentation } from './models/BlockcertsV3'; import type { IVerificationMapItem } from './models/VerificationMap'; import { VERIFICATION_STATUSES } from './constants/verificationStatuses'; export interface ExplorerURLs { main: string; test: string; } export interface CertificateOptions { // language option locale?: string; // provide your own custom explorer API to either pass an identifier to a default service // or use your own blockchain explorer explorerAPIs?: ExplorerAPI[]; // specify your own DID resolver url didResolverUrl?: string; // allows to define a specific purpose verification for the verifier (authentication, assertionMethod, etc) // https://www.w3.org/TR/vc-data-integrity/#proof-purposes proofPurpose?: string; // restricts the verification to (a) specific domain(s) - useful for authentication, should match the domain property in the proof // https://www.w3.org/TR/vc-data-integrity/#defn-domain domain?: string | string[]; // the property must match the one provided by the proof // https://www.w3.org/TR/vc-data-integrity/#defn-challenge // https://github.com/w3c/vc-data-integrity/issues/324#issuecomment-2521054375 challenge?: string; } export interface Signers { chain?: IBlockchainObject; issuerName?: string; issuerProfileDomain?: string; issuerProfileUrl?: string; issuerPublicKey: string; rawTransactionLink?: string; signatureSuiteType: string; signingDate: string; transactionId?: string; transactionLink?: string; } export default class Certificate { public certificateImage?: string; public certificateJson: Blockcerts; public description?: string; // v1, v3.2 public display?: BlockcertsV3Display; public expires: string; public validFrom: string; public explorerAPIs: ExplorerAPI[] = []; public id: string; public isFormatValid: boolean; public isVerifiablePresentation: boolean; public issuedOn: string; public issuer: Issuer; public locale: string; // enum? public metadataJson: any; // TODO: define metadataJson interface. public name?: string; public options: CertificateOptions; public proofPurpose: string; public proofDomain?: string | string[]; public proofChallenge?: string; public recipientFullName: string; public recordLink: string; public revocationKey: string; public sealImage?: string; // v1 public signature?: string; // v1 public signatureImage?: SignatureImage[]; // v1 public signers: Signers[] = []; public subtitle?: string; // v1 public hashlinkVerifier: HashlinkVerifier; public hasHashlinks: boolean = false; public verifiableCredentials: Certificate[]; public verificationSteps: IVerificationMapItem[]; public verifier: Verifier; public verificationStatus: IFinalVerificationStatus; constructor (certificateDefinition: Blockcerts | string, options: CertificateOptions = {}) { // Options this._setOptions(options); if (typeof certificateDefinition !== 'object') { try { certificateDefinition = JSON.parse(certificateDefinition) as Blockcerts; } catch (err) { throw new Error(domain.i18n.getText('errors', 'certificateNotValid')); } } // Keep certificate JSON object this.certificateJson = deepCopy(certificateDefinition); this.isVerifiablePresentation = isVerifiablePresentation(this.certificateJson as any); if (this.isVerifiablePresentation) { this.verifiableCredentials = (this.certificateJson as any) .verifiableCredential?.map((vc: Blockcerts) => new Certificate(vc, options)) ?? []; } } async init (): Promise { if (this.isVerifiablePresentation) { for (const vc of this.verifiableCredentials) { await vc.init(); } } // Parse certificate if ((this.certificateJson as BlockcertsV3).display?.content) { const hashlinks = getHashlinksFrom((this.certificateJson as BlockcertsV3).display.content); if (hashlinks.length) { await import('@blockcerts/hashlink-verifier').then((hashlinkLib) => { this.hashlinkVerifier = new hashlinkLib.HashlinkVerifier(); }); } } await this.parseJson(this.certificateJson); this.verifier = new Verifier({ certificateJson: this.certificateJson, expires: this.expires, validFrom: this.validFrom, id: this.id, issuer: this.issuer, hashlinkVerifier: this.hashlinkVerifier, revocationKey: this.revocationKey, explorerAPIs: deepCopy(this.explorerAPIs), proofPurpose: this.proofPurpose, proofDomain: this.proofDomain, proofChallenge: this.proofChallenge }); await this.verifier.init(); this.verificationSteps = this.verifier.getVerificationSteps(); } async verify (stepCallback?: IVerificationStepCallbackFn): Promise { let mainDocumentVerificationStatus = await this.verifier.verify(stepCallback); this.setSigners(); if (this.isVerifiablePresentation) { for (const vc of this.verifiableCredentials) { const verificationStatus = await vc.verify(); vc.verificationStatus = verificationStatus; if (verificationStatus.status !== VERIFICATION_STATUSES.SUCCESS) { mainDocumentVerificationStatus = { ...mainDocumentVerificationStatus, status: VERIFICATION_STATUSES.FAILURE, message: `Credential ${vc.name ? vc.name + ' ' : ''}with id ${vc.id} failed verification. Error: ${verificationStatus.message}`, errors: [verificationStatus] }; } } } this.verificationStatus = mainDocumentVerificationStatus; return mainDocumentVerificationStatus; } private async parseJson (certificateDefinition: Blockcerts): Promise { const parsedCertificate: ParsedCertificate = await parseJSON(certificateDefinition, this.locale); if (!parsedCertificate.isFormatValid) { throw new Error(parsedCertificate.error); } await this._setProperties(parsedCertificate); } private setSigners (): void { this.signers = this.verifier.getSignersData(); } private _setOptions (options: CertificateOptions): void { this.options = Object.assign({}, DEFAULT_OPTIONS, options); // Set locale this.locale = domain.i18n.ensureIsSupported(this.options.locale === 'auto' ? domain.i18n.detectLocale() : this.options.locale); this.explorerAPIs = this.options.explorerAPIs ?? []; this.proofPurpose = this.options.proofPurpose; this.proofDomain = this.options.domain; this.proofChallenge = this.options.challenge; if (options.didResolverUrl) { domain.did.didResolver.url = options.didResolverUrl; } domain.i18n.setLocale(this.locale); } private async _setProperties ({ certificateImage, description, display, expires, validFrom, id, isFormatValid, issuedOn, issuer, metadataJson, name, recipientFullName, recordLink, revocationKey, sealImage, signature, signatureImage, subtitle }: ParsedCertificate): Promise { this.isFormatValid = isFormatValid; this.certificateImage = certificateImage; this.description = description; this.expires = expires; this.validFrom = validFrom; this.id = id; this.issuedOn = issuedOn; this.issuer = issuer; this.metadataJson = metadataJson; this.name = name; this.recipientFullName = recipientFullName; this.recordLink = recordLink; this.revocationKey = revocationKey; this.sealImage = sealImage; this.signature = signature; this.signatureImage = signatureImage; this.subtitle = subtitle; this.display = await this.parseHashlinksInDisplay(display); } async parseHashlinksInDisplay (display: BlockcertsV3Display): Promise { const modifiedDisplay = deepCopy(display); if (!modifiedDisplay) { return; } if (modifiedDisplay.contentMediaType !== 'text/html') { // TODO: enum supported content media types return modifiedDisplay; } modifiedDisplay.content = await convertHashlink(modifiedDisplay.content, this.hashlinkVerifier); return modifiedDisplay; } }