Skip to content

Commit

Permalink
Implement Bitcoin's method for arbitrary message signatures.
Browse files Browse the repository at this point in the history
  • Loading branch information
justmoon committed Aug 15, 2012
1 parent 6bf363b commit 9b2f94a
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 14 deletions.
265 changes: 255 additions & 10 deletions src/ecdsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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";
}
};

Expand Down
29 changes: 26 additions & 3 deletions src/eckey.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 9b2f94a

Please sign in to comment.