All files / src/domain/verifier/useCases validateVerifiableCredential.ts

89.83% Statements 53/59
88.23% Branches 15/17
100% Functions 5/5
89.65% Lines 52/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197                      192x 192x       266x       184x         4x         142x   1x   282x   1x         140x     1x     1x     1x     1x         136x   2x   4x   2x           8x         192x           5x 5x           6x       1x       7x 11x           48x 48x   4x   51x   4x             1x       1x               3x 6x     4x       2x     149x   142x 140x   136x       52x     2x         75x     63x         9x       39x                               1x     110x   98x   110x 122x      
import { CONTEXT_URLS } from '@blockcerts/schemas';
import { isValidUrl } from '../../../helpers/url';
import type {
  BlockcertsV3,
  VerifiablePresentation, VCProof, VCObject
} 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))$/;
  return regex.test(date);
}
 
function isV1VerifiableCredential (context: JsonLDContext): boolean {
  return context.includes(CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V1_CONTEXT);
}
 
function isV2VerifiableCredential (context: JsonLDContext): boolean {
  return context.includes(CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V2_CONTEXT);
}
 
function validateUrl (url: string, property: string = ''): void {
  if (!isValidUrl(url)) {
    throw new Error(`Invalid URL: ${url}. ${property ? `Property: ${property}` : ''}`);
  }
}
 
function validateType (certificateType: string[]): void {
  const compulsoryTypes = ['VerifiableCredential', 'VerifiablePresentation'];
  if (!Array.isArray(certificateType)) {
    throw new Error('`type` property must be an array');
  }
  const containsCompulsoryTypes = compulsoryTypes.filter(type => certificateType.includes(type));
  if (certificateType.length === 0 || containsCompulsoryTypes.length === 0) {
    throw new Error('`type` property must include `VerifiableCredential` or `VerifiablePresentation`');
  }
}
 
function validateContext (context: JsonLDContext, type: string[]): void {
  const vcContextUrls: string[] = [CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V1_CONTEXT, CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V2_CONTEXT];
 
  if (!Array.isArray(context)) {
    throw new Error('`@context` property must be an array');
  }
  if (!vcContextUrls.includes(context[0] as string)) {
    throw new Error(`First @context must be one of ${vcContextUrls.join(', ')}, given ${context[0] as string}`);
  }
  if (isV1VerifiableCredential(context) && isV2VerifiableCredential(context)) {
    throw new Error('Cannot have both v1 and v2 Verifiable Credential contexts');
  }
  if (type.length > 1 && context.length === 1) {
    throw new Error(`More specific type: ${type[1]} was detected but no additional context provided`);
  }
}
 
function validateIssuer (certificateIssuer: string | Issuer): void {
  let hasError = false;
  if (certificateIssuer == null) {
    hasError = true;
  } else if (typeof certificateIssuer === 'string' && !isValidUrl(certificateIssuer)) {
    hasError = true;
  } else if (typeof certificateIssuer === 'object' && !isValidUrl(certificateIssuer.id)) {
    hasError = true;
  } else if (Array.isArray(certificateIssuer)) {
    hasError = true;
  }
 
  if (hasError) {
    throw new Error('`issuer` must be a URL string or an object with an `id` URL string');
  }
}
 
function validateDateRFC3339StringFormat (date: string, propertyName: string): void {
  let errorMessage = `${propertyName} must be a valid RFC3339 string.`;
  if (typeof date !== 'string') {
    errorMessage += `${propertyName}: ${date as any} is not a string`;
    throw new Error(errorMessage);
  }
  if (!validateRFC3339Date(date)) {
    errorMessage += ` Received: \`${date}\``;
    throw new Error(errorMessage);
  }
}
 
function validateCredentialSubject (credentialSubject: any): void {
  if (typeof credentialSubject !== 'object') {
    throw new Error('`credentialSubject` must be an object');
  }
 
  if (Array.isArray(credentialSubject) && credentialSubject.length === 0) {
    throw new Error('`credentialSubject` cannot be an empty array');
  }
 
  if (Array.isArray(credentialSubject)) {
    credentialSubject.forEach(subject => {
      validateCredentialSubject(subject);
    });
  }
}
 
function validatePropTypeAndId (prop: VCObject | VCObject[], propName: string): void {
  const props = Array.isArray(prop) ? prop : [prop];
  props.forEach(p => {
    if (!p.id) {
      throw new Error(`${propName}.id must be defined`);
    }
    validateUrl(p.id, `${propName}.id`);
    if (!p.type) {
      throw new Error(`${propName}.type must be defined`);
    }
  });
}
 
function validateProof (proof: VCProof): void {
  if (!proof.created) {
    throw new Error('`proof.created` must be defined');
  }
 
  if (!proof.proofPurpose) {
    throw new Error('`proof.proofPurpose` must be defined');
  }
}
 
export default function validateVerifiableCredential (credential: BlockcertsV3 | VerifiablePresentation): void {
  if (isVerifiablePresentation(credential)) {
    // verifiableCredential property is optional
    if (credential.verifiableCredential) {
      credential.verifiableCredential.forEach(vc => {
        validateVerifiableCredential(vc);
      });
    }
    return;
  }
 
  if (!credential.credentialSubject) {
    throw new Error('`credentialSubject` must be defined');
  }
 
  validateCredentialSubject(credential.credentialSubject);
 
  validateType(credential.type);
  validateContext(credential['@context'], credential.type);
 
  validateIssuer(credential.issuer);
 
  if (isV1VerifiableCredential(credential['@context'])) {
    if (credential.issuanceDate) {
      validateDateRFC3339StringFormat(credential.issuanceDate, 'issuanceDate');
    }
    if (credential.expirationDate) {
      validateDateRFC3339StringFormat(credential.expirationDate, 'expirationDate');
    }
  }
  if (isV2VerifiableCredential(credential['@context'])) {
    if (credential.validFrom) {
      validateDateRFC3339StringFormat(credential.validFrom, 'validFrom');
    }
    if (credential.validUntil) {
      validateDateRFC3339StringFormat(credential.validUntil, 'validUntil');
    }
  }
 
  if (credential.credentialStatus) {
    validatePropTypeAndId(credential.credentialStatus, 'credentialStatus');
  }
 
  if (credential.credentialSchema) {
    validatePropTypeAndId(credential.credentialSchema, 'credentialSchema');
  }
 
  if (credential.termsOfUse) {
    validatePropTypeAndId(credential.termsOfUse, 'termsOfUse');
  }
 
  if (credential.evidence) {
    validatePropTypeAndId(credential.evidence, 'evidence');
  }
 
  if (credential.refreshService) {
    validatePropTypeAndId(credential.refreshService, 'refreshService');
  }
 
  if (!credential.proof) {
    throw new Error('`proof` must be defined');
  }
 
  let { proof } = credential;
  if (!Array.isArray(proof)) {
    proof = [proof];
  }
  proof.forEach(p => {
    validateProof(p);
  });
}