@transmute/bbs-bls12381-signature-2020
Version:
Transmute implementation of https://w3id.org/security/suites/bls12381-2020
359 lines (300 loc) • 10.1 kB
text/typescript
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 };
}
}
}