├── .gitignore ├── jest.config.js ├── CODE_OF_CONDUCT.md ├── SUPPORT.md ├── .github └── workflows │ ├── nodejs-ci.yml │ └── codeql-analysis.yml ├── package.json ├── LICENSE ├── fetch-test-vectors.sh ├── src ├── index.ts ├── ciphersuite.ts ├── utils.ts ├── math.ts └── bbs.ts ├── tests ├── bbs.test.ts ├── math.test.ts └── fixture.test.ts ├── SECURITY.md ├── README.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # compiled js files 5 | js/* 6 | 7 | # fixture files (retrieved by script) 8 | fixtures/* -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | coverageProvider: "v8", 8 | testEnvironment: "node", 9 | preset: "ts-jest", 10 | testTimeout: 15000, 11 | testMatch: ["**/?(*.)+(spec|test).ts"], 12 | coverageReporters: ['json-summary'], 13 | }; 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, use the discussion forum or email msrhcv@microsoft.com. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for this project is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - 'main' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | node-version: [16.x, 18.x, 19.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bbs-node-reference", 3 | "version": "0.1.0", 4 | "description": "BBS node reference implementation", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "fetch-fixtures": "./fetch-test-vectors.sh", 8 | "build": "npm run fetch-fixtures && tsc", 9 | "bbs": "ts-node --files src/index.ts", 10 | "test": "npm run fetch-fixtures && jest --verbose", 11 | "coverage": "jest --ci --coverage" 12 | }, 13 | "author": "Christian Paquin", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@noble/curves": "^1.2.0", 17 | "@noble/hashes": "^1.3.2" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^27.5.0", 21 | "fs": "^0.0.1-security", 22 | "got": "^12.5.3", 23 | "jest": "^27.5.1", 24 | "ts-jest": "^27.1.3", 25 | "ts-node": "^10.7.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /fetch-test-vectors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FILE=fixtures 4 | if [ -d "$FILE" ]; then 5 | echo "$FILE directory already fetched; delete to refetch." 6 | exit 7 | fi 8 | 9 | mkdir -p fixtures 10 | 11 | urlPrefix="https://raw.githubusercontent.com/decentralized-identity/bbs-signature/main/tooling/fixtures/fixture_data/" 12 | 13 | fetch_file() { 14 | local suite=$1 15 | local file=$2 16 | local url="$urlPrefix/$suite/$file" 17 | echo "Fetching $url" 18 | wget -q -O "fixtures/$suite/$file" "$url" 19 | } 20 | suites=("bls12-381-sha-256" "bls12-381-shake-256") 21 | for suite in "${suites[@]}"; do 22 | echo "Fetching $suite fixtures" 23 | mkdir -p fixtures/$suite 24 | files=( 25 | "generators.json" 26 | "MapMessageToScalarAsHash.json" 27 | "h2s.json" 28 | "keypair.json" 29 | "mockedRng.json" 30 | ) 31 | 32 | for file in "${files[@]}"; do 33 | fetch_file "$suite" "$file" 34 | done 35 | 36 | for ((i = 1; i <= 10; i++)); do 37 | mkdir -p fixtures/$suite/signature 38 | fetch_file "$suite" "signature/signature$(printf "%.3d" "$i").json" 39 | done 40 | 41 | for ((i = 1; i <= 15; i++)); do 42 | mkdir -p fixtures/$suite/proof 43 | fetch_file "$suite" "proof/proof$(printf "%.3d" "$i").json" 44 | done 45 | done 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { BBS } from './bbs'; 5 | import * as crypto from 'crypto'; 6 | import * as utils from './utils'; 7 | 8 | void (async () => { 9 | try { 10 | const bbs = new BBS(); 11 | 12 | // generate issuer keys 13 | const SK = bbs.KeyGen(crypto.randomBytes(32)); 14 | const PK = bbs.SkToPk(SK); 15 | 16 | // create the generators 17 | const length = 5; 18 | const generators = await bbs.create_generators(length); 19 | 20 | // create random messages 21 | let msg = Array(length).fill(null).map(v => bbs.MapMessageToScalarAsHash(crypto.randomBytes(20))); 22 | 23 | // create the signature 24 | const header = Buffer.from("HEADER", "utf-8"); 25 | const signature = bbs.Sign(SK, PK, header, msg, generators); 26 | 27 | // validate the signature 28 | bbs.Verify(PK, signature, header, msg, generators); 29 | 30 | // randomly disclose each message 31 | const disclosed_indexes = Array(length).fill(0).map((v, i, a) => i).filter(v => { return Math.random() > 0.5; }); // random coin flip for each message 32 | const ph = Buffer.from("PRESENTATION HEADER", "utf-8"); 33 | 34 | const proof = bbs.ProofGen(PK, signature, header, ph, msg, generators, disclosed_indexes); 35 | const disclosed_msg = utils.filterDisclosedMessages(msg, disclosed_indexes); 36 | 37 | bbs.ProofVerify(PK, proof, header, ph, disclosed_msg, generators, disclosed_indexes); 38 | 39 | console.log("Success"); 40 | } 41 | catch (e) { 42 | console.log(e); 43 | } 44 | })(); -------------------------------------------------------------------------------- /src/ciphersuite.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { G1Point } from './math'; 5 | import { hexToBytes, expand_message_xmd, expand_message_xof } from './utils'; 6 | 7 | export class Ciphersuite { 8 | ciphersuite_id: string; 9 | octet_scalar_length = 32; 10 | octet_point_length = 48; 11 | hash_to_curve_suite: string; 12 | P1: G1Point; 13 | generator_seed: Uint8Array; 14 | hash_to_curve_g1 = async (input: Uint8Array, dst: string) => { 15 | return G1Point.hashToCurve(input, dst); 16 | } 17 | expand_len = 48; // ceil((ceil(log2(r)) + k)/8) 18 | expand_message: (message: Uint8Array, dst: Uint8Array, len: number) => Uint8Array; 19 | 20 | constructor(cipherSuiteBaseId: string, P1: G1Point, expand_message: (message: Uint8Array, dst: Uint8Array, len: number) => Uint8Array) { 21 | this.ciphersuite_id = cipherSuiteBaseId + "H2G_HM2S_"; 22 | this.hash_to_curve_suite = cipherSuiteBaseId; 23 | this.P1 = P1; 24 | this.generator_seed = Buffer.from(this.ciphersuite_id + "MESSAGE_GENERATOR_SEED", 'utf-8'); 25 | this.expand_message = expand_message; 26 | } 27 | } 28 | 29 | export const BLS12_381_SHA256_Ciphersuite = new Ciphersuite( 30 | "BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_", 31 | G1Point.fromOctets(hexToBytes("a8ce256102840821a3e94ea9025e4662b205762f9776b3a766c872b948f1fd225e7c59698588e70d11406d161b4e28c9")), 32 | expand_message_xmd 33 | ); 34 | 35 | export const BLS12_381_SHAKE_256_Ciphersuite = new Ciphersuite( 36 | "BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_", 37 | G1Point.fromOctets(hexToBytes("8929dfbc7e6642c4ed9cba0856e493f8b9d7d5fcb0c31ef8fdcd34d50648a56c795e106e9eada6e0bda386b414150755")), 38 | expand_message_xof 39 | ); 40 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { FrScalar } from "./math"; 5 | import * as utils from '@noble/curves/abstract/utils'; 6 | import * as hash from '@noble/curves/abstract/hash-to-curve'; 7 | import {sha256} from '@noble/hashes/sha256'; 8 | import {shake256} from '@noble/hashes/sha3'; 9 | 10 | // Octet Stream to Integer 11 | export function os2ip(bytes: Uint8Array, nonZero: boolean = false): FrScalar { 12 | return FrScalar.create(utils.bytesToNumberBE(bytes), nonZero); 13 | } 14 | 15 | // Integer to Octet Stream 16 | export function i2osp(value: number | bigint, length: number): Uint8Array { 17 | return utils.numberToBytesBE(value, length); 18 | } 19 | 20 | export function concat(...arrays: Uint8Array[]): Uint8Array { 21 | return utils.concatBytes(...arrays); 22 | } 23 | 24 | export function expand_message_xmd(msg: Uint8Array, DST: Uint8Array, len_in_bytes: number): Uint8Array { 25 | return hash.expand_message_xmd(msg, DST, len_in_bytes, sha256); 26 | } 27 | 28 | export function expand_message_xof(msg: Uint8Array, DST: Uint8Array, len_in_bytes: number): Uint8Array { 29 | return hash.expand_message_xof(msg, DST, len_in_bytes, 128, shake256); // TODO: is k = 128 the right value? 30 | } 31 | 32 | export function hexToBytes(hex: string): Uint8Array { 33 | return utils.hexToBytes(hex); 34 | } 35 | 36 | export function bytesToHex(bytes: Uint8Array): string { 37 | return utils.bytesToHex(bytes); 38 | } 39 | 40 | export function filterDisclosedMessages(msg: FrScalar[], disclosed_indexes: number[]): FrScalar[] { 41 | return msg.filter((v, i, a) => { return disclosed_indexes.includes(i) }); 42 | } 43 | 44 | export function log(...s: any): void { 45 | // console.log(...s); // uncomment to print out debug statements 46 | } -------------------------------------------------------------------------------- /tests/bbs.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { BBS } from '../src/bbs'; 5 | import * as crypto from 'crypto'; 6 | import * as utils from '../src/utils'; 7 | import { BLS12_381_SHA256_Ciphersuite, BLS12_381_SHAKE_256_Ciphersuite } from '../src/ciphersuite'; 8 | 9 | const ciphersuites = [BLS12_381_SHAKE_256_Ciphersuite, BLS12_381_SHA256_Ciphersuite]; 10 | 11 | ciphersuites.forEach(cs => { 12 | test("End-to-end test: " + cs.ciphersuite_id, async () => { 13 | const bbs = new BBS(cs); 14 | 15 | // generate random issuer keys 16 | const SK = bbs.KeyGen(crypto.randomBytes(32)); 17 | const PK = bbs.SkToPk(SK); 18 | 19 | // create the generators 20 | const length = 5; 21 | const generators = await bbs.create_generators(length); 22 | 23 | // create random messages 24 | let msg = Array(length).fill(null).map(v => bbs.MapMessageToScalarAsHash(crypto.randomBytes(20))); 25 | 26 | // create the signature 27 | const header = Buffer.from("HEADER", "utf-8"); 28 | const signature = bbs.Sign(SK, PK, header, msg, generators); 29 | 30 | // validate the signature 31 | bbs.Verify(PK, signature, header, msg, generators); 32 | 33 | // randomly disclose each message 34 | const disclosed_indexes = Array(length).fill(0).map((v, i, a) => i ).filter(v => { return Math.random() > 0.5; }); // random coin flip for each message 35 | const ph = Buffer.from("PRESENTATION HEADER", "utf-8"); 36 | 37 | const proof = bbs.ProofGen(PK, signature, header, ph, msg, generators, disclosed_indexes); 38 | const disclosed_msg = utils.filterDisclosedMessages(msg, disclosed_indexes); 39 | 40 | bbs.ProofVerify(PK, proof, header, ph, disclosed_msg, generators, disclosed_indexes); 41 | }); 42 | }); 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/math.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { FrScalar, G1Point, G2Point } from "../src/math"; 5 | 6 | test("G1 tests", async () => { 7 | const I = G1Point.Identity; 8 | const P = await G1Point.hashToCurve(Buffer.from("test msg"), "test dst"); 9 | 10 | // identity and commutativity tests 11 | expect(P.add(I).equals(P)).toBe(true); 12 | expect(I.add(P).equals(P)).toBe(true); 13 | 14 | // scalar multiplication 15 | const five = FrScalar.create(5n); 16 | expect(P.mul(five).equals(P.add(P).add(P).add(P).add(P))).toBe(true); 17 | 18 | // serialization tests 19 | expect(G1Point.fromOctets(P.toOctets()).equals(P)).toBe(true); 20 | }); 21 | 22 | // G2 tests 23 | test("G2 tests", async () => { 24 | const I = G2Point.Identity; 25 | const P = G2Point.Base; 26 | 27 | // identity and commutativity tests 28 | expect(P.add(I).equals(P)).toBe(true); 29 | expect(I.add(P).equals(P)).toBe(true); 30 | 31 | // scalar multiplication 32 | const five = FrScalar.create(5n); 33 | expect(P.mul(five).equals(P.add(P).add(P).add(P).add(P))).toBe(true); 34 | 35 | // serialization tests 36 | expect(G2Point.fromOctets(P.toOctets()).equals(P)).toBe(true); 37 | }); 38 | 39 | // TODO: pairing tests 40 | 41 | // FrScalar tests 42 | test("FrScalar tests", async () => { 43 | const zero = FrScalar.Zero; 44 | expect(zero.equals(FrScalar.create(0n))).toBe(true); 45 | const one = FrScalar.create(1n); 46 | 47 | expect(() => FrScalar.create(0n, true)).toThrow(); 48 | 49 | // addition tests 50 | const bn = 1234567890n; 51 | const n = FrScalar.create(bn); 52 | expect(n.add(zero).equals(n)).toBe(true); 53 | expect(zero.add(n).equals(n)).toBe(true); 54 | expect(n.add(one).equals(FrScalar.create(bn + 1n))).toBe(true); 55 | 56 | // negation tests 57 | expect(n.neg().equals(FrScalar.create(-bn))).toBe(true); 58 | expect(zero.neg().equals(zero)).toBe(true); 59 | 60 | // multiplication tests 61 | expect(n.mul(zero).equals(zero)).toBe(true); 62 | expect(n.mul(one).equals(n)).toBe(true); 63 | expect(one.mul(n).equals(n)).toBe(true); 64 | expect(n.mul(FrScalar.create(2n)).equals(FrScalar.create(bn * 2n))).toBe(true); 65 | 66 | // inverse tests 67 | expect(one.inv().equals(one)).toBe(true); 68 | expect(n.inv().mul(n).equals(one)).toBe(true); 69 | }); -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "dev" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "dev" ] 20 | schedule: 21 | - cron: '39 17 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BBS Reference Implementation 2 | 3 | TypeScript reference implementation for the [BBS signature scheme](https://github.com/decentralized-identity/bbs-signature). The goal is to help understand and verify the specification. This is NOT a production-ready implementation; testing is minimal and no effort is made to optimize and protect against specialized attacks (e.g., side-channel resistance). 4 | 5 | This project aims to keep up to date with the [latest specification](https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html), but may be behind since the specification changes often; the current implementation matches the *7 July 2025* version of the specification, matching the [draft-irtf-cfrg-bbs-signatures-09](https://datatracker.ietf.org/doc/draft-irtf-cfrg-bbs-signatures/09/) version submitted to the CFRG. 6 | 7 | Given the rapid evolution of the BBS scheme, there might be inconsistencies between the specification and the code; please open issues or file PRs if you find any! 8 | 9 | ## Setup 10 | 11 | Make sure [node.js](https://nodejs.org/) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) are installed on your system; the latest Long-Term Support (LTS) version is recommended for both. 12 | 13 | 1. Get the source, for example using `git` 14 | ``` 15 | git clone -b main https://github.com/microsoft/bbs-node-reference.git 16 | cd bbs-node-reference 17 | ``` 18 | 19 | 2. Build the `npm` package 20 | ``` 21 | npm install 22 | npm run build 23 | ``` 24 | 25 | 3. Optionally, run the unit tests 26 | 27 | ``` 28 | npm test 29 | ``` 30 | 31 | 4. Optionally, run the sample program 32 | 33 | ``` 34 | npm run bbs 35 | ``` 36 | 37 | 38 | ## Contributing 39 | 40 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 41 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 42 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 43 | 44 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 45 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 46 | provided by the bot. You will only need to do this once across all repos using our CLA. 47 | 48 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 49 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 50 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 51 | 52 | ## Trademarks 53 | 54 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 55 | trademarks or logos is subject to and must follow 56 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 57 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 58 | Any use of third-party trademarks or logos are subject to those third-party's policies. 59 | -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // implement the math using the bls library 5 | 6 | import { doWhileStatement } from '@babel/types'; 7 | import { ProjPointType } from '@noble/curves/abstract/weierstrass'; 8 | import { bls12_381 as bls } from '@noble/curves/bls12-381'; 9 | import { shake256 } from '@noble/hashes/sha3'; 10 | 11 | // unexported types from bls 12 | type Fp = bigint; 13 | type Fp2 = { c0: bigint; c1: bigint }; 14 | type Fp6 = { c0: Fp2; c1: Fp2; c2: Fp2 }; 15 | type Fp12 = { c0: Fp6; c1: Fp6 }; 16 | 17 | // abstract point class 18 | export class Point> { 19 | point: ProjPointType; 20 | constructor(point: ProjPointType) { 21 | this.point = point; 22 | } 23 | toOctets(): Uint8Array { 24 | return this.point.toRawBytes(true /* compressed */); 25 | } 26 | mul(s: FrScalar): U { 27 | return new Point(this.point.multiply(s.scalar)) as U; 28 | } 29 | add(p: U): U { 30 | return new Point(this.point.add(p.point)) as U; 31 | } 32 | neg(): U { 33 | return new Point(this.point.negate()) as U; 34 | } 35 | equals(p: U): boolean { 36 | return this.point.equals(p.point); 37 | } 38 | } 39 | 40 | // G1 point 41 | export class G1Point extends Point { 42 | constructor(point: ProjPointType) { 43 | super(point); 44 | } 45 | static Identity = new G1Point(bls.G1.ProjectivePoint.ZERO); 46 | static fromOctets(bytes: Uint8Array): G1Point { 47 | return new G1Point(bls.G1.ProjectivePoint.fromHex(bytes)); // fromHex takes bytes... 48 | } 49 | static async hashToCurve(msg: Uint8Array, dst: string): Promise { 50 | let options; 51 | if (dst.includes('SHAKE')) { 52 | // TODO: this is unclear in the spec, but matches the fixtures 53 | options = { DST: dst, expand: 'xof', hash: shake256 } as any; 54 | } else { 55 | options = { DST: dst }; 56 | } 57 | const candidate = await bls.G1.hashToCurve(msg, options); 58 | return new G1Point(candidate as ProjPointType); 59 | } 60 | } 61 | 62 | // G2 point 63 | export class G2Point extends Point { 64 | constructor(point: ProjPointType) { 65 | super(point); 66 | } 67 | static Identity = new G2Point(bls.G2.ProjectivePoint.ZERO); 68 | static Base = new G2Point(bls.G2.ProjectivePoint.BASE); 69 | static fromOctets(bytes: Uint8Array, subgroupCheck = false): G2Point { 70 | // note: we ignore the subgroupCheck parameter, because the bls library fromHex function 71 | // always checks the subgroup membership 72 | return new G2Point(bls.G2.ProjectivePoint.fromHex(bytes)); // fromHex takes bytes... 73 | } 74 | } 75 | 76 | // checks that e(pointG1_1, pointG2_1) * e(pointG1_2, pointG2_2) = GT_Identity 77 | export function checkPairingIsIdentity(pointG1_1: G1Point, pointG2_1: G2Point, pointG1_2: G1Point, pointG2_2: G2Point): boolean { 78 | // (using the pairing optimization to skip final exponentiation in the pairing 79 | // and do it after the multiplication) 80 | const lh = bls.pairing(pointG1_1.point, pointG2_1.point, false); 81 | const rh = bls.pairing(pointG1_2.point, pointG2_2.point, false); 82 | let result = bls.fields.Fp12.mul(lh, rh); 83 | // note: bls12-381 has a final exponentiate function, but it's not visible 84 | result = (bls.fields.Fp12 as unknown as { finalExponentiate(f: Fp12): Fp12; }).finalExponentiate(result); 85 | return bls.fields.Fp12.eql(result, bls.fields.Fp12.ONE); 86 | } 87 | 88 | // scalar field of order r 89 | export class FrScalar { 90 | static blsFr = bls.fields.Fr; 91 | 92 | scalar: bigint; 93 | private constructor(scalar: bigint) { 94 | this.scalar = FrScalar.blsFr.create(scalar); 95 | } 96 | static Zero = new FrScalar(0n); 97 | 98 | mul(s: FrScalar): FrScalar { 99 | return new FrScalar(FrScalar.blsFr.mul(this.scalar, s.scalar)); 100 | } 101 | inv(): FrScalar { 102 | return new FrScalar(FrScalar.blsFr.inv(this.scalar)); 103 | } 104 | add(s: FrScalar): FrScalar { 105 | return new FrScalar(FrScalar.blsFr.add(this.scalar, s.scalar)); 106 | } 107 | neg(): FrScalar { 108 | return new FrScalar(FrScalar.blsFr.neg(this.scalar)); 109 | } 110 | equals(s: FrScalar): boolean { 111 | return this.scalar === s.scalar; 112 | } 113 | static create(scalar: bigint, nonZero: boolean = false) { 114 | if (nonZero && scalar === 0n) throw new Error("scalar is 0"); 115 | return new FrScalar(FrScalar.blsFr.create(scalar)); 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | "lib": [ 10 | "es2020" 11 | ], /* Specify library files to be included in the compilation. */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "js", /* Redirect output structure to the directory. */ 20 | "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | /* Strict Type-Checking Options */ 29 | "strict": true /* Enable all strict type-checking options. */, 30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | /* Advanced Options */ 63 | "skipLibCheck": true /* Skip type checking of declaration files. */, 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ 67 | "src/**/*.ts", 68 | "tests/**/*.ts" 69 | , "src/check-version.ts" ], 70 | "exclude": [ 71 | "node_modules" 72 | ] 73 | } -------------------------------------------------------------------------------- /tests/fixture.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // test fixtures 5 | 6 | import * as fs from 'fs'; 7 | import { BBS } from '../src/bbs'; 8 | import { os2ip } from '../src/utils'; 9 | import { bytesToHex, hexToBytes } from '../src/utils'; 10 | import { FrScalar, G1Point } from '../src/math'; 11 | import { BLS12_381_SHA256_Ciphersuite, BLS12_381_SHAKE_256_Ciphersuite } from '../src/ciphersuite'; 12 | 13 | interface Generators { 14 | P1: string; 15 | Q1: string; 16 | MsgGenerators: string[]; 17 | } 18 | 19 | interface MockedRng { 20 | seed: string; 21 | dst: string; 22 | mockedScalars: string[]; 23 | } 24 | 25 | interface Signature { 26 | caseName: string; 27 | signerKeyPair: { 28 | secretKey: string; 29 | publicKey: string; 30 | }; 31 | header: string; 32 | messages: string[]; 33 | signature: string; 34 | result: { 35 | valid: boolean; 36 | reason?: string; 37 | }; 38 | } 39 | 40 | interface Proof { 41 | caseName: string; 42 | signerPublicKey: string; 43 | header: string; 44 | presentationHeader: string; 45 | messages: string[]; 46 | disclosedIndexes: number[]; 47 | proof: string; 48 | result: { 49 | valid: boolean; 50 | reason: string; 51 | }; 52 | } 53 | 54 | const ciphersuites = ["bls12-381-sha-256", "bls12-381-shake-256"]; 55 | ciphersuites.forEach(cs => { 56 | // load fixtures 57 | const fixturePath = "fixtures/" + cs; 58 | 59 | const generatorFixture = require(`../${fixturePath}/generators.json`) as Generators; 60 | const h2sFixture = require(`../${fixturePath}/h2s.json`); 61 | const keypairFixture = require(`../${fixturePath}/keypair.json`); 62 | const mmtsahFixture = require(`../${fixturePath}/MapMessageToScalarAsHash.json`); 63 | const mockedRngFixture = require(`../${fixturePath}/mockedRng.json`) as MockedRng; 64 | 65 | const sigFiles = fs.readdirSync(`${fixturePath}/signature`); 66 | const signaturesFixture = sigFiles.filter(f => f.endsWith('.json')).map(f => require(`../${fixturePath}/signature/${f}`) as Signature); 67 | 68 | const proofFiles = fs.readdirSync(`${fixturePath}/proof`); 69 | const proofsFixture = proofFiles.filter(f => f.endsWith('.json')).map(f => require(`../${fixturePath}/proof/${f}`) as Proof); 70 | 71 | // create the BBS instance 72 | const bbs = new BBS(cs.includes("shake") ? BLS12_381_SHAKE_256_Ciphersuite : BLS12_381_SHA256_Ciphersuite); 73 | 74 | // common generators 75 | const HGenerators = generatorFixture.MsgGenerators.map(v => G1Point.fromOctets(hexToBytes(v))); 76 | const generators = { 77 | Q1: G1Point.fromOctets(hexToBytes(generatorFixture.Q1)), 78 | H: HGenerators 79 | } 80 | 81 | // mock random number generator (for proof generation) 82 | const MOCK_RNG_SEED = mockedRngFixture.seed; 83 | const seeded_random_scalars = (SEED: Uint8Array, count: number): FrScalar[] => { 84 | const dst = Buffer.from(bbs.cs.ciphersuite_id + "MOCK_RANDOM_SCALARS_DST_", "utf8"); 85 | const out_len = bbs.cs.expand_len * count; 86 | const v = bbs.cs.expand_message(SEED, dst, out_len); 87 | const r: FrScalar[] = []; 88 | for (let i = 0; i < count; i++) { 89 | const start_idx = i * bbs.cs.expand_len; 90 | const end_idx = (i + 1) * bbs.cs.expand_len; 91 | r.push(os2ip(v.slice(start_idx, end_idx))); 92 | } 93 | return r; 94 | } 95 | 96 | test(`mocked_calculate_random_scalars (${cs})`, async () => { 97 | const expected = mockedRngFixture.mockedScalars.map(v => FrScalar.create(BigInt('0x' + v))); 98 | const actual = seeded_random_scalars(hexToBytes(MOCK_RNG_SEED), expected.length); 99 | expect(expected.length).toBe(actual.length); 100 | for (let i = 0; i < expected.length; i++) { 101 | expect(expected[i].equals(actual[i])).toBe(true); 102 | }; 103 | }); 104 | 105 | test(`hash_to_scalar (${cs})`, async () => { 106 | const dst = hexToBytes(h2sFixture.dst); 107 | const scalar = bbs.hash_to_scalar(hexToBytes(h2sFixture.message), dst); 108 | const expected = FrScalar.create(BigInt('0x' + h2sFixture.scalar)); 109 | expect(scalar.equals(expected)).toBe(true); 110 | }); 111 | 112 | test(`MapMessageToScalarAsHash (${cs})`, async () => { 113 | const dst = hexToBytes(mmtsahFixture.dst); 114 | for (let i = 0; i < mmtsahFixture.cases.length; i++) { 115 | const scalar = bbs.MapMessageToScalarAsHash(hexToBytes(mmtsahFixture.cases[i].message), dst); 116 | const expected = FrScalar.create(BigInt('0x' + mmtsahFixture.cases[i].scalar)); 117 | expect(scalar.equals(expected)).toBe(true); 118 | } 119 | }); 120 | 121 | test(`create_generators (${cs})`, async () => { 122 | const actualGenerators = await bbs.create_generators(generatorFixture.MsgGenerators.length); 123 | if (!generators.Q1.equals(actualGenerators.Q1)) { throw `invalid Q1 generator; expected: ${generators.Q1}, actual: ${actualGenerators.Q1}`; } 124 | generators.H.forEach((H, idx, a) => { 125 | if (!H.equals(actualGenerators.H[idx])) { throw `invalid H${idx} generator; expected: ${H}, actual: ${actualGenerators.H[idx]}`; } 126 | }) 127 | // check base point 128 | const P1 = G1Point.fromOctets(hexToBytes(generatorFixture.P1)); 129 | if (!P1.equals(bbs.cs.P1)) { throw `invalid base point; expected: ${P1}, actual: ${bbs.cs.P1}`; } 130 | }); 131 | 132 | test(`keypair (${cs})`, async () => { 133 | const SK = bbs.KeyGen(hexToBytes(keypairFixture.keyMaterial), hexToBytes(keypairFixture.keyInfo)); 134 | const expected = FrScalar.create(BigInt('0x' + keypairFixture.keyPair.secretKey)); 135 | expect(SK.equals(expected)).toBe(true); 136 | const PK = bbs.SkToPk(SK); 137 | const pkOctets = PK.toOctets(); 138 | const expectedPK = hexToBytes(keypairFixture.keyPair.publicKey); 139 | expect(pkOctets).toEqual(expectedPK); 140 | }); 141 | 142 | for (let i = 0; i < signaturesFixture.length; i++) { 143 | test(`signature${String(i + 1).padStart(3, '0')}: ${signaturesFixture[i].caseName} (${cs})`, async () => { 144 | 145 | const PK = bbs.octets_to_pubkey(hexToBytes(signaturesFixture[i].signerKeyPair.publicKey)); 146 | const header = hexToBytes(signaturesFixture[i].header); 147 | const msg = signaturesFixture[i].messages.map(v => bbs.MapMessageToScalarAsHash(hexToBytes(v))); 148 | const generators = { 149 | Q1: G1Point.fromOctets(hexToBytes(generatorFixture.Q1)), 150 | H: HGenerators.slice(0, signaturesFixture[i].messages.length) 151 | } 152 | if (signaturesFixture[i].result.valid) { 153 | // recreate the signature 154 | const SK = FrScalar.create(BigInt('0x' + signaturesFixture[i].signerKeyPair.secretKey)); 155 | const signature = bbs.Sign(SK, PK, header, msg, generators); 156 | const actualSignature = bytesToHex(signature); 157 | if (actualSignature != signaturesFixture[i].signature) { 158 | throw `invalid signature ${i}; expected: ${signaturesFixture[i].signature}, actual: ${actualSignature}`; 159 | } 160 | 161 | bbs.Verify(PK, hexToBytes(signaturesFixture[i].signature), header, msg, generators); 162 | } else { 163 | // validation of the non-valid signature should fail 164 | let failed = false; 165 | try { 166 | bbs.Verify(PK, hexToBytes(signaturesFixture[i].signature), header, msg, generators); 167 | } catch (e) { 168 | failed = true; 169 | } 170 | if (!failed) throw `signature ${i} should be invalid`; 171 | } 172 | }) 173 | }; 174 | 175 | for (let i = 0; i < proofsFixture.length; i++) { 176 | test(`proof${String(i + 1).padStart(3, '0')}: ${proofsFixture[i].caseName} (${cs})`, async () => { 177 | const disclosed_indexes = proofsFixture[i].disclosedIndexes; 178 | const disclosed_messages = proofsFixture[i].messages.filter((v,i,a) => disclosed_indexes.includes(i)).map(v => bbs.MapMessageToScalarAsHash(hexToBytes(v))); 179 | 180 | let failed = false; 181 | try { 182 | // TODO: implement the proof generation too, now that we have mockup random values 183 | bbs.ProofVerify( 184 | bbs.octets_to_pubkey(hexToBytes(proofsFixture[i].signerPublicKey)), 185 | hexToBytes(proofsFixture[i].proof), 186 | hexToBytes(proofsFixture[i].header), 187 | hexToBytes(proofsFixture[i].presentationHeader), 188 | disclosed_messages, 189 | generators, 190 | disclosed_indexes); 191 | } catch (e) { 192 | failed = true; 193 | } 194 | if (proofsFixture[i].result.valid && failed) throw `proof ${i + 1} should have been valid`; // proof files suffix are 1-based 195 | }) 196 | }; 197 | }); -------------------------------------------------------------------------------- /src/bbs.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as utils from './utils'; 5 | import { G1Point, G2Point, FrScalar, checkPairingIsIdentity, Point } from './math'; 6 | import { Ciphersuite, BLS12_381_SHAKE_256_Ciphersuite } from './ciphersuite'; 7 | import * as crypto from 'crypto'; 8 | import { concat, i2osp } from './utils'; 9 | 10 | // NOTE: we don't check for the max array lengths, because the JavaScript max is 2^53-1, smaller than the spec's 2^64-1 11 | 12 | export interface Generators { 13 | Q1: G1Point, 14 | H: G1Point[] 15 | } 16 | 17 | export interface BBSSignature { 18 | A: G1Point, 19 | e: FrScalar 20 | } 21 | 22 | export interface BBSProof { 23 | Abar: G1Point, 24 | Bbar: G1Point, 25 | D: G1Point, 26 | eHat: FrScalar, 27 | r1Hat: FrScalar, 28 | r3Hat: FrScalar, 29 | mHat: FrScalar[], 30 | c: FrScalar 31 | } 32 | 33 | type SerializeInput = G1Point | G2Point | FrScalar | string | number | Uint8Array; 34 | 35 | export class BBS { 36 | cs: Ciphersuite; 37 | 38 | constructor(cs = BLS12_381_SHAKE_256_Ciphersuite) { 39 | this.cs = cs; 40 | } 41 | 42 | // 43 | // 3.4 Key Generation Operations 44 | // 45 | 46 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-secret-key 47 | KeyGen(key_material: Uint8Array, 48 | key_info: Uint8Array = new Uint8Array(), 49 | key_dst: Uint8Array = Buffer.from(this.cs.ciphersuite_id + "KEYGEN_DST_", 'utf-8')): FrScalar { 50 | if (key_material.length < 32) { 51 | throw "key_material too short, MUST be at least 32 bytes"; 52 | } 53 | if (key_info.length > 65535) { 54 | throw "key_material too short, MUST be at least 32 bytes"; 55 | } 56 | const derive_input = utils.concat(key_material, utils.i2osp(key_info.length, 2), key_info); 57 | const SK = this.hash_to_scalar(derive_input, key_dst); 58 | return SK; 59 | } 60 | 61 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-public-key 62 | SkToPk(SK: FrScalar): G2Point { 63 | return G2Point.Base.mul(SK); 64 | } 65 | 66 | // 67 | // 3.6. Core Operations (the functions implement both the BBS interface 68 | // and the core operations) 69 | // 70 | 71 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-signature-generation-sign 72 | Sign(SK: FrScalar, PK: G2Point, header: Uint8Array, messages: FrScalar[], generators: Generators): Uint8Array { 73 | utils.log("Sign"); 74 | 75 | if (messages.length !== generators.H.length) { 76 | throw "msg and H should have the same length"; 77 | } 78 | const L = messages.length; 79 | const domain = this.calculate_domain(PK, generators, header); 80 | utils.log("domain", domain); 81 | const e = this.hash_to_scalar(this.serialize([SK, ...messages, domain])); 82 | utils.log("e", e); 83 | 84 | // B = P1 + Q_1 * domain + H_1 * msg_1 + ... + H_L * msg_L 85 | let B = this.cs.P1; 86 | B = B.add(generators.Q1.mul(domain)); 87 | for (let i = 0; i < L; i++) { 88 | B = B.add(generators.H[i].mul(messages[i])); 89 | } 90 | utils.log("B", B); 91 | 92 | // A = B * (1 / (SK + e)) 93 | const A = B.mul(SK.add(e).inv()); 94 | utils.log("A", A); 95 | 96 | // signature_octets = signature_to_octets(A, e, s) 97 | const signature_octets = this.signature_to_octets({ A: A, e: e }); 98 | return signature_octets; 99 | } 100 | 101 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-signature-verification-veri 102 | Verify(PK: G2Point, signature: Uint8Array, header: Uint8Array, messages: FrScalar[], generators: Generators): void { 103 | utils.log("Verify"); 104 | 105 | const sig = this.octets_to_signature(signature); 106 | const L = messages.length; 107 | const domain = this.calculate_domain(PK, generators, header); 108 | utils.log("domain", domain); 109 | 110 | // B = P1 + Q1 * domain + H_1 * msg_1 + ... + H_L * msg_L 111 | let B = this.cs.P1; 112 | B = B.add(generators.Q1.mul(domain)); 113 | for (let i = 0; i < L; i++) { 114 | B = B.add(generators.H[i].mul(messages[i])); 115 | } 116 | utils.log("B", B); 117 | 118 | // check that e(A, W + P2 * e) * e(B, -P2) == Identity_GT 119 | if (!checkPairingIsIdentity(sig.A, PK.add(G2Point.Base.mul(sig.e)), 120 | B, G2Point.Base.neg())) { 121 | throw "Invalid signature (pairing)"; 122 | } 123 | } 124 | 125 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-proof-generation-proofgen 126 | ProofGen(PK: G2Point, signature: Uint8Array, header: Uint8Array, ph: Uint8Array, messages: FrScalar[], generators: Generators, disclosed_indexes: number[]): Uint8Array { 127 | utils.log("ProofGen"); 128 | const L = messages.length; 129 | const R = disclosed_indexes.length; 130 | const U = L - R; 131 | const signature_result = this.octets_to_signature(signature); 132 | 133 | const iSet = new Set(disclosed_indexes); 134 | const i = Array.from(iSet).sort((a, b) => a - b); 135 | utils.log("i", i); 136 | const jSet = new Set(Array.from({ length: L }, (e, i) => i)); 137 | iSet.forEach(v => jSet.delete(v)); 138 | const j = Array.from(jSet).sort((a, b) => a - b); 139 | utils.log("j", j); 140 | 141 | const domain = this.calculate_domain(PK, generators, header) 142 | utils.log("domain", domain); 143 | const scalars = this.calculate_random_scalars(5 + U); 144 | let index = 0; 145 | const r1 = scalars[index++]; 146 | utils.log("r1", r1); 147 | const r2 = scalars[index++]; 148 | utils.log("r2", r2); 149 | const eTilda = scalars[index++]; 150 | utils.log("eTilda", eTilda); 151 | const r1Tilda = scalars[index++]; 152 | utils.log("r1Tilda", r1Tilda); 153 | const r3Tilda = scalars[index++]; 154 | utils.log("r3Tilda", r3Tilda); 155 | 156 | const mTilda = new Array(U).fill(0n).map((v, i, a) => scalars[index + i]); 157 | utils.log("mTilda", mTilda); 158 | 159 | // B = P1 + Q_1 * domain + H_1 * msg_1 + ... + H_L * msg_L 160 | let B = this.cs.P1; 161 | B = B.add(generators.Q1.mul(domain)); 162 | for (let k = 0; k < L; k++) { 163 | B = B.add(generators.H[k].mul(messages[k])); 164 | } 165 | utils.log("B", B); 166 | 167 | const D = B.mul(r2); // D = B * r2 168 | const Abar = signature_result.A.mul(r1.mul(r2)); // Abar = A * (r1 * r2) 169 | const Bbar = D.mul(r1).add(Abar.mul(signature_result.e).neg()); // Bbar = D * r1 - Abar * e 170 | 171 | // T1 = Abar * e~ + D * r1~ 172 | let T1 = Abar.mul(eTilda).add(D.mul(r1Tilda)); 173 | // T2 = D * r3~ + H_j1 * m~_j1 + ... + H_jU * m~_jU 174 | let T2 = D.mul(r3Tilda); 175 | for (let k = 0; k < U; k++) { 176 | T2 = T2.add(generators.H[j[k]].mul(mTilda[k])); 177 | } 178 | 179 | // c = calculate_challenge(Abar, Bbar, D, T1, T2, (i1, ..., iR), (m_i1, ..., m_iR), domain, ph) 180 | const disclosedMsg = utils.filterDisclosedMessages(messages, disclosed_indexes); 181 | const c = this.calculate_challenge(Abar, Bbar, D, T1, T2, i, disclosedMsg, domain, ph); 182 | 183 | // r3 = r2^-1 (mod r) 184 | const r3 = r2.inv(); 185 | // e^ = e~ + e_value * challenge 186 | const eHat = eTilda.add(signature_result.e.mul(c)); 187 | // r1^ = r1~ - r1 * challenge 188 | const r1Hat = r1Tilda.add(r1.mul(c).neg()); 189 | // r3^ = r3~ - r3 * challenge 190 | const r3Hat = r3Tilda.add(r3.mul(c).neg()); 191 | const mHat: FrScalar[] = []; 192 | for (let k = 0; k < U; k++) { 193 | mHat[k] = messages[j[k]].mul(c).add(mTilda[k]); // m^_j = m~_j + m_j * c (mod r) 194 | } 195 | 196 | const proof = { Abar: Abar, Bbar: Bbar, D: D, eHat: eHat, r1Hat: r1Hat, r3Hat: r3Hat, mHat: mHat, c: c } 197 | return this.proof_to_octets(proof); 198 | } 199 | 200 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-proof-verification-proofver 201 | ProofVerify(PK: G2Point, encodedProof: Uint8Array, header: Uint8Array, ph: Uint8Array, disclosed_messages: FrScalar[], extGenerators: Generators, RevealedIndexes: number[]): void { 202 | utils.log("ProofVerify"); 203 | const proof = this.octets_to_proof(encodedProof); 204 | utils.log("proof", proof); 205 | const R = RevealedIndexes.length; 206 | const U = proof.mHat.length; 207 | const L = R + U; 208 | 209 | const iSet = new Set(RevealedIndexes); 210 | const i = Array.from(iSet).sort((a, b) => a - b); 211 | utils.log("i", i); 212 | const jSet = new Set(Array.from({ length: L }, (e, i) => i)); 213 | iSet.forEach(v => jSet.delete(v)); 214 | const j = Array.from(jSet).sort((a, b) => a - b); 215 | utils.log("j", j); 216 | 217 | // copy and trim generators 218 | if (extGenerators.H.length < L) throw new Error("Not enough generators provided"); 219 | const generators: Generators = { 220 | H: extGenerators.H.slice(0, L), 221 | Q1: extGenerators.Q1, 222 | } 223 | const domain = this.calculate_domain(PK, generators, header); 224 | utils.log("domain", domain); 225 | 226 | // T1 = Bbar * c + Abar * e^ + D * r1^ 227 | let T1 = proof.Bbar.mul(proof.c).add(proof.Abar.mul(proof.eHat)).add(proof.D.mul(proof.r1Hat)); 228 | // Bv = P1 + Q_1 * domain + H_i1 * msg_i1 + ... + H_iR * msg_iR 229 | let Bv = this.cs.P1; 230 | Bv = Bv.add(generators.Q1.mul(domain)); 231 | for (let k = 0; k < R; k++) { 232 | Bv = Bv.add(generators.H[i[k]].mul(disclosed_messages[k])); 233 | } 234 | // T2 = Bv * c + D * r3^ + H_j1 * m^_j1 + ... + H_jU * m^_jU 235 | let T2 = Bv.mul(proof.c).add(proof.D.mul(proof.r3Hat)); 236 | for (let k = 0; k < U; k++) { 237 | T2 = T2.add(generators.H[j[k]].mul(proof.mHat[k])); 238 | } 239 | 240 | // const iZeroBased = i.map(v => v - 1); // spec's fixtures assume these are 0-based 241 | const cv = this.calculate_challenge(proof.Abar, proof.Bbar, proof.D, T1, T2, i, disclosed_messages, domain, ph); 242 | utils.log("cv", cv); 243 | 244 | if (!proof.c.equals(cv)) { 245 | utils.log("c", proof.c); 246 | utils.log("cv", cv); 247 | throw "Invalid proof (cv)"; 248 | } 249 | 250 | if (!checkPairingIsIdentity(proof.Abar, PK, 251 | proof.Bbar, G2Point.Base.neg())) { 252 | throw "Invalid proof (pairing)" 253 | } 254 | } 255 | 256 | // 257 | // 4. Utility operations 258 | // 259 | 260 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-random-scalars 261 | calculate_random_scalars(count: number): FrScalar[] { 262 | const random_scalars: FrScalar[] = []; 263 | for (let i = 0; i < count; i++) { 264 | random_scalars.push(utils.os2ip(crypto.randomBytes(this.cs.expand_len))); 265 | } 266 | return random_scalars; 267 | } 268 | 269 | // implements the hash_to_generators operation 270 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-generators-calculation 271 | async create_generators(length: number): Promise { 272 | const count = length + 1; // Q1, and generators 273 | const seed_dst = Buffer.from(this.cs.ciphersuite_id + 'SIG_GENERATOR_SEED_', 'utf-8'); 274 | let v = this.cs.expand_message(this.cs.generator_seed, seed_dst, this.cs.expand_len); 275 | const generators: G1Point[] = []; 276 | for (let i = 1; i <= count; i++) { 277 | v = this.cs.expand_message(utils.concat(v, utils.i2osp(i, 8)), seed_dst, this.cs.expand_len); 278 | const generator = await this.cs.hash_to_curve_g1(v, this.cs.ciphersuite_id + 'SIG_GENERATOR_DST_'); 279 | generators.push(generator); 280 | utils.log("generator " + i + ": ", utils.bytesToHex(generator.toOctets())); 281 | } 282 | return { 283 | Q1: generators[0], 284 | H: generators.slice(1) 285 | }; 286 | } 287 | 288 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-message-to-scalar-as-hash 289 | MapMessageToScalarAsHash(msg: Uint8Array, dst: Uint8Array = Buffer.from(this.cs.ciphersuite_id + "MAP_MSG_TO_SCALAR_AS_HASH_", "utf8")): FrScalar { 290 | if (dst.length > 255) { 291 | throw "dst too long"; 292 | } 293 | return this.hash_to_scalar(msg, dst); 294 | } 295 | 296 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-hash-to-scalar 297 | hash_to_scalar(msg_octets: Uint8Array, dst: Uint8Array = Buffer.from(this.cs.ciphersuite_id + "H2S_", "utf8")): FrScalar { 298 | const uniform_bytes = this.cs.expand_message(msg_octets, dst, this.cs.expand_len); 299 | const hashed_scalar = utils.os2ip(uniform_bytes); 300 | return hashed_scalar; 301 | } 302 | 303 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-domain-calculation 304 | calculate_domain(PK: G2Point, generators: Generators, header: Uint8Array): FrScalar { 305 | const dom_octs = utils.concat( 306 | this.serialize([PK, generators.H.length, generators.Q1, ...generators.H]), 307 | Buffer.from(this.cs.ciphersuite_id, 'utf8')); 308 | const dom_input = utils.concat(dom_octs, utils.i2osp(header.length, 8), header); 309 | utils.log("dom_input", dom_input); 310 | const domain = this.hash_to_scalar(dom_input); 311 | return domain; 312 | } 313 | 314 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-challenge-calculation 315 | calculate_challenge(Abar: G1Point, Bbar: G1Point, D: G1Point, T1: G1Point, T2: G1Point, i_array: number[], msg_array: FrScalar[], domain: FrScalar, ph: Uint8Array): FrScalar { 316 | if (i_array.length !== msg_array.length) { 317 | throw "i_array and msg_array should have the same length"; 318 | } 319 | const challenge = this.hash_to_scalar( 320 | utils.concat( 321 | this.serialize([ 322 | // R 323 | i_array.length, 324 | // i_1, msg_1, i_2, msg_2, ... 325 | ...i_array.flatMap((val, idx) => [val, msg_array[idx]]), 326 | Abar, Bbar, D, T1, T2, domain]), 327 | utils.i2osp(ph.length, 8), 328 | ph)); 329 | return challenge; 330 | } 331 | 332 | // 333 | // 4.2.4 Serialization 334 | // 335 | 336 | private serializeInputToBytes(data: SerializeInput): Uint8Array { 337 | 338 | if (typeof data === 'string') { 339 | return Buffer.from(data, 'utf-8'); 340 | } else if (data instanceof Point) { 341 | return data.toOctets(); 342 | } else if (data instanceof FrScalar) { 343 | return i2osp(data.scalar, this.cs.octet_scalar_length); 344 | } else if (typeof data === 'number') { 345 | return i2osp(data, 8); 346 | } else if (data instanceof Uint8Array) { 347 | return concat(i2osp(data.length, 8), data); 348 | } else { 349 | throw "invalid serialize type"; 350 | } 351 | } 352 | 353 | 354 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-serialize 355 | serialize(input_array: SerializeInput[]): Uint8Array { 356 | let octets_result = new Uint8Array(); 357 | input_array.forEach(v => { 358 | octets_result = utils.concat(octets_result, this.serializeInputToBytes(v)); 359 | }); 360 | return octets_result; 361 | } 362 | 363 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-signature-to-octets 364 | signature_to_octets(signature: BBSSignature): Uint8Array { 365 | return this.serialize([signature.A, signature.e]); 366 | } 367 | 368 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-octets-to-signature 369 | octets_to_signature(signature_octets: Uint8Array): BBSSignature { 370 | if (signature_octets.length !== this.cs.octet_point_length + this.cs.octet_scalar_length) { 371 | throw "Invalid signature_octets length"; 372 | } 373 | const A_octets = signature_octets.slice(0, this.cs.octet_point_length); 374 | const A = G1Point.fromOctets(A_octets); 375 | if (A.equals(G1Point.Identity)) { 376 | throw "Invalid A"; 377 | } 378 | let index = this.cs.octet_point_length; 379 | const e = utils.os2ip(signature_octets.slice(index, index + this.cs.octet_scalar_length), true); 380 | return { A: A, e: e } 381 | } 382 | 383 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-proof-to-octets 384 | proof_to_octets(proof: BBSProof): Uint8Array { 385 | const serialized = this.serialize([proof.Abar, proof.Bbar, proof.D, proof.eHat, proof.r1Hat, proof.r3Hat, ...proof.mHat, proof.c]); 386 | return serialized; 387 | } 388 | 389 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-octets-to-proof 390 | octets_to_proof(proof_octets: Uint8Array): BBSProof { 391 | const proof_len_floor = 2 * this.cs.octet_point_length + 4 * this.cs.octet_scalar_length; 392 | if (proof_octets.length < proof_len_floor) { 393 | throw "invalid proof (length)"; 394 | } 395 | 396 | let index = 0; 397 | const Abar = G1Point.fromOctets(proof_octets.slice(index, index + this.cs.octet_point_length)); 398 | index += this.cs.octet_point_length; 399 | const Bbar = G1Point.fromOctets(proof_octets.slice(index, index + this.cs.octet_point_length)); 400 | index += this.cs.octet_point_length; 401 | const D = G1Point.fromOctets(proof_octets.slice(index, index + this.cs.octet_point_length)); 402 | index += this.cs.octet_point_length; 403 | 404 | const eHat = utils.os2ip(proof_octets.slice(index, index + this.cs.octet_scalar_length), true); 405 | index += this.cs.octet_scalar_length; 406 | const r1Hat = utils.os2ip(proof_octets.slice(index, index + this.cs.octet_scalar_length), true); 407 | index += this.cs.octet_scalar_length; 408 | const r3Hat = utils.os2ip(proof_octets.slice(index, index + this.cs.octet_scalar_length), true); 409 | index += this.cs.octet_scalar_length; 410 | 411 | const mHat: FrScalar[] = []; 412 | const end_index = proof_octets.length - this.cs.octet_scalar_length; 413 | while (index < end_index) { 414 | const msg = utils.os2ip(proof_octets.slice(index, index + this.cs.octet_scalar_length), true); 415 | index += this.cs.octet_scalar_length; 416 | mHat.push(msg); 417 | } 418 | const c = utils.os2ip(proof_octets.slice(index, index + this.cs.octet_scalar_length), true); 419 | 420 | return { Abar: Abar, Bbar: Bbar, D: D, eHat: eHat, r1Hat: r1Hat, r3Hat: r3Hat, mHat: mHat, c: c } 421 | } 422 | 423 | // https://identity.foundation/bbs-signature/draft-irtf-cfrg-bbs-signatures.html#name-octets-to-public-key 424 | octets_to_pubkey(PK: Uint8Array, skipPKValidation: boolean = false): G2Point { 425 | const W = G2Point.fromOctets(PK, true /* check subgroup */); 426 | if (!skipPKValidation) { 427 | if (W.equals(G2Point.Identity)) { 428 | throw "Invalid public key"; 429 | } 430 | } 431 | return W; 432 | } 433 | } 434 | --------------------------------------------------------------------------------