Skip to content

Commit

Permalink
Merge pull request #1900 from blockchain-certificates/feat/multiple-c…
Browse files Browse the repository at this point in the history
…redential-subjects

Feat/multiple credential subjects
  • Loading branch information
lemoustachiste authored Nov 7, 2024
2 parents 5a1ddfa + 86cbc64 commit 01740fc
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 30 deletions.
14 changes: 10 additions & 4 deletions src/inspectors/checkCredentialSchemaConformity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { SUB_STEPS } from '../domain/verifier/entities/verificationSteps';
import { getText } from '../domain/i18n/useCases';
import type { VCCredentialSchema } from '../models/BlockcertsV3';

export default async function checkCredentialSchemaConformity (credentialSubject: any, credentialSchema: VCCredentialSchema | VCCredentialSchema[]): Promise<void> {
export default async function checkCredentialSchemaConformity (credentialSubject: any | any[], credentialSchema: VCCredentialSchema | VCCredentialSchema[]): Promise<void> {
if (!Array.isArray(credentialSchema)) {
credentialSchema = [credentialSchema];
}

if (!Array.isArray(credentialSubject)) {
credentialSubject = [credentialSubject];
}

for (const schemaInfo of credentialSchema) {
const rawSchema = await request({ url: schemaInfo.id });
let schema;
Expand All @@ -21,9 +25,11 @@ export default async function checkCredentialSchemaConformity (credentialSubject
}

const validate = validator(schema);
const result = validate(credentialSubject);
if (!result) {
throw new VerifierError(SUB_STEPS.checkCredentialSchemaConformity, getText('errors', 'checkCredentialSchemaConformity'));
for (const subject of credentialSubject) {
const result = validate(subject);
if (!result) {
throw new VerifierError(SUB_STEPS.checkCredentialSchemaConformity, getText('errors', 'checkCredentialSchemaConformity'));
}
}
}
}
27 changes: 15 additions & 12 deletions src/models/BlockcertsV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface VerifiableCredential {
credentialStatus?: VCCredentialStatus | VCCredentialStatus[];
credentialSchema?: VCCredentialSchema | VCCredentialSchema[];
issuer: string | Issuer;
credentialSubject?: any;
credentialSubject?: any | any[];
expirationDate?: string;
evidence?: Array<{
type: string[];
Expand Down Expand Up @@ -83,20 +83,23 @@ export interface VCCredentialSchema {
id: string;
}

export interface BlockcertsV3 extends VerifiableCredential {
credentialSubject: {
// CredentialSubject can technically be anything the issuer wants, but we define a basic structure
export interface BlockcertsV3BasicCredentialSubject {
id?: string;
publicKey?: string;
name?: string;
type?: string;
claim?: {
type?: string;
id?: string;
publicKey?: string;
name?: string;
type?: string;
claim?: {
type?: string;
id?: string;
name?: string;
description?: string;
criteria?: string;
};
description?: string;
criteria?: string;
};
}

export interface BlockcertsV3 extends VerifiableCredential {
credentialSubject: BlockcertsV3BasicCredentialSubject | BlockcertsV3BasicCredentialSubject[];
metadata?: string;
display?: BlockcertsV3Display;
nonce?: string;
Expand Down
31 changes: 18 additions & 13 deletions src/parsers/parseV3.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import domain from '../domain';
import type { Issuer } from '../models/Issuer';
import type { BlockcertsV3, MultilingualVcField, VCProof } from '../models/BlockcertsV3';
import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3';
import type { ParsedCertificate } from './index';

function getRecipientFullName (certificateJson): string {
const { credentialSubject } = certificateJson;
return credentialSubject.name || '';
}
function getPropertyValueForCurrentLanguage (propertyName: string, field: any, locale: string): string {
if (typeof field === 'undefined') {
return '';
}

function getPropertyValueForCurrentLanguage (property: string, field: string | MultilingualVcField[] = '', locale: string): string {
if (typeof field === 'string') {
return field;
}

if (!Array.isArray(field)) {
field = [field];
}

return field
// find value by exact match
.find((f) => f['@language'] === locale)?.['@value'] ??
.find((f) => f['@language'] === locale)?.[propertyName] ??
// find value by shorthand (ie. 'en-US' -> 'en')
field.find((f) => f['@language'].split('-')[0] === locale.split('-')[0])?.['@value'] ??
field[0]['@value'];
field.find((f) => f['@language']?.split('-')[0] === locale.split('-')[0])?.[propertyName] ??
field[0][propertyName];
}

export default async function parseV3 (certificateJson: BlockcertsV3, locale: string): Promise<ParsedCertificate> {
Expand All @@ -31,7 +35,8 @@ export default async function parseV3 (certificateJson: BlockcertsV3, locale: st
validUntil,
proof,
name,
description
description,
credentialSubject
} = certificateJson;
let { validFrom } = certificateJson;
const certificateMetadata = metadata ?? metadataJson;
Expand All @@ -50,10 +55,10 @@ export default async function parseV3 (certificateJson: BlockcertsV3, locale: st
issuedOn: issuanceDate ?? validFrom, // maintain backwards compatibility
id,
issuer,
name: getPropertyValueForCurrentLanguage('name', name, locale),
description: getPropertyValueForCurrentLanguage('description', description, locale),
name: getPropertyValueForCurrentLanguage('@value', name, locale),
description: getPropertyValueForCurrentLanguage('@value', description, locale),
metadataJson: certificateMetadata,
recipientFullName: getRecipientFullName(certificateJson),
recipientFullName: getPropertyValueForCurrentLanguage('name', credentialSubject, locale),
recordLink: id
};
}
33 changes: 32 additions & 1 deletion test/application/parsers/parser-v3.2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import parseJSON from '../../../src/parsers';
import MonolingualBlockcertsV3 from '../../fixtures/v3/mocknet-vc-v2-name-description.json';
import MultilingualBlockcertsV3 from '../../fixtures/v3/mocknet-vc-v2-name-description-multilingual.json';
import MultipleCredentialSubjectsBlockcertsV3 from '../../fixtures/v3/mocknet-vc-v2-credential-subject-array.json';
import v3IssuerProfile from '../../fixtures/issuer-blockcerts.json';

describe('Parser v3 test suite', function () {
describe('given it is called with valid v3 certificate data', function () {
describe('given it is called with valid v3.2 certificate data', function () {
beforeAll(function () {
vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => {
const explorerLookup = await importOriginal();
Expand Down Expand Up @@ -102,5 +103,35 @@ describe('Parser v3 test suite', function () {
});
});
});

describe('Getting the recipient full name', function () {
describe('when the credential subject is not an array', function () {
it('should return the recipient full name', async function () {
const parsedCertificate = await parseJSON(MonolingualBlockcertsV3);
expect(parsedCertificate.recipientFullName).toBe('John Smith');
});
});

describe('when the credential subject is an array', function () {
describe('and the current language is specified in the certificate', function () {
it('should return the recipient full name according to the language', async function () {
const parsedCertificate = await parseJSON(MultipleCredentialSubjectsBlockcertsV3, 'fr');
expect(parsedCertificate.recipientFullName).toBe('Jean Forgeron');
});

it('should return the recipient full name according to the language if a subset', async function () {
const parsedCertificate = await parseJSON(MultipleCredentialSubjectsBlockcertsV3, 'fr-CA');
expect(parsedCertificate.recipientFullName).toBe('Jean Forgeron');
});
});

describe('and the current language is not specified in the certificate', function () {
it('should return the first recipient full name', async function () {
const parsedCertificate = await parseJSON(MultipleCredentialSubjectsBlockcertsV3, 'it');
expect(parsedCertificate.recipientFullName).toBe('John Smith');
});
});
});
});
});
});
40 changes: 40 additions & 0 deletions test/e2e/verifier/mocknet-vc-v2-credential-subject-array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect, beforeAll, vi, afterAll } from 'vitest';
import { Certificate, VERIFICATION_STATUSES } from '../../../src';
import MocknetVCV2CredentialSubjectArray from '../../fixtures/v3/mocknet-vc-v2-credential-subject-array.json';
import fixtureBlockcertsIssuerProfile from '../../fixtures/issuer-blockcerts.json';
import fixtureCredentialSchema from '../../fixtures/credential-schema-example-id-card.json';

describe('given the certificate is a valid mocknet (v3.2)', function () {
beforeAll(function () {
vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => {
const explorerLookup = await importOriginal();
return {
...explorerLookup,
// replace some exports
request: async function ({ url }) {
if (url === 'https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json') {
return JSON.stringify(fixtureBlockcertsIssuerProfile);
}

if (url === 'https://www.blockcerts.org/samples/3.0/example-id-card-schema.json') {
return JSON.stringify(fixtureCredentialSchema);
}
}
};
});
});

afterAll(function () {
vi.restoreAllMocks();
});

describe('and complies with its json schema definition', function () {
// this test will expire in 2039
it('should verify successfully', async function () {
const certificate = new Certificate(MocknetVCV2CredentialSubjectArray);
await certificate.init();
const result = await certificate.verify();
expect(result.status).toBe(VERIFICATION_STATUSES.SUCCESS);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@context": ["https://www.w3.org/ns/credentials/v2", {"DOB": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/DOB", "@type": "https://schema.org/Text"}, "nationality": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/nationality", "@type": "https://schema.org/Text"}, "height": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/height", "@type": "https://schema.org/Text"}, "residentialAddressStreet": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/residentialAddressStreet", "@type": "https://schema.org/Text"}, "residentialAddressTown": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/residentialAddressTown", "@type": "https://schema.org/Text"}, "residentialAddressPostCode": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/residentialAddressPostCode", "@type": "https://schema.org/Text"}, "IdCardCredential": {"@id": "https://schemas.learningmachine.com/2017/blockcerts/IdCardCredential", "@type": "https://schema.org/DataType"}}, "https://w3id.org/security/data-integrity/v2", "https://w3id.org/blockcerts/v3.2"], "type": ["VerifiableCredential", "BlockcertsCredential", "IdCardCredential"], "name": [{"@value": "Canadian Id Card", "@language": "en"}, {"@value": "Carte d'Identit\u00e9 Canadienne", "@language": "fr"}, {"@value": "\u0628\u0637\u0627\u0642\u0629 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0646\u062f\u064a\u0629", "@language": "ar", "@direction": "rtl"}], "description": [{"@value": "A Blockcerts example (not an official document) highlighting various VC v2 spec items", "@language": "en"}, {"@value": "Un example Blockcerts (document non officiel) d\u00e9montrant diff\u00e9rents \u00e9l\u00e9ments de la sp\u00e9cification VC v2", "@language": "fr"}, {"@value": "\u0645\u062b\u0627\u0644 \u0639\u0644\u0649 Blockcerts (\u0644\u064a\u0633 \u0645\u0633\u062a\u0646\u062f\u064b\u0627 \u0631\u0633\u0645\u064a\u064b\u0627) \u064a\u0633\u0644\u0637 \u0627\u0644\u0636\u0648\u0621 \u0639\u0644\u0649 \u0639\u0646\u0627\u0635\u0631 \u0645\u0648\u0627\u0635\u0641\u0627\u062a VC v2 \u0627\u0644\u0645\u062e\u062a\u0644\u0641\u0629", "@language": "ar", "@direction": "rtl"}], "issuer": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json", "validFrom": "2024-03-01T00:00:00Z", "validUntil": "2039-02-28T23:59:59Z", "id": "urn:uuid:4f5f0100-ccbf-4ca9-9cfc-4f5fc3052d28", "credentialSchema": [{"id": "https://www.blockcerts.org/samples/3.0/example-id-card-schema.json", "type": "JsonSchema"}], "credentialSubject": [{"id": "did:example:ebfeb1f712ebc6f1c276e12ec21", "@language": "en", "name": "John Smith", "nationality": "Canada", "DOB": "05/10/1983", "height": "1.80m", "residentialAddressStreet": "6 Maple Tree street", "residentialAddressTown": "Toronto", "residentialAddressPostCode": "YYZYUL"}, {"id": "did:example:ebfeb1f712ebc6f1c276e12ec21", "@language": "fr", "name": "Jean Forgeron", "nationality": "Canada", "DOB": "05/10/1983", "height": "1.80m", "residentialAddressStreet": "6 rue des \u00c9rables", "residentialAddressTown": "Montr\u00e9al", "residentialAddressPostCode": "YYZYUL"}], "display": {"contentMediaType": "text/html", "content": "<div style=\"background-color:transparent;padding:6px;display:inline-flex;align-items:center;flex-direction:column\">Yo</div>"}, "proof": {"id": "urn:uuid:277f7b2a-3567-4182-b0e1-199441fae5fb", "type": "DataIntegrityProof", "cryptosuite": "merkle-proof-2019", "proofPurpose": "assertionMethod", "created": "2024-11-07T15:51:05Z", "proofValue": "zEuZQLZTYrdsDv2EpwzQGUBe6PfabCRBgw919pBUVimNgpE5188vhyTySWTDvFLMDKMgLNdi3RQouwP5UrjLJkPSLb6JSBao3zi7nXAUFs8pKvv1AS4FC38QicsCL7oihbLLTG3dgkfjoVZAgNujrGfVTbJU3vw5QmViEEMFzupwA4vXF5zentFinyoELzpZGwDGTHVtgSEwGGFXaxDNNUMw6d3cbDNRvZT5mBneyG6GEZMtaccQ4f9PoPFi8Wac5rXje15UmMQfPuzoX6QyVvDZjCofhGBogYBSZAyG2rNqRT6", "verificationMethod": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#key-1"}}

0 comments on commit 01740fc

Please sign in to comment.