mirror of
https://github.com/Xahau/xahau.js.git
synced 2026-08-23 16:40:52 +00:00
Merge pull request #72 from emschwartz/message-signing
[FEATURE] New Message class for sigs on arbitrary data
This commit is contained in:
@@ -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'
|
||||
|
||||
455
build/sjcl.js
455
build/sjcl.js
@@ -2914,8 +2914,16 @@ sjcl.ecc.point = function(curve,x,y) {
|
||||
if (x === undefined) {
|
||||
this.isIdentity = true;
|
||||
} else {
|
||||
if (x instanceof sjcl.bn) {
|
||||
x = new curve.field(x);
|
||||
}
|
||||
if (y instanceof sjcl.bn) {
|
||||
y = new curve.field(y);
|
||||
}
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
this.isIdentity = false;
|
||||
}
|
||||
this.curve = curve;
|
||||
@@ -3428,6 +3436,90 @@ sjcl.keyexchange.srp = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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() +
|
||||
')';
|
||||
};
|
||||
|
||||
// ----- for secp256k1 ------
|
||||
|
||||
// Overwrite NIST-P256 with secp256k1
|
||||
@@ -3492,7 +3584,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);
|
||||
};
|
||||
|
||||
@@ -4012,35 +4104,43 @@ sjcl.bn.prototype.powermodMontgomery = function (e, m)
|
||||
return z.revert(r);
|
||||
}
|
||||
|
||||
sjcl.ecc.ecdsa.secretKey.prototype = {
|
||||
sign: function(hash, paranoia) {
|
||||
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);
|
||||
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) {
|
||||
var R = this._curve.r,
|
||||
l = R.bitLength();
|
||||
|
||||
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
|
||||
// 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));
|
||||
};
|
||||
|
||||
sjcl.ecc.ecdsa.publicKey.prototype = {
|
||||
verify: function(hash, rs) {
|
||||
var w = sjcl.bitArray,
|
||||
R = this._curve.r,
|
||||
l = R.bitLength(),
|
||||
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
|
||||
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
|
||||
sInv = s.inverseMod(R),
|
||||
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
|
||||
hA = r.mul(sInv).mod(R),
|
||||
r2 = this._curve.G.mult2(hG, hA, this._point).x;
|
||||
sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) {
|
||||
var w = sjcl.bitArray,
|
||||
R = this._curve.r,
|
||||
l = R.bitLength(),
|
||||
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
|
||||
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
|
||||
sInv = s.inverseMod(R),
|
||||
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
|
||||
hA = r.mul(sInv).mod(R),
|
||||
r2 = this._curve.G.mult2(hG, hA, this._point).x;
|
||||
|
||||
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
|
||||
throw (new sjcl.exception.corrupt("signature didn't check out"));
|
||||
}
|
||||
return true;
|
||||
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
|
||||
throw (new sjcl.exception.corrupt("signature didn't check out"));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) {
|
||||
@@ -4096,6 +4196,313 @@ sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
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));
|
||||
|
||||
};
|
||||
|
||||
|
||||
sjcl.bn.prototype.jacobi = function (that) {
|
||||
var a = this;
|
||||
that = new sjcl.bn(that);
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
//
|
||||
|
||||
// var network = require("./network.js");
|
||||
|
||||
var async = require('async');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
var extend = require('extend');
|
||||
var Amount = require('./amount').Amount;
|
||||
var UInt160 = require('./uint160').UInt160;
|
||||
var TransactionManager = require('./transactionmanager').TransactionManager;
|
||||
var sjcl = require('./utils').sjcl;
|
||||
var Base = require('./base').Base;
|
||||
|
||||
/**
|
||||
* @constructor Account
|
||||
@@ -278,6 +280,124 @@ Account.prototype.submit = function(transaction) {
|
||||
this._transactionManager.submit(transaction);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the given public key is valid for this account
|
||||
*
|
||||
* @param {Hex-encoded String|RippleAddress} public_key
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} true if the public key is valid and active, false otherwise
|
||||
*/
|
||||
Account.prototype.publicKeyIsActive = function(public_key, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var public_key_as_uint160;
|
||||
try {
|
||||
public_key_as_uint160 = Account._publicKeyToAddress(public_key);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
function getAccountInfo(async_callback) {
|
||||
self.getInfo(function(err, account_info_res){
|
||||
|
||||
// If the remote responds with an Account Not Found error then the account
|
||||
// is unfunded and thus we can assume that the master key is active
|
||||
if (err && err.remote && err.remote.error === 'actNotFound') {
|
||||
async_callback(null, null);
|
||||
} else {
|
||||
async_callback(err, account_info_res);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function publicKeyIsValid(account_info_res, async_callback) {
|
||||
// Catch the case of unfunded accounts
|
||||
if (!account_info_res) {
|
||||
|
||||
if (public_key_as_uint160 === self._account_id) {
|
||||
async_callback(null, true);
|
||||
} else {
|
||||
async_callback(null, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a hex-encoded public key to a Ripple Address
|
||||
*
|
||||
* @static
|
||||
*
|
||||
* @param {Hex-encoded string|RippleAddress} public_key
|
||||
* @returns {RippleAddress}
|
||||
*/
|
||||
Account._publicKeyToAddress = 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'));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.Account = Account;
|
||||
|
||||
// vim:sw=2:sts=2:ts=8:et
|
||||
|
||||
@@ -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');
|
||||
|
||||
203
src/js/ripple/message.js
Normal file
203
src/js/ripple/message.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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 Account = require('./account').Account;
|
||||
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
|
||||
* @param {RippleAddress} [The first key] account Field to specify the signing account.
|
||||
* If this is omitted the first account produced by the secret generator will be used.
|
||||
* @returns {Base64-encoded String} signature
|
||||
*/
|
||||
Message.signMessage = function(message, secret_key, account) {
|
||||
|
||||
return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key, account);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {RippleAddress} [The first key] account Field to specify the signing account.
|
||||
* If this is omitted the first account produced by the secret generator will be used.
|
||||
* @returns {Base64-encoded String} signature
|
||||
*/
|
||||
Message.signHash = function(hash, secret_key, account) {
|
||||
|
||||
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(account)._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);
|
||||
}
|
||||
|
||||
if (public_key) {
|
||||
async_callback(null, public_key);
|
||||
} else {
|
||||
async_callback(new Error('Could not recover public key from signature'));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
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 account_class_instance = new Account(remote, account);
|
||||
account_class_instance.publicKeyIsActive(public_key_hex, async_callback);
|
||||
|
||||
};
|
||||
|
||||
var steps = [
|
||||
recoverPublicKey,
|
||||
checkPublicKeyIsValid
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
|
||||
};
|
||||
|
||||
module.exports = Message;
|
||||
@@ -1311,7 +1311,7 @@ Remote.prototype.getAccount = function(accountID) {
|
||||
Remote.prototype.addAccount = function(accountID) {
|
||||
var account = new Account(this, accountID);
|
||||
|
||||
if (account.is_valid()) {
|
||||
if (account.isValid()) {
|
||||
this._accounts[accountID] = account;
|
||||
}
|
||||
|
||||
|
||||
83
src/js/sjcl-custom/sjcl-ecc-pointextras.js
Normal file
83
src/js/sjcl-custom/sjcl-ecc-pointextras.js
Normal 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 left_hand_side = self.y.mul(self.y).mod(field_modulus);
|
||||
var right_hand_side = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus);
|
||||
|
||||
return left_hand_side.equals(right_hand_side);
|
||||
|
||||
};
|
||||
|
||||
|
||||
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() +
|
||||
')';
|
||||
};
|
||||
306
src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js
Normal file
306
src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
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));
|
||||
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
sjcl.ecc.ecdsa.secretKey.prototype = {
|
||||
sign: function(hash, paranoia) {
|
||||
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);
|
||||
sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) {
|
||||
var R = this._curve.r,
|
||||
l = R.bitLength();
|
||||
|
||||
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l));
|
||||
// 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));
|
||||
};
|
||||
|
||||
sjcl.ecc.ecdsa.publicKey.prototype = {
|
||||
verify: function(hash, rs) {
|
||||
var w = sjcl.bitArray,
|
||||
R = this._curve.r,
|
||||
l = R.bitLength(),
|
||||
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
|
||||
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
|
||||
sInv = s.inverseMod(R),
|
||||
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
|
||||
hA = r.mul(sInv).mod(R),
|
||||
r2 = this._curve.G.mult2(hG, hA, this._point).x;
|
||||
sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) {
|
||||
var w = sjcl.bitArray,
|
||||
R = this._curve.r,
|
||||
l = R.bitLength(),
|
||||
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)),
|
||||
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)),
|
||||
sInv = s.inverseMod(R),
|
||||
hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R),
|
||||
hA = r.mul(sInv).mod(R),
|
||||
r2 = this._curve.G.mult2(hG, hA, this._point).x;
|
||||
|
||||
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
|
||||
throw (new sjcl.exception.corrupt("signature didn't check out"));
|
||||
}
|
||||
return true;
|
||||
if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) {
|
||||
throw (new sjcl.exception.corrupt("signature didn't check out"));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -11,8 +11,16 @@ sjcl.ecc.point = function(curve,x,y) {
|
||||
if (x === undefined) {
|
||||
this.isIdentity = true;
|
||||
} else {
|
||||
if (x instanceof sjcl.bn) {
|
||||
x = new curve.field(x);
|
||||
}
|
||||
if (y instanceof sjcl.bn) {
|
||||
y = new curve.field(y);
|
||||
}
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
this.isIdentity = false;
|
||||
}
|
||||
this.curve = curve;
|
||||
|
||||
181
test/account-test.js
Normal file
181
test/account-test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
var assert = require('assert');
|
||||
var Account = require('../src/js/ripple/account').Account;
|
||||
|
||||
describe('Account', function(){
|
||||
|
||||
describe('._publicKeyToAddress()', function(){
|
||||
|
||||
it('should throw an error if the key is invalid', function(){
|
||||
try {
|
||||
Account._publicKeyToAddress('not a real key');
|
||||
} catch (e) {
|
||||
assert(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return unchanged a valid UINT160', function(){
|
||||
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'));
|
||||
});
|
||||
|
||||
it('should parse a hex-encoded public key as a UINT160', function(){
|
||||
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332'));
|
||||
|
||||
assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === Account._publicKeyToAddress('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23'));
|
||||
|
||||
assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === Account._publicKeyToAddress('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7'));
|
||||
|
||||
assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === Account._publicKeyToAddress('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('.publicKeyIsActive()', function(){
|
||||
|
||||
it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){
|
||||
|
||||
var account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: 65536,
|
||||
LedgerEntryType: 'AccountRoot'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){
|
||||
assert(err === null);
|
||||
assert(is_valid === false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond false if the public key is invalid', function(){
|
||||
|
||||
var account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('not a real public key', function(err, is_valid){
|
||||
assert(err);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should assume the master key is valid for unfunded accounts', function(){
|
||||
|
||||
var account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ') {
|
||||
callback({ error: 'remoteError',
|
||||
error_message: 'Remote reported an error.',
|
||||
remote:
|
||||
{ account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ',
|
||||
error: 'actNotFound',
|
||||
error_code: 15,
|
||||
error_message: 'Account not found.',
|
||||
id: 3,
|
||||
ledger_current_index: 6391106,
|
||||
request:
|
||||
{ account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ',
|
||||
command: 'account_info',
|
||||
id: 3,
|
||||
ident: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' },
|
||||
status: 'error',
|
||||
type: 'response' },
|
||||
result: 'remoteError',
|
||||
engine_result: 'remoteError',
|
||||
result_message: 'Remote reported an error.',
|
||||
engine_result_message: 'Remote reported an error.',
|
||||
message: 'Remote reported an error.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ');
|
||||
account.publicKeyIsActive('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){
|
||||
assert(!err);
|
||||
assert(is_valid);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
327
test/message-test.js
Normal file
327
test/message-test.js
Normal file
@@ -0,0 +1,327 @@
|
||||
var assert = require('assert');
|
||||
var sjcl = require('../build/sjcl');
|
||||
var Message = require('../src/js/ripple/message');
|
||||
var Seed = require('../src/js/ripple/seed').Seed;
|
||||
var Remote = require('../src/js/ripple/remote').Remote;
|
||||
|
||||
describe('Message', function(){
|
||||
|
||||
describe('signMessage', function(){
|
||||
|
||||
it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call signHash', function(){
|
||||
|
||||
var normal_signHash = Message.signHash;
|
||||
|
||||
var message_text = 'Hello World!';
|
||||
|
||||
var signHash_called = false;
|
||||
Message.signHash = function(hash) {
|
||||
signHash_called = true;
|
||||
assert.deepEqual(hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + message_text));
|
||||
};
|
||||
|
||||
Message.signMessage(message_text);
|
||||
assert(signHash_called);
|
||||
|
||||
Message.signHash = normal_signHash;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('signHash', function(){
|
||||
|
||||
it('should accept the hash as either a hex string or a bitArray', function(){
|
||||
|
||||
var normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words){
|
||||
var words = [];
|
||||
for (var w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
var signature1 = Message.signHash(hash, secret_string);
|
||||
var signature2 = Message.signHash(sjcl.codec.hex.toBits(hash), secret_string);
|
||||
|
||||
assert.strictEqual(signature1, signature2);
|
||||
|
||||
sjcl.random.randomWords = normal_random;
|
||||
|
||||
});
|
||||
|
||||
it('should accept the secret as a string or scjl.ecc.ecdsa.secretKey object', function(){
|
||||
|
||||
var normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words){
|
||||
var words = [];
|
||||
for (var w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
var signature1 = Message.signHash(hash, secret_string);
|
||||
var signature2 = Message.signHash(hash, Seed.from_json(secret_string).get_key()._secret);
|
||||
|
||||
assert.strictEqual(signature1, signature2);
|
||||
|
||||
sjcl.random.randomWords = normal_random;
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error if given an invalid secret key', function(){
|
||||
|
||||
var secret_string = 'badsafRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
assert.throws(function(){
|
||||
Message.signHash(hash, secret_string);
|
||||
}, /Cannot\ generate\ keys\ from\ invalid\ seed/);
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error if the parameters are reversed', function(){
|
||||
|
||||
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
assert.throws(function(){
|
||||
Message.signHash(secret_string, hash);
|
||||
}, Error);
|
||||
|
||||
assert.throws(function(){
|
||||
Message.signHash(secret_string, sjcl.codec.hex.toBits(hash));
|
||||
}, Error);
|
||||
|
||||
assert.throws(function(){
|
||||
Message.signHash(Seed.from_json(secret_string).get_key()._secret, hash);
|
||||
}, Error);
|
||||
|
||||
assert.throws(function(){
|
||||
Message.signHash(Seed.from_json(secret_string).get_key()._secret, sjcl.codec.hex.toBits(hash));
|
||||
}, Error);
|
||||
|
||||
});
|
||||
|
||||
it('should produce a base64-encoded signature', function(){
|
||||
var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/;
|
||||
|
||||
var normal_random = sjcl.random.randomWords;
|
||||
|
||||
sjcl.random.randomWords = function(num_words){
|
||||
var words = [];
|
||||
for (var w = 0; w < num_words; w++) {
|
||||
words.push(sjcl.codec.hex.toBits('00000000'));
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC';
|
||||
// var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3';
|
||||
var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778';
|
||||
|
||||
var signature = Message.signHash(hash, secret_string);
|
||||
|
||||
assert(REGEX_BASE64.test(signature));
|
||||
|
||||
sjcl.random.randomWords = normal_random;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('verifyMessageSignature', function(){
|
||||
|
||||
it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call verifyHashSignature', function(){
|
||||
|
||||
var normal_verifyHashSignature = Message.verifyHashSignature;
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I='
|
||||
};
|
||||
|
||||
var verifyHashSignature_called = false;
|
||||
Message.verifyHashSignature = function(vhs_data, remote, callback) {
|
||||
verifyHashSignature_called = true;
|
||||
|
||||
assert.deepEqual(vhs_data.hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message));
|
||||
assert.strictEqual(vhs_data.signature, data.signature);
|
||||
callback();
|
||||
|
||||
};
|
||||
|
||||
Message.verifyMessageSignature(data, {}, function(err){
|
||||
assert(!err);
|
||||
});
|
||||
assert(verifyHashSignature_called);
|
||||
|
||||
Message.verifyHashSignature = normal_verifyHashSignature;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('verifyHashSignature', function(){
|
||||
|
||||
it('should throw an error if a callback function is not supplied', function(){
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
|
||||
assert.throws(function(){
|
||||
Message.verifyHashSignature(data);
|
||||
}, /(?=.*callback\ function).*/);
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('should respond with an error if the hash is missing or invalid', function(done){
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err, valid){
|
||||
assert(/hash/i.test(err.message));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond with an error if the account is missing or invalid', function(done){
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk='
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err, valid){
|
||||
assert(/account|address/i.test(err.message));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond with an error if the signature is missing or invalid', function(done){
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
test_remote.state = 'online';
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err, valid){
|
||||
assert(/signature/i.test(err.message));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond true if the signature is valid and corresponds to an active public key for the account', function(done){
|
||||
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs='
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
test_remote.state = 'online';
|
||||
test_remote.request_account_info = function(account, callback) {
|
||||
if (account === data.account) {
|
||||
callback(null, {
|
||||
"account_data": {
|
||||
"Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz",
|
||||
"Flags": 1114112,
|
||||
"LedgerEntryType": "AccountRoot",
|
||||
"RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(new Error('wrong account'));
|
||||
}
|
||||
};
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err, valid){
|
||||
assert(!err);
|
||||
assert(valid);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond false if a key can be recovered from the signature but it does not correspond to an active public key', function(done){
|
||||
|
||||
// Signature created by disabled master key
|
||||
var data = {
|
||||
message: 'Hello world!',
|
||||
hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752',
|
||||
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk='
|
||||
};
|
||||
|
||||
Remote.prototype.addServer = function(){};
|
||||
var test_remote = new Remote({});
|
||||
test_remote.state = 'online';
|
||||
test_remote.request_account_info = function(account, callback) {
|
||||
if (account === data.account) {
|
||||
callback(null, {
|
||||
"account_data": {
|
||||
"Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz",
|
||||
"Flags": 1114112,
|
||||
"LedgerEntryType": "AccountRoot",
|
||||
"RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(new Error('wrong account'));
|
||||
}
|
||||
};
|
||||
|
||||
Message.verifyHashSignature(data, test_remote, function(err, valid){
|
||||
assert(!err);
|
||||
assert(!valid);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
245
test/sjcl-ecdsa-recoverablepublickey-test.js
Normal file
245
test/sjcl-ecdsa-recoverablepublickey-test.js
Normal 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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user