Implement Bitcoin's method for arbitrary message signatures.
This commit is contained in:
parent
6bf363b9de
commit
9b2f94a028
4 changed files with 365 additions and 15 deletions
263
src/ecdsa.js
263
src/ecdsa.js
|
@ -10,6 +10,118 @@ function integerToBytes(i, len) {
|
||||||
return bytes;
|
return bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a quadratic residue (mod p) of this number. p must be an odd prime.
|
||||||
|
*
|
||||||
|
* For a given number a, this function solves the congruence of the form
|
||||||
|
*
|
||||||
|
* x^2 = a (mod p)
|
||||||
|
*
|
||||||
|
* And returns x. Note that p - x is also a root.
|
||||||
|
*
|
||||||
|
* 0 is returned if no square root exists for these a and p.
|
||||||
|
*
|
||||||
|
* The Tonelli-Shanks algorithm is used (except for some simple cases
|
||||||
|
* in which the solution is known from an identity). This algorithm
|
||||||
|
* runs in polynomial time (unless the generalized Riemann hypothesis
|
||||||
|
* is false).
|
||||||
|
*
|
||||||
|
* Originally implemented in Python by Eli Bendersky:
|
||||||
|
* http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
|
||||||
|
*
|
||||||
|
* Ported to JavaScript by Stefan Thomas.
|
||||||
|
*/
|
||||||
|
BigInteger.prototype.modSqrt = function (p) {
|
||||||
|
var ONE = BigInteger.ONE,
|
||||||
|
TWO = BigInteger.valueOf(2);
|
||||||
|
|
||||||
|
// Simple cases
|
||||||
|
if (this.legendre(p) != 1) {
|
||||||
|
return BigInteger.ZERO;
|
||||||
|
} else if (this.equals(BigInteger.ZERO)) {
|
||||||
|
return BigInteger.ZERO;
|
||||||
|
} else if (p.equals(TWO)) {
|
||||||
|
return p;
|
||||||
|
} else if (p.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3))) {
|
||||||
|
return this.modPow(p.add(ONE).divide(BigInteger.valueOf(4)), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partition p-1 to s * 2^e for an odd s (i.e. reduce all the powers
|
||||||
|
// of 2 from p-1)
|
||||||
|
var s = p.subtract(ONE);
|
||||||
|
var e = 0;
|
||||||
|
while (s.isEven()) {
|
||||||
|
s = s.divide(TWO);
|
||||||
|
++e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find some 'n' with a legendre symbol n|p = -1.
|
||||||
|
// Shouldn't take long.
|
||||||
|
var n = TWO;
|
||||||
|
while (n.legendre(p) != -1) {
|
||||||
|
n = n.add(ONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here be dragons!
|
||||||
|
// Read the paper "Square roots from 1; 24, 51, 10 to Dan Shanks" by
|
||||||
|
// Ezra Brown for more information
|
||||||
|
|
||||||
|
// x is a guess of the square root that gets better with each
|
||||||
|
// iteration.
|
||||||
|
//
|
||||||
|
// b is the "fudge factor" - by how much we're off with the guess.
|
||||||
|
// The invariant x^2 = ab (mod p) is maintained throughout the loop.
|
||||||
|
//
|
||||||
|
// g is used for successive powers of n to update both a and b
|
||||||
|
//
|
||||||
|
// r is the exponent - decreases with each update
|
||||||
|
|
||||||
|
var x = this.modPow(s.add(ONE).divide(TWO), p);
|
||||||
|
var b = this.modPow(s, p);
|
||||||
|
var g = n.modPow(s, p);
|
||||||
|
var r = e;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
var t = b;
|
||||||
|
|
||||||
|
var m;
|
||||||
|
for (m = 0; m < r; m++) {
|
||||||
|
if (t.equals(ONE)) break;
|
||||||
|
|
||||||
|
t = t.modPowInt(2, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m == 0) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gs = g.modPow(TWO.pow(BigInteger.valueOf(r - m - 1)), p);
|
||||||
|
g = gs.multiply(gs).mod(p);
|
||||||
|
x = x.multiply(gs).mod(p);
|
||||||
|
b = b.multiply(g).mod(p);
|
||||||
|
r = m;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the Legendre symbol a|p using Euler's criterion.
|
||||||
|
*
|
||||||
|
* p is a prime, a is relatively prime to p
|
||||||
|
* (if p divides a, then a | p = 0)
|
||||||
|
*
|
||||||
|
* Returns 1 if a has a square root modulo p, -1 otherwise.
|
||||||
|
*/
|
||||||
|
BigInteger.prototype.legendre = function (p) {
|
||||||
|
var ls = this.modPow(p.subtract(BigInteger.ONE).shiftRight(1), p);
|
||||||
|
if (ls.equals(p.subtract(BigInteger.ONE))) {
|
||||||
|
return -1;
|
||||||
|
} else if (ls.equals(BigInteger.ZERO)) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ECFieldElementFp.prototype.getByteLength = function () {
|
ECFieldElementFp.prototype.getByteLength = function () {
|
||||||
return Math.floor((this.toBigInteger().bitLength() + 7) / 8);
|
return Math.floor((this.toBigInteger().bitLength() + 7) / 8);
|
||||||
};
|
};
|
||||||
|
@ -139,6 +251,11 @@ ECPointFp.prototype.isOnCurve = function () {
|
||||||
return lhs.equals(rhs);
|
return lhs.equals(rhs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ECPointFp.prototype.toString = function () {
|
||||||
|
return '('+this.getX().toBigInteger().toString()+','+
|
||||||
|
this.getY().toBigInteger().toString()+')';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate an elliptic curve point.
|
* Validate an elliptic curve point.
|
||||||
*
|
*
|
||||||
|
@ -239,13 +356,35 @@ Bitcoin.ECDSA = (function () {
|
||||||
},
|
},
|
||||||
|
|
||||||
verify: function (hash, sig, pubkey) {
|
verify: function (hash, sig, pubkey) {
|
||||||
var obj = ECDSA.parseSig(sig);
|
var r,s;
|
||||||
var r = obj.r;
|
if (Bitcoin.Util.isArray(sig)) {
|
||||||
var s = obj.s;
|
var obj = stringECDSA.parseSig(sig);
|
||||||
|
r = obj.r;
|
||||||
|
s = obj.s;
|
||||||
|
} else if ("object" === typeof sig && sig.r && sig.s) {
|
||||||
|
r = sig.r;
|
||||||
|
s = sig.s;
|
||||||
|
} else {
|
||||||
|
throw "Invalid value for signature";
|
||||||
|
}
|
||||||
|
|
||||||
var n = ecparams.getN();
|
var Q;
|
||||||
|
if (pubkey instanceof ECPointFp) {
|
||||||
|
Q = pubkey;
|
||||||
|
} else if (Bitcoin.Util.isArray(pubkey)) {
|
||||||
|
Q = ECPointFp.decodeFrom(ecparams.getCurve(), pubkey);
|
||||||
|
} else {
|
||||||
|
throw "Invalid format for pubkey value, must be byte array or ECPointFp";
|
||||||
|
}
|
||||||
var e = BigInteger.fromByteArrayUnsigned(hash);
|
var e = BigInteger.fromByteArrayUnsigned(hash);
|
||||||
|
|
||||||
|
return ECDSA.verifyRaw(e, r, s, Q);
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyRaw: function (e, r, s, Q) {
|
||||||
|
var n = ecparams.getN();
|
||||||
|
var G = ecparams.getG();
|
||||||
|
|
||||||
if (r.compareTo(BigInteger.ONE) < 0 ||
|
if (r.compareTo(BigInteger.ONE) < 0 ||
|
||||||
r.compareTo(n) >= 0)
|
r.compareTo(n) >= 0)
|
||||||
return false;
|
return false;
|
||||||
|
@ -259,12 +398,13 @@ Bitcoin.ECDSA = (function () {
|
||||||
var u1 = e.multiply(c).mod(n);
|
var u1 = e.multiply(c).mod(n);
|
||||||
var u2 = r.multiply(c).mod(n);
|
var u2 = r.multiply(c).mod(n);
|
||||||
|
|
||||||
var G = ecparams.getG();
|
// TODO(!!!): For some reason Shamir's trick isn't working with
|
||||||
var Q = ECPointFp.decodeFrom(ecparams.getCurve(), pubkey);
|
// signed message verification!? Probably an implementation
|
||||||
|
// error!
|
||||||
|
//var point = implShamirsTrick(G, u1, Q, u2);
|
||||||
|
var point = G.multiply(u1).add(Q.multiply(u2));
|
||||||
|
|
||||||
var point = implShamirsTrick(G, u1, Q, u2);
|
var v = point.getX().toBigInteger().mod(n);
|
||||||
|
|
||||||
var v = point.x.toBigInteger().mod(n);
|
|
||||||
|
|
||||||
return v.equals(r);
|
return v.equals(r);
|
||||||
},
|
},
|
||||||
|
@ -327,6 +467,111 @@ Bitcoin.ECDSA = (function () {
|
||||||
var s = BigInteger.fromByteArrayUnsigned(sBa);
|
var s = BigInteger.fromByteArrayUnsigned(sBa);
|
||||||
|
|
||||||
return {r: r, s: s};
|
return {r: r, s: s};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseSigCompact: function (sig) {
|
||||||
|
if (sig.length !== 65) {
|
||||||
|
throw "Signature has the wrong length";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature is prefixed with a type byte storing three bits of
|
||||||
|
// information.
|
||||||
|
var i = sig[0] - 27;
|
||||||
|
if (i < 0 || i > 7) {
|
||||||
|
throw "Invalid signature type";
|
||||||
|
}
|
||||||
|
|
||||||
|
var n = ecparams.getN();
|
||||||
|
var r = BigInteger.fromByteArrayUnsigned(sig.slice(1, 33)).mod(n);
|
||||||
|
var s = BigInteger.fromByteArrayUnsigned(sig.slice(33, 65)).mod(n);
|
||||||
|
|
||||||
|
return {r: r, s: s, i: i};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover a public key from a signature.
|
||||||
|
*
|
||||||
|
* See SEC 1: Elliptic Curve Cryptography, section 4.1.6, "Public
|
||||||
|
* Key Recovery Operation".
|
||||||
|
*
|
||||||
|
* http://www.secg.org/download/aid-780/sec1-v2.pdf
|
||||||
|
*/
|
||||||
|
recoverPubKey: function (r, s, hash, i) {
|
||||||
|
// The recovery parameter i has two bits.
|
||||||
|
i = i & 3;
|
||||||
|
|
||||||
|
// The less significant bit specifies whether the y coordinate
|
||||||
|
// of the compressed point is even or not.
|
||||||
|
var isYEven = i & 1;
|
||||||
|
|
||||||
|
// The more significant bit specifies whether we should use the
|
||||||
|
// first or second candidate key.
|
||||||
|
var isSecondKey = i >> 1;
|
||||||
|
|
||||||
|
var n = ecparams.getN();
|
||||||
|
var G = ecparams.getG();
|
||||||
|
var curve = ecparams.getCurve();
|
||||||
|
var p = curve.getQ();
|
||||||
|
var a = curve.getA().toBigInteger();
|
||||||
|
var b = curve.getB().toBigInteger();
|
||||||
|
|
||||||
|
// 1.1 Compute x
|
||||||
|
var x = isSecondKey ? r.add(n) : r;
|
||||||
|
|
||||||
|
// 1.3 Convert x to point
|
||||||
|
var alpha = x.multiply(x).multiply(x).add(a.multiply(x)).add(b).mod(p);
|
||||||
|
var beta = alpha.modSqrt(p);
|
||||||
|
|
||||||
|
var xorOdd = beta.isEven() ? (i % 2) : ((i+1) % 2);
|
||||||
|
// If beta is even, but y isn't or vice versa, then convert it,
|
||||||
|
// otherwise we're done and y == beta.
|
||||||
|
var y = (beta.isEven() ? !isYEven : isYEven) ? beta : p.subtract(beta);
|
||||||
|
|
||||||
|
// 1.4 Check that nR is at infinity
|
||||||
|
var R = new ECPointFp(curve,
|
||||||
|
curve.fromBigInteger(x),
|
||||||
|
curve.fromBigInteger(y));
|
||||||
|
R.validate();
|
||||||
|
|
||||||
|
// 1.5 Compute e from M
|
||||||
|
var e = BigInteger.fromByteArrayUnsigned(hash);
|
||||||
|
var eNeg = BigInteger.ZERO.subtract(e).mod(n);
|
||||||
|
|
||||||
|
// 1.6 Compute Q = r^-1 (sR - eG)
|
||||||
|
var rInv = r.modInverse(n);
|
||||||
|
var Q = implShamirsTrick(R, s, G, eNeg).multiply(rInv);
|
||||||
|
|
||||||
|
Q.validate();
|
||||||
|
if (!ECDSA.verifyRaw(e, r, s, Q)) {
|
||||||
|
throw "Pubkey recovery unsuccessful";
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubKey = new Bitcoin.ECKey();
|
||||||
|
pubKey.pub = Q;
|
||||||
|
return pubKey;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate pubkey extraction parameter.
|
||||||
|
*
|
||||||
|
* When extracting a pubkey from a signature, we have to
|
||||||
|
* distinguish four different cases. Rather than putting this
|
||||||
|
* burden on the verifier, Bitcoin includes a 2-bit value with the
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* This function simply tries all four cases and returns the value
|
||||||
|
* that resulted in a successful pubkey recovery.
|
||||||
|
*/
|
||||||
|
calcPubkeyRecoveryParam: function (r, s, hash)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
try {
|
||||||
|
if (Bitcoin.ECDSA.recoverPubKey(r, s, hash, i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
throw "Unable to find valid recovery factor";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
31
src/eckey.js
31
src/eckey.js
|
@ -23,12 +23,35 @@ Bitcoin.ECKey = (function () {
|
||||||
this.priv = BigInteger.fromByteArrayUnsigned(Crypto.util.base64ToBytes(input));
|
this.priv = BigInteger.fromByteArrayUnsigned(Crypto.util.base64ToBytes(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.compressed = !!ECKey.compressByDefault;
|
||||||
};
|
};
|
||||||
|
|
||||||
ECKey.prototype.getPub = function () {
|
/**
|
||||||
if (this.pub) return this.pub;
|
* Whether public keys should be returned compressed by default.
|
||||||
|
*/
|
||||||
|
ECKey.compressByDefault = false;
|
||||||
|
|
||||||
return this.pub = ecparams.getG().multiply(this.priv).getEncoded();
|
/**
|
||||||
|
* Set whether the public key should be returned compressed or not.
|
||||||
|
*/
|
||||||
|
ECKey.prototype.setCompressed = function (v) {
|
||||||
|
this.compressed = !!v;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public key in DER encoding.
|
||||||
|
*/
|
||||||
|
ECKey.prototype.getPub = function () {
|
||||||
|
return this.getPubPoint().getEncoded(this.compressed);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public point as ECPoint object.
|
||||||
|
*/
|
||||||
|
ECKey.prototype.getPubPoint = function () {
|
||||||
|
if (!this.pub) this.pub = ecparams.getG().multiply(this.priv);
|
||||||
|
|
||||||
|
return this.pub;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,7 +81,7 @@ Bitcoin.ECKey = (function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
ECKey.prototype.setPub = function (pub) {
|
ECKey.prototype.setPub = function (pub) {
|
||||||
this.pub = pub;
|
this.pub = ECPointFp.decodeFrom(ecparams.getCurve(), pub);
|
||||||
};
|
};
|
||||||
|
|
||||||
ECKey.prototype.toString = function (format) {
|
ECKey.prototype.toString = function (format) {
|
||||||
|
|
82
src/message.js
Normal file
82
src/message.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* Implements Bitcoin's feature for signing arbitrary messages.
|
||||||
|
*/
|
||||||
|
Bitcoin.Message = (function () {
|
||||||
|
var Message = {};
|
||||||
|
|
||||||
|
Message.magicPrefix = "Bitcoin Signed Message:\n";
|
||||||
|
|
||||||
|
Message.makeMagicMessage = function (message) {
|
||||||
|
var magicBytes = Crypto.charenc.UTF8.stringToBytes(Message.magicPrefix);
|
||||||
|
var messageBytes = Crypto.charenc.UTF8.stringToBytes(message);
|
||||||
|
|
||||||
|
var buffer = [];
|
||||||
|
buffer = buffer.concat(Bitcoin.Util.numToVarInt(magicBytes.length));
|
||||||
|
buffer = buffer.concat(magicBytes);
|
||||||
|
buffer = buffer.concat(Bitcoin.Util.numToVarInt(messageBytes.length));
|
||||||
|
buffer = buffer.concat(messageBytes);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Message.getHash = function (message) {
|
||||||
|
var buffer = Message.makeMagicMessage(message);
|
||||||
|
return Crypto.SHA256(Crypto.SHA256(buffer, {asBytes: true}), {asBytes: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
Message.signMessage = function (key, message, compressed) {
|
||||||
|
var hash = Message.getHash(message);
|
||||||
|
|
||||||
|
var sig = key.sign(hash);
|
||||||
|
|
||||||
|
var obj = Bitcoin.ECDSA.parseSig(sig);
|
||||||
|
|
||||||
|
var i = Bitcoin.ECDSA.calcPubkeyRecoveryParam(obj.r, obj.s, hash);
|
||||||
|
|
||||||
|
i += 27;
|
||||||
|
if (compressed) i += 4;
|
||||||
|
|
||||||
|
var rBa = obj.r.toByteArrayUnsigned();
|
||||||
|
var sBa = obj.r.toByteArrayUnsigned();
|
||||||
|
|
||||||
|
// Pad to 32 bytes per value
|
||||||
|
while (rBa.length < 32) rBa.unshift(0);
|
||||||
|
while (sBa.length < 32) sBa.unshift(0);
|
||||||
|
|
||||||
|
sig = [i].concat(rBa).concat(sBa);
|
||||||
|
|
||||||
|
return Crypto.util.bytesToBase64(sig);
|
||||||
|
};
|
||||||
|
|
||||||
|
Message.verifyMessage = function (address, sig, message) {
|
||||||
|
sig = Crypto.util.base64ToBytes(sig);
|
||||||
|
sig = Bitcoin.ECDSA.parseSigCompact(sig);
|
||||||
|
|
||||||
|
var hash = Message.getHash(message);
|
||||||
|
|
||||||
|
var isCompressed = !!(sig.i & 4);
|
||||||
|
var pubKey = Bitcoin.ECDSA.recoverPubKey(sig.r, sig.s, hash, sig.i);
|
||||||
|
|
||||||
|
pubKey.setCompressed(isCompressed);
|
||||||
|
|
||||||
|
var expectedAddress = pubKey.getBitcoinAddress().toString();
|
||||||
|
|
||||||
|
return (address === expectedAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log("should be true:", Bitcoin.Message.verifyMessage('16vqGo3KRKE9kTsTZxKoJKLzwZGTodK3ce',
|
||||||
|
'HPDs1TesA48a9up4QORIuub67VHBM37X66skAYz0Esg23gdfMuCTYDFORc6XGpKZ2/flJ2h/DUF569FJxGoVZ50=',
|
||||||
|
'test message'));
|
||||||
|
console.log("should be false:", Bitcoin.Message.verifyMessage('16vqGo3KRKE9kTsTZxKoJKLzwZGTodK3ce',
|
||||||
|
'HPDs1TesA48a9up4QORIuub67VHBM37X66skAYz0Esg23gdfMuCTYDFORc6XGpKZ2/flJ2h/DUF569FJxGoVZ50=',
|
||||||
|
'test message 2'));
|
||||||
|
console.log("should be true:", Bitcoin.Message.verifyMessage('1GdKjTSg2eMyeVvPV5Nivo6kR8yP2GT7wF',
|
||||||
|
'GyMn9AdYeZIPWLVCiAblOOG18Qqy4fFaqjg5rjH6QT5tNiUXLS6T2o7iuWkV1gc4DbEWvyi8yJ8FvSkmEs3voWE=',
|
||||||
|
'freenode:#bitcoin-otc:b42f7e7ea336db4109df6badc05c6b3ea8bfaa13575b51631c5178a7'));
|
||||||
|
console.log("should be true:", Bitcoin.Message.verifyMessage('1Hpj6xv9AzaaXjPPisQrdAD2tu84cnPv3f',
|
||||||
|
'INEJxQnSu6mwGnLs0E8eirl5g+0cAC9D5M7hALHD9sK0XQ66CH9mas06gNoIX7K1NKTLaj3MzVe8z3pt6apGJ34=',
|
||||||
|
'testtest'));
|
||||||
|
|
|
@ -40,7 +40,7 @@ Bitcoin.Wallet = (function () {
|
||||||
if ("string" === typeof pub) {
|
if ("string" === typeof pub) {
|
||||||
pub = Crypto.util.base64ToBytes(pub);
|
pub = Crypto.util.base64ToBytes(pub);
|
||||||
}
|
}
|
||||||
key.pub = pub;
|
key.setPub(pub);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addressHashes.push(key.getBitcoinAddress().getHashBase64());
|
this.addressHashes.push(key.getBitcoinAddress().getHashBase64());
|
||||||
|
|
Loading…
Add table
Reference in a new issue