From 60940505f1478f8d34781b470691c5d739a11677 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 4 Feb 2015 12:09:58 -0500 Subject: [PATCH] Added message verification and signing --- .coveralls.yml | 1 + .gitignore | 11 +++++ .jshintrc | 44 +++++++++++++++++++ .travis.yml | 10 +++++ CONTRIBUTING.md | 3 ++ README.md | 45 ++++++++++++++++++++ gulpfile.js | 8 ++++ index.js | 4 ++ lib/message.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 31 ++++++++++++++ test/message.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 375 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 gulpfile.js create mode 100644 index.js create mode 100644 lib/message.js create mode 100644 package.json create mode 100644 test/message.js diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..e28b9ec --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: OMJRNZCl018Yjy44nlG1hF6maKEyXcwPx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2c2572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.sw[a-z] +coverage +node_modules + +npm-debug.log +bitcore-message.js +bitcore-message.min.js +tests.js + +report +.DS_Store diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..82597a3 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,44 @@ +{ + "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). + "browser": true, // Standard browser globals e.g. `window`, `document`. + "camelcase": false, // Permit only camelcase for `var` and `object indexes`. + "curly": true, // Require {} for every new block or scope. + "devel": false, // Allow development statements e.g. `console.log();`. + "eqeqeq": true, // Require triple equals i.e. `===`. + "esnext": true, // Allow ES.next specific features such as `const` and `let`. + "freeze": true, // Forbid overwriting prototypes of native objects such as Array, Date and so on. + "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` + "indent": 2, // Specify indentation spacing + "latedef": true, // Prohibit variable use before definition. + "newcap": false, // Require capitalization of all constructor functions e.g. `new F()`. + "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. + "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. + "noempty": true, // Prohibit use of empty blocks. + "nonew": true, // Prohibits the use of constructor functions for side-effects + "quotmark": "single", // Define quotes to string values. + "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. + "smarttabs": false, // Supress warnings about mixed tabs and spaces + "strict": true, // Require `use strict` pragma in every file. + "trailing": true, // Prohibit trailing whitespaces. + "undef": true, // Require all non-global variables be declared before they are used. + "unused": true, // Warn unused variables. + + "maxparams": 4, // Maximum number of parameters for a function + "maxstatements": 15, // Maximum number of statements in a function + "maxcomplexity": 6, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) + "maxdepth": 4, // Maximum depth of nested control structures + "maxlen": 120, // Maximum number of cols in a line + + "predef": [ // Extra globals. + "after", + "afterEach", + "before", + "beforeEach", + "define", + "describe", + "exports", + "it", + "module", + "require" + ] +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe29353 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: +- '0.10' +before_install: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +install: + - npm install +after_script: + - gulp coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e5fcc1d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Please see [CONTRIBUTING.md](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) on the main bitcore repo. diff --git a/README.md b/README.md index 8b13789..0b06bba 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ +======= +Bitcoin Message Verification and Signing for Bitcore +======= + +[![NPM Package](https://img.shields.io/npm/v/bitcore-message.svg?style=flat-square)](https://www.npmjs.org/package/bitcore-message) +[![Build Status](https://img.shields.io/travis/bitpay/bitcore-message.svg?branch=master&style=flat-square)](https://travis-ci.org/bitpay/bitcore-message) +[![Coverage Status](https://img.shields.io/coveralls/bitpay/bitcore-message.svg?style=flat-square)](https://coveralls.io/r/bitpay/bitcore-message?branch=master) + +bitcore-message adds support for verifying and signing bitcoin messages in [Node.js](http://nodejs.org/) and web browsers. + +See [the main bitcore repo](https://github.com/bitpay/bitcore) for more information. + +## Getting Started + +```sh +npm install bitcore-message +``` + +```sh +bower install bitcore-message +``` + +To sign a message: + +```javascript +var privateKey = PrivateKey.fromWIF('cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4'); +var signature = Message('hello, world').sign(privateKey); +``` + +To verify a message: + +```javascript +var address = 'n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx'; +var verified = Message('hello, world').verify(address, signature); +``` + +## Contributing + +See [CONTRIBUTING.md](https://github.com/bitpay/bitcore) on the main bitcore repo for information about how to contribute. + +## License + +Code released under [the MIT license](https://github.com/bitpay/bitcore/blob/master/LICENSE). + +Copyright 2013-2015 BitPay, Inc. Bitcore is a trademark maintained by BitPay, Inc. diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..9abecf7 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,8 @@ +'use strict'; + +var gulp = require('gulp'); +var bitcoreTasks = require('bitcore-build'); + +bitcoreTasks('message'); + +gulp.task('default', ['lint', 'coverage']); diff --git a/index.js b/index.js new file mode 100644 index 0000000..fa9d743 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +var bitcore = require('bitcore'); +bitcore.Message = require('./lib/message'); + +module.exports = bitcore.Message; \ No newline at end of file diff --git a/lib/message.js b/lib/message.js new file mode 100644 index 0000000..b12afb5 --- /dev/null +++ b/lib/message.js @@ -0,0 +1,111 @@ +'use strict'; + +var bitcore = require('bitcore'); +var PrivateKey = bitcore.PrivateKey; +var PublicKey = bitcore.PublicKey; +var Address = bitcore.Address; +var BufferWriter = bitcore.encoding.BufferWriter; +var ECDSA = bitcore.crypto.ECDSA; +var Signature = bitcore.crypto.Signature; +var sha256sha256 = bitcore.crypto.Hash.sha256sha256; + +/** + * Will construct a new message to sign and verify. + * + * @param {String} message + * @returns {Message} + */ +var Message = function Message(message) { + if (!(this instanceof Message)) { + return new Message(message); + } + if (typeof message !== 'string') { + throw new TypeError('First argument should be a string'); + } + this.message = message; + + return this; +}; + +Message.MAGIC_BYTES = new Buffer('Bitcoin Signed Message:\n'); + +Message.prototype.magicHash = function magicHash() { + var prefix1 = BufferWriter.varintBufNum(Message.MAGIC_BYTES.length); + var messageBuffer = new Buffer(this.message); + var prefix2 = BufferWriter.varintBufNum(messageBuffer.length); + var buf = Buffer.concat([prefix1, Message.MAGIC_BYTES, prefix2, messageBuffer]); + var hash = sha256sha256(buf); + return hash; +}; + +Message.prototype._sign = function _sign(privateKey) { + if (!(privateKey instanceof PrivateKey)) { + throw new TypeError('First argument should be an instance of PrivateKey'); + } + var hash = this.magicHash(); + var ecdsa = new ECDSA(); + ecdsa.hashbuf = hash; + ecdsa.privkey = privateKey; + ecdsa.pubkey = privateKey.toPublicKey(); + ecdsa.signRandomK(); + ecdsa.calci(); + return ecdsa.sig; +}; + +/** + * Will sign a message with a given bitcoin private key. + * + * @param {PrivateKey} privateKey - An instance of PrivateKey + * @returns {String} A base64 encoded compact signature + */ +Message.prototype.sign = function sign(privateKey) { + var signature = this._sign(privateKey); + return signature.toCompact().toString('base64'); +}; + +Message.prototype._verify = function _verify(publicKey, signature) { + if (!(publicKey instanceof PublicKey)) { + throw new TypeError('First argument should be an instance of PublicKey'); + } + if (!(signature instanceof Signature)) { + throw new TypeError('Second argument should be an instance of Signature'); + } + var hash = this.magicHash(); + var verified = ECDSA.verify(hash, signature, publicKey); + if (!verified) { + this.error = 'The signature was invalid'; + } + return verified; +}; + +/** + * Will return a boolean of the signature is valid for a given bitcoin address. + * If it isn't the specific reason is accessible via the "error" member. + * + * @param {String} bitcoinAddress - A bitcoin address + * @param {String} signatureString - A base64 encoded compact signature + * @returns {Boolean} + */ +Message.prototype.verify = function verify(bitcoinAddress, signatureString) { + var signature = Signature.fromCompact(new Buffer(signatureString, 'base64')); + + // recover the public key + var ecdsa = new ECDSA(); + ecdsa.hashbuf = this.magicHash(); + ecdsa.sig = signature; + var publicKey = ecdsa.toPublicKey(); + + var expectedAddress = Address.fromString(bitcoinAddress); + var signatureAddress = Address.fromPublicKey(publicKey, expectedAddress.network); + + // check that the recovered address and specified address match + if (expectedAddress.toString() !== signatureAddress.toString()) { + this.error = 'The signature did not match the message digest'; + return false; + } + + return this._verify(publicKey, signature); +}; + +module.exports = Message; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa5e9ab --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "bitcore-message", + "version": "0.9.0", + "description": "Bitcoin Messages for Bitcore", + "author": "BitPay ", + "main": "index.js", + "scripts": { + "lint": "gulp lint", + "test": "gulp test:node", + "coverage": "gulp coverage", + "build": "gulp" + }, + "keywords": [ + "bitcoin", + "bitcore" + ], + "repository": { + "type": "git", + "url": "https://github.com/bitpay/bitcore-message.git" + }, + "dependencies": { + "bitcore": "^0.9.4" + }, + "devDependencies": { + "bitcore-build": "bitpay/bitcore-build", + "brfs": "^1.3.0", + "chai": "~1.10.0", + "gulp": "^3.8.10" + }, + "license": "MIT" +} diff --git a/test/message.js b/test/message.js new file mode 100644 index 0000000..c37e5d2 --- /dev/null +++ b/test/message.js @@ -0,0 +1,107 @@ +'use strict'; + +var chai = require('chai'); +var expect = chai.expect; +var should = chai.should(); + +var bitcore = require('bitcore'); +var Signature = bitcore.crypto.Signature; +var Message = require('../'); + +describe('Message', function() { + + var address = 'n1ZCYg9YXtB5XCZazLxSmPDa8iwJRZHhGx'; + var badAddress = 'mmRcrB5fTwgxaFJmVLNtaG8SV454y1E3kC'; + var privateKey = bitcore.PrivateKey.fromWIF('cPBn5A4ikZvBTQ8D7NnvHZYCAxzDZ5Z2TSGW2LkyPiLxqYaJPBW4'); + var text = 'hello, world'; + var signatureString = 'H/DIn8uA1scAuKLlCx+/9LnAcJtwQQ0PmcPrJUq90aboLv3fH5fFvY+vmbfOSFEtGarznYli6ShPr9RXwY9UrIY='; + + var badSignatureString = 'H69qZ4mbZCcvXk7CWjptD5ypnYVLvQ3eMXLM8+1gX21SLH/GaFnAjQrDn37+TDw79i9zHhbiMMwhtvTwnPigZ6k='; + + var signature = Signature.fromCompact(new Buffer(signatureString, 'base64')); + var badSignature = Signature.fromCompact(new Buffer(badSignatureString, 'base64')); + + var publicKey = privateKey.toPublicKey(); + + it('will error with incorrect message type', function() { + expect(function(){ + return new Message(new Date()); + }).to.throw(TypeError); + }); + + it('will instantiate without "new"', function() { + var message = Message(text); + should.exist(message); + }); + + var signature2; + var signature3; + + it('can sign a message', function() { + var message2 = new Message(text); + signature2 = message2._sign(privateKey); + signature3 = Message(text).sign(privateKey); + should.exist(signature2); + should.exist(signature3); + }); + + it('sign will error with incorrect private key argument', function() { + expect(function(){ + var message3 = new Message(text); + return message3.sign('not a private key'); + }).to.throw(TypeError); + }); + + it('can verify a message with signature', function() { + var message4 = new Message(text); + var verified = message4._verify(publicKey, signature2); + verified.should.equal(true); + }); + + it('can verify a message with existing signature', function() { + var message5 = new Message(text); + var verified = message5._verify(publicKey, signature); + verified.should.equal(true); + }); + + it('verify will error with incorrect public key argument', function() { + expect(function(){ + var message6 = new Message(text); + return message6._verify('not a public key', signature); + }).to.throw(TypeError); + }); + + it('verify will error with incorrect signature argument', function() { + expect(function(){ + var message7 = new Message(text); + return message7._verify(publicKey, 'not a signature'); + }).to.throw(TypeError); + }); + + it('verify will correctly identify a bad signature', function() { + var message8 = new Message(text); + var verified = message8._verify(publicKey, badSignature); + should.exist(message8.error); + verified.should.equal(false); + }); + + it('can verify a message with address and generated signature string', function() { + var message9 = new Message(text); + var verified = message9.verify(address, signature3); + should.not.exist(message9.error); + verified.should.equal(true); + }); + + it('will not verify with address mismatch', function() { + var message10 = new Message(text); + var verified = message10.verify(badAddress, signatureString); + should.exist(message10.error); + verified.should.equal(false); + }); + + it('can chain methods', function() { + var verified = Message(text).verify(address, signatureString); + verified.should.equal(true); + }); + +});