diff --git a/src/ecdsa.js b/src/ecdsa.js index cc7aea487..4f1ee8cd3 100644 --- a/src/ecdsa.js +++ b/src/ecdsa.js @@ -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,19 +398,20 @@ 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); - - var point = implShamirsTrick(G, u1, Q, u2); + // 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 v = point.x.toBigInteger().mod(n); + var v = point.getX().toBigInteger().mod(n); return v.equals(r); }, /** * Serialize a signature into DER format. - * + * * Takes two BigIntegers representing r and s and returns a byte array. */ serializeSig: function (r, s) { @@ -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"; } }; diff --git a/src/eckey.js b/src/eckey.js index 5529abc50..05b5fda6f 100644 --- a/src/eckey.js +++ b/src/eckey.js @@ -23,12 +23,35 @@ Bitcoin.ECKey = (function () { this.priv = BigInteger.fromByteArrayUnsigned(Crypto.util.base64ToBytes(input)); } } + this.compressed = !!ECKey.compressByDefault; }; + /** + * Whether public keys should be returned compressed by default. + */ + ECKey.compressByDefault = false; + + /** + * 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 () { - if (this.pub) return this.pub; + 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 = ecparams.getG().multiply(this.priv).getEncoded(); + 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) { diff --git a/src/message.js b/src/message.js new file mode 100644 index 000000000..57ef6e0c0 --- /dev/null +++ b/src/message.js @@ -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')); + diff --git a/src/wallet.js b/src/wallet.js index 3d12f60c8..501ec3419 100755 --- a/src/wallet.js +++ b/src/wallet.js @@ -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());