Skip to content

Instantly share code, notes, and snippets.

@xoriole
Created June 13, 2017 07:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xoriole/4c2a9630dba5a20ee28fb58edf193375 to your computer and use it in GitHub Desktop.
Save xoriole/4c2a9630dba5a20ee28fb58edf193375 to your computer and use it in GitHub Desktop.
Web3j Sign.java modified to avoid double hashing when signing to make it compatible with geth version result
package org.web3j.crypto;
import java.math.BigInteger;
import java.security.SignatureException;
import java.util.Arrays;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
import org.web3j.utils.Numeric;
import static org.web3j.utils.Assertions.verifyPrecondition;
/**
* <p>Transaction signing logic.</p>
*
* <p>Adapted from the
* <a href="https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java">
* BitcoinJ ECKey</a> implementation.
*/
public class Sign {
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
private static final ECDomainParameters CURVE = new ECDomainParameters(
CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
private static final BigInteger HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
public static SignatureData signMessage(byte[] message, ECKeyPair keyPair) {
BigInteger privateKey = keyPair.getPrivateKey();
BigInteger publicKey = keyPair.getPublicKey();
// UPDATE: DO NOT HASH HERE; EXPECT THE HASH IN THE PARAMETER
byte[] messageHash = message;//Hash.sha3(message);
ECDSASignature sig = sign(messageHash, privateKey);
// Now we have to work backwards to figure out the recId needed to recover the signature.
int recId = -1;
for (int i = 0; i < 4; i++) {
BigInteger k = recoverFromSignature(i, sig, messageHash);
if (k != null && k.equals(publicKey)) {
recId = i;
break;
}
}
if (recId == -1) {
throw new RuntimeException(
"Could not construct a recoverable key. This should never happen.");
}
int headerByte = recId + 27;
// 1 header + 32 bytes for R + 32 bytes for S
byte v = (byte) headerByte;
byte[] r = Numeric.toBytesPadded(sig.r, 32);
byte[] s = Numeric.toBytesPadded(sig.s, 32);
return new SignatureData(v, r, s);
}
private static ECDSASignature sign(byte[] transactionHash, BigInteger privateKey) {
ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKey, CURVE);
signer.init(true, privKey);
BigInteger[] components = signer.generateSignature(transactionHash);
return new ECDSASignature(components[0], components[1]).toCanonicalised();
}
/**
* <p>Given the components of a signature and a selector value, recover and return the public
* key that generated the signature according to the algorithm in SEC1v2 section 4.1.6.</p>
*
* <p>The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the
* correct one. Because the key recovery operation yields multiple potential keys, the correct
* key must either be stored alongside the
* signature, or you must be willing to try each recId in turn until you find one that outputs
* the key you are expecting.</p>
*
* <p>If this method returns null it means recovery was not possible and recId should be
* iterated.</p>
*
* <p>Given the above two points, a correct usage of this method is inside a for loop from
* 0 to 3, and if the output is null OR a key that is not the one you expect, you try again
* with the next recId.</p>
*
* @param recId Which possible key to recover.
* @param sig the R and S components of the signature, wrapped.
* @param message Hash of the data that was signed.
* @return An ECKey containing only the public part, or null if recovery wasn't possible.
*/
private static BigInteger recoverFromSignature(int recId, ECDSASignature sig, byte[] message) {
verifyPrecondition(recId >= 0, "recId must be positive");
verifyPrecondition(sig.r.signum() >= 0, "r must be positive");
verifyPrecondition(sig.s.signum() >= 0, "s must be positive");
verifyPrecondition(message != null, "message cannot be null");
// 1.0 For j from 0 to h (h == recId here and the loop is outside this function)
// 1.1 Let x = r + jn
BigInteger n = CURVE.getN(); // Curve order.
BigInteger i = BigInteger.valueOf((long) recId / 2);
BigInteger x = sig.r.add(i.multiply(n));
// 1.2. Convert the integer x to an octet string X of length mlen using the conversion
// routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉.
// 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R
// using the conversion routine specified in Section 2.3.4. If this conversion
// routine outputs “invalid”, then do another iteration of Step 1.
//
// More concisely, what these points mean is to use X as a compressed public key.
BigInteger prime = SecP256K1Curve.q;
if (x.compareTo(prime) >= 0) {
// Cannot have point co-ordinates larger than this as everything takes place modulo Q.
return null;
}
// Compressed keys require you to know an extra bit of data about the y-coord as there are
// two possibilities. So it's encoded in the recId.
ECPoint R = decompressKey(x, (recId & 1) == 1);
// 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers
// responsibility).
if (!R.multiply(n).isInfinity()) {
return null;
}
// 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification.
BigInteger e = new BigInteger(1, message);
// 1.6. For k from 1 to 2 do the following. (loop is outside this function via
// iterating recId)
// 1.6.1. Compute a candidate public key as:
// Q = mi(r) * (sR - eG)
//
// Where mi(x) is the modular multiplicative inverse. We transform this into the following:
// Q = (mi(r) * s ** R) + (mi(r) * -e ** G)
// Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n).
// In the above equation ** is point multiplication and + is point addition (the EC group
// operator).
//
// We can find the additive inverse by subtracting e from zero then taking the mod. For
// example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and
// -3 mod 11 = 8.
BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
BigInteger rInv = sig.r.modInverse(n);
BigInteger srInv = rInv.multiply(sig.s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
byte[] qBytes = q.getEncoded(false);
// We remove the prefix
return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length));
}
/** Decompress a compressed public key (x co-ord and low-bit of y-coord). */
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
X9IntegerConverter x9 = new X9IntegerConverter();
byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve()));
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
return CURVE.getCurve().decodePoint(compEnc);
}
/**
* Given an arbitrary piece of text and an Ethereum message signature encoded in bytes,
* returns the public key that was used to sign it. This can then be compared to the expected
* public key to determine if the signature was correct.
*
* @param message RLP encoded message.
* @param signatureData The message signature components
* @return the public key used to sign the message
* @throws SignatureException If the public key could not be recovered or if there was a
* signature format error.
*/
public static BigInteger signedMessageToKey(
byte[] message, SignatureData signatureData) throws SignatureException {
byte[] r = signatureData.getR();
byte[] s = signatureData.getS();
verifyPrecondition(r != null && r.length == 32, "r must be 32 bytes");
verifyPrecondition(s != null && s.length == 32, "s must be 32 bytes");
int header = signatureData.getV() & 0xFF;
// The header byte: 0x1B = first key with even y, 0x1C = first key with odd y,
// 0x1D = second key with even y, 0x1E = second key with odd y
if (header < 27 || header > 34) {
throw new SignatureException("Header byte out of range: " + header);
}
ECDSASignature sig = new ECDSASignature(
new BigInteger(1, signatureData.getR()),
new BigInteger(1, signatureData.getS()));
// UPDATE: DO NOT HASH HERE; EXPECT THE HASH IN THE PARAMETER
byte[] messageHash = message;//Hash.sha3(message);
int recId = header - 27;
BigInteger key = recoverFromSignature(recId, sig, messageHash);
if (key == null) {
throw new SignatureException("Could not recover public key from signature");
}
return key;
}
/**
* Returns public key from the given private key.
*
* @param privKey the private key to derive the public key from
* @return BigInteger encoded public key
*/
public static BigInteger publicKeyFromPrivate(BigInteger privKey) {
ECPoint point = publicPointFromPrivate(privKey);
byte[] encoded = point.getEncoded(false);
return new BigInteger(1, Arrays.copyOfRange(encoded, 1, encoded.length)); // remove prefix
}
/**
* Returns public key point from the given private key.
*/
private static ECPoint publicPointFromPrivate(BigInteger privKey) {
/*
* TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group
* order, but that could change in future versions.
*/
if (privKey.bitLength() > CURVE.getN().bitLength()) {
privKey = privKey.mod(CURVE.getN());
}
return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey);
}
private static class ECDSASignature {
private final BigInteger r;
private final BigInteger s;
ECDSASignature(BigInteger r, BigInteger s) {
this.r = r;
this.s = s;
}
/**
* Returns true if the S component is "low", that means it is below
* {@link Sign#HALF_CURVE_ORDER}. See
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures">
* BIP62</a>.
*/
public boolean isCanonical() {
return s.compareTo(HALF_CURVE_ORDER) <= 0;
}
/**
* Will automatically adjust the S component to be less than or equal to half the curve
* order, if necessary. This is required because for every signature (r,s) the signature
* (r, -s (mod N)) is a valid signature of the same message. However, we dislike the
* ability to modify the bits of a Bitcoin transaction after it's been signed, as that
* violates various assumed invariants. Thus in future only one of those forms will be
* considered legal and the other will be banned.
*/
public ECDSASignature toCanonicalised() {
if (!isCanonical()) {
// The order of the curve is the number of valid points that exist on that curve.
// If S is in the upper half of the number of valid points, then bring it back to
// the lower half. Otherwise, imagine that
// N = 10
// s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions.
// 10 - 8 == 2, giving us always the latter solution, which is canonical.
return new ECDSASignature(r, CURVE.getN().subtract(s));
} else {
return this;
}
}
}
public static class SignatureData {
private final byte v;
private final byte[] r;
private final byte[] s;
public SignatureData(byte v, byte[] r, byte[] s) {
this.v = v;
this.r = r;
this.s = s;
}
public byte getV() {
return v;
}
public byte[] getR() {
return r;
}
public byte[] getS() {
return s;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SignatureData that = (SignatureData) o;
if (v != that.v) {
return false;
}
if (!Arrays.equals(r, that.r)) {
return false;
}
return Arrays.equals(s, that.s);
}
@Override
public int hashCode() {
int result = (int) v;
result = 31 * result + Arrays.hashCode(r);
result = 31 * result + Arrays.hashCode(s);
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment