diff --git a/secp256k1/.gitignore b/secp256k1/.gitignore new file mode 100644 index 0000000..f99b5ca --- /dev/null +++ b/secp256k1/.gitignore @@ -0,0 +1,38 @@ +**/node_modules/ +**/*.log +test/repo-tests* + +# Logs +logs +*.log + +coverage + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +build + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +dist +docs +package-lock.json +yarn.lock +.vscode diff --git a/secp256k1/.npmignore b/secp256k1/.npmignore new file mode 100644 index 0000000..59335fd --- /dev/null +++ b/secp256k1/.npmignore @@ -0,0 +1,34 @@ +**/node_modules/ +**/*.log +test/repo-tests* + +# Logs +logs +*.log + +coverage + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +build + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +test diff --git a/secp256k1/.travis.yml b/secp256k1/.travis.yml new file mode 100644 index 0000000..cb1d170 --- /dev/null +++ b/secp256k1/.travis.yml @@ -0,0 +1,45 @@ + +language: node_js + +cache: npm + +stages: + - check + - test + - cov + +node_js: + - '10' + - '12' + +os: + - linux + - osx + - windows + +script: npx nyc -s npm run test:node -- --bail +after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov + +jobs: + include: + - stage: check + script: + - npx aegir dep-check + - npm run lint + + - stage: test + name: chrome + addons: + chrome: stable + script: + - npx aegir test -t browser + + - stage: test + name: firefox + addons: + firefox: latest + script: + - npx aegir test -t browser -- --browsers FirefoxHeadless + +notifications: + email: false \ No newline at end of file diff --git a/secp256k1/CHANGELOG.md b/secp256k1/CHANGELOG.md new file mode 100644 index 0000000..4dd03a4 --- /dev/null +++ b/secp256k1/CHANGELOG.md @@ -0,0 +1,134 @@ + +## [0.4.3](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.4.2...v0.4.3) (2020-03-25) + + + + +## [0.4.2](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.4.1...v0.4.2) (2020-03-17) + + +### Bug Fixes + +* add buffer and update deps ([#25](https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues/25)) ([35f196e](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/35f196e)) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.4.0...v0.4.1) (2020-01-06) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.3.1...v0.4.0) (2019-07-10) + + +### Features + +* use async await ([#18](https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues/18)) ([1974eb9](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/1974eb9)) + + +### BREAKING CHANGES + +* Callback support has been dropped in favor of async/await. + +* feat: use async/await + +This PR changes this module to remove callbacks and use async/await. The API is unchanged aside from the obvious removal of the `callback` parameter. + +refs https://github.com/ipfs/js-ipfs/issues/1670 + +* fix: use latest multihashing-async as it is all promises now + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.2.2...v0.3.1) (2019-07-10) + + +### Bug Fixes + +* update deps and repo setup ([cfdcbe0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/cfdcbe0)) +* **unmarshal:** provide only one arg to callback ([#17](https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues/17)) ([3bb8451](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/3bb8451)) + + +### Features + +* add `id()` method to Secp256k1PrivateKey ([f4dbd62](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/f4dbd62)) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.2.3...v0.3.0) (2019-02-20) + + +### Features + +* add `id()` method to Secp256k1PrivateKey ([f4dbd62](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/f4dbd62)) + + + + +## [0.2.3](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.2.2...v0.2.3) (2019-01-08) + + +### Bug Fixes + +* update deps and repo setup ([cfdcbe0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/cfdcbe0)) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.2.1...v0.2.2) (2017-07-22) + + +### Bug Fixes + +* circular circular dep -> DI ([0dcf1a6](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/0dcf1a6)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.2.0...v0.2.1) (2017-07-22) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.1.4...v0.2.0) (2017-07-22) + + +### Features + +* next libp2p-crypto ([#4](https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues/4)) ([4ee48a7](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/4ee48a7)) + + + + +## [0.1.4](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.1.3...v0.1.4) (2017-02-12) + + + + +## [0.1.3](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.1.2...v0.1.3) (2017-02-11) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.1.1...v0.1.2) (2017-02-09) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/v0.1.0...v0.1.1) (2017-02-09) + + + + +# [0.1.0](https://github.com/libp2p/js-libp2p-crypto-secp256k1/compare/4c36aeb...v0.1.0) (2017-02-09) + + +### Features + +* initial implementation ([4c36aeb](https://github.com/libp2p/js-libp2p-crypto-secp256k1/commit/4c36aeb)) + + + diff --git a/secp256k1/LICENSE b/secp256k1/LICENSE new file mode 100644 index 0000000..bbfffbf --- /dev/null +++ b/secp256k1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 libp2p + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/secp256k1/README.md b/secp256k1/README.md new file mode 100644 index 0000000..7063200 --- /dev/null +++ b/secp256k1/README.md @@ -0,0 +1,126 @@ +# js-libp2p-crypto-secp256k1 + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) +[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) +[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) +[![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1) +[![](https://img.shields.io/travis/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1) +[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) +![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) +![](https://img.shields.io/badge/Node.js-%3E%3D4.0.0-orange.svg?style=flat-square) + +> Support for secp256k1 keys in js-libp2p-crypto + +This repo contains a [js-libp2p-crypto](https://github.com/libp2p/js-libp2p-crypto)-compatible +implementation of cryptographic signature generation and verification using the +[secp256k1 elliptic curve](https://en.bitcoin.it/wiki/Secp256k1) popularized by Bitcoin and other +crypto currencies. + +## Lead Captain + +[Friedel Ziegelmayer](https://github.com/dignifiedquire/) + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) + - [Example](#example) +- [API](#api) + - [`generateKeyPair([bits])`](#generatekeypairbits) + - [`unmarshalSecp256k1PublicKey(bytes)`](#unmarshalsecp256k1publickeybytes) + - [`unmarshalSecp256k1PrivateKey(bytes)`](#unmarshalsecp256k1privatekeybytes) + - [`Secp256k1PublicKey`](#secp256k1publickey) + - [`.verify(data, sig)`](#verifydata-sig) + - [`Secp256k1PrivateKey`](#secp256k1privatekey) + - [`.public`](#public) + - [`.sign(data)`](#signdata) +- [Contribute](#contribute) +- [License](#license) + +## Install + +```sh +npm install --save libp2p-crypto-secp256k1 +``` + +## Usage + +This module is designed to work with [js-libp2p-crypto](https://github.com/libp2p/js-libp2p-crypto). +Installing `libp2p-crypto-secp256k1` will automatically add support for the `'secp256k1'` key type, which +can be used as an argument to the [libp2p-crypto API functions](https://github.com/libp2p/js-libp2p-crypto#api) +`generateKeyPair`, `unmarshalPublicKey`, and `marshalPrivateKey`. The keys returned from those functions will be +instances of the `Secp256k1PublicKey` or `Secp256k1PrivateKey` classes provided by this module. + +### Example + +```js +const crypto = require('libp2p-crypto') +const msg = Buffer.from('Hello World') + +const key = await crypto.generateKeyPair('secp256k1', 256) +// assuming no error, key will be an instance of Secp256k1PrivateKey +// the public key is available as key.public +const sig = await key.sign(msg) + +const valid = await key.public.verify(msg, sig) +assert(valid, 'Something went horribly wrong') +``` + +## API + +The functions below are the public API of this module. +For usage within `libp2p-crypto`, see the [`libp2p-crypto` API documentation](https://github.com/libp2p/js-libp2p-crypto#api). + +### `generateKeyPair([bits])` +- `bits: Number` - Optional, included for compatibility with js-libp2p-crypto. Ignored if present; private keys will always be 256 bits. + +Returns `Promise` + +### `unmarshalSecp256k1PublicKey(bytes)` +- `bytes: Buffer` + +Converts a serialized secp256k1 public key into an instance of `Secp256k1PublicKey` and returns it + +### `unmarshalSecp256k1PrivateKey(bytes)` +- `bytes: Buffer` + +Returns `Promise` + +Converts a serialized secp256k1 private key into an instance of `Secp256k1PrivateKey`. + +### `Secp256k1PublicKey` + +#### `.verify(data, sig)` +- `data: Buffer` +- `sig: Buffer` + +Returns `Promise` + +Calculates the SHA-256 hash of `data`, and verifies the DER-encoded signature in `sig`. + +### `Secp256k1PrivateKey` + +#### `.public` + +Accessor for the `Secp256k1PublicKey` associated with this private key. + +#### `.sign(data)` +- `data: Buffer` + +Returns `Promise` + +Calculates the SHA-256 hash of `data` and signs it, resolves with the DER-encoded signature. + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## License + +[MIT](LICENSE) diff --git a/secp256k1/package.json b/secp256k1/package.json new file mode 100644 index 0000000..0a41bd6 --- /dev/null +++ b/secp256k1/package.json @@ -0,0 +1,66 @@ +{ + "name": "libp2p-crypto-secp256k1", + "version": "0.4.3", + "description": "Support for secp256k1 keys in libp2p-crypto", + "leadMaintainer": "Friedel Ziegelmayer ", + "main": "src/index.js", + "scripts": { + "lint": "aegir lint", + "build": "aegir build", + "test": "aegir test -t node -t browser", + "test:node": "aegir test -t node", + "test:browser": "aegir test -t browser", + "release": "aegir release", + "release-minor": "aegir release --type minor", + "release-major": "aegir release --type major", + "coverage": "aegir coverage", + "coverage-publish": "aegir coverage publish" + }, + "keywords": [ + "IPFS", + "libp2p", + "crypto", + "secp256k1" + ], + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "is-typedarray": "^1.0.0", + "multibase": "^0.7.0", + "multihashing-async": "^0.8.1", + "secp256k1": "^4.0.0" + }, + "devDependencies": { + "aegir": "^21.0.2", + "benchmark": "^2.1.4", + "chai": "^4.2.0", + "dirty-chai": "^2.0.1", + "libp2p-crypto": "~0.17.2", + "protons": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/libp2p/js-libp2p-crypto-secp256k1.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-crypto-secp256k1/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-crypto-secp256k1", + "contributors": [ + "David Dias ", + "Jacob Heun ", + "Friedel Ziegelmayer ", + "Hugo Dias ", + "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", + "Yusef Napora ", + "Alan Shaw ", + "Alberto Elias ", + "Alex Potsides ", + "Arve Knudsen ", + "Vasco Santos " + ] +} diff --git a/secp256k1/src/crypto.js b/secp256k1/src/crypto.js new file mode 100644 index 0000000..1db25c4 --- /dev/null +++ b/secp256k1/src/crypto.js @@ -0,0 +1,86 @@ +'use strict' + +const { Buffer } = require('buffer') +var isTypedArray = require('is-typedarray').strict +const secp256k1 = require('secp256k1') +const sha = require('multihashing-async/src/sha') +const HASH_ALGORITHM = 'sha2-256' + +function typedArrayTobuffer (arr) { + if (isTypedArray(arr)) { + // To avoid a copy, use the typed array's underlying ArrayBuffer to back new Buffer + var buf = Buffer.from(arr.buffer) + if (arr.byteLength !== arr.buffer.byteLength) { + // Respect the "view", i.e. byteOffset and byteLength, without doing a copy + buf = buf.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) + } + return buf + } else { + // Pass through all other types to `Buffer.from` + return Buffer.from(arr) + } +} + +module.exports = (randomBytes) => { + const privateKeyLength = 32 + + function generateKey () { + let privateKey + do { + privateKey = randomBytes(32) + } while (!secp256k1.privateKeyVerify(privateKey)) + return privateKey + } + + async function hashAndSign (key, msg) { + const digest = await sha.digest(msg, HASH_ALGORITHM) + const sig = secp256k1.ecdsaSign(digest, key) + return typedArrayTobuffer(secp256k1.signatureExport(sig.signature)) + } + + async function hashAndVerify (key, sig, msg) { + const digest = await sha.digest(msg, HASH_ALGORITHM) + sig = typedArrayTobuffer(secp256k1.signatureImport(sig)) + return secp256k1.ecdsaVerify(sig, digest, key) + } + + function compressPublicKey (key) { + if (!secp256k1.publicKeyVerify(key)) { + throw new Error('Invalid public key') + } + return typedArrayTobuffer(secp256k1.publicKeyConvert(key, true)) + } + + function decompressPublicKey (key) { + return typedArrayTobuffer(secp256k1.publicKeyConvert(key, false)) + } + + function validatePrivateKey (key) { + if (!secp256k1.privateKeyVerify(key)) { + throw new Error('Invalid private key') + } + } + + function validatePublicKey (key) { + if (!secp256k1.publicKeyVerify(key)) { + throw new Error('Invalid public key') + } + } + + function computePublicKey (privateKey) { + validatePrivateKey(privateKey) + return typedArrayTobuffer(secp256k1.publicKeyCreate(privateKey)) + } + + return { + generateKey, + privateKeyLength, + hashAndSign, + hashAndVerify, + compressPublicKey, + decompressPublicKey, + validatePrivateKey, + validatePublicKey, + computePublicKey + } +} diff --git a/secp256k1/src/index.js b/secp256k1/src/index.js new file mode 100644 index 0000000..13ebfe8 --- /dev/null +++ b/secp256k1/src/index.js @@ -0,0 +1,110 @@ +'use strict' + +const multibase = require('multibase') +const sha = require('multihashing-async/src/sha') + +module.exports = (keysProtobuf, randomBytes, crypto) => { + crypto = crypto || require('./crypto')(randomBytes) + + class Secp256k1PublicKey { + constructor (key) { + crypto.validatePublicKey(key) + this._key = key + } + + verify (data, sig) { + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal () { + return crypto.compressPublicKey(this._key) + } + + get bytes () { + return keysProtobuf.PublicKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }) + } + + equals (key) { + return this.bytes.equals(key.bytes) + } + + hash () { + return sha.multihashing(this.bytes, 'sha2-256') + } + } + + class Secp256k1PrivateKey { + constructor (key, publicKey) { + this._key = key + this._publicKey = publicKey || crypto.computePublicKey(key) + crypto.validatePrivateKey(this._key) + crypto.validatePublicKey(this._publicKey) + } + + sign (message) { + return crypto.hashAndSign(this._key, message) + } + + get public () { + return new Secp256k1PublicKey(this._publicKey) + } + + marshal () { + return this._key + } + + get bytes () { + return keysProtobuf.PrivateKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }) + } + + equals (key) { + return this.bytes.equals(key.bytes) + } + + hash () { + return sha.multihashing(this.bytes, 'sha2-256') + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @param {function(Error, id)} callback + * @returns {undefined} + */ + async id () { + const hash = await this.public.hash() + return multibase.encode('base58btc', hash).toString().slice(1) + } + } + + function unmarshalSecp256k1PrivateKey (bytes) { + return new Secp256k1PrivateKey(bytes) + } + + function unmarshalSecp256k1PublicKey (bytes) { + return new Secp256k1PublicKey(bytes) + } + + async function generateKeyPair () { + const privateKeyBytes = await crypto.generateKey() + return new Secp256k1PrivateKey(privateKeyBytes) + } + + return { + Secp256k1PublicKey, + Secp256k1PrivateKey, + unmarshalSecp256k1PrivateKey, + unmarshalSecp256k1PublicKey, + generateKeyPair + } +} diff --git a/secp256k1/test/fixtures/go-interop.js b/secp256k1/test/fixtures/go-interop.js new file mode 100644 index 0000000..bebda0b --- /dev/null +++ b/secp256k1/test/fixtures/go-interop.js @@ -0,0 +1,31 @@ +'use strict' + +const { Buffer } = require('buffer') + +// The keypair and signature below were generated in a gore repl session (https://github.com/motemen/gore) +// using the secp256k1 fork of go-libp2p-crypto by github user @vyzo +// +// gore> :import github.com/vyzo/go-libp2p-crypto +// gore> :import crypto/rand +// gore> :import io/ioutil +// gore> priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Secp256k1, 256, rand.Reader) +// gore> privBytes, err := priv.Bytes() +// gore> pubBytes, err := pub.Bytes() +// gore> msg := []byte("hello! and welcome to some awesome crypto primitives") +// gore> sig, err := priv.Sign(msg) +// gore> ioutil.WriteFile("/tmp/secp-go-priv.bin", privBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-pub.bin", pubBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-sig.bin", sig, 0644) +// +// The generated files were then read in a node repl with e.g.: +// > fs.readFileSync('/tmp/secp-go-pub.bin').toString('hex') +// '08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf' +// +// and the results copy/pasted in here + +module.exports = { + privateKey: Buffer.from('08021220358f15db8c2014d570e8e3a622454e2273975a3cca443ec0c45375b13d381d18', 'hex'), + publicKey: Buffer.from('08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf', 'hex'), + message: Buffer.from('hello! and welcome to some awesome crypto primitives', 'utf-8'), + signature: Buffer.from('304402200e4c629e9f5d99439115e60989cd40087f6978c36078b0b50cf3d30af5c38d4102204110342c8e7f0809897c1c7a66e49e1c6b7cb0a6ed6993640ec2fe742c1899a9', 'hex') +} diff --git a/secp256k1/test/secp256k1.spec.js b/secp256k1/test/secp256k1.spec.js new file mode 100644 index 0000000..0863ca4 --- /dev/null +++ b/secp256k1/test/secp256k1.spec.js @@ -0,0 +1,268 @@ +/* eslint-env mocha */ +'use strict' + +const { Buffer } = require('buffer') +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const protobuf = require('protons') +const keysPBM = protobuf(require('libp2p-crypto/src/keys/keys.proto')) +const randomBytes = require('libp2p-crypto/src/random-bytes') +const crypto = require('../src/crypto')(randomBytes) + +describe('secp256k1 keys', () => { + let key + const secp256k1 = require('../src')(keysPBM, randomBytes) + + before(async () => { + key = await secp256k1.generateKeyPair() + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.public).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + + const digest = await key.hash() + expect(digest).to.have.length(34) + + const publicDigest = await key.public.hash() + expect(publicDigest).to.have.length(34) + }) + + it('optionally accepts a `bits` argument when generating a key', async () => { + const _key = await secp256k1.generateKeyPair(256) + expect(_key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + }) + + it('signs', async () => { + const text = randomBytes(512) + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.equal(true) + }) + + it('encoding', async () => { + const keyMarshal = key.marshal() + const key2 = await secp256k1.unmarshalSecp256k1PrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = secp256k1.unmarshalSecp256k1PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const id = await key.id() + expect(id).to.exist() + expect(id).to.be.a('string') + }) + + describe('key equals', () => { + it('equals itself', () => { + expect(key.equals(key)).to.eql(true) + + expect(key.public.equals(key.public)).to.eql(true) + }) + + it('not equals other key', async () => { + const key2 = await secp256k1.generateKeyPair(256) + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = Buffer.from('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('fails to verify for different data', async () => { + const data = Buffer.from('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(Buffer.from('hello'), sig) + expect(valid).to.eql(false) + }) +}) + +describe('key generation error', () => { + let generateKey + let secp256k1 + + before(() => { + generateKey = crypto.generateKey + crypto.generateKey = () => { throw new Error('Error generating key') } + secp256k1 = require('../src')(keysPBM, randomBytes, crypto) + }) + + after(() => { + crypto.generateKey = generateKey + }) + + it('returns an error if key generation fails', async () => { + try { + await secp256k1.generateKeyPair() + } catch (err) { + return expect(err.message).to.equal('Error generating key') + } + throw new Error('Expected error to be thrown') + }) +}) + +describe('handles generation of invalid key', () => { + let generateKey + let secp256k1 + + before(() => { + generateKey = crypto.generateKey + crypto.generateKey = () => Buffer.from('not a real key') + secp256k1 = require('../src')(keysPBM, randomBytes, crypto) + }) + + after(() => { + crypto.generateKey = generateKey + }) + + it('returns an error if key generator returns an invalid key', async () => { + try { + await secp256k1.generateKeyPair() + } catch (err) { + return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + } + throw new Error('Expected error to be thrown') + }) +}) + +describe('crypto functions', () => { + let privKey + let pubKey + + before(async () => { + privKey = await crypto.generateKey() + pubKey = crypto.computePublicKey(privKey) + }) + + it('generates valid keys', () => { + expect(() => { + crypto.validatePrivateKey(privKey) + crypto.validatePublicKey(pubKey) + }).to.not.throw() + }) + + it('does not validate an invalid key', () => { + expect(() => crypto.validatePublicKey(Buffer.from('42'))).to.throw() + expect(() => crypto.validatePrivateKey(Buffer.from('42'))).to.throw() + }) + + it('validates a correct signature', async () => { + const sig = await crypto.hashAndSign(privKey, Buffer.from('hello')) + const valid = await crypto.hashAndVerify(pubKey, sig, Buffer.from('hello')) + expect(valid).to.equal(true) + }) + + it('errors if given a null buffer to sign', async () => { + try { + await crypto.hashAndSign(privKey, null) + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + + it('errors when signing with an invalid key', async () => { + try { + await crypto.hashAndSign(Buffer.from('42'), Buffer.from('Hello')) + } catch (err) { + return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + } + throw new Error('Expected error to be thrown') + }) + + it('errors if given a null buffer to validate', async () => { + const sig = await crypto.hashAndSign(privKey, Buffer.from('hello')) + + try { + await crypto.hashAndVerify(privKey, sig, null) + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + + it('errors when validating a message with an invalid signature', async () => { + try { + await crypto.hashAndVerify(pubKey, Buffer.from('invalid-sig'), Buffer.from('hello')) + } catch (err) { + return expect(err.message).to.equal('Signature could not be parsed') + } + throw new Error('Expected error to be thrown') + }) + + it('errors when signing with an invalid key', async () => { + try { + await crypto.hashAndSign(Buffer.from('42'), Buffer.from('Hello')) + } catch (err) { + return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + } + throw new Error('Expected error to be thrown') + }) + + it('throws when compressing an invalid public key', () => { + expect(() => crypto.compressPublicKey(Buffer.from('42'))).to.throw() + }) + + it('throws when decompressing an invalid public key', () => { + expect(() => crypto.decompressPublicKey(Buffer.from('42'))).to.throw() + }) + + it('compresses/decompresses a valid public key', () => { + const decompressed = crypto.decompressPublicKey(pubKey) + expect(decompressed).to.exist() + expect(decompressed.length).to.be.eql(65) + const recompressed = crypto.compressPublicKey(decompressed) + expect(recompressed).to.eql(pubKey) + }) +}) + +describe('go interop', () => { + const secp256k1 = require('../src')(keysPBM, randomBytes) + const fixtures = require('./fixtures/go-interop') + + it('loads a private key marshaled by go-libp2p-crypto', async () => { + // we need to first extract the key data from the protobuf, which is + // normally handled by js-libp2p-crypto + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = await secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.bytes).to.eql(fixtures.privateKey) + }) + + it('loads a public key marshaled by go-libp2p-crypto', () => { + const decoded = keysPBM.PublicKey.decode(fixtures.publicKey) + expect(decoded.Type).to.be.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + expect(key.bytes).to.eql(fixtures.publicKey) + }) + + it('generates the same signature as go-libp2p-crypto', async () => { + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = await secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data) + const sig = await key.sign(fixtures.message) + expect(sig).to.eql(fixtures.signature) + }) +})