├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── lib ├── bip340.dart └── src │ ├── bip340.dart │ ├── helpers.dart │ └── hex.dart ├── pubspec.yaml └── test ├── key_test.dart └── vectors_test.dart /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' # tag pattern on pub.dev: 'v{{version}' 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write # required for authentication using OIDC 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dart-lang/setup-dart@v1 16 | - name: dependencies 17 | run: dart pub get 18 | - name: publish 19 | run: dart pub publish --force 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | pubspec.lock 3 | .packages 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 fiatjaf@alhur.es 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=dart 2 | FMT=format 3 | 4 | default: fmt 5 | 6 | fmt: 7 | $(CC) $(FMT) . 8 | $(CC) analyze . 9 | 10 | check: 11 | $(CC) example/example.dart -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | bip340 [![Pub](https://img.shields.io/pub/v/bip340.svg?style=flat)](https://pub.dev/packages/bip340) 4 | ====== 5 | 6 | Implements basic signing and verification functions for the [BIP-340](https://bips.xyz/340) Schnorr Signature Scheme. 7 | 8 | It passes the [tests](https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv) attached to the BIP (`dart test` to run that), but no guarantees are made of anything and _this is not safe cryptography_, do not use to store Bitcoins. 9 | 10 | Provides these functions: 11 | 12 | 1. `String sign(String privateKey, String message, String aux)` 13 | 14 | Generates a schnorr signature using the BIP-340 scheme. 15 | 16 | * `privateKey` must be 32-bytes lowercase hex-encoded, i.e., 64 characters. 17 | * `message` must also be lowercase hex-encoded (a hash of the _actual_ message). 18 | * `aux ` must be 32-bytes random bytes, generated at signature time. 19 | * Returns the signature as a string of 64 bytes hex-encoded, i.e., 128 characters. 20 | 21 | 2. `bool verify(String publicKey, String message, String signature)` 22 | 23 | Verifies a schnorr signature using the BIP-340 scheme. 24 | 25 | * `publicKey` must be 32-bytes lowercase hex-encoded, i.e., 64 characters (if you have a pubkey with 33 bytes just remove the first one). 26 | * `message` must also be lowercase hex-encoded (a hash of the _actual_ message). 27 | * `signature` must be 64-bytes lowercase hex-encoded, i.e., 128 characters. 28 | * Returns true if the signature is valid, false otherwise. 29 | 30 | 3. `String getPublicKey(String privateKey)` 31 | 32 | Produces the public key from a private key 33 | 34 | * `privateKey` must be a 32-bytes hex-encoded string, i.e. 64 characters. 35 | * Returns a public key as also 32-bytes hex-encoded. 36 | 37 | Made for integration with [Nostr](https://github.com/fiatjaf/nostr). 38 | -------------------------------------------------------------------------------- /lib/bip340.dart: -------------------------------------------------------------------------------- 1 | export 'src/bip340.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/bip340.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'package:pointycastle/ecc/api.dart'; 3 | import './helpers.dart'; 4 | import './hex.dart'; 5 | 6 | /// Generates a schnorr signature using the BIP-340 scheme. 7 | /// 8 | /// privateKey must be 32-bytes hex-encoded, i.e., 64 characters. 9 | /// message must also be 32-bytes hex-encoded (a hash of the _actual_ message). 10 | /// aux must be 32-bytes random bytes, generated at signature time. 11 | /// It returns the signature as a string of 64 bytes hex-encoded, i.e., 128 characters. 12 | /// For more information on BIP-340 see bips.xyz/340. 13 | String sign(String privateKey, String message, String aux) { 14 | final List bmessage = hex.decode(message, 0, 64); 15 | final List baux = hex.decode(aux.padLeft(64, '0'), 0, 64); 16 | final BigInt d0 = BigInt.parse(privateKey, radix: 16); 17 | 18 | if ((d0 < BigInt.one) || (d0 > (secp256k1.n - BigInt.one))) { 19 | throw new Error(); 20 | } 21 | 22 | final ECPoint P = (secp256k1.G * d0)!; 23 | 24 | BigInt d; 25 | if (P.y!.toBigInteger()! % BigInt.two == BigInt.zero) { 26 | // even 27 | d = d0; 28 | } else { 29 | d = secp256k1.n - d0; 30 | } 31 | 32 | if (baux.length != 32) { 33 | throw new Error(); 34 | } 35 | 36 | final t = d ^ bigFromBytes(taggedHash("BIP0340/aux", baux)); 37 | 38 | final BigInt k0 = bigFromBytes( 39 | taggedHash( 40 | "BIP0340/nonce", 41 | bigToBytes(t) + bigToBytes(P.x!.toBigInteger()!) + bmessage, 42 | ), 43 | ) % 44 | secp256k1.n; 45 | 46 | if (k0.sign == 0) { 47 | throw new Error(); 48 | } 49 | 50 | final R = (secp256k1.G * k0)!; 51 | 52 | BigInt k; 53 | if (R.y!.toBigInteger()! % BigInt.two == BigInt.zero) { 54 | // is even 55 | k = k0; 56 | } else { 57 | k = secp256k1.n - k0; 58 | } 59 | 60 | final rX = bigToBytes(R.x!.toBigInteger()!); 61 | final e = getE(P, rX, bmessage); 62 | 63 | final List signature = rX + bigToBytes((k + e * d) % secp256k1.n); 64 | 65 | return hex.encode(signature); 66 | } 67 | 68 | /// Verifies a schnorr signature using the BIP-340 scheme. 69 | /// 70 | /// publicKey must be 32-bytes hex-encoded, i.e., 64 characters 71 | /// (if you have a pubkey with 33 bytes just remove the first one). 72 | /// message must also be 32-bytes hex-encoded (a hash of the _actual_ message). 73 | /// signature must be 64-bytes hex-encoded, i.e., 128 characters. 74 | /// It returns true if the signature is valid, false otherwise. 75 | /// For more information on BIP-340 see bips.xyz/340. 76 | bool verify(String publicKey, String message, String signature) { 77 | ECPoint point; 78 | try { 79 | point = publicKeyToPoint(publicKey); 80 | } catch (err) { 81 | return false; 82 | } 83 | return verifyWithPoint(point, message, signature); 84 | } 85 | 86 | bool verifyWithPoint(ECPoint P, String message, String signature) { 87 | final List bmessage = hex.decode(message, 0, 64); 88 | 89 | // signature = signature.padLeft(128, '0'); 90 | final r = hex.decode(signature, 0, 64); 91 | final r_num = BigInt.parse(signature.substring(0, 64), radix: 16); 92 | final s_num = BigInt.parse(signature.substring(64, 128), radix: 16); 93 | if (r_num >= curveP || s_num >= secp256k1.n) { 94 | return false; 95 | } 96 | 97 | // not sure what these things mean 98 | BigInt e = getE(P, r, bmessage); 99 | ECPoint sG = (secp256k1.G * s_num)!; 100 | ECPoint eP_ = (P * e)!; 101 | BigInt ePy = curveP - eP_.y!.toBigInteger()!; 102 | ECPoint eP = secp256k1.curve.createPoint(eP_.x!.toBigInteger()!, ePy); 103 | 104 | // R is something important 105 | final ECPoint R = (sG + eP)!; 106 | if (R.isInfinity) { 107 | return false; 108 | } 109 | 110 | // now that we have R we get its coords 111 | final Rx = R.x!.toBigInteger()!; 112 | final Ry = R.y!.toBigInteger(); 113 | 114 | // and we them in these checks which I don't understand 115 | if ((Rx.sign == 0 && Ry!.sign == 0) || 116 | (Ry! % BigInt.two != BigInt.zero /* is odd */) || 117 | (Rx != r_num)) { 118 | return false; 119 | } 120 | 121 | // the checks passed, it means the signature is good 122 | return true; 123 | } 124 | 125 | /// Produces the public key from a private key 126 | /// 127 | /// Takes privateKey, a 32-bytes hex-encoded string, i.e. 64 characters. 128 | /// Returns a public key as also 32-bytes hex-encoded. 129 | String getPublicKey(String privateKey) { 130 | final d0 = BigInt.parse(privateKey, radix: 16); 131 | ECPoint P = (secp256k1.G * d0)!; 132 | return P.x!.toBigInteger()!.toRadixString(16).padLeft(64, "0"); 133 | } 134 | -------------------------------------------------------------------------------- /lib/src/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'dart:convert'; 3 | import 'package:bip340/src/hex.dart'; 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:pointycastle/ecc/api.dart'; 6 | 7 | List taggedHash(String tag, List msg) { 8 | var tagHash = sha256.convert(utf8.encode(tag)).bytes; 9 | return sha256.convert(tagHash + tagHash + msg).bytes; 10 | } 11 | 12 | List bigToBytes(BigInt integer) { 13 | return hex.decode(integer.toRadixString(16).padLeft(64, "0"), 0, 64); 14 | } 15 | 16 | BigInt bigFromBytes(List bytes) { 17 | return BigInt.parse(hex.encode(bytes), radix: 16); 18 | } 19 | 20 | var secp256k1 = ECDomainParameters("secp256k1"); 21 | var curveP = BigInt.parse( 22 | 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 23 | radix: 16); 24 | 25 | // helper methods: 26 | // liftX returns Y for this X 27 | BigInt liftX(BigInt x) { 28 | if (x >= curveP) { 29 | throw new Error(); 30 | } 31 | var ySq = (x.modPow(BigInt.from(3), curveP) + BigInt.from(7)) % curveP; 32 | var y = ySq.modPow((curveP + BigInt.one) ~/ BigInt.from(4), curveP); 33 | if (y.modPow(BigInt.two, curveP) != ySq) { 34 | throw new Error(); 35 | } 36 | return y % BigInt.two == BigInt.zero /* even */ ? y : curveP - y; 37 | } 38 | 39 | // this one I don't know what it means 40 | BigInt getE(ECPoint P, List rX, List m) { 41 | return bigFromBytes( 42 | taggedHash( 43 | "BIP0340/challenge", 44 | rX + bigToBytes(P.x!.toBigInteger()!) + m, 45 | ), 46 | ) % 47 | secp256k1.n; 48 | } 49 | 50 | ECPoint publicKeyToPoint(String publicKey) { 51 | // turn public key into a point (we only get y, but we find out the y) 52 | BigInt x = BigInt.parse(publicKey, radix: 16); 53 | BigInt y = liftX(x); 54 | return secp256k1.curve.createPoint(x, y); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/hex.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | class hex { 4 | static const String _ALPHABET = "0123456789abcdef"; 5 | 6 | static Uint8List decode(String str, int offset, int length) { 7 | Uint8List result = new Uint8List(length ~/ 2); 8 | final end = offset + length; 9 | for (int i = offset; i < end; i += 2) { 10 | int firstDigit = _ALPHABET.indexOf(str[i]); 11 | int secondDigit = _ALPHABET.indexOf(str[i + 1]); 12 | if (firstDigit == -1 || secondDigit == -1) { 13 | throw new FormatException("Non-hex character detected in $hex"); 14 | } 15 | result[i ~/ 2] = (firstDigit << 4) + secondDigit; 16 | } 17 | return result; 18 | } 19 | 20 | static encode(List bytes) { 21 | StringBuffer buffer = new StringBuffer(); 22 | for (int part in bytes) { 23 | if (part & 0xff != part) { 24 | throw new FormatException("Non-byte integer detected"); 25 | } 26 | buffer.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}'); 27 | } 28 | return buffer.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bip340 2 | version: 0.3.1 3 | description: >- 4 | BIP340 (Schnorr) signature and verification. 5 | repository: https://github.com/fiatjaf/dart-bip340 6 | 7 | environment: 8 | sdk: '>=2.13.0 <4.0.0' 9 | 10 | dependencies: 11 | crypto: ^3.0.6 12 | pointycastle: ^4.0.0 13 | 14 | dev_dependencies: 15 | http: ^1.5.0 16 | csv: ^6.0.0 17 | test: ^1.26.3 18 | -------------------------------------------------------------------------------- /test/key_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'package:bip340/bip340.dart' as bip340; 3 | import 'package:test/test.dart'; 4 | 5 | void main() async { 6 | test('Public key derivation', () { 7 | var privateKey = 8 | "ea7daa0537b93aa3ae4495a274ecc05077e3dc168809d77a7afa4ec1db0fb3bd"; 9 | var publicKey = bip340.getPublicKey(privateKey); 10 | var expected = 11 | "0ba0206887bd61579bf65ec09d7806bea32c64be1cf2c978cf031a811cd238db"; 12 | 13 | expect(publicKey, expected); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/vectors_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:bip340/bip340.dart' as bip340; 4 | import 'package:csv/csv.dart' as csv; 5 | import 'package:test/test.dart'; 6 | 7 | void main() async { 8 | // test bip340 vectors from the bip itself 9 | var data = await http.read(Uri.parse( 10 | "https://raw.githubusercontent.com/bitcoin/bips/afa13249ed45826c2d7086714026c9bc1ccbf963/bip-0340/test-vectors.csv")); 11 | List> vectors = csv.CsvToListConverter().convert( 12 | data.toLowerCase(), 13 | shouldParseNumbers: false, 14 | ); 15 | 16 | for (var i = 1; i < vectors.length; i++) { 17 | group('Vector $i', () { 18 | var line = vectors[i]; 19 | var errorMessage = line[7].length == 0 ? "" : line[7]; 20 | 21 | if (line[3].length > 0) { 22 | test('Sign $errorMessage', () { 23 | var sig = bip340.sign(line[1], line[4], line[3]); 24 | var expected = line[5]; 25 | 26 | expect(sig, expected, reason: "signature doesn't match expected"); 27 | }); 28 | } 29 | 30 | test('Verification $errorMessage', () { 31 | var ok = bip340.verify(line[2], line[4], line[5]); 32 | var expected = line[6]; 33 | 34 | expect(ok.toString(), expected, 35 | reason: "verification result doesn't match expected"); 36 | }); 37 | }); 38 | } 39 | } 40 | --------------------------------------------------------------------------------