├── .github └── workflows │ └── main_ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── cjs │ ├── index.cjs │ └── index.d.ts └── esm │ └── index.js ├── test ├── fixtures.json └── index.js ├── ts_src └── index.ts ├── tsconfig.base.json ├── tsconfig.cjs.json └── tsconfig.json /.github/workflows/main_ci.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 18 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: npm run unit 20 | format: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | registry-url: https://registry.npmjs.org/ 28 | - run: npm ci 29 | - run: npm run standard 30 | gitdiff: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 18 37 | registry-url: https://registry.npmjs.org/ 38 | - run: npm ci 39 | - run: npm run gitdiff 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Cousens 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bip66 2 | 3 | [![NPM Package](https://img.shields.io/npm/v/bip66.svg?style=flat-square)](https://www.npmjs.org/package/bip66) 4 | [![Build Status](https://img.shields.io/travis/bitcoinjs/bip66.svg?branch=master&style=flat-square)](https://travis-ci.org/bitcoinjs/bip66) 5 | 6 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 7 | 8 | Strict DER signature encoding/decoding. 9 | 10 | See [bip66](https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki). 11 | 12 | - This module **works only with [two's complement](https://en.wikipedia.org/wiki/Two's_complement) numbers**. 13 | - BIP66 doesn't check that `r` or `s` are fully valid. 14 | - `check`/`decode` doesn't check that `r` or `s` great than 33 bytes or that this number represent valid point on elliptic curve. 15 | - `encode` doesn't check that `r`/`s` represent valid point on elliptic curve. 16 | 17 | ## Example 18 | 19 | ``` javascript 20 | import * as bip66 from"bip66" 21 | const r = Buffer.from('1ea1fdff81b3a271659df4aad19bc4ef83def389131a36358fe64b245632e777', 'hex') 22 | const s = Buffer.from('29e164658be9ce810921bf81d6b86694785a79ea1e52dbfa5105148d1f0bc1', 'hex') 23 | 24 | // Buffer or UInt8Array can be passed in to the encode/decode functions 25 | const signature = bip66.encode(r, s) 26 | // Uint8Array(69) [ 27 | // 48, 67, 2, 32, 30, 161, 253, 255, 129, 179, 162, 28 | // 113, 101, 157, 244, 170, 209, 155, 196, 239, 131, 222, 29 | // 243, 137, 19, 26, 54, 53, 143, 230, 75, 36, 86, 30 | // 50, 231, 119, 2, 31, 41, 225, 100, 101, 139, 233, 31 | // 206, 129, 9, 33, 191, 129, 214, 184, 102, 148, 120, 32 | // 90, 121, 234, 30, 82, 219, 250, 81, 5, 20, 141, 33 | // 31, 11, 193 34 | // ] 35 | 36 | bip66.decode(signature) 37 | // => { 38 | // r: Uint8Array(32) [ 39 | // 30, 161, 253, 255, 129, 179, 162, 40 | // 113, 101, 157, 244, 170, 209, 155, 41 | // 196, 239, 131, 222, 243, 137, 19, 42 | // 26, 54, 53, 143, 230, 75, 36, 43 | // 86, 50, 231, 119 44 | // ], 45 | // s: Uint8Array(31) [ 46 | // 41, 225, 100, 101, 139, 233, 206, 129, 47 | // 9, 33, 191, 129, 214, 184, 102, 148, 48 | // 120, 90, 121, 234, 30, 82, 219, 250, 49 | // 81, 5, 20, 141, 31, 11, 193 50 | // ] 51 | // } 52 | ``` 53 | 54 | A catch-all exception regex: 55 | ``` javascript 56 | /Expected DER (integer|sequence)|(R|S) value (excessively padded|is negative)|(R|S|DER sequence) length is (zero|too short|too long|invalid)/ 57 | ``` 58 | 59 | ## LICENSE [MIT](LICENSE) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bip66", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "description": "Strict DER signature encoding/decoding.", 6 | "keywords": [ 7 | "bip66", 8 | "bitcoin" 9 | ], 10 | "homepage": "https://github.com/bitcoinjs/bip66", 11 | "bugs": { 12 | "url": "https://github.com/bitcoinjs/bip66/issues" 13 | }, 14 | "license": "MIT", 15 | "author": "Daniel Cousens", 16 | "files": [ 17 | "src" 18 | ], 19 | "main": "src/cjs/index.cjs", 20 | "module": "src/esm/index.js", 21 | "types": "src/cjs/index.d.ts", 22 | "exports": { 23 | ".": { 24 | "types": "./src/cjs/index.d.ts", 25 | "import": "./src/esm/index.js", 26 | "require": "./src/cjs/index.cjs" 27 | } 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/bitcoinjs/bip66.git" 32 | }, 33 | "scripts": { 34 | "build": "npm run clean && tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json; npm run standard -- --fix", 35 | "postbuild": "find src/cjs -type f -name \"*.js\" -exec bash -c 'mv \"$0\" \"${0%.js}.cjs\"' {} \\;", 36 | "standard": "ts-standard --ignore test --ignore src", 37 | "gitdiff": "npm run build && git diff --exit-code", 38 | "clean": "rimraf src", 39 | "coverage": "c8 --check-coverage --branches 100 --functions 100 tape test/*.js", 40 | "lint": "npm run standard", 41 | "test": "npm run lint && npm run unit", 42 | "unit": "tape test/*.js" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^20.14.8", 46 | "c8": "^10.1.2", 47 | "rimraf": "^5.0.7", 48 | "tape": "^5.3.0", 49 | "ts-standard": "^12.0.2", 50 | "typescript": "~5.1.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Reference https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki 3 | // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 4 | // NOTE: SIGHASH byte ignored AND restricted, truncate before use 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.encode = exports.decode = exports.check = void 0; 7 | function check(buffer) { 8 | var ret = internalCheck(buffer); 9 | if (typeof ret === 'string') { 10 | return false; 11 | } 12 | else { 13 | return true; 14 | } 15 | } 16 | exports.check = check; 17 | function internalCheck(buffer) { 18 | if (buffer.length < 8) 19 | return 'DER sequence length is too short'; 20 | if (buffer.length > 72) 21 | return 'DER sequence length is too long'; 22 | if (buffer[0] !== 0x30) 23 | return 'Expected DER sequence (48)'; 24 | if (buffer[1] !== buffer.length - 2) 25 | return 'DER sequence length is invalid'; 26 | if (buffer[2] !== 0x02) 27 | return 'Expected DER integer (2)'; 28 | var lenR = buffer[3]; 29 | if (lenR === 0) 30 | return 'R length is zero'; 31 | if (5 + lenR >= buffer.length) 32 | return 'R length is too long'; 33 | if (buffer[4 + lenR] !== 0x02) 34 | return 'Expected DER integer (2)'; 35 | var lenS = buffer[5 + lenR]; 36 | if (lenS === 0) 37 | return 'S length is zero'; 38 | if ((6 + lenR + lenS) !== buffer.length) 39 | return 'S length is invalid'; 40 | if ((buffer[4] & 0x80) !== 0) 41 | return 'R value is negative'; 42 | if (lenR > 1 && (buffer[4] === 0x00) && (buffer[5] & 0x80) === 0) 43 | return 'R value excessively padded'; 44 | if ((buffer[lenR + 6] & 0x80) !== 0) 45 | return 'S value is negative'; 46 | if (lenS > 1 && (buffer[lenR + 6] === 0x00) && (buffer[lenR + 7] & 0x80) === 0) 47 | return 'S value excessively padded'; 48 | return lenR; 49 | } 50 | function decode(buffer) { 51 | var ret = internalCheck(buffer); 52 | if (typeof ret === 'string') { 53 | throw new Error(ret); 54 | } 55 | // non-BIP66 - extract R, S values 56 | return { 57 | r: buffer.subarray(4, 4 + ret), 58 | s: buffer.subarray(6 + ret) 59 | }; 60 | } 61 | exports.decode = decode; 62 | /* 63 | * Expects r and s to be positive DER integers. 64 | * 65 | * The DER format uses the most significant bit as a sign bit (& 0x80). 66 | * If the significant bit is set AND the integer is positive, a 0x00 is prepended. 67 | * 68 | * Examples: 69 | * 70 | * 0 => 0x00 71 | * 1 => 0x01 72 | * -1 => 0xff 73 | * 127 => 0x7f 74 | * -127 => 0x81 75 | * 128 => 0x0080 76 | * -128 => 0x80 77 | * 255 => 0x00ff 78 | * -255 => 0xff01 79 | * 16300 => 0x3fac 80 | * -16300 => 0xc054 81 | * 62300 => 0x00f35c 82 | * -62300 => 0xff0ca4 83 | */ 84 | function encode(r, s) { 85 | var lenR = r.length; 86 | var lenS = s.length; 87 | if (lenR === 0) 88 | throw new Error('R length is zero'); 89 | if (lenS === 0) 90 | throw new Error('S length is zero'); 91 | if (lenR > 33) 92 | throw new Error('R length is too long'); 93 | if (lenS > 33) 94 | throw new Error('S length is too long'); 95 | if ((r[0] & 0x80) !== 0) 96 | throw new Error('R value is negative'); 97 | if ((s[0] & 0x80) !== 0) 98 | throw new Error('S value is negative'); 99 | if (lenR > 1 && (r[0] === 0x00) && (r[1] & 0x80) === 0) 100 | throw new Error('R value excessively padded'); 101 | if (lenS > 1 && (s[0] === 0x00) && (s[1] & 0x80) === 0) 102 | throw new Error('S value excessively padded'); 103 | var signature = new Uint8Array(6 + lenR + lenS); 104 | // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 105 | signature[0] = 0x30; 106 | signature[1] = signature.length - 2; 107 | signature[2] = 0x02; 108 | signature[3] = r.length; 109 | signature.set(r, 4); 110 | signature[4 + lenR] = 0x02; 111 | signature[5 + lenR] = s.length; 112 | signature.set(s, 6 + lenR); 113 | return signature; 114 | } 115 | exports.encode = encode; 116 | -------------------------------------------------------------------------------- /src/cjs/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare function check(buffer: Uint8Array): boolean; 2 | export declare function decode(buffer: Uint8Array): { 3 | r: Uint8Array; 4 | s: Uint8Array; 5 | }; 6 | export declare function encode(r: Uint8Array, s: Uint8Array): Uint8Array; 7 | -------------------------------------------------------------------------------- /src/esm/index.js: -------------------------------------------------------------------------------- 1 | // Reference https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki 2 | // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 3 | // NOTE: SIGHASH byte ignored AND restricted, truncate before use 4 | export function check(buffer) { 5 | var ret = internalCheck(buffer); 6 | if (typeof ret === 'string') { 7 | return false; 8 | } 9 | else { 10 | return true; 11 | } 12 | } 13 | function internalCheck(buffer) { 14 | if (buffer.length < 8) 15 | return 'DER sequence length is too short'; 16 | if (buffer.length > 72) 17 | return 'DER sequence length is too long'; 18 | if (buffer[0] !== 0x30) 19 | return 'Expected DER sequence (48)'; 20 | if (buffer[1] !== buffer.length - 2) 21 | return 'DER sequence length is invalid'; 22 | if (buffer[2] !== 0x02) 23 | return 'Expected DER integer (2)'; 24 | var lenR = buffer[3]; 25 | if (lenR === 0) 26 | return 'R length is zero'; 27 | if (5 + lenR >= buffer.length) 28 | return 'R length is too long'; 29 | if (buffer[4 + lenR] !== 0x02) 30 | return 'Expected DER integer (2)'; 31 | var lenS = buffer[5 + lenR]; 32 | if (lenS === 0) 33 | return 'S length is zero'; 34 | if ((6 + lenR + lenS) !== buffer.length) 35 | return 'S length is invalid'; 36 | if ((buffer[4] & 0x80) !== 0) 37 | return 'R value is negative'; 38 | if (lenR > 1 && (buffer[4] === 0x00) && (buffer[5] & 0x80) === 0) 39 | return 'R value excessively padded'; 40 | if ((buffer[lenR + 6] & 0x80) !== 0) 41 | return 'S value is negative'; 42 | if (lenS > 1 && (buffer[lenR + 6] === 0x00) && (buffer[lenR + 7] & 0x80) === 0) 43 | return 'S value excessively padded'; 44 | return lenR; 45 | } 46 | export function decode(buffer) { 47 | var ret = internalCheck(buffer); 48 | if (typeof ret === 'string') { 49 | throw new Error(ret); 50 | } 51 | // non-BIP66 - extract R, S values 52 | return { 53 | r: buffer.subarray(4, 4 + ret), 54 | s: buffer.subarray(6 + ret) 55 | }; 56 | } 57 | /* 58 | * Expects r and s to be positive DER integers. 59 | * 60 | * The DER format uses the most significant bit as a sign bit (& 0x80). 61 | * If the significant bit is set AND the integer is positive, a 0x00 is prepended. 62 | * 63 | * Examples: 64 | * 65 | * 0 => 0x00 66 | * 1 => 0x01 67 | * -1 => 0xff 68 | * 127 => 0x7f 69 | * -127 => 0x81 70 | * 128 => 0x0080 71 | * -128 => 0x80 72 | * 255 => 0x00ff 73 | * -255 => 0xff01 74 | * 16300 => 0x3fac 75 | * -16300 => 0xc054 76 | * 62300 => 0x00f35c 77 | * -62300 => 0xff0ca4 78 | */ 79 | export function encode(r, s) { 80 | var lenR = r.length; 81 | var lenS = s.length; 82 | if (lenR === 0) 83 | throw new Error('R length is zero'); 84 | if (lenS === 0) 85 | throw new Error('S length is zero'); 86 | if (lenR > 33) 87 | throw new Error('R length is too long'); 88 | if (lenS > 33) 89 | throw new Error('S length is too long'); 90 | if ((r[0] & 0x80) !== 0) 91 | throw new Error('R value is negative'); 92 | if ((s[0] & 0x80) !== 0) 93 | throw new Error('S value is negative'); 94 | if (lenR > 1 && (r[0] === 0x00) && (r[1] & 0x80) === 0) 95 | throw new Error('R value excessively padded'); 96 | if (lenS > 1 && (s[0] === 0x00) && (s[1] & 0x80) === 0) 97 | throw new Error('S value excessively padded'); 98 | var signature = new Uint8Array(6 + lenR + lenS); 99 | // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 100 | signature[0] = 0x30; 101 | signature[1] = signature.length - 2; 102 | signature[2] = 0x02; 103 | signature[3] = r.length; 104 | signature.set(r, 4); 105 | signature[4 + lenR] = 0x02; 106 | signature[5 + lenR] = s.length; 107 | signature.set(s, 6 + lenR); 108 | return signature; 109 | } 110 | -------------------------------------------------------------------------------- /test/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "DER": "3044022029db2d5f4e1dcc04e19266cce3cb135865784c62ab653b307f0e0bb744f5c2aa022000a97f5826912cac8b44d9f577a26f169a2f8db781f2ddb7de2bc886e93b6844", 5 | "r": "29db2d5f4e1dcc04e19266cce3cb135865784c62ab653b307f0e0bb744f5c2aa", 6 | "s": "00a97f5826912cac8b44d9f577a26f169a2f8db781f2ddb7de2bc886e93b6844" 7 | }, 8 | { 9 | "DER": "304302201ea1fdff81b3a271659df4aad19bc4ef83def389131a36358fe64b245632e777021f29e164658be9ce810921bf81d6b86694785a79ea1e52dbfa5105148d1f0bc1", 10 | "r": "1ea1fdff81b3a271659df4aad19bc4ef83def389131a36358fe64b245632e777", 11 | "s": "29e164658be9ce810921bf81d6b86694785a79ea1e52dbfa5105148d1f0bc1" 12 | }, 13 | { 14 | "DER": "304402201b19519b38ca1e6813cd25649ad36be8bc6a6f2ad9758089c429acd9ce0b572f02203bf32193c8a3a3de1f847cd6e6eebf43c96df1ffa4d7fe920f8f71708920c65f", 15 | "r": "1b19519b38ca1e6813cd25649ad36be8bc6a6f2ad9758089c429acd9ce0b572f", 16 | "s": "3bf32193c8a3a3de1f847cd6e6eebf43c96df1ffa4d7fe920f8f71708920c65f" 17 | }, 18 | { 19 | "DER": "3044022000c8da1836747d05a6a3d2c395825edce827147d15909e66939a5037d5916e6f022017823c2da62f539d7f8e1e186eaea7a401ab3c077dcfc44aeaf3e13fac99bdbc", 20 | "r": "00c8da1836747d05a6a3d2c395825edce827147d15909e66939a5037d5916e6f", 21 | "s": "17823c2da62f539d7f8e1e186eaea7a401ab3c077dcfc44aeaf3e13fac99bdbc" 22 | }, 23 | { 24 | "DER": "3042021e2ff2609c8dc0392d3731a2c6312841e09c76f10b83e2b52604dc84886dd502200090ac80e787c063618192bc04758e6666d0179c377fb2f3d6105d58000f33ac", 25 | "r": "2ff2609c8dc0392d3731a2c6312841e09c76f10b83e2b52604dc84886dd5", 26 | "s": "0090ac80e787c063618192bc04758e6666d0179c377fb2f3d6105d58000f33ac" 27 | }, 28 | { 29 | "DER": "3041021d00feb1a12c27e5fe261acc64c0923add082573883e0800d8e4080fa9bb02202e7aeb97f4046ea3be60d2896a19c8dc269ab5eb2de968912cd52a076a0a42e9", 30 | "r": "00feb1a12c27e5fe261acc64c0923add082573883e0800d8e4080fa9bb", 31 | "s": "2e7aeb97f4046ea3be60d2896a19c8dc269ab5eb2de968912cd52a076a0a42e9" 32 | }, 33 | { 34 | "DER": "3042021d00feb1a12c27e5fe261acc64c0923add082573883e0800d8e4080fa9bb0221008aeb97f4046ea3be60d2896a19c8dc269ab5eb2de968912cd52a076a0a42e925", 35 | "r": "00feb1a12c27e5fe261acc64c0923add082573883e0800d8e4080fa9bb", 36 | "s": "008aeb97f4046ea3be60d2896a19c8dc269ab5eb2de968912cd52a076a0a42e925" 37 | }, 38 | { 39 | "DER": "303e021d00c1d545da2e4edfbc65e9267d3c0a6fdda41793d0fd945f15acbcf0dd021d009acffda3ca5e7c349c35ba606f0a8f1ec7815b653b51695ca9ee69a6", 40 | "r": "00c1d545da2e4edfbc65e9267d3c0a6fdda41793d0fd945f15acbcf0dd", 41 | "s": "009acffda3ca5e7c349c35ba606f0a8f1ec7815b653b51695ca9ee69a6" 42 | }, 43 | { 44 | "DER": "3006020100020100", 45 | "r": "00", 46 | "s": "00" 47 | } 48 | ], 49 | "invalid": { 50 | "encode": [ 51 | { 52 | "exception": "^Error: R length is zero$", 53 | "r": "", 54 | "s": "0080000000000000000000000000000000000000000000000000000000000000" 55 | }, 56 | { 57 | "exception": "^Error: S length is zero$", 58 | "r": "0080000000000000000000000000000000000000000000000000000000000000", 59 | "s": "" 60 | }, 61 | { 62 | "exception": "^Error: R value is negative$", 63 | "r": "80", 64 | "s": "7f" 65 | }, 66 | { 67 | "exception": "^Error: S value is negative$", 68 | "r": "7f", 69 | "s": "80" 70 | }, 71 | { 72 | "exception": "^Error: R value excessively padded$", 73 | "r": "0010000000000000000000000000000000000000000000000000000000000000", 74 | "s": "0080000000000000000000000000000000000000000000000000000000000000" 75 | }, 76 | { 77 | "exception": "^Error: S value excessively padded$", 78 | "r": "0080000000000000000000000000000000000000000000000000000000000000", 79 | "s": "0010000000000000000000000000000000000000000000000000000000000000" 80 | }, 81 | { 82 | "exception": "^Error: R length is too long$", 83 | "r": "00800000000000000000000000000000000000000000000000000000000000000000", 84 | "s": "008000000000000000000000000000000000000000000000000000000000000000" 85 | }, 86 | { 87 | "exception": "^Error: S length is too long$", 88 | "r": "008000000000000000000000000000000000000000000000000000000000000000", 89 | "s": "00800000000000000000000000000000000000000000000000000000000000000000" 90 | } 91 | ], 92 | "decode": [ 93 | { 94 | "exception": "^Error: DER sequence length is too short$", 95 | "DER": "ffffffffffffff" 96 | }, 97 | { 98 | "exception": "^Error: DER sequence length is too long$", 99 | "DER": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" 100 | }, 101 | { 102 | "exception": "^Error: Expected DER sequence \\(48\\)$", 103 | "DER": "00ffff0400ffffff020400ffffff" 104 | }, 105 | { 106 | "exception": "^Error: DER sequence length is invalid$", 107 | "DER": "30ff020400ffffff020400ffffff" 108 | }, 109 | { 110 | "exception": "^Error: DER sequence length is invalid$", 111 | "DER": "300c030400ffffff030400ffffff0000" 112 | }, 113 | { 114 | "exception": "^Error: Expected DER integer \\(2\\)$", 115 | "DER": "300cff0400ffffff020400ffffff" 116 | }, 117 | { 118 | "exception": "^Error: Expected DER integer \\(2\\)$", 119 | "DER": "300c020200ffffff020400ffffff" 120 | }, 121 | { 122 | "exception": "^Error: R length is zero$", 123 | "DER": "30080200020400ffffff" 124 | }, 125 | { 126 | "exception": "^Error: S length is zero$", 127 | "DER": "3008020400ffffff0200" 128 | }, 129 | { 130 | "exception": "^Error: R length is too long$", 131 | "DER": "300c02dd00ffffff020400ffffff" 132 | }, 133 | { 134 | "exception": "^Error: S length is invalid$", 135 | "DER": "300c020400ffffff02dd00ffffff" 136 | }, 137 | { 138 | "exception": "^Error: R value is negative$", 139 | "DER": "300c020480000000020400ffffff" 140 | }, 141 | { 142 | "exception": "^Error: S value is negative$", 143 | "DER": "300c020400ffffff020480000000" 144 | }, 145 | { 146 | "exception": "^Error: R value excessively padded$", 147 | "DER": "300c02040000ffff020400ffffff" 148 | }, 149 | { 150 | "exception": "^Error: S value excessively padded$", 151 | "DER": "300c020400ffffff02040000ffff" 152 | } 153 | ] 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape' 2 | import { check, decode, encode } from '../src/esm/index.js' 3 | import fixtures from './fixtures.json' assert { type: 'json' } 4 | const { valid, invalid } = fixtures 5 | 6 | valid.forEach(function (fixture) { 7 | tape('check: returns true for ' + fixture.DER, function (t) { 8 | const buffer = Buffer.from(fixture.DER, 'hex') 9 | t.same(check(buffer), true) 10 | t.end() 11 | }) 12 | 13 | tape('decode: ' + fixture.DER, function (t) { 14 | const buffer = Buffer.from(fixture.DER, 'hex') 15 | const signature = decode(buffer) 16 | t.same(signature.r.toString('hex'), fixture.r) 17 | t.same(signature.s.toString('hex'), fixture.s) 18 | t.end() 19 | }) 20 | 21 | tape('encode: ' + fixture.r + ', ' + fixture.s, function (t) { 22 | const r = Buffer.from(fixture.r, 'hex') 23 | const s = Buffer.from(fixture.s, 'hex') 24 | const DER = Buffer.from(encode(r, s)) 25 | t.same(DER.toString('hex'), fixture.DER) 26 | t.end() 27 | }) 28 | }) 29 | 30 | invalid.decode.forEach(function (fixture) { 31 | tape('check: returns false for ' + fixture.DER + ' (' + fixture.exception + ')', function (t) { 32 | const buffer = Buffer.from(fixture.DER, 'hex') 33 | t.same(check(buffer), false) 34 | t.end() 35 | }) 36 | 37 | tape('throws "' + fixture.exception + '" for ' + fixture.DER, function (t) { 38 | const buffer = Buffer.from(fixture.DER, 'hex') 39 | t.throws(function () { 40 | decode(buffer) 41 | }, new RegExp(fixture.exception)) 42 | t.end() 43 | }) 44 | }) 45 | 46 | invalid.encode.forEach(function (fixture) { 47 | tape('throws "' + fixture.exception + '" for ' + fixture.r + ', ' + fixture.s, function (t) { 48 | const r = Buffer.from(fixture.r, 'hex') 49 | const s = Buffer.from(fixture.s, 'hex') 50 | t.throws(function () { 51 | encode(r, s) 52 | }, new RegExp(fixture.exception)) 53 | t.end() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /ts_src/index.ts: -------------------------------------------------------------------------------- 1 | // Reference https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki 2 | // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 3 | // NOTE: SIGHASH byte ignored AND restricted, truncate before use 4 | 5 | export function check (buffer: Uint8Array): boolean { 6 | const ret = internalCheck(buffer) 7 | if (typeof ret === 'string') { 8 | return false 9 | } else { 10 | return true 11 | } 12 | } 13 | 14 | function internalCheck (buffer: Uint8Array): string | number { 15 | if (buffer.length < 8) return 'DER sequence length is too short' 16 | if (buffer.length > 72) return 'DER sequence length is too long' 17 | if (buffer[0] !== 0x30) return 'Expected DER sequence (48)' 18 | if (buffer[1] !== buffer.length - 2) return 'DER sequence length is invalid' 19 | 20 | if (buffer[2] !== 0x02) return 'Expected DER integer (2)' 21 | const lenR = buffer[3] 22 | if (lenR === 0) return 'R length is zero' 23 | if (5 + lenR >= buffer.length) return 'R length is too long' 24 | 25 | if (buffer[4 + lenR] !== 0x02) return 'Expected DER integer (2)' 26 | const lenS = buffer[5 + lenR] 27 | if (lenS === 0) return 'S length is zero' 28 | if ((6 + lenR + lenS) !== buffer.length) return 'S length is invalid' 29 | 30 | if ((buffer[4] & 0x80) !== 0) return 'R value is negative' 31 | if (lenR > 1 && (buffer[4] === 0x00) && (buffer[5] & 0x80) === 0) return 'R value excessively padded' 32 | 33 | if ((buffer[lenR + 6] & 0x80) !== 0) return 'S value is negative' 34 | if (lenS > 1 && (buffer[lenR + 6] === 0x00) && (buffer[lenR + 7] & 0x80) === 0) return 'S value excessively padded' 35 | return lenR 36 | } 37 | 38 | export function decode (buffer: Uint8Array): { 39 | r: Uint8Array 40 | s: Uint8Array 41 | } { 42 | const ret = internalCheck(buffer) 43 | if (typeof ret === 'string') { 44 | throw new Error(ret) 45 | } 46 | // non-BIP66 - extract R, S values 47 | return { 48 | r: buffer.subarray(4, 4 + ret), 49 | s: buffer.subarray(6 + ret) 50 | } 51 | } 52 | 53 | /* 54 | * Expects r and s to be positive DER integers. 55 | * 56 | * The DER format uses the most significant bit as a sign bit (& 0x80). 57 | * If the significant bit is set AND the integer is positive, a 0x00 is prepended. 58 | * 59 | * Examples: 60 | * 61 | * 0 => 0x00 62 | * 1 => 0x01 63 | * -1 => 0xff 64 | * 127 => 0x7f 65 | * -127 => 0x81 66 | * 128 => 0x0080 67 | * -128 => 0x80 68 | * 255 => 0x00ff 69 | * -255 => 0xff01 70 | * 16300 => 0x3fac 71 | * -16300 => 0xc054 72 | * 62300 => 0x00f35c 73 | * -62300 => 0xff0ca4 74 | */ 75 | export function encode (r: Uint8Array, s: Uint8Array): Uint8Array { 76 | const lenR = r.length 77 | const lenS = s.length 78 | if (lenR === 0) throw new Error('R length is zero') 79 | if (lenS === 0) throw new Error('S length is zero') 80 | if (lenR > 33) throw new Error('R length is too long') 81 | if (lenS > 33) throw new Error('S length is too long') 82 | if ((r[0] & 0x80) !== 0) throw new Error('R value is negative') 83 | if ((s[0] & 0x80) !== 0) throw new Error('S value is negative') 84 | if (lenR > 1 && (r[0] === 0x00) && (r[1] & 0x80) === 0) throw new Error('R value excessively padded') 85 | if (lenS > 1 && (s[0] === 0x00) && (s[1] & 0x80) === 0) throw new Error('S value excessively padded') 86 | 87 | const signature = new Uint8Array(6 + lenR + lenS) 88 | 89 | // 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] 90 | signature[0] = 0x30 91 | signature[1] = signature.length - 2 92 | signature[2] = 0x02 93 | signature[3] = r.length 94 | signature.set(r, 4) 95 | signature[4 + lenR] = 0x02 96 | signature[5 + lenR] = s.length 97 | signature.set(s, 6 + lenR) 98 | 99 | return signature 100 | } 101 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "rootDir": "./ts_src", 5 | "types": ["node"], 6 | "allowJs": false, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "strictBindCallApply": true, 12 | "strictPropertyInitialization": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "esModuleInterop": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true 18 | }, 19 | "include": ["**/*.ts"], 20 | "exclude": ["node_modules/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "outDir": "src/cjs", 7 | "module": "commonjs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "src/esm", 5 | "resolveJsonModule": true, 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext" 8 | }, 9 | } 10 | --------------------------------------------------------------------------------