Skip to content

Commit

Permalink
Merge pull request #1881 from blockchain-certificates/feat/vc-v2
Browse files Browse the repository at this point in the history
Add date format validation to comply with VC v2 spec
  • Loading branch information
lemoustachiste authored Oct 15, 2024
2 parents cf5ac95 + d74db96 commit c91cc2e
Show file tree
Hide file tree
Showing 33 changed files with 675 additions and 83 deletions.
10 changes: 5 additions & 5 deletions bundle-esm-stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -2199,7 +2199,7 @@
step = start, start = stop, stop = step;
step = i0, i0 = i1, i1 = step;
}

while (maxIter-- > 0) {
step = tickIncrement(start, stop, count);
if (step === prestep) {
Expand Down Expand Up @@ -2599,7 +2599,7 @@
} else {
return path.replace(/\/$/, '').replace(/.*\//, '');
}
};
};
} (utils$3));

const utils$2 = utils$3;
Expand Down Expand Up @@ -2951,9 +2951,9 @@
if (opts.tokens) {
if (idx === 0 && start !== 0) {
tokens[idx].isPrefix = true;
tokens[idx].value = prefix;
tokens[idx].dateTimeStamp = prefix;
} else {
tokens[idx].value = value;
tokens[idx].dateTimeStamp = value;
}
depth(tokens[idx]);
state.maxDepth += tokens[idx].depth;
Expand All @@ -2969,7 +2969,7 @@
parts.push(value);

if (opts.tokens) {
tokens[tokens.length - 1].value = value;
tokens[tokens.length - 1].dateTimeStamp = value;
depth(tokens[tokens.length - 1]);
state.maxDepth += tokens[tokens.length - 1].depth;
}
Expand Down
44 changes: 31 additions & 13 deletions src/data/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"compareIssuingAddressLabel": "Compare addresses",
"compareIssuingAddressLabelPending": "Comparing addresses",
"checkImagesIntegrityLabel": "Verify Images Integrity",
"checkImagesIntegrityLabelPending": "Verifying Images Integrity"
"checkImagesIntegrityLabelPending": "Verifying Images Integrity",
"validateDateFormatLabel": "Validate date format",
"validateDateFormatLabelPending": "Validating date format"
},
"revocation": {
"preReason": "Reason given:",
Expand Down Expand Up @@ -106,8 +108,9 @@
"issuerProfileNotSet": "no issuer address given",
"issuerProfileInvalid": "retrieved file does not seem to be a valid profile",
"parseIssuerKeys": "Unable to parse JSON out of issuer identification data.",
"checkImagesIntegrity":"Image integrity verification proved that the content was modified after issuance.",
"ensureValidityPeriodStarted":"This certificate is not yet valid."
"checkImagesIntegrity": "Image integrity verification proved that the content was modified after issuance.",
"ensureValidityPeriodStarted": "This certificate is not yet valid.",
"validateDateFormat":"The date format specified does not conform with the spec requirements (RFC3339). Property:"
}
},
"fr": {
Expand Down Expand Up @@ -159,7 +162,9 @@
"compareIssuingAddressLabel": "Comparaison des adresses",
"compareIssuingAddressLabelPending": "Comparaison des adresses",
"checkImagesIntegrityLabel": "Vérification de l'Intégrité des Images",
"checkImagesIntegrityLabelPending": "Vérification de l'Intégrité des Images"
"checkImagesIntegrityLabelPending": "Vérification de l'Intégrité des Images",
"validateDateFormatLabel": "Vérification du format des dates",
"validateDateFormatLabelPending": "Vérification du format des dates"
},
"revocation": {
"preReason": "Raison :",
Expand Down Expand Up @@ -218,7 +223,8 @@
"issuerProfileInvalid": "le document distant ne semble pas être un profil d'émetteur valide",
"parseIssuerKeys": "Impossible de lire le JSON d'identification de l'émetteur",
"checkImagesIntegrity": "La vérification des images a prouvé que le contenu a été modifié après l'émission.",
"ensureValidityPeriodStarted": "Ce certificat n'est pas encore valide."
"ensureValidityPeriodStarted": "Ce certificat n'est pas encore valide.",
"validateDateFormat": "Le format de date spécifié n'est pas conforme aux exigences de la spécification (RFC3339). Propriété :"
}
},
"es": {
Expand Down Expand Up @@ -270,7 +276,9 @@
"compareIssuingAddressLabel": "Comparar direcciones",
"compareIssuingAddressLabelPending": "Comparando direcciones",
"checkImagesIntegrityLabel": "Comprobar Integridad de las Imagens",
"checkImagesIntegrityLabelPending": "Comprobando Integridad de las Imagens"
"checkImagesIntegrityLabelPending": "Comprobando Integridad de las Imagens",
"validateDateFormatLabel": "Verificar formato de fecha",
"validateDateFormatLabelPending": "Verificando formato de fecha"
},
"revocation": {
"preReason": "Razón dada:",
Expand Down Expand Up @@ -329,7 +337,8 @@
"issuerProfileInvalid": "el documento recogido no parece ser un perfil de emisor valido",
"parseIssuerKeys": "No se ha podido analizar el JSON de la información de identificación del emisor",
"checkImagesIntegrity": "La verificación de la integridad de los imagens provó alteración del contenido después de la emisión.",
"ensureValidityPeriodStarted": "Este certificado aún no está valido."
"ensureValidityPeriodStarted": "Este certificado aún no está valido.",
"validateDateFormat": "El formato de fecha especificado no cumple con los requisitos de la especificación (RFC3339). Propiedad:"
}
},
"mt": {
Expand Down Expand Up @@ -381,7 +390,9 @@
"compareIssuingAddressLabel": "Compare addresses",
"compareIssuingAddressLabelPending": "Comparing addresses",
"checkImagesIntegrityLabel": "Verify Images Integrity",
"checkImagesIntegrityLabelPending": "Verifying Images Integrity"
"checkImagesIntegrityLabelPending": "Verifying Images Integrity",
"validateDateFormatLabel": "Validate date format",
"validateDateFormatLabelPending": "Validating date format"
},
"revocation": {
"preReason": "Raġuni mogħtija:",
Expand Down Expand Up @@ -440,7 +451,8 @@
"issuerProfileInvalid": "retrieved file does not seem to be a valid profile",
"parseIssuerKeys": "Ma jistax jiġi estratt JSON mid-data tal-identifikazzjoni tal-emittent",
"checkImagesIntegrity": "Image integrity verification proved that the content was modified after issuance.",
"ensureValidityPeriodStarted": "This certificate is not yet valid."
"ensureValidityPeriodStarted": "This certificate is not yet valid.",
"validateDateFormat": "The date format specified does not conform with the spec requirements (RFC3339). Property:"
}
},
"it-IT": {
Expand Down Expand Up @@ -492,7 +504,9 @@
"compareIssuingAddressLabel": "Compare addresses",
"compareIssuingAddressLabelPending": "Comparing addresses",
"checkImagesIntegrityLabel": "Verify Images Integrity",
"checkImagesIntegrityLabelPending": "Verifying Images Integrity"
"checkImagesIntegrityLabelPending": "Verifying Images Integrity",
"validateDateFormatLabel": "Validate date format",
"validateDateFormatLabelPending": "Validating date format"
},
"revocation": {
"preReason": "Motivo indicato:",
Expand Down Expand Up @@ -551,7 +565,8 @@
"issuerProfileInvalid": "retrieved file does not seem to be a valid profile",
"parseIssuerKeys": "Impossibile analizzare JSON dai dati di identificazione dell'emittente.",
"checkImagesIntegrity": "Image integrity verification proved that the content was modified after issuance.",
"ensureValidityPeriodStarted": "This certificate is not yet valid."
"ensureValidityPeriodStarted": "This certificate is not yet valid.",
"validateDateFormat": "The date format specified does not conform with the spec requirements (RFC3339). Property:"
}
},
"ja": {
Expand Down Expand Up @@ -603,7 +618,9 @@
"compareIssuingAddressLabel": "アドレスの照合",
"compareIssuingAddressLabelPending": "アドレスを照合",
"checkImagesIntegrityLabel": "画像の保全性の確認",
"checkImagesIntegrityLabelPending": "画像の保全性を確認"
"checkImagesIntegrityLabelPending": "画像の保全性を確認",
"validateDateFormatLabel": "Validate date format",
"validateDateFormatLabelPending": "Validating date format"
},
"revocation": {
"preReason": "理由:",
Expand Down Expand Up @@ -662,7 +679,8 @@
"issuerProfileInvalid": "取得されたファイルが有効なプロフィールではないようです",
"parseIssuerKeys": "発行者識別データからJSONの解析ができません",
"checkImagesIntegrity": "データが発行後に変更されたことが画像保全性の検証によって証明されました。",
"ensureValidityPeriodStarted": "This certificate is not yet valid."
"ensureValidityPeriodStarted": "This certificate is not yet valid.",
"validateDateFormat": "The date format specified does not conform with the spec requirements (RFC3339). Property:"
}
}
}
6 changes: 4 additions & 2 deletions src/domain/verifier/entities/verificationSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export enum SUB_STEPS {
checkExpiresDate = 'checkExpiresDate',
controlVerificationMethod = 'controlVerificationMethod',
ensureValidityPeriodStarted = 'ensureValidityPeriodStarted',
checkCredentialSchemaConformity = 'checkCredentialSchemaConformity'
checkCredentialSchemaConformity = 'checkCredentialSchemaConformity',
validateDateFormat = 'validateDateFormat'
}

export type TVerificationStepsList = {
Expand All @@ -30,7 +31,8 @@ export type TVerificationStepsList = {
export const verificationMap = {
[VerificationSteps.formatValidation]: [
SUB_STEPS.checkImagesIntegrity,
SUB_STEPS.checkCredentialSchemaConformity
SUB_STEPS.checkCredentialSchemaConformity,
SUB_STEPS.validateDateFormat
],
[VerificationSteps.proofVerification]: [],
[VerificationSteps.identityVerification]: [
Expand Down
35 changes: 35 additions & 0 deletions src/domain/verifier/useCases/getDatesToValidate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { BlockcertsV3 } from '../../../models/BlockcertsV3';

export interface DatesToValidate {
property: string;
dateTimeStamp: string;
}

export default function getDatesToValidate (credential: BlockcertsV3): DatesToValidate[] {
const dates: DatesToValidate[] = [];
if (credential.validFrom) {
dates.push({
property: 'validFrom',
dateTimeStamp: credential.validFrom
});
}

if (credential.validUntil) {
dates.push({
property: 'validUntil',
dateTimeStamp: credential.validUntil
});
}

const proof = !Array.isArray(credential.proof) ? [credential.proof] : credential.proof;
for (const proofItem of proof) {
if (proofItem.created) {
dates.push({
property: `proof ${proofItem.cryptosuite ?? proofItem.type} created`,
dateTimeStamp: proofItem.created
});
}
}

return dates;
}
34 changes: 28 additions & 6 deletions src/domain/verifier/useCases/getVerificationMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ 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): SUB_STEPS[] {
export function getVerificationStepsForCurrentCase (
hasDid: boolean,
hasHashlinks: boolean,
hasValidFrom: boolean,
hasCredentialSchema: boolean,
isVCV2: boolean
): SUB_STEPS[] {
const verificationSteps = Object.values(SUB_STEPS);

if (!hasDid) {
Expand All @@ -23,6 +29,10 @@ export function getVerificationStepsForCurrentCase (hasDid: boolean, hasHashlink
removeEntry(verificationSteps, SUB_STEPS.checkCredentialSchemaConformity);
}

if (!isVCV2) {
removeEntry(verificationSteps, SUB_STEPS.validateDateFormat);
}

return verificationSteps;
}

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

export default function getVerificationMap (hasDid: boolean = false, hasHashlinks: boolean = false, hasValidFrom: boolean = false, hasCredentialSchema: boolean = false): {
verificationMap: IVerificationMapItem[];
verificationProcess: SUB_STEPS[];
} {
const verificationProcess: SUB_STEPS[] = getVerificationStepsForCurrentCase(hasDid, hasHashlinks, hasValidFrom, hasCredentialSchema);
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
);
return {
verificationProcess,
verificationMap: getFullStepsWithSubSteps(verificationProcess)
Expand Down
2 changes: 2 additions & 0 deletions src/domain/verifier/useCases/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import convertToVerificationSubsteps from './convertToVerificationSubsteps';
import findVerificationSubstep from './findVerificationSubstep';
import getDatesToValidate from './getDatesToValidate';
import getIssuerProfile from './getIssuerProfile';
import getRevokedAssertions from './getRevokedAssertions';
import getVerificationMap from './getVerificationMap';
Expand All @@ -10,6 +11,7 @@ import parseRevocationKey from './parseRevocationKey';
export {
convertToVerificationSubsteps,
findVerificationSubstep,
getDatesToValidate,
getIssuerProfile,
getRevokedAssertions,
getVerificationMap,
Expand Down
12 changes: 10 additions & 2 deletions src/helpers/date.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint no-useless-escape: 0 prefer-spread: 0 */ // TODO: at some point fix this

// https://www.w3.org/TR/vc-data-model-2.0/#example-regular-expression-to-detect-a-valid-xml-schema-1-1-part-2-datetimestamp
const RFC3339_DATE_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))$/;

function noOffset (s): Date {
let day = s.slice(0, -5).split(/\D/).map(function (itm) {
return parseInt(itm, 10) || 0;
Expand All @@ -18,8 +21,8 @@ function noOffset (s): Date {
function dateFromRegex (s: string): Date {
let day;
let tz;
const rx = /^(\d{4}\-\d\d\-\d\d([tT][\d:\.]*)?)([zZ]|([+\-])(\d\d):?(\d\d))?$/;
const p = rx.exec(s) ?? [];

const p = RFC3339_DATE_REGEX.exec(s) ?? [];
if (p[1]) {
day = p[1].split(/\D/).map(function (itm) {
return parseInt(itm, 10) || 0;
Expand Down Expand Up @@ -73,3 +76,8 @@ export function dateToUnixTimestamp (date: Date | string): number { // TODO: cle
export function timestampToDateObject (timestamp: number): Date {
return new Date(timestamp * 1000);
}

// https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp
export function validateDateTimeStamp (dateTimeStamp: string): boolean {
return RFC3339_DATE_REGEX.test(dateTimeStamp);
}
14 changes: 14 additions & 0 deletions src/inspectors/validateDateFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { validateDateTimeStamp } from '../helpers/date';
import { VerifierError } from '../models';
import { SUB_STEPS } from '../domain/verifier/entities/verificationSteps';
import { getText } from '../domain/i18n/useCases';
import type { DatesToValidate } from '../domain/verifier/useCases/getDatesToValidate';

export default function validateDateFormat (dates: DatesToValidate[]): void {
for (const { dateTimeStamp, property } of dates) {
if (!validateDateTimeStamp(dateTimeStamp)) {
console.error('Date', dateTimeStamp, 'is not valid:', property);
throw new VerifierError(SUB_STEPS.validateDateFormat, `${getText('errors', 'validateDateFormat')} ${property}`);
}
}
}
35 changes: 35 additions & 0 deletions src/parsers/helpers/retrieveVCVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CONTEXT_URLS } from '@blockcerts/schemas';
import { isString } from '../../helpers/string';
import type { JsonLDContext } from '../../models/Blockcerts';

export interface VCVersion {
versionNumber: number;
}

export function isVCV2 (context: JsonLDContext | string): boolean {
return retrieveVCVersion(context).versionNumber === 2;
}

export default function retrieveVCVersion (context: JsonLDContext | string): VCVersion {
if (typeof context === 'string') {
context = [context];
}

const VCContextsUrls = [CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V1_CONTEXT, CONTEXT_URLS.VERIFIABLE_CREDENTIAL_V2_CONTEXT];

const VCContext: string = context.filter(isString).find((ctx: string) => VCContextsUrls.includes(ctx));

let versionNumber: number = -1;

if (VCContext?.includes('v1')) {
versionNumber = 1;
}

if (VCContext?.includes('v2')) {
versionNumber = 2;
}

return {
versionNumber
};
}
Loading

0 comments on commit c91cc2e

Please sign in to comment.