Implement Bitcoin's method for arbitrary message signatures.

This commit is contained in:
Stefan Thomas 2012-08-16 00:25:06 +02:00
parent 6bf363b9de
commit 9b2f94a028
4 changed files with 365 additions and 15 deletions

View file

@ -10,6 +10,118 @@ function integerToBytes(i, len) {
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 () {
return Math.floor((this.toBigInteger().bitLength() + 7) / 8);
};
@ -139,6 +251,11 @@ ECPointFp.prototype.isOnCurve = function () {
return lhs.equals(rhs);
};
ECPointFp.prototype.toString = function () {
return '('+this.getX().toBigInteger().toString()+','+
this.getY().toBigInteger().toString()+')';
};
/**
* Validate an elliptic curve point.
*
@ -239,13 +356,35 @@ Bitcoin.ECDSA = (function () {
},
verify: function (hash, sig, pubkey) {
var obj = ECDSA.parseSig(sig);
var r = obj.r;
var s = obj.s;
var r,s;
if (Bitcoin.Util.isArray(sig)) {
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);
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 ||
r.compareTo(n) >= 0)
return false;
@ -259,12 +398,13 @@ Bitcoin.ECDSA = (function () {
var u1 = e.multiply(c).mod(n);
var u2 = r.multiply(c).mod(n);
var G = ecparams.getG();
var Q = ECPointFp.decodeFrom(ecparams.getCurve(), pubkey);
// TODO(!!!): For some reason Shamir's trick isn't working with
// 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.x.toBigInteger().mod(n);
var v = point.getX().toBigInteger().mod(n);
return v.equals(r);
},
@ -327,6 +467,111 @@ Bitcoin.ECDSA = (function () {
var s = BigInteger.fromByteArrayUnsigned(sBa);
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";
}
};

View file

@ -23,12 +23,35 @@ Bitcoin.ECKey = (function () {
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) {
this.pub = pub;
this.pub = ECPointFp.decodeFrom(ecparams.getCurve(), pub);
};
ECKey.prototype.toString = function (format) {

82
src/message.js Normal file
View 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'));

View file

@ -40,7 +40,7 @@ Bitcoin.Wallet = (function () {
if ("string" === typeof pub) {
pub = Crypto.util.base64ToBytes(pub);
}
key.pub = pub;
key.setPub(pub);
}
this.addressHashes.push(key.getBitcoinAddress().getHashBase64());