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
265
src/ecdsa.js
265
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);
|
||||
// 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
31
src/eckey.js
31
src/eckey.js
|
@ -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
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) {
|
||||
pub = Crypto.util.base64ToBytes(pub);
|
||||
}
|
||||
key.pub = pub;
|
||||
key.setPub(pub);
|
||||
}
|
||||
|
||||
this.addressHashes.push(key.getBitcoinAddress().getHashBase64());
|
||||
|
|
Loading…
Add table
Reference in a new issue