-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1830 from blockchain-certificates/feat/support-ec…
…dsa-sd-2023-verification Feat/support ecdsa sd 2023 verification
- Loading branch information
Showing
10 changed files
with
5,828 additions
and
3,808 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.