Skip to content

Commit

Permalink
Merge pull request #1912 from blockchain-certificates/feat/support-ve…
Browse files Browse the repository at this point in the history
…rifiable-presentation

Feat/support verifiable presentation
  • Loading branch information
lemoustachiste authored Nov 26, 2024
2 parents db4abbb + b469ff4 commit afd4953
Show file tree
Hide file tree
Showing 16 changed files with 612 additions and 54 deletions.
4 changes: 2 additions & 2 deletions bundle-esm-stats.html

Large diffs are not rendered by default.

46 changes: 42 additions & 4 deletions src/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ 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;
Expand Down Expand Up @@ -53,6 +55,7 @@ export default class Certificate {
public explorerAPIs: ExplorerAPI[] = [];
public id: string;
public isFormatValid: boolean;
public isVerifiablePresentation: boolean;
public issuedOn: string;
public issuer: Issuer;
public locale: string; // enum?
Expand All @@ -69,8 +72,10 @@ export default class Certificate {
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
Expand All @@ -86,9 +91,20 @@ export default class Certificate {

// Keep certificate JSON object
this.certificateJson = deepCopy<Blockcerts>(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<void> {
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);
Expand All @@ -99,7 +115,6 @@ export default class Certificate {
}
}
await this.parseJson(this.certificateJson);

this.verifier = new Verifier({
certificateJson: this.certificateJson,
expires: this.expires,
Expand All @@ -115,11 +130,34 @@ export default class Certificate {
}

async verify (stepCallback?: IVerificationStepCallbackFn): Promise<IFinalVerificationStatus> {
const verificationStatus = await this.verifier.verify(stepCallback);

let mainDocumentVerificationStatus = await this.verifier.verify(stepCallback);
this.setSigners();

return verificationStatus;
if (this.isVerifiablePresentation) {
let i = 0;
console.log('VP has', this.verifiableCredentials.length, 'credentials');
for (const vc of this.verifiableCredentials) {
i++;
console.log('now verifying certificate', i, vc.id);
const verificationStatus = await vc.verify();
console.log('verificationStatus', vc.id, verificationStatus);

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]
};
break;
}
}
}

this.verificationStatus = mainDocumentVerificationStatus;
return mainDocumentVerificationStatus;
}

private async parseJson (certificateDefinition: Blockcerts): Promise<void> {
Expand Down
47 changes: 22 additions & 25 deletions src/domain/verifier/useCases/getVerificationMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ import { removeEntry } from '../../../helpers/array';
import type VerificationSubstep from '../valueObjects/VerificationSubstep';
import type { IVerificationMapItem } from '../../../models/VerificationMap';

export function getVerificationStepsForCurrentCase (
hasDid: boolean,
hasHashlinks: boolean,
hasValidFrom: boolean,
hasCredentialSchema: boolean,
isVCV2: boolean
): SUB_STEPS[] {
export interface VerificationMapFilters {
hasDid?: boolean;
hasHashlinks?: boolean;
hasValidFrom?: boolean;
hasCredentialSchema?: boolean;
isVCV2?: boolean;
isVerifiablePresentation?: boolean;
}
export function getVerificationStepsForCurrentCase ({
hasDid = false,
hasHashlinks = false,
hasValidFrom = false,
hasCredentialSchema = false,
isVCV2 = false,
isVerifiablePresentation = false
}: VerificationMapFilters): SUB_STEPS[] {
const verificationSteps = Object.values(SUB_STEPS);

if (!hasDid) {
Expand All @@ -21,7 +30,7 @@ export function getVerificationStepsForCurrentCase (
removeEntry(verificationSteps, SUB_STEPS.checkImagesIntegrity);
}

if (!hasValidFrom) {
if (!hasValidFrom || isVerifiablePresentation) {
removeEntry(verificationSteps, SUB_STEPS.ensureValidityPeriodStarted);
}

Expand Down Expand Up @@ -54,23 +63,11 @@ function getFullStepsWithSubSteps (verificationSubStepsList: SUB_STEPS[]): IVeri
}));
}

export default function getVerificationMap (
hasDid: boolean = false,
hasHashlinks: boolean = false,
hasValidFrom: boolean = false,
hasCredentialSchema: boolean = false,
isVCV2: boolean = false
): {
verificationMap: IVerificationMapItem[];
verificationProcess: SUB_STEPS[];
} {
const verificationProcess: SUB_STEPS[] = getVerificationStepsForCurrentCase(
hasDid,
hasHashlinks,
hasValidFrom,
hasCredentialSchema,
isVCV2
);
export default function getVerificationMap (filters: VerificationMapFilters = {}): {
verificationMap: IVerificationMapItem[];
verificationProcess: SUB_STEPS[];
} {
const verificationProcess: SUB_STEPS[] = getVerificationStepsForCurrentCase(filters);
return {
verificationProcess,
verificationMap: getFullStepsWithSubSteps(verificationProcess)
Expand Down
15 changes: 13 additions & 2 deletions src/domain/verifier/useCases/validateVerifiableCredential.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { CONTEXT_URLS } from '@blockcerts/schemas';
import { isValidUrl } from '../../../helpers/url';
import type { BlockcertsV3, VCCredentialStatus, VCCredentialSchema } from '../../../models/BlockcertsV3';
import type {
BlockcertsV3,
VCCredentialStatus,
VCCredentialSchema,
VerifiablePresentation
} from '../../../models/BlockcertsV3';
import type { JsonLDContext } from '../../../models/Blockcerts';
import { type Issuer } from '../../../models/Issuer';
import { isVerifiablePresentation } from '../../../models/BlockcertsV3';

function validateRFC3339Date (date: string): boolean {
const regex = /^-?([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?|(24:00:00(\.0+)?))(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$/;
Expand Down Expand Up @@ -107,7 +113,12 @@ function validateCredentialSchema (certificateCredentialSchema: VCCredentialSche
});
}

export default function validateVerifiableCredential (credential: BlockcertsV3): void {
export default function validateVerifiableCredential (credential: BlockcertsV3 | VerifiablePresentation): void {
if (isVerifiablePresentation(credential)) {
credential.verifiableCredential.forEach(vc => { validateVerifiableCredential(vc); });
return;
}

if (!credential.credentialSubject) {
throw new Error('`credentialSubject` must be defined');
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export { getSupportedLanguages } from './domain/i18n/useCases';
export { BLOCKCHAINS, CERTIFICATE_VERSIONS } from './constants';
export { SignatureImage } from './models';
export { retrieveBlockcertsVersion } from './parsers';
export { isVerifiablePresentation } from './models/BlockcertsV3';
export { CONTENT_MEDIA_TYPES } from './models/contentMediaTypes';
17 changes: 14 additions & 3 deletions src/models/BlockcertsV3.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Issuer } from './Issuer';
import type { JsonLDContext } from './Blockcerts';
import type { CONTENT_MEDIA_TYPES } from './contentMediaTypes';

export interface VCProof {
type: string;
Expand All @@ -21,12 +22,24 @@ export function getVCProofVerificationMethod (proof: VCProof | VCProof[]): strin
return proof.verificationMethod;
}

export function isVerifiablePresentation (credential: BlockcertsV3 | VerifiablePresentation): credential is VerifiablePresentation {
return credential.type.includes('VerifiablePresentation');
}

export interface MultilingualVcField {
'@value': string;
'@language': string;
'@direction'?: string;
}

export interface VerifiablePresentation {
'@context': JsonLDContext;
id?: string;
type: string[];
verifiableCredential?: BlockcertsV3[];
holder?: string;
}

export interface VerifiableCredential {
'@context': JsonLDContext;
id: string;
Expand Down Expand Up @@ -65,7 +78,7 @@ export interface VerifiableCredential {
}

export interface BlockcertsV3Display {
contentMediaType: string;
contentMediaType: CONTENT_MEDIA_TYPES;
content: string;
contentEncoding?: string;
}
Expand Down Expand Up @@ -103,8 +116,6 @@ export interface BlockcertsV3 extends VerifiableCredential {
metadata?: string;
display?: BlockcertsV3Display;
nonce?: string;
proof: VCProof | VCProof[];

/**
* @deprecated v3 alpha only
*/
Expand Down
8 changes: 8 additions & 0 deletions src/models/contentMediaTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum CONTENT_MEDIA_TYPES {
TEXT_HTML = 'text/html',
IMAGE_PNG = 'image/png',
IMAGE_JPEG = 'image/jpeg',
IMAGE_GIF = 'image/gif',
IMAGE_BMP = 'image/bmp',
APPLICATION_PDF = 'application/pdf'
}
24 changes: 18 additions & 6 deletions src/parsers/parseV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import domain from '../domain';
import type { Issuer } from '../models/Issuer';
import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3';
import type { ParsedCertificate } from './index';
import { isVerifiablePresentation } from '../models/BlockcertsV3';

function getPropertyValueForCurrentLanguage (propertyName: string, field: any, locale: string): string {
if (typeof field === 'undefined') {
Expand All @@ -24,9 +25,16 @@ function getPropertyValueForCurrentLanguage (propertyName: string, field: any, l
field[0][propertyName];
}

function getProofObject (proof: VCProof | VCProof[]): VCProof {
let proofObject = proof;
if (Array.isArray(proof)) {
proofObject = proof[0];
}
return proofObject as VCProof;
}

export default async function parseV3 (certificateJson: BlockcertsV3, locale: string): Promise<ParsedCertificate> {
const {
issuer: issuerProfileUrl,
metadataJson, metadata,
issuanceDate,
id,
Expand All @@ -38,20 +46,24 @@ export default async function parseV3 (certificateJson: BlockcertsV3, locale: st
description,
credentialSubject
} = certificateJson;
let {
issuer: issuerProfileUrl
} = certificateJson;
try {
domain.verifier.validateVerifiableCredential(certificateJson);
} catch (error) {
throw new Error(`Document presented is not a valid Verifiable Credential: ${error.message}`);
}
let { validFrom } = certificateJson;
const certificateMetadata = metadata ?? metadataJson;
if (isVerifiablePresentation(certificateJson)) {
const proofObject = getProofObject(proof);
issuerProfileUrl = proofObject.verificationMethod?.split('#')[0];
}
const issuer: Issuer = await domain.verifier.getIssuerProfile(issuerProfileUrl);
if (!validFrom) {
let proofObject = proof;
if (Array.isArray(proof)) {
proofObject = proof[0];
}
validFrom = (proofObject as VCProof).created;
const proofObject = getProofObject(proof);
validFrom = proofObject.created;
}
return {
display,
Expand Down
18 changes: 10 additions & 8 deletions src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SUB_STEPS, VerificationSteps } from './domain/verifier/entities/verific
import { VerifierError } from './models';
import { getText } from './domain/i18n/useCases';
import { difference } from './helpers/array';
import { getVCProofVerificationMethod } from './models/BlockcertsV3';
import { getVCProofVerificationMethod, isVerifiablePresentation } from './models/BlockcertsV3';
import type { ExplorerAPI, TransactionData } from '@blockcerts/explorer-lookup';
import type { HashlinkVerifier } from '@blockcerts/hashlink-verifier';
import type { Blockcerts } from './models/Blockcerts';
Expand Down Expand Up @@ -34,6 +34,7 @@ export interface IFinalVerificationStatus {
code: VerificationSteps.final;
status: VERIFICATION_STATUSES;
message: string;
errors?: IFinalVerificationStatus[];
}

interface StepVerificationStatus {
Expand Down Expand Up @@ -254,13 +255,14 @@ export default class Verifier {
}

private prepareVerificationProcess (): void {
const verificationModel = domain.verifier.getVerificationMap(
!!this.issuer.didDocument,
this.hashlinkVerifier?.hasHashlinksToVerify() ?? false,
!!this.validFrom,
!!(this.documentToVerify as BlockcertsV3).credentialSchema,
isVCV2(this.documentToVerify['@context'])
);
const verificationModel = domain.verifier.getVerificationMap({
hasDid: !!this.issuer.didDocument,
hasHashlinks: this.hashlinkVerifier?.hasHashlinksToVerify() ?? false,
hasValidFrom: !!this.validFrom,
hasCredentialSchema: !!(this.documentToVerify as BlockcertsV3).credentialSchema,
isVCV2: isVCV2(this.documentToVerify['@context']),
isVerifiablePresentation: isVerifiablePresentation(this.documentToVerify as BlockcertsV3)
});
this.verificationSteps = verificationModel.verificationMap;
this.verificationProcess = verificationModel.verificationProcess;

Expand Down
Loading

0 comments on commit afd4953

Please sign in to comment.