Skip to content

Commit

Permalink
Merge pull request #1830 from blockchain-certificates/feat/support-ec…
Browse files Browse the repository at this point in the history
…dsa-sd-2023-verification

Feat/support ecdsa sd 2023 verification
  • Loading branch information
lemoustachiste authored Jun 17, 2024
2 parents e93520e + 1ee2dca commit 10e12fb
Show file tree
Hide file tree
Showing 10 changed files with 5,828 additions and 3,808 deletions.
9,275 changes: 5,471 additions & 3,804 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"@blockcerts/hashlink-verifier": "^1.6.0",
"@blockcerts/schemas": "^3.6.3",
"@did-core/did-ld-json": "npm:@blockcerts/did-ld-json@^0.1.2",
"@digitalbazaar/data-integrity": "^2.1.0",
"@digitalbazaar/ecdsa-sd-2023-cryptosuite": "github:blockchain-certificates/ecdsa-sd-2023-cryptosuite#chore/secp256k1-with-multikey-fork",
"@digitalbazaar/ed25519-signature-2020": "^5.2.0",
"@digitalbazaar/ed25519-verification-key-2020": "^4.1.0",
"@digitalbazaar/vc-revocation-list": "github:blockchain-certificates/vc-revocation-list#fix/memory-leak",
Expand Down
203 changes: 203 additions & 0 deletions src/suites/EcdsaSd2023.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import domain from '../domain';
import jsigs from 'jsonld-signatures';
import jsonld from 'jsonld';
// @ts-expect-error: not a typescript package
import { createVerifyCryptosuite } from '@digitalbazaar/ecdsa-sd-2023-cryptosuite';
// @ts-expect-error: not a typescript package
import { DataIntegrityProof } from '@digitalbazaar/data-integrity';
import { Suite } from '../models/Suite';
import { preloadedContexts } from '../constants';
import * as inspectors from '../inspectors';
import type { Issuer } from '../models/Issuer';
import type VerificationSubstep from '../domain/verifier/valueObjects/VerificationSubstep';
import type { SuiteAPI } from '../models/Suite';
import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3';
import type { IDidDocument } from '../models/DidDocument';
import { VerifierError } from '../models';

const { purposes: { AssertionProofPurpose } } = jsigs;

enum SUB_STEPS {
retrieveVerificationMethodPublicKey = 'retrieveVerificationMethodPublicKey',
checkDocumentSignature = 'checkDocumentSignature'
}

export default class EcdsaSd2023 extends Suite {
public verificationProcess = [
SUB_STEPS.retrieveVerificationMethodPublicKey,
SUB_STEPS.checkDocumentSignature
];

public documentToVerify: BlockcertsV3;
public issuer: Issuer;
public proof: VCProof;
public type = 'EcdsaSd2023';
public cryptosuite = 'ecdsa-sd-2023';
public publicKey: string;
public verificationKey: any;

constructor (props: SuiteAPI) {
super(props);
if (props.executeStep) {
this.executeStep = props.executeStep;
}
this.documentToVerify = props.document as BlockcertsV3;
this.issuer = props.issuer;
this.proof = props.proof as VCProof;
this.validateProofType();
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
async init (): Promise<void> {}

async verifyProof (): Promise<void> {
for (const verificationStep of this.verificationProcess) {
if (!this[verificationStep]) {
console.error('verification logic for', verificationStep, 'not implemented');
return;
}
await this[verificationStep]();
}
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
async verifyIdentity (): Promise<void> {}

getProofVerificationSteps (parentStepKey): VerificationSubstep[] {
return this.verificationProcess.map(childStepKey =>
domain.verifier.convertToVerificationSubsteps(parentStepKey, childStepKey)
);
}

getIdentityVerificationSteps (): VerificationSubstep[] {
return [];
}

getIssuerPublicKey (): string {
return this.publicKey;
}

getIssuerName (): string {
return this.issuer.name ?? '';
}

getIssuerProfileDomain (): string {
try {
const issuerProfileUrl = new URL(this.getIssuerProfileUrl());
return issuerProfileUrl.hostname ?? '';
} catch (e) {
return '';
}
}

getIssuerProfileUrl (): string {
return this.issuer.id ?? '';
}

getSigningDate (): string {
return this.proof.created;
}

async executeStep (step: string, action, verificationSuite: string): Promise<any> {
throw new Error('doAction method needs to be overwritten by injecting from CVJS');
}

private validateProofType (): void {
const proofType = this.proof.type;
if (proofType === 'DataIntegrityProof') {
const proofCryptoSuite = this.proof.cryptosuite;
if (!proofCryptoSuite) {
throw new Error(`Malformed proof passed. With DataIntegrityProof a cryptosuite must be defined. Expected: ${this.cryptosuite}`);
}

if (proofCryptoSuite !== this.cryptosuite) {
throw new Error(`Incompatible proof cryptosuite passed. Expected: ${this.cryptosuite}, Got: ${proofCryptoSuite}`);
}
return;
}
if (proofType !== this.type) {
throw new Error(`Incompatible proof type passed. Expected: ${this.type}, Got: ${proofType}`);
}
}

private generateDocumentLoader (documents: Array<{ url: string; value: string }> = []): any {
documents.forEach(document => {
preloadedContexts[document.url] = document.value;
});
preloadedContexts[this.documentToVerify.issuer as string] = this.getTargetVerificationMethodContainer();
const customLoader = function (url): any {
if (url in preloadedContexts) {
return {
contextUrl: null,
document: preloadedContexts[url],
documentUrl: url
};
}
return jsonld.documentLoader(url);
};
return customLoader;
}

private getErrorMessage (verificationStatus): string {
return verificationStatus.results[0].error.cause.message;
}

private getTargetVerificationMethodContainer (): Issuer | IDidDocument {
return this.issuer.didDocument ?? this.issuer;
}

private async retrieveVerificationMethodPublicKey (): Promise<void> {
this.verificationKey = await this.executeStep(
SUB_STEPS.retrieveVerificationMethodPublicKey,
async (): Promise<any> => {
const verificationMethodPublicKey = inspectors.retrieveVerificationMethodPublicKey(
this.getTargetVerificationMethodContainer(),
this.proof.verificationMethod
);

if (!verificationMethodPublicKey) {
throw new VerifierError(SUB_STEPS.retrieveVerificationMethodPublicKey, 'Could not derive the verification key');
}

// TODO: revoked property should exist but we are currently using a forked implementation which does not expose it
if ((verificationMethodPublicKey as any).revoked) {
throw new VerifierError(SUB_STEPS.retrieveVerificationMethodPublicKey, 'The verification key has been revoked');
}

return verificationMethodPublicKey;
},
this.type
);
}

private async checkDocumentSignature (): Promise<void> {
await this.executeStep(
SUB_STEPS.checkDocumentSignature,
async (): Promise<void> => {
const suite = new DataIntegrityProof({
cryptosuite: createVerifyCryptosuite({ requiredAlgorithm: 'K-256' })
});
const verificationMethod = (this.documentToVerify.proof as VCProof).verificationMethod;
const verificationStatus = await jsigs.verify(this.documentToVerify, {
suite,
purpose: new AssertionProofPurpose(),
documentLoader: this.generateDocumentLoader([
{
url: verificationMethod,
value: this.verificationKey
}
])
});

if (!verificationStatus.verified) {
console.error(JSON.stringify(verificationStatus, null, 2));
throw new VerifierError(SUB_STEPS.checkDocumentSignature,
`The document's ${this.type} signature could not be confirmed: ${this.getErrorMessage(verificationStatus)}`);
} else {
console.log(`Credential ${this.type} signature successfully verified`);
}
},
this.type
);
}
}
10 changes: 9 additions & 1 deletion src/suites/MerkleProof2019.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,15 @@ export default class MerkleProof2019 extends Suite {

private validateProofType (): void {
const proofType = this.isProofChain() ? this.proof.chainedProofType : this.proof.type;
if (proofType === 'DataIntegrityProof' && this.proof.cryptosuite === this.cryptosuite) {
if (proofType === 'DataIntegrityProof') {
const proofCryptoSuite = this.proof.cryptosuite;
if (!proofCryptoSuite) {
throw new Error(`Malformed proof passed. With DataIntegrityProof a cryptosuite must be defined. Expected: ${this.cryptosuite}`);
}

if (proofCryptoSuite !== this.cryptosuite) {
throw new Error(`Incompatible proof cryptosuite passed. Expected: ${this.cryptosuite}, Got: ${proofCryptoSuite}`);
}
return;
}
if (proofType !== this.type) {
Expand Down
11 changes: 9 additions & 2 deletions src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export enum SupportedVerificationSuites {
MerkleProof2017 = 'MerkleProof2017',
MerkleProof2019 = 'MerkleProof2019',
Ed25519Signature2020 = 'Ed25519Signature2020',
EcdsaSecp256k1Signature2019 = 'EcdsaSecp256k1Signature2019'
EcdsaSecp256k1Signature2019 = 'EcdsaSecp256k1Signature2019',
EcdsaSd2023 = 'EcdsaSd2023'
}

export default class Verifier {
Expand All @@ -63,7 +64,8 @@ export default class Verifier {
[SupportedVerificationSuites.MerkleProof2017]: null,
[SupportedVerificationSuites.MerkleProof2019]: null,
[SupportedVerificationSuites.Ed25519Signature2020]: null,
[SupportedVerificationSuites.EcdsaSecp256k1Signature2019]: null
[SupportedVerificationSuites.EcdsaSecp256k1Signature2019]: null,
[SupportedVerificationSuites.EcdsaSd2023]: null
}; // defined here to later check if the proof type of the document is supported for verification

public proofVerifiers: Suite[] = [];
Expand Down Expand Up @@ -242,6 +244,11 @@ export default class Verifier {
const { default: EcdsaSecp256k1Signature2019VerificationSuite } = await import('./suites/EcdsaSecp256k1Signature2019');
this.supportedVerificationSuites.EcdsaSecp256k1Signature2019 = EcdsaSecp256k1Signature2019VerificationSuite as unknown as Suite;
}

if (documentProofTypes.includes(SupportedVerificationSuites.EcdsaSd2023)) {
const { default: EcdsaSd2023VerificationSuite } = await import('./suites/EcdsaSd2023');
this.supportedVerificationSuites.EcdsaSd2023 = EcdsaSd2023VerificationSuite as unknown as Suite;
}
}

private prepareVerificationProcess (): void {
Expand Down
25 changes: 25 additions & 0 deletions test/e2e/verifier/ecdsa-sd-2023-derived.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect, vi } from 'vitest';
import { Certificate, VERIFICATION_STATUSES } from '../../../src';
import fixture from '../../fixtures/v3/ecdsa-sd-2023-derived-credential.json';
import fixtureIssuerProfile from '../../fixtures/issuer-blockcerts.json';

describe('ecdsa-sd-2023 signed and derived document test suite', function () {
it('should verify successfully', async function () {
vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => {
const explorerLookup = await importOriginal();
return {
...explorerLookup,
request: async function ({ url }) {
if (url === 'https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json') {
return JSON.stringify(fixtureIssuerProfile);
}
}
};
});
const certificate = new Certificate(fixture as any);
await certificate.init();
const result = await certificate.verify();

expect(result.status).toBe(VERIFICATION_STATUSES.SUCCESS);
});
});
37 changes: 37 additions & 0 deletions test/e2e/verifier/ecdsa-sd-2023-signed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { Certificate, VERIFICATION_STATUSES } from '../../../src';
import fixture from '../../fixtures/v3/ecdsa-sd-2023-signed-credential.json';
import fixtureIssuerProfile from '../../fixtures/issuer-blockcerts.json';

describe('ecdsa-sd-2023 signed and derived document test suite', function () {
let result;

beforeAll(async function () {
vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => {
const explorerLookup = await importOriginal();
return {
...explorerLookup,
request: async function ({ url }) {
if (url === 'https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json') {
return JSON.stringify(fixtureIssuerProfile);
}
}
};
});
const certificate = new Certificate(fixture as any);
await certificate.init();
result = await certificate.verify();
});

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

it('should fail verification', function () {
expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE);
});

it('should expose the expected error message', function () {
expect(result.message).toBe('The document\'s EcdsaSd2023 signature could not be confirmed: "proof.proofValue" must be a derived proof.');
});
});
10 changes: 9 additions & 1 deletion test/fixtures/issuer-blockcerts.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,20 @@
"x": "YHzDbKmTSWUisCw4AzY4hzOWDsjM-O8tyJTnFTSR79g",
"y": "bWCdCls4ukZq7KyWCTOJ8Sta_PJHvCdDIeiRaBD7e98"
}
},
{
"id": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#key-1-multibase",
"controller": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json",
"type": "Multikey",
"publicKeyMultibase": "zQ3shvX9Dd7cAG7ZcJN4d9DksshpVYSGpqEyrLjopoGpk97CR",
"keyAgreement": false
}
],
"assertionMethod": [
"https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#key-1",
"https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#secp256k1-verification-public-key",
"https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#secp256k1-derived-public-key"
"https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#secp256k1-derived-public-key",
"https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#key-1-multibase"
],
"revocationList": "https://www.blockcerts.org/samples/3.0/revocation-list-blockcerts.json",
"email": "",
Expand Down
26 changes: 26 additions & 0 deletions test/fixtures/v3/ecdsa-sd-2023-derived-credential.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/blockcerts/v3",
"https://w3id.org/security/data-integrity/v2"
],
"id": "urn:uuid:397f1f4f-0f1b-4d7d-8f0d-0b1f7d2f3d6d",
"type": [
"VerifiableCredential",
"BlockcertsCredential"
],
"issuer": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json",
"issuanceDate": "2022-05-25T15:15:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"name": "Julien Fraichot"
},
"proof": {
"type": "DataIntegrityProof",
"created": "2024-06-11T12:24:33Z",
"verificationMethod": "https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json#key-1-multibase",
"cryptosuite": "ecdsa-sd-2023",
"proofPurpose": "assertionMethod",
"proofValue": "u2V0BhVhAYqXJA37cuiZNty3NGt7ExHHtfMiPrONuak6oGxH4vEcb21ccERMjcvfxur_0bG8GYxrNUKdnB2sRMJgvCp8ZMFgjgCQCG_TxEzX83htPF8KZAZIelbrT74n1pqws0-uRqSCsMimCWEAsXVlORrDB_-tq7Pkj5rLxyPmTcZpsgPm2iJK3MaYjwEyfLvmMAXgn1HXzKFn4rGhh0WdyVCHpDfXL2fhy_qNzWEDQ5s9OnZYjtoW_8A5uYfl0U_9GkRysCtA1roPI4pIHuPt7jWBWZATwGawjkaHiJDpDU5EdWkC2K60VMqH3Uda3oIQBAgQF"
}
}
Loading

0 comments on commit 10e12fb

Please sign in to comment.