UNPKG

@transmute/bbs-bls12381-signature-2020

Version:
359 lines (300 loc) 10.1 kB
import jsonld from "@transmute/jsonld"; import { randomBytes } from "@stablelib/random"; import { DeriveProofOptions, VerifyProofOptions, CreateVerifyDataOptions, CanonizeOptions, VerifyProofResult } from "./types"; import { BbsBlsSignature2020 } from "./BbsBlsSignature2020"; import { BbsBlsSignatureProof2020ProofType } from "./types"; import { Bls12381G2KeyPair } from "@transmute/bls12381-key-pair"; import { blsCreateProof, blsVerifyProof } from "@mattrglobal/bbs-signatures"; const suiteContexts = [ "https://w3id.org/security/suites/jws-2020/v1", "https://w3id.org/security/suites/bls12381-2020/v1" ]; export class BbsBlsSignatureProof2020 { public static type = "BbsBlsSignatureProof2020"; ensureSuiteContext({ document }: any) { const contextUrl = "https://w3id.org/security/suites/bls12381-2020/v1"; if ( document["@context"] === contextUrl || (Array.isArray(document["@context"]) && document["@context"].includes(contextUrl)) ) { // document already includes the required context return; } throw new TypeError( `The document to be signed must contain this suite's @context, ` + `"${contextUrl}".` ); } async getVerificationMethod({ proof, documentLoader }: any): Promise<any> { let { verificationMethod } = proof; if (typeof verificationMethod === "object") { verificationMethod = verificationMethod.id; } if (!verificationMethod) { throw new Error('No "verificationMethod" found in proof.'); } // Note: `expansionMap` is intentionally not passed; we can safely drop // properties here and must allow for it const result = await jsonld.frame( verificationMethod, { "@context": suiteContexts, "@embed": "@always", id: verificationMethod }, { documentLoader, compactToRelative: false, expandContext: suiteContexts } ); if (!result) { throw new Error(`Verification method ${verificationMethod} not found.`); } // ensure verification method has not been revoked if (result.revoked !== undefined) { throw new Error("The verification method has been revoked."); } return result; } async deriveProof( options: DeriveProofOptions ): Promise<{ document: any; proof: any }> { const { document, proof, revealDocument, documentLoader, expansionMap } = options; let { nonce } = options; if (proof.type !== "BbsBlsSignature2020") { throw new TypeError( `proof type unacceptable, expected "BbsBlsSignature2020", received ${proof.type}` ); } const signature = Uint8Array.from(Buffer.from(proof.proofValue, "base64")); delete proof.proofValue; const suite = new BbsBlsSignature2020(); const derivedProof: BbsBlsSignatureProof2020ProofType = { "@context": document["@context"], type: BbsBlsSignatureProof2020.type }; const documentStatements = await suite.createVerifyDocumentData(document, { documentLoader, expansionMap, compactProof: true }); const proofStatements = await suite.createVerifyProofData(proof, { documentLoader, expansionMap, compactProof: true }); const transformedInputDocumentStatements = documentStatements.map(element => element.replace(/(_:c14n[0-9]+)/g, "<urn:bnid:$1>") ); const compactInputProofDocument = await jsonld.fromRDF( transformedInputDocumentStatements.join("\n") ); const revealDocumentResult = await jsonld.frame( compactInputProofDocument, revealDocument, { documentLoader } ); const revealDocumentStatements = await suite.createVerifyDocumentData( revealDocumentResult, { documentLoader, expansionMap } ); const numberOfProofStatements = proofStatements.length; const proofRevealIndicies = Array.from( Array(numberOfProofStatements).keys() ); const documentRevealIndicies = revealDocumentStatements.map( key => transformedInputDocumentStatements.indexOf(key) + numberOfProofStatements ); if (documentRevealIndicies.length !== revealDocumentStatements.length) { throw new Error( "Some statements in the reveal document not found in original proof" ); } const revealIndicies = proofRevealIndicies.concat(documentRevealIndicies); // Create a nonce if one is not supplied if (!nonce) { nonce = await randomBytes(50); } derivedProof.nonce = Buffer.from(nonce).toString("base64"); const allInputStatements: Uint8Array[] = proofStatements .concat(documentStatements) .map(item => new Uint8Array(Buffer.from(item))); // Fetch the verification method const verificationMethod = await this.getVerificationMethod({ proof, document, documentLoader, expansionMap }); const key = await Bls12381G2KeyPair.from(verificationMethod); const outputProof = await blsCreateProof({ messages: allInputStatements, revealed: revealIndicies, signature: new Uint8Array(signature), nonce: nonce, publicKey: new Uint8Array(key.publicKey) }); // Set the proof value on the derived proof derivedProof.proofValue = Buffer.from(outputProof).toString("base64"); // Set the relevant proof elements on the derived proof from the input proof derivedProof.verificationMethod = proof.verificationMethod; derivedProof.proofPurpose = proof.proofPurpose; derivedProof.created = proof.created; const compactedRevealedDocument = await jsonld.compact( { ...revealDocumentResult }, document["@context"], { documentLoader } ); return { document: { ...compactedRevealedDocument }, proof: derivedProof }; } async canonize(input: any, options: CanonizeOptions): Promise<string> { const { documentLoader, expansionMap, skipExpansion } = options; return jsonld.safeCanonize(input, { algorithm: "URDNA2015", format: "application/n-quads", documentLoader, expansionMap, skipExpansion, useNative: false }); } async canonizeProof(proof: any, options: CanonizeOptions): Promise<string> { const { documentLoader, expansionMap } = options; proof = { ...proof }; delete proof.nonce; delete proof.proofValue; return this.canonize(proof, { documentLoader, expansionMap, skipExpansion: false }); } /** * @param document {CreateVerifyDataOptions} options to create verify data * * @returns {Promise<{string[]>}. */ async createVerifyData(options: CreateVerifyDataOptions): Promise<string[]> { const { proof, document, documentLoader, expansionMap } = options; const proofStatements = await this.createVerifyProofData(proof, { documentLoader, expansionMap }); const documentStatements = await this.createVerifyDocumentData(document, { documentLoader, expansionMap }); // concatenate c14n proof options and c14n document return proofStatements.concat(documentStatements); } /** * @param proof to canonicalize * @param options to create verify data * * @returns {Promise<{string[]>}. */ async createVerifyProofData( proof: any, { documentLoader, expansionMap }: any ): Promise<string[]> { const c14nProofOptions = await this.canonizeProof(proof, { documentLoader, expansionMap }); return c14nProofOptions.split("\n").filter(_ => _.length > 0); } /** * @param document to canonicalize * @param options to create verify data * * @returns {Promise<{string[]>}. */ async createVerifyDocumentData( document: any, { documentLoader, expansionMap }: any ): Promise<string[]> { const c14nDocument = await this.canonize(document, { documentLoader, expansionMap }); return c14nDocument.split("\n").filter(_ => _.length > 0); } async verifyProof(options: VerifyProofOptions): Promise<VerifyProofResult> { const { document, documentLoader, expansionMap, purpose } = options; const { proof } = options; try { proof.type = "BbsBlsSignature2020"; // Get the proof statements const proofStatements = await this.createVerifyProofData(proof, { documentLoader, expansionMap }); // Get the document statements const documentStatements = await this.createVerifyProofData(document, { documentLoader, expansionMap }); // Transform the blank node identifier placeholders for the document statements // back into actual blank node identifiers const transformedDocumentStatements = documentStatements.map( (element: any) => element.replace(/<urn:bnid:(_:c14n[0-9]+)>/g, "$1") ); // Combine all the statements to be verified const statementsToVerify: Uint8Array[] = proofStatements .concat(transformedDocumentStatements) .map((item: any) => new Uint8Array(Buffer.from(item))); const verificationMethod = await this.getVerificationMethod({ proof, document, documentLoader, expansionMap }); const key = await Bls12381G2KeyPair.from(verificationMethod); // Verify the proof const verified = await blsVerifyProof({ proof: new Uint8Array(Buffer.from(proof.proofValue, "base64")), publicKey: new Uint8Array(key.publicKey), messages: statementsToVerify, nonce: new Uint8Array(Buffer.from(proof.nonce as string, "base64")) }); // Ensure proof was performed for a valid purpose const { valid, error } = await purpose.validate(proof, { document, suite: this, verificationMethod, documentLoader, expansionMap }); if (!valid) { throw error; } return verified; } catch (error) { return { verified: false, error }; } } }