@grottonetworking/bbs-signatures
v0.1.5
Published
JavaScript implementation of the BBS+ signature suite for node or browser
Downloads
65
Readme
JavaScript BBS Signatures
This repository contains an all JavaScript implementation of the emerging BBS+ digital signature standard being developed in conjunction with the Decentralized Identity Foundation (DIF) and the Crypto Forum Research Group (CFRG) of the IRTF.
This version of our implementation conforms to the procedures and validates against the test vectors in the 05 draft, i.e., draft-irtf-cfrg-bbs-signatures-5.
You can find the latest draft specification at The BBS Signature Scheme (DIF) this work then feeds into the IRTF draft which is available at draft-irtf-cfrg-bbs-signatures.
Note that there are other draft implementations available notably in Rust, Typescript, etc... This is a JavaScript only implementation with a single dependency on the noble-curves project which has a single dependency on the noble-hash project. This implementation can be used with Node.js or in the browser. We provide Node.js based example code and you can see our BBS Signature Demo where we use this library in the browser to demonstrate interactively the properties of BBS signatures.
BBS Signature Basics
BBS signatures are different from other common digital signatures schemes such as EdDSA or ECDSA in that it supports:
- The signing of multiple messages at a time, rather than a single message.
- A single short signature covering all the messages.
- The ability to derive a secondary signature, called a proof in BBS terminology, that may choose not to disclose some of the messages in the original signature. This is called selective disclosure.
- The digital proofs generated in the previous section are unlinkable to each other in the cryptographic sense, and cannot be used for tracking. Note that this does not apply to any information disclosed.
Both the original signature and any proofs generated are verified with the original issuers public key. The contents of individual messages covered by a signature may not be changed, nor can new messages be added to a proof only messages can be withheld from a proof and still have the proof verify.
One prime example of the use of BBS signatures is in a three party issuer, holder, verifier model. For example suppose the issuer is the department of motor vehicles (DMV) and wants to issue the digital equivalent of a drivers license which contains an assortment of information. A fictitious set of information is shown below in JSON format for a tree living in northern California:
[ "FirstName: Sequoia",
"LastName: Sempervirens",
"Address: Jedediah Smith Redwoods State Park, California",
"Date of Birth: 1200/03/21",
"Height: 296 feet",
"Eyes: None",
"Hair: Brown bark, green needles",
"Picture: Encoded photo",
"License Class: None, Trees can't drive" ]
The DMV would use its secret key to produce a signature on this list of messages. It would then return to the holder, the aforementioned tree in out case, a bundle of information containing the signature, i.e., something like:
{
"publicKey": "b65b7cbff4e81b723456a13936b6bcc77a078bf6291765f3ae13170072249dd7daa7ec1bd82b818ab60198030b45b8fa159c155fc3841a9ad4045e37161c9f0d9a4f361b93cfdc67d365f3be1a398e56aa173d7a55e01b4a8dd2494e7fb90da7",
"header": "11223344556677889900aabbccddeeff",
"messages": [
"FirstName: Sequoia",
"LastName: Sempervirens",
"Address: Jedediah Smith Redwoods State Park, California",
"Date of Birth: 1200/03/21",
"Height: 296 feet",
"Eyes: None",
"Hair: Brown bark, green needles",
"Picture: Encoded photo",
"License Class: None, Trees can't drive"
],
"signature": "94bb93062e05bc702d0ab222b861fd0311533d6dcbcad4050e45dd2392de951a912915af08bd87b2284807432245f9960e1f59680a59cd9fae490dee659d63fd3922a728e2ba3ee33db6bcc806ec2cea3d7264489a42ca09deeac7ca88b1811c2158b51d81560832daf6a0000037a87a"
}
Note that this "document" contains a lot of potentially sensitive information. Hence should be sent securely from the issuer to the holder .
Now suppose that the tree needs to prove its age, e.g., in order to gain entrance to a club or bar, but doesn't want to divulge any extra information besides the minimum necessary, i.e., it just wants to show the following to the bouncer at the door in a verifiable manner:
[ "Date of Birth: 1200/03/21",
"Picture: Encoded photo" ]
It then uses the BBS proof generation algorithm to generate proof information such as:
{
"pk": "b65b7cbff4e81b723456a13936b6bcc77a078bf6291765f3ae13170072249dd7daa7ec1bd82b818ab60198030b45b8fa159c155fc3841a9ad4045e37161c9f0d9a4f361b93cfdc67d365f3be1a398e56aa173d7a55e01b4a8dd2494e7fb90da7",
"header": "11223344556677889900aabbccddeeff",
"ph": "",
"disclosedIndexes": [
3,
7
],
"disclosedMsgs": [
"Date of Birth: 1200/03/21",
"Picture: Encoded photo"
],
"proof": "8432472ce9cb174043856753306c290d87ecc8564038d34f18f5ac0127166aefd2e0c67b680285aff3401efc0c834a9ca133d85e8695d9085847c5d537cda5c37740df5d9eb5abf3d8e5f1242336d01eaf11dd80a06efd43e887ade2c3f5352ca8c53d8b7217d90c46af1f45a70ebeff512e6dc689bea361c91c2d1bc38b063ae78958d6e3a09d88a3cd3d78bc94c6c96f8788dad9c17e8afac3e0bb1a8c2e83f6c3d718c723ea36a06c9ac0b81561b66bacbf68b56ff09007df6b5258f36eb2ff1886e081702b829cd43da22510e1b35f07bc316f391f88807843d9a81ccfd42731196f5d3ac377c0679e7a0e58d30a5529e7d04bc350769bbe44d26416a77b7d799594e570370c649ddfbabbfc3e830e223ba53d86b19a9a3291038e425419af90f5ecd9c25d309cf6ce17b8339177363eb113a065b853cd4a6b47ff8b04ac7799fb931f4a42d754685c81852c34734aba447ec2a414df702e66dc67570b4603d2cd1f0af406925e4525738fbd4b8a4799d9fd6cd0a2c7761f49c76cb048f004cf09939621a7368da2ae5ac671b16a00f108fd57002bd835c9760758ef11f6b074ab40e7e47eb3fc31ecb1af00cf033c8dd2cfba0ea4d36388d9f15f8e58813988abe14b89075b37a3caa98cb4328e4daf98253ce16b1f4152358ef86f34695997086a1927f67fe69d7df94391a3661a9cd0340cf5618d9334f97cf6db5f25c203b55c866a014bc7981668fd208f55"
}
The bouncer at the door, the verifier, verifies this against the DMV's public key, i.e., the two messages disclosed were part of the orginal signed information and have not been modified.
Now suppose this tree is very thirsty and visits another bar, given California's issue with drought the next bar states it will only serve trees from a state or national parks. So the tree includes its address information as well as its photo and date of birth as follows:
[ "Address: Jedediah Smith Redwoods State Park, California",
"Date of Birth: 1200/03/21",
"Picture: Encoded photo" ]
It produces another BBS proof such as:
{
"pk": "b65b7cbff4e81b723456a13936b6bcc77a078bf6291765f3ae13170072249dd7daa7ec1bd82b818ab60198030b45b8fa159c155fc3841a9ad4045e37161c9f0d9a4f361b93cfdc67d365f3be1a398e56aa173d7a55e01b4a8dd2494e7fb90da7",
"header": "11223344556677889900aabbccddeeff",
"ph": "",
"disclosedIndexes": [
2,
3,
7
],
"disclosedMsgs": [
"Address: Jedediah Smith Redwoods State Park, California",
"Date of Birth: 1200/03/21",
"Picture: Encoded photo"
],
"proof": "80c4b1301cd5964e1e69fd1a95d074fb30b085a733ae56d4bc427da89dfaf9dc63bf32e6d5d027d9ba6d3f59091aeb8e92e3baead45b9be6d501013478d4fbdd6cdbf637f201bd2c8c840cc55b2b33562e6429c88b96866e0ca86ce242408e8aaf3abb41d8c89bf436058504f43f8e7f31bec5a28a571666421201971341e731cafba531188d67f40553166c616a7c4839f7a605b3a774c158e1a45841d1eb07bd8534390010027f14bae9dc59bd75a63b6b532940d1ea98bdfb2354bf25b464f8b685fd4723aca13fbb58a5b3c8a1f143cba92acda6a731ba876b12b55b4c1fffa79699d3cc3f271f27b4a34ef4b356582147fdd19477c9bd9cfc7ba270de1bbe3c68038df39bebc289e265008631720b17aa4ca2bc89769f791b78bec9bcf7b19886f14130a232f7368649ea2fa2c843ecb9014ba4d622e80cef0fcbbc557aad02e3e5fac75569e6d0428662034ba93c2a50c4c17336656d4b44c3d9170e22460f699b514af2583e9011877443d30d50515db20845242b2d55a0a8960ae4b19cb6604bca7c47cd3fbef486a0ed76953eb450096123121826fa12a5b45a7a57554d92ab1d6b0c9e1df880504093f7a87159173ade25480fdce339611c96ad0dcf8b83543bbded15b21af56f8aff918957743e4ca81a5113be17b0a834807b374df76a1196b3152cca3646acc7055667"
}
Which the bartender at the second bar can verify with the DMV's public key. By unlinkable we mean the cryptographic proof field in these two proofs are uncorrelated and can't be used to track the tree from bar to bar. The tree, however, has disclosed its "date of birth" and "photo" to both bars which could be used for tracking purpose.
You may have noticed that the proofs in these two examples have different sizes. Currently BBS signatures have a size of 112 bytes, while BBS proofs have a size of $304 + U*32$ bytes where U is the number of undisclosed messages. We say currently since new work may reduce these numbers a bit while preserving security properties.
Library Usage
Key Generation
For signing a secret (also known as a private) key is needed. For signature and proof verification the verifiers need the corresponding public key. A recomended procedure for deriving an appropriate secret key from some initial random bytes is given in the BBS specification and available via the keyGen()
function. See the KeyGenExample.js file for details.
import {API_ID_BBS_SHAKE, bytesToHex, keyGen, publicFromPrivate} from '@grottonetworking/bbs-signatures';
import crypto from 'crypto';
const bytesLength = 40; // >= 32 bytes
// Generate random initial key material -- Node.js
const keyMaterial = new Uint8Array(crypto.randomBytes(bytesLength).buffer);
const keyInfo = new TextEncoder().encode('BBS-Example Key info');
const sk_bytes = await keyGen(keyMaterial, keyInfo, API_ID_BBS_SHAKE);
console.log(`Private key, length ${sk_bytes.length}, (hex):`);
console.log(bytesToHex(sk_bytes));
const pub_bytes = publicFromPrivate(sk_bytes);
console.log(`Public key, length ${pub_bytes.length}, (hex):`);
console.log(bytesToHex(pub_bytes));
Message Encoding
Since BBS works works with multiple messages the encoding (really cryptographic processing) of the messages is done as separate step. This is accomplished with the messages_to_scalars()
function. For example:
import {API_ID_BBS_SHAKE, messages_to_scalars, numberToHex} from '@grottonetworking/bbs-signatures';
const messages = [
'FirstName: Sequoia',
'LastName: Sempervirens',
'Address: Jedediah Smith Redwoods State Park, California',
'Date of Birth: 1200/03/21',
'Height: 296 feet',
'Eyes: None',
'Hair: Brown bark, green needles',
'Picture: Encoded photo',
'License Class: None, Trees can\'t drive'
];
const te = new TextEncoder(); // To convert strings to byte arrays
const messagesOctets = messages.map(msg => te.encode(msg));
const msg_scalars = await messages_to_scalars(messagesOctets, API_ID_BBS_SHAKE);
for(let i = 0; i < messages.length; i++) {
console.log(`msg ${i} ${messages[i]}`);
console.log(`scalar (hex): ${numberToHex(msg_scalars[i], 32)}`);
}
Generator Preparation
All major BBS operations (signing, signature verification, proof generation, and proof verification) require the use of cryptographic group "generators". There need to be as many "generators" as messages that are or were originally signed. These take a bit of time to compute but can be reused in all the aforementioned operations and do not take up too much space. Hence we break out the preparation of these "generators" into a separate step via the prepareGenerators()
function.
Below we show an example that creates the generators and these can be confirmed against the "message generator" test vectors given in the specification. You would never have a need to look at these in a real application.
import {API_ID_BBS_SHAKE, prepareGenerators} from '@grottonetworking/bbs-signatures';
const L = 10;
const gens = await prepareGenerators(L, API_ID_BBS_SHAKE); // Default SHA-256 hash
console.log(`Q1:${gens.Q1.toHex(true)}`); // Elliptic point to compressed hex
console.log(`Q2:${gens.Q2.toHex(true)}`);
for(let i = 0; i < gens.H.length; i++) {
console.log(`H${i}:${gens.H[i].toHex(true)}`);
}
Signing and Verifying
Generate a signature for a list of messages we need the following: (1) private/public key pair, (2) list of encoded messages, (3) prepared generators. Optionally a "header" (as a byte array) maybe supplied that contains context or application information. To verify a signature we need all of the above but not the private key. This is shown below with the full example given in TreeDMVExample.js. An additional example is given in SignVerifyExaple.js.
// Excerp from TreeDMVExample.js
const header = hexToBytes('11223344556677889900aabbccddeeff');
const signature = await sign(sk_bytes, pk_bytes, header, msg_scalars, gens, API_ID_BBS_SHAKE);
console.log('Signature:');
console.log(bytesToHex(signature));
const verified = await verify(pk_bytes, signature, header, msg_scalars, gens, API_ID_BBS_SHAKE);
console.log(`Algorithm verified: ${verified}`);
Proof Generation and Verification
For proof generation the holder needs the signature, messages, and public key. Then they need to decide which messages they want to disclose. This can be done in code as shown below with the full example given in TreeDMVExample.js. An additional example is contained in ProofGenVerifyExample.js.
const ph = new Uint8Array();
const disclosed_indexes = [3, 7]; // Selective disclosure
const proof = await proofGen(pk_bytes, signature, header, ph, msg_scalars,
disclosed_indexes, gens, API_ID_BBS_SHAKE);
console.log(`Proof for selective disclosure of messages ${disclosed_indexes}:`);
console.log(bytesToHex(proof));
const disclosedMsgs = msg_scalars.filter(
(m, i) => disclosed_indexes.includes(i)); // Only the disclosed messages!
const proofValid = await proofVerify(pk_bytes, proof, header, ph, disclosedMsgs,
disclosed_indexes, gens);
console.log(`Proof verified: ${proofValid}`);
API
Functions
keyGen(key_material, key_info, key_dst, api_id) ⇒ Uint8Array
Produces an appropriate secret key starting from initial key material. This procedure enhances the entropy of the key material but is deterministic so initial key material must be kept secret.
Kind: global function Returns: Uint8Array - Derived secret key as an array of bytes.
| Param | Type | Description | | --- | --- | --- | | key_material | Uint8Array | Secret key material. Must be >= 32 bytes long. | | key_info | Uint8Array | Optional key information. | | key_dst | string | Key domain separation tag, defaults to 'KEYGEN_DST_'. | | api_id | string | The API id for the signature suite. |
publicFromPrivate(privateBytes) ⇒ Uint8Array
Kind: global function Returns: Uint8Array - Containing encoded public key in G2.
| Param | Type | Description | | --- | --- | --- | | privateBytes | Uint8Array | Private key bytes must have length 32. |
sign(SK, PK, header, messages, generators, api_id)
Creates a BBS signature over a list of "messages".
Kind: global function
| Param | Type | Description | | --- | --- | --- | | SK | bigint | Uint8Array | A scalar or byte array for the secret key. | | PK | Uint8Array | Public key as a compressed G2 point raw bytes. | | header | Uint8Array | Header as a byte array. | | messages | Array | Array of scalars (bigint) derived from actual messages. Computed by messages_to_scalars. | | generators | Array | Array of group G1 generators created by the prepareGenerators function. | | api_id | string | The API id for the signature suite. |
verify(PK, signature, header, messages, generators, api_id) ⇒ boolean
Verify a BBS signature against a public key.
Kind: global function Returns: boolean - - True or False depending on whether the signature is valid.
| Param | Type | Description | | --- | --- | --- | | PK | Uint8Array | Public key as a compressed G2 point raw bytes. | | signature | Uint8Array | A previously computed signature. | | header | Uint8Array | Header used when signature was created. | | messages | Array | Array of scalars (bigint) derived from actual messages. Computed by messages_to_scalars. | | generators | Array | Array of group G1 generators created by the prepareGenerators function. | | api_id | string | The API id for the signature suite. |
proofGen(PK, signature, header, ph, messages, disclosed_indexes, generators, api_id, rand_scalars) ⇒ Uint8Array
Generates an unlinkable, selective disclosure proof based on a signature and message set, and related information.
Kind: global function Returns: Uint8Array - - The proof as an byte array.
| Param | Type | Description | | --- | --- | --- | | PK | Uint8Array | Public key as a compressed G2 point raw bytes. | | signature | Uint8Array | A previously computed signature. | | header | Uint8Array | Header used when signature was created. | | ph | Uint8Array | Presentation header, used during proof creation. | | messages | Array | Array of scalars (bigint) derived from actual messages. Computed by messages_to_scalars. | | disclosed_indexes | Array | Array of sorted (non-repeating) zero based indices of the messages to be disclosed. | | generators | Array | Array of group G1 generators created by the prepareGenerators function. | | api_id | string | The API id for the signature suite. | | rand_scalars | function | A function for generating cryptographically secure random or pseudo random scalars. |
proofVerify(PK, proof, header, ph, disclosed_messages, disclosed_indexes, generators, api_id) ⇒ boolean
Verifies a previously generated proof against original signers public key, and additional information.
Kind: global function Returns: boolean - - True or False depending on whether the proof is valid.
| Param | Type | Description | | --- | --- | --- | | PK | Uint8Array | Public key as a compressed G2 point raw bytes. | | proof | Uint8Array | The proof as a byte array. | | header | Uint8Array | Header used when original signature was created. | | ph | Uint8Array | Presentation header that was used during proof creation. | | disclosed_messages | Array | Array of scalars (bigint) derived from actual disclosed messages. Computed by messages_to_scalars. | | disclosed_indexes | Array | Array of sorted (non-repeating) zero based indices corresponding to the disclosed messages. | | generators | Array | Array of group G1 generators created by the prepareGenerators function. | | api_id | string | The API id for the signature suite. |
numUndisclosed(proofOctets) ⇒ number
Helper function to give the number of undisclosed messages in a proof. This can be added to the number of disclosed messages to calculate the number of generators needed in proof verification.
Kind: global function Returns: number - - The number of undisclosed messages, U.
| Param | Type | Description | | --- | --- | --- | | proofOctets | Uint8Array | Byte array containing the raw bytes of a proof. |
messages_to_scalars(messages, api_id) ⇒ Array
This function converts (hashes) byte array messages into scalars representing the messages for use in signature/proof operations.
Kind: global function Returns: Array - - An array of scalars (bigint) representing the messages.
| Param | Type | Description | | --- | --- | --- | | messages | Array | Messages as an Array of Uint8Arrays, i.e., these byte arrays not strings. | | api_id | string | The API id for the signature suite. |
prepareGenerators(L, api_id) ⇒ Array
Prepares the "group G1 generators" used by the BBS signature suite. These values can be reused in many calls to the sign, verify, proofGen, and proofVerify functions. You must have enough generators for the number of messages. You do not need to know what the "group G1" or a "generator" is! These take a while to compute so we prepare them separately and reuse them.
Kind: global function Returns: Array - - A array of group generators used by the signature/proof suite.
| Param | Type | Description | | --- | --- | --- | | L | number | An integer that indicates the number of generators to be created. This number must be large than the total number of messages in a signature or proof. | | api_id | string | The API id for the signature suite. |