[FEATURE] New Message class for sigs on arbitrary data

This includes supporting files that can sign arbitrary data
with a signature that enables public key recovery. It also
includes the PublicKeyValidator class that can verify whether
a given public key is active for an account by looking in its
AccountRoot.
This commit is contained in:
Evan Schwartz
2014-04-22 17:21:42 -07:00
parent f56a20d697
commit 904082a86c
10 changed files with 1112 additions and 6 deletions

View File

@@ -36,6 +36,7 @@ module.exports = function(grunt) {
"src/js/sjcl/core/bn.js",
"src/js/sjcl/core/ecc.js",
"src/js/sjcl/core/srp.js",
"src/js/sjcl-custom/sjcl-ecc-pointextras.js",
"src/js/sjcl-custom/sjcl-secp256k1.js",
"src/js/sjcl-custom/sjcl-ripemd160.js",
"src/js/sjcl-custom/sjcl-extramath.js",
@@ -43,6 +44,7 @@ module.exports = function(grunt) {
"src/js/sjcl-custom/sjcl-validecc.js",
"src/js/sjcl-custom/sjcl-ecdsa-canonical.js",
"src/js/sjcl-custom/sjcl-ecdsa-der.js",
"src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js",
"src/js/sjcl-custom/sjcl-jacobi.js"
],
dest: 'build/sjcl.js'

View File

@@ -11,6 +11,7 @@ exports.Seed = require('./seed').Seed;
exports.Meta = require('./meta').Meta;
exports.SerializedObject = require('./serializedobject').SerializedObject;
exports.RippleError = require('./rippleerror').RippleError;
exports.Message = require('./message');
exports.binformat = require('./binformat');
exports.utils = require('./utils');

194
src/js/ripple/message.js Normal file
View File

@@ -0,0 +1,194 @@
var async = require('async');
var crypto = require('crypto');
var sjcl = require('./utils').sjcl;
var Remote = require('./remote').Remote;
var Seed = require('./seed').Seed;
var KeyPair = require('./keypair').KeyPair;
var PublicKeyValidator = require('./pubkeyvalidator');
var UInt160 = require('./uint160').UInt160;
// Message class (static)
var Message = {};
Message.HASH_FUNCTION = sjcl.hash.sha512.hash;
Message.MAGIC_BYTES = 'Ripple Signed Message:\n';
var REGEX_HEX = /^[0-9a-fA-F]+$/;
var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/;
/**
* Produce a Base64-encoded signature on the given message with
* the string 'Ripple Signed Message:\n' prepended.
*
* Note that this signature uses the signing function that includes
* a recovery_factor to be able to extract the public key from the signature
* without having to pass the public key along with the signature.
*
* @static
*
* @param {String} message
* @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key
* @returns {Base64-encoded String} signature
*/
Message.signMessage = function(message, secret_key) {
return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key);
};
/**
* Produce a Base64-encoded signature on the given hex-encoded hash.
*
* Note that this signature uses the signing function that includes
* a recovery_factor to be able to extract the public key from the signature
* without having to pass the public key along with the signature.
*
* @static
*
* @param {bitArray|Hex-encoded String} hash
* @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key
* @returns {Base64-encoded String} signature
*/
Message.signHash = function(hash, secret_key) {
if (typeof hash === 'string' && /^[0-9a-fA-F]+$/.test(hash)) {
hash = sjcl.codec.hex.toBits(hash);
}
if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') {
throw new Error('Hash must be a bitArray or hex-encoded string');
}
if (!(secret_key instanceof sjcl.ecc.ecdsa.secretKey)) {
secret_key = Seed.from_json(secret_key).get_key()._secret;
}
var signature_bits = secret_key.signWithRecoverablePublicKey(hash);
var signature_base64 = sjcl.codec.base64.fromBits(signature_bits);
return signature_base64;
};
/**
* Verify the signature on a given message.
*
* Note that this function is asynchronous.
* The ripple-lib remote is used to check that the public
* key extracted from the signature corresponds to one that is currently
* active for the given account.
*
* @static
*
* @param {String} data.message
* @param {RippleAddress} data.account
* @param {Base64-encoded String} data.signature
* @param {ripple-lib Remote} remote
* @param {Function} callback
*
* @callback callback
* @param {Error} error
* @param {boolean} is_valid true if the signature is valid, false otherwise
*/
Message.verifyMessageSignature = function(data, remote, callback) {
if (typeof data.message === 'string') {
data.hash = Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message);
} else {
return callback(new Error('Data object must contain message field to verify signature'));
}
return Message.verifyHashSignature(data, remote, callback);
};
/**
* Verify the signature on a given hash.
*
* Note that this function is asynchronous.
* The ripple-lib remote is used to check that the public
* key extracted from the signature corresponds to one that is currently
* active for the given account.
*
* @static
*
* @param {bitArray|Hex-encoded String} data.hash
* @param {RippleAddress} data.account
* @param {Base64-encoded String} data.signature
* @param {ripple-lib Remote} remote
* @param {Function} callback
*
* @callback callback
* @param {Error} error
* @param {boolean} is_valid true if the signature is valid, false otherwise
*/
Message.verifyHashSignature = function(data, remote, callback) {
var hash,
account,
signature;
if(typeof callback !== 'function') {
throw new Error('Must supply callback function');
}
hash = data.hash;
if (hash && typeof hash === 'string' && REGEX_HEX.test(hash)) {
hash = sjcl.codec.hex.toBits(hash);
}
if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') {
return callback(new Error('Hash must be a bitArray or hex-encoded string'));
}
account = data.account || data.address;
if (!account || !UInt160.from_json(account).is_valid()) {
return callback(new Error('Account must be a valid ripple address'));
}
signature = data.signature;
if (typeof signature !== 'string' || !REGEX_BASE64.test(signature)) {
return callback(new Error('Signature must be a Base64-encoded string'));
}
signature = sjcl.codec.base64.toBits(signature);
if (!(remote instanceof Remote) || remote.state !== 'online') {
return callback(new Error('Must supply connected Remote to verify signature'));
}
function recoverPublicKey (async_callback) {
var public_key;
try {
public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
} catch (err) {
return async_callback(err);
}
async_callback(null, public_key);
};
function checkPublicKeyIsValid (public_key, async_callback) {
// Get hex-encoded public key
var key_pair = new KeyPair();
key_pair._pubkey = public_key;
var public_key_hex = key_pair.to_hex_pub();
var public_key_validator = new PublicKeyValidator(remote);
public_key_validator.validate(account, public_key_hex, async_callback);
};
var steps = [
recoverPublicKey,
checkPublicKeyIsValid
];
async.waterfall(steps, callback);
};
module.exports = Message;

View File

@@ -0,0 +1,104 @@
var async = require('async');
var UInt160 = require('./uint160').UInt160;
var sjcl = require('./utils').sjcl;
var Base = require('./base').Base;
/**
* @constructor PubKeyValidator
* @param {Remote} remote
*/
function PubKeyValidator(remote) {
var self = this;
if (remote) {
self._remote = remote;
} else {
throw(new Error('Must instantiate the PubKeyValidator with a ripple-lib Remote'));
}
// Convert hex string to UInt160
self._parsePublicKey = function(public_key) {
// Based on functions in /src/js/ripple/keypair.js
function hexToUInt160(public_key) {
var bits = sjcl.codec.hex.toBits(public_key);
var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
var address = UInt160.from_bits(hash);
address.set_version(Base.VER_ACCOUNT_ID);
return address.to_json();
}
if (UInt160.is_valid(public_key)) {
return public_key;
} else if (/^[0-9a-fA-F]+$/.test(public_key)) {
return hexToUInt160(public_key);
} else {
throw(new Error('Public key is invalid. Must be a UInt160 or a hex string'));
}
};
}
/**
* Check whether the public key is valid for the specified address.
*
* @param {String} address
* @param {String} public_key
* @param {Function} callback
*
* callback function is called with (err, is_valid), where is_valid
* is a boolean indicating whether the public_key supplied is active
*/
PubKeyValidator.prototype.validate = function(address, public_key, callback) {
var self = this;
var public_key_as_uint160;
try {
public_key_as_uint160 = self._parsePublicKey(public_key);
} catch (e) {
return callback(e);
}
function getAccountInfo(async_callback) {
self._remote.account(address).getInfo(async_callback);
};
function publicKeyIsValid(account_info_res, async_callback) {
var account_info = account_info_res.account_data;
// Respond with true if the RegularKey is set and matches the given public key or
// if the public key matches the account address and the lsfDisableMaster is not set
if (account_info.RegularKey &&
account_info.RegularKey === public_key_as_uint160) {
async_callback(null, true);
} else if (account_info.Account === public_key_as_uint160 &&
((account_info.Flags & 0x00100000) === 0)) {
async_callback(null, true);
} else {
async_callback(null, false);
}
};
var steps = [
getAccountInfo,
publicKeyIsValid
];
async.waterfall(steps, callback);
};
module.exports = PubKeyValidator;

View File

@@ -0,0 +1,83 @@
/**
* Check that the point is valid based on the method described in
* SEC 1: Elliptic Curve Cryptography, section 3.2.2.1:
* Elliptic Curve Public Key Validation Primitive
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isValidPoint = function() {
var self = this;
var field_modulus = self.curve.field.modulus;
if (self.isIdentity) {
return false;
}
// Check that coordinatres are in bounds
// Return false if x < 1 or x > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.x)) &&
!self.x.equals(1)) ||
(self.x.greaterEquals(field_modulus.sub(1))) &&
!self.x.equals(1)) {
return false;
}
// Return false if y < 1 or y > (field_modulus - 1)
if (((new sjcl.bn(1).greaterEquals(self.y)) &&
!self.y.equals(1)) ||
(self.y.greaterEquals(field_modulus.sub(1))) &&
!self.y.equals(1)) {
return false;
}
if (!self.isOnCurve()) {
return false;
}
// TODO check to make sure point is a scalar multiple of base_point
return true;
};
/**
* Check that the point is on the curve
*
* @returns {Boolean}
*/
sjcl.ecc.point.prototype.isOnCurve = function() {
var self = this;
var field_order = self.curve.r;
var component_a = self.curve.a;
var component_b = self.curve.b;
var field_modulus = self.curve.field.modulus;
var y_squared_mod_field_order = self.y.mul(self.y).mod(field_modulus);
var x_cubed_plus_ax_plus_b = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus);
return y_squared_mod_field_order.equals(x_cubed_plus_ax_plus_b);
};
sjcl.ecc.point.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() +
')';
};
sjcl.ecc.pointJac.prototype.toString = function() {
return '(' +
this.x.toString() + ', ' +
this.y.toString() + ', ' +
this.z.toString() +
')';
};

View File

@@ -0,0 +1,308 @@
/**
* This module uses the public key recovery method
* described in SEC 1: Elliptic Curve Cryptography,
* section 4.1.6, "Public Key Recovery Operation".
* http://www.secg.org/download/aid-780/sec1-v2.pdf
*
* Implementation based on:
* https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js
*/
// Defined here so that this value only needs to be calculated once
var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR;
/**
* Sign the given hash such that the public key, prepending an extra byte
* so that the public key will be recoverable from the signature
*
* @param {bitArray} hash
* @param {Number} paranoia
* @returns {bitArray} Signature formatted as bitArray
*/
sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) {
var self = this;
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
// Sign hash with standard, canonicalized method
var standard_signature = self.sign(hash_bits, paranoia, k_for_testing);
var canonical_signature = self.canonicalizeSignature(standard_signature);
// Extract r and s signature components from canonical signature
var r_and_s = getRandSFromSignature(self._curve, canonical_signature);
// Rederive public key
var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get()));
// Determine recovery factor based on which possible value
// returns the correct public key
var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key);
// Prepend recovery_factor to signature and encode in DER
// The value_to_prepend should be 4 bytes total
var value_to_prepend = recovery_factor + 27;
var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature);
// Return value in bits
return final_signature_bits;
};
/**
* Recover the public key from a signature created with the
* signWithRecoverablePublicKey method in this module
*
* @static
*
* @param {bitArray} hash
* @param {bitArray} signature
* @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve
* @returns {sjcl.ecc.ecdsa.publicKey} Public key
*/
sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) {
var self = this;
if (!signature || signature instanceof sjcl.ecc.curve) {
throw new sjcl.exception.invalid('must supply hash and signature to recover public key');
}
if (!curve) {
curve = sjcl.ecc.curves['c256'];
}
// Convert hash to bits and determine encoding for output
var hash_bits;
if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') {
hash_bits = hash;
} else {
throw new sjcl.exception.invalid('hash. Must be a bitArray');
}
var signature_bits;
if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') {
signature_bits = signature;
} else {
throw new sjcl.exception.invalid('signature. Must be a bitArray');
}
// Extract recovery_factor from first 4 bytes
var recovery_factor = signature_bits[0] - 27;
if (recovery_factor < 0 || recovery_factor > 3) {
throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' +
'that prepends the recovery factor in order to recover the public key');
}
// Separate r and s values
var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1));
var signature_r = r_and_s.r;
var signature_s = r_and_s.s;
// Recover public key using recovery_factor
var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor);
var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point);
return recovered_public_key;
};
/**
* Retrieve the r and s components of a signature
*
* @param {sjcl.ecc.curve} curve
* @param {bitArray} signature
* @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn
*/
function getRandSFromSignature(curve, signature) {
var r_length = curve.r.bitLength();
return {
r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)),
s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature)))
};
};
/**
* Determine the recovery factor by trying all four
* possibilities and figuring out which results in the
* correct public key
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {sjcl.ecc.point} original_public_key_point
* @returns {Number, 0-3} Recovery factor
*/
function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) {
var original_public_key_point_bits = original_public_key_point.toBits();
// TODO: verify that it is possible for the recovery_factor to be 2 or 3,
// we may only need 1 bit because the canonical signature might remove the
// possibility of us needing to "use the second candidate key"
for (var possible_factor = 0; possible_factor < 4; possible_factor++) {
var resulting_public_key_point;
try {
resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor);
} catch (err) {
// console.log(err, err.stack);
continue;
}
if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) {
return possible_factor;
}
}
throw new sjcl.exception.bug('unable to calculate recovery factor from signature');
};
/**
* Recover the public key from the signature.
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {bitArray} hash_bits
* @param {Number, 0-3} recovery_factor
* @returns {sjcl.point} Public key corresponding to signature
*/
function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) {
var field_order = curve.r;
var field_modulus = curve.field.modulus;
// Reduce the recovery_factor to the two bits used
recovery_factor = recovery_factor & 3;
// The less significant bit specifies whether the y coordinate
// of the compressed point is even or not.
var compressed_point_y_coord_is_even = recovery_factor & 1;
// The more significant bit specifies whether we should use the
// first or second candidate key.
var use_second_candidate_key = recovery_factor >> 1;
// Calculate (field_order + 1) / 4
if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) {
FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4);
}
// In the paper they write "1. For j from 0 to h do the following..."
// That is not necessary here because we are given the recovery_factor
// step 1.1 Let x = r + jn
// Here "j" is either 0 or 1
var x;
if (use_second_candidate_key) {
x = signature_r.add(field_order);
} else {
x = signature_r;
}
// step 1.2 and 1.3 convert x to an elliptic curve point
// Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion
var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus);
var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus);
// If beta is even but y isn't or
// if beta is odd and y is even
// then subtract beta from the field_modulus
var y;
var beta_is_even = beta.mod(2).equals(0);
if (beta_is_even && !compressed_point_y_coord_is_even ||
!beta_is_even && compressed_point_y_coord_is_even) {
y = beta;
} else {
y = field_modulus.sub(beta);
}
// generated_point_R is the point generated from x and y
var generated_point_R = new sjcl.ecc.point(curve, x, y);
// step 1.4 check that R is valid and R x field_order !== infinity
// TODO: add check for R x field_order === infinity
if (!generated_point_R.isValidPoint()) {
throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key');
}
// step 1.5 Compute e from M
var message_e = sjcl.bn.fromBits(hash_bits);
var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order);
// step 1.6 Compute Q = r^-1 (sR - eG)
// console.log('r: ', signature_r);
var signature_r_inv = signature_r.inverseMod(field_order);
var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv);
// Validate public key point
if (!public_key_point.isValidPoint()) {
throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key');
}
// Verify that this public key matches the signature
if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) {
throw new sjcl.exception.corrupt('cannot recover public key');
}
return public_key_point;
};
/**
* Verify a signature given the raw components
* using method defined in section 4.1.5:
* "Alternative Verifying Operation"
*
* @param {sjcl.ecc.curve} curve
* @param {sjcl.bn} e
* @param {sjcl.bn} r
* @param {sjcl.bn} s
* @param {sjcl.ecc.point} public_key_point
* @returns {Boolean}
*/
function verify_raw(curve, e, r, s, public_key_point) {
var field_order = curve.r;
// Return false if r is out of bounds
if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Return false if s is out of bounds
if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) {
return false;
}
// Check that r = (u1 + u2)G
// u1 = e x s^-1 (mod field_order)
// u2 = r x s^-1 (mod field_order)
var s_mod_inverse_field_order = s.inverseMod(field_order);
var u1 = e.mul(s_mod_inverse_field_order).mod(field_order);
var u2 = r.mul(s_mod_inverse_field_order).mod(field_order);
var point_computed = curve.G.mult2(u1, u2, public_key_point);
return r.equals(point_computed.x.mod(field_order));
};

View File

@@ -62,7 +62,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () {
var f = e.square();
var x = f.sub(d.copy().doubleM());
var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM());
var z = this.y.mul(this.z).doubleM();
var z = this.z.mul(this.y).doubleM();
return new sjcl.ecc.pointJac(this.curve, x, y, z);
};

View File

@@ -1,9 +1,21 @@
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia) {
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) {
var R = this._curve.r,
l = R.bitLength(),
k = sjcl.bn.random(R.sub(1), paranoia).add(1),
r = this._curve.G.mult(k).x.mod(R),
s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
l = R.bitLength();
// k_for_testing should ONLY BE SPECIFIED FOR TESTING
// specifying it will make the signature INSECURE
var k;
if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') {
k = k_for_testing;
} else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) {
k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing));
} else {
// This is the only option that should be used in production
k = sjcl.bn.random(R.sub(1), paranoia).add(1);
}
var r = this._curve.G.mult(k).x.mod(R);
var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R);
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
};

View File

@@ -0,0 +1,157 @@
var assert = require('assert');
var PubKeyValidator = require('../src/js/ripple/pubkeyvalidator');
describe('PubKeyValidator', function(){
describe('._parsePublicKey()', function(){
var pkv = new PubKeyValidator({});
it('should throw an error if the key is invalid', function(){
try {
pkv._parsePublicKey('not a real key');
} catch (e) {
assert(e);
}
});
it('should return unchanged a valid UINT160', function(){
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'));
});
it('should parse a hex-encoded public key as a UINT160', function(){
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332'));
assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === pkv._parsePublicKey('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23'));
assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === pkv._parsePublicKey('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7'));
});
});
describe('.validate()', function(){
it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){
var pkv = new PubKeyValidator({
account: function(address){
return {
getInfo: function(callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: 65536,
LedgerEntryType: 'AccountRoot'
}});
}
}
}
}
});
pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){
assert(err === null);
assert(is_valid === true);
});
});
it('should respond false if the public key corresponds to the account address and the master key IS disabled', function(){
var pkv = new PubKeyValidator({
account: function(address){
return {
getInfo: function(callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot'
}});
}
}
}
}
});
pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){
assert(err === null);
assert(is_valid === false);
});
});
it('should respond true if the public key corresponds to the regular key', function(){
var pkv = new PubKeyValidator({
account: function(address){
return {
getInfo: function(callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}
}
});
pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){
assert(err === null);
assert(is_valid === true);
});
});
it('should respond false if the public key does not correspond to an active public key for the account', function(){
var pkv = new PubKeyValidator({
account: function(address){
return {
getInfo: function(callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}
}
});
pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){
assert(err === null);
assert(is_valid === false);
});
});
it('should respond false if the public key is invalid', function(){
var pkv = new PubKeyValidator({
account: function(address){
return {
getInfo: function(callback) {
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
callback(null, { account_data: {
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
Flags: parseInt(65536 | 0x00100000),
LedgerEntryType: 'AccountRoot',
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
}});
}
}
}
}
});
pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', 'not a real public key', function(err, is_valid){
assert(err);
});
});
});
});

View File

@@ -0,0 +1,245 @@
var assert = require('assert');
var utils = require('./testutils');
var sjcl = require('../build/sjcl');
describe('ECDSA signing with recoverable public key', function(){
describe('Sign and recover public key from signature', function(){
it('should recover public keys from signatures it generates', function(){
var messages = [{
message: 'Hello world!',
secret_hex: '9931c08f61f127d5735fa3c60e702212ce7ed9a2ac90d5dbade99c689728cd9b',
random_value: '5473a3dbdc13ec9efbad7f7f929fbbea404af556a48041dd9d41d29fdbc989ad',
hash_function: sjcl.hash.sha512.hash
// signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I='
}, {
// Correct recovery value for this one is 0
message: 'ua5pdcG0I1JuhSr9Fwai2UoZ9ll5leUtHE5NzSSNnPkw8nSPH5mT1gE1fe0sn',
secret_hex: '84814318ffe6e612694ad59b9084b7b66d68b6979567c619171a67b05e2b654b',
random_value: '14261d30b319709c10ab13cabe595313b99dd2d5c76b8b38d7eb445f0b81cc9a',
hash_function: sjcl.hash.sha512.hash
// signature: 'AAAAHGjpBM7wnTHbPGo0TXsxKbr+d7KvACuJ/eGQsp3ZJfOOQHszaciRo3ClenwKixcquFcBlaVfHlOc3JWOZq1RjpQ='
}, {
// Correct recovery value for this one is 1
message: 'rxc76UnmVTp',
secret_hex: '37eac47c212be8ea8372f506b11673c281cd9ea29a035c2c9e90d027c3dbecc6',
random_value: '61b53ca6de0543f911765ae216a3a4d851918a0733fba9ac80cf29de5bec8032',
hash_function: sjcl.hash.sha256.hash
// signature: 'AAAAG8L/yOA3nNqK4aOiQWJmOaWvkvr3NoTk6wCdX97U3qowdgFd98UK3evWV16qO3RHgFMEnUW/Vt4+kcidqW6hMo0='
}];
var curve = sjcl.ecc.curves['c256'];
for (var m = 0; m < messages.length; m++) {
var message = messages[m].message;
var secret_hex = messages[m].secret_hex;
var random_value = messages[m].random_value;
var hash_function = messages[m].hash_function;
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var pub_val_point = secret_key._curve.G.mult(secret_key._exponent);
var public_key = new sjcl.ecc.ecdsa.publicKey(curve, pub_val_point);
var hash = hash_function(message);
var recoverable_signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, recoverable_signature);
assert.deepEqual(public_key.get().x, recovered_public_key.get().x, 'The x value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().x.toString() + '. Actual: ' + recovered_public_key.get().x.toString());
assert.deepEqual(public_key.get().y, recovered_public_key.get().y, 'The y value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().y.toString() + '. Actual: ' + recovered_public_key.get().y.toString());
}
});
});
describe('signWithRecoverablePublicKey', function(){
// it('should produce the same values as bitcoinjs-lib\'s implementation', function(){
// // TODO: figure out why bitcoinjs-lib and this produce different signature values
// var curve = sjcl.ecc.curves['c256'];
// var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
// var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
// var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
// // var public_key = '0217b9f5b3ba8d550f19fdfb5233818cd27d19aaea029b667f547f5918c307ed3b';
// var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
// var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
// var bitcoin_signature_base64 = 'IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=';
// var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
// var signature_base64 = sjcl.codec.base64.fromBits(signature);
// assert.equal(signature_base64, bitcoin_signature_base64);
// });
it('should produce an error if the hash is not given as a bitArray', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
assert.throws(function(){
secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
}, /(?=.*hash)(?=.*bitArray).+/);
});
it('should return a bitArray', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
assert(typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number');
});
it('should return a bitArray where the first word contains the recovery factor', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var recovery_factor = signature[0] - 27;
assert(recovery_factor >= 0 && recovery_factor < 4);
});
});
describe('recoverFromSignature', function(){
// it('should be able to recover public keys from bitcoinjs-lib\'s implementation', function(){
// // TODO: figure out why bitcoinjs-lib and this produce different signature values
// var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
// var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=');
// var public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
// });
it('should produce an error if the signature given does not have the recovery factor prefix', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.sign(hash, 0, random_value);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
}, /(?=.*signature)(?=.*recovery factor)(?=.*public key).*/);
});
it('should produce an error if it is not given both the hash and the signature', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash);
}, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/);
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(signature);
}, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/);
});
it('should produce an error if it cannot generate a valid public key from the the signature', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM=');
signature[0] = 27;
signature[3] = 0 - signature[3];
assert.throws(function(){
sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
}, /(?=.*Cannot\ recover\ public\ key).*/);
});
it('should return a publicKey object', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
var key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
assert(key instanceof sjcl.ecc.ecdsa.publicKey);
});
it('tampering with the signature should produce a different public key, if it produces a valid one at all', function(){
var curve = sjcl.ecc.curves['c256'];
var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d';
var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex));
var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn);
var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a';
var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778');
var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value);
signature[3]++;
var original_public_key = new sjcl.ecc.ecdsa.publicKey(curve, curve.G.mult(secret_key._exponent));
var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature);
assert.notDeepEqual(original_public_key.get().x, recovered_public_key.get().x);
assert.notDeepEqual(original_public_key.get().y, recovered_public_key.get().y);
});
});
});