Added message verification and signing

This commit is contained in:
Braydon Fuller 2015-02-04 12:09:58 -05:00
parent ed48ded0c6
commit 60940505f1
11 changed files with 375 additions and 0 deletions

1
.coveralls.yml Normal file
View File

@ -0,0 +1 @@
repo_token: OMJRNZCl018Yjy44nlG1hF6maKEyXcwPx

11
.gitignore vendored Normal file
View File

@ -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

44
.jshintrc Normal file
View File

@ -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"
]
}

10
.travis.yml Normal file
View File

@ -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

3
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,3 @@
# Contributing
Please see [CONTRIBUTING.md](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) on the main bitcore repo.

View File

@ -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.

8
gulpfile.js Normal file
View File

@ -0,0 +1,8 @@
'use strict';
var gulp = require('gulp');
var bitcoreTasks = require('bitcore-build');
bitcoreTasks('message');
gulp.task('default', ['lint', 'coverage']);

4
index.js Normal file
View File

@ -0,0 +1,4 @@
var bitcore = require('bitcore');
bitcore.Message = require('./lib/message');
module.exports = bitcore.Message;

111
lib/message.js Normal file
View File

@ -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;

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "bitcore-message",
"version": "0.9.0",
"description": "Bitcoin Messages for Bitcore",
"author": "BitPay <dev@bitpay.com>",
"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"
}

107
test/message.js Normal file
View File

@ -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);
});
});