@transmute/bbs-bls12381-signature-2020
Version:
Transmute implementation of https://w3id.org/security/suites/bls12381-2020
298 lines (254 loc) • 7.54 kB
text/typescript
import jsonld from "@transmute/jsonld";
import { Bls12381G2KeyPair } from "@transmute/bls12381-key-pair";
import {
BbsBlsSignature2020ProofType,
CreateVerifyDataOptions,
VerifyProofOptions,
VerifySignatureOptions
} from "./types";
const suiteContexts = [
"https://w3id.org/security/suites/jws-2020/v1",
"https://w3id.org/security/suites/bls12381-2020/v1"
];
// const debugMessages = (data: any) => {
// console.log(
// "messages:\n" +
// JSON.stringify(
// data.map((i: any) => {
// return (
// Buffer.from(i)
// .toString()
// .substring(0, 128) + "..."
// );
// }),
// null,
// 2
// )
// );
// };
export class BbsBlsSignature2020 {
public static type = "BbsBlsSignature2020";
public verificationMethod?: string;
private key?: Bls12381G2KeyPair;
private date?: string;
constructor({ key, date }: { key?: Bls12381G2KeyPair; date?: string } = {}) {
this.key = key;
this.date = date;
if (key) {
this.verificationMethod = key.id;
}
}
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 matchProof({ proof }: any) {
return proof.type === BbsBlsSignature2020.type;
}
async canonize(
input: any,
{ documentLoader, expansionMap, skipExpansion }: any
) {
return jsonld.safeCanonize(input, {
algorithm: "URDNA2015",
format: "application/n-quads",
documentLoader,
expansionMap,
skipExpansion,
useNative: false
});
}
async canonizeProof(proof: any, { documentLoader, expansionMap }: any) {
// `jws`,`signatureValue`,`proofValue` must not be included in the proof
// options
proof = { ...proof };
delete proof.jws;
return this.canonize(proof, {
documentLoader,
expansionMap,
skipExpansion: false
});
}
async createVerifyProofData(
proof: any,
{ documentLoader, expansionMap }: any
): Promise<string[]> {
const c14nProofOptions = await this.canonizeProof(proof, {
documentLoader,
expansionMap
});
return c14nProofOptions.split("\n").filter((_: string) => _.length > 0);
}
async createVerifyDocumentData(
document: any,
{ documentLoader, expansionMap }: any
): Promise<string[]> {
const c14nDocument = await this.canonize(document, {
documentLoader,
expansionMap
});
return c14nDocument.split("\n").filter((_: string) => _.length > 0);
}
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);
}
async createProof({
document,
purpose,
documentLoader,
expansionMap,
compactProof
}: any) {
const context = document["@context"];
let proof: BbsBlsSignature2020ProofType = {
"@context": context
};
proof.type = BbsBlsSignature2020.type;
if (this.date !== undefined) {
proof.created = this.date;
}
if (!proof.created) {
const date = new Date().toISOString();
proof.created = date.substr(0, date.length - 5) + "Z";
}
if (this.key?.id !== undefined) {
proof.verificationMethod = this.key.id;
}
proof = await purpose.update(proof, {
document,
suite: this,
documentLoader,
expansionMap
});
const verifyData = (
await this.createVerifyData({
document,
proof,
documentLoader,
expansionMap,
compactProof
})
).map((item: any) => new Uint8Array(Buffer.from(item)));
const signer: any = await this.key?.signer();
const proofValue = await signer.sign({ data: verifyData });
proof.proofValue = Buffer.from(proofValue).toString("base64");
return proof;
}
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 verifySignature(options: VerifySignatureOptions): Promise<boolean> {
const { verifyData, proof, verificationMethod } = options;
const key = await Bls12381G2KeyPair.from(verificationMethod);
const verifier = key.verifier();
const signature = new Uint8Array(
Buffer.from(proof.proofValue as string, "base64")
);
return await verifier.verify({
data: verifyData,
signature
});
}
async verifyProof(
options: VerifyProofOptions
): Promise<{ verified: boolean; error?: any }> {
const { proof, document, documentLoader, expansionMap, purpose } = options;
const { proofValue } = proof;
delete proof.proofValue;
try {
// create data to verify
const verifyData = (
await this.createVerifyData({
document,
proof,
documentLoader,
expansionMap,
compactProof: false
})
).map(item => new Uint8Array(Buffer.from(item)));
// fetch verification method
const verificationMethod = await this.getVerificationMethod({
proof,
document,
documentLoader,
expansionMap
});
// verify signature on data
const verified = await this.verifySignature({
verifyData,
verificationMethod,
document,
proof: { ...proof, proofValue },
documentLoader,
expansionMap
});
if (!verified) {
throw new Error("Invalid signature.");
}
// 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: true };
} catch (error) {
return { verified: false, error };
}
}
}