mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
support for message signing and verifying
This commit is contained in:
parent
6b4b2529c8
commit
ee49ddd94b
1 changed files with 220 additions and 3 deletions
|
@ -3,18 +3,24 @@ package com.sparrowwallet.drongo.crypto;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.SignatureDecodeException;
|
import com.sparrowwallet.drongo.protocol.SignatureDecodeException;
|
||||||
|
import com.sparrowwallet.drongo.protocol.VarInt;
|
||||||
import org.bouncycastle.asn1.*;
|
import org.bouncycastle.asn1.*;
|
||||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||||
|
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||||
import org.bouncycastle.crypto.params.*;
|
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
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.ECPoint;
|
||||||
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
||||||
import org.bouncycastle.math.ec.FixedPointUtil;
|
import org.bouncycastle.math.ec.FixedPointUtil;
|
||||||
|
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
|
||||||
import org.bouncycastle.util.Properties;
|
import org.bouncycastle.util.Properties;
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -24,7 +30,8 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.*;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.SignatureException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
@ -702,6 +709,193 @@ public class ECKey implements EncryptableItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs a text message using the standard Bitcoin messaging signing format and returns the signature as a base64
|
||||||
|
* encoded string.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this ECKey does not have the private part.
|
||||||
|
* @throws KeyCrypterException if this ECKey is encrypted and no AESKey is provided or it does not decrypt the ECKey.
|
||||||
|
*/
|
||||||
|
public String signMessage(String message, Key aesKey) throws KeyCrypterException {
|
||||||
|
byte[] data = formatMessageForSigning(message);
|
||||||
|
Sha256Hash hash = Sha256Hash.twiceOf(data);
|
||||||
|
ECDSASignature sig = sign(hash, aesKey);
|
||||||
|
byte recId = findRecoveryId(hash, sig);
|
||||||
|
int headerByte = recId + 27 + (isCompressed() ? 4 : 0);
|
||||||
|
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
|
||||||
|
sigData[0] = (byte) headerByte;
|
||||||
|
System.arraycopy(Utils.bigIntegerToBytes(sig.r, 32), 0, sigData, 1, 32);
|
||||||
|
System.arraycopy(Utils.bigIntegerToBytes(sig.s, 32), 0, sigData, 33, 32);
|
||||||
|
return new String(Base64.getEncoder().encode(sigData), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an arbitrary piece of text and a Bitcoin-format message signature encoded in base64, returns an ECKey
|
||||||
|
* containing 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. These sorts of signatures are compatible with the Bitcoin-Qt/bitcoind
|
||||||
|
* format generated by signmessage/verifymessage RPCs and GUI menu options. They are intended for humans to verify
|
||||||
|
* their communications with each other, hence the base64 format and the fact that the input is text.
|
||||||
|
*
|
||||||
|
* @param message Some piece of human readable text.
|
||||||
|
* @param signatureBase64 The Bitcoin-format message signature in base64
|
||||||
|
* @throws SignatureException If the public key could not be recovered or if there was a signature format error.
|
||||||
|
*/
|
||||||
|
public static ECKey signedMessageToKey(String message, String signatureBase64) throws SignatureException {
|
||||||
|
byte[] signatureEncoded;
|
||||||
|
try {
|
||||||
|
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
|
||||||
|
} catch(RuntimeException e) {
|
||||||
|
// This is what you get back from Bouncy Castle if base64 doesn't decode :(
|
||||||
|
throw new SignatureException("Could not decode base64", e);
|
||||||
|
}
|
||||||
|
// Parse the signature bytes into r/s and the selector value.
|
||||||
|
if(signatureEncoded.length < 65) {
|
||||||
|
throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length);
|
||||||
|
}
|
||||||
|
int header = signatureEncoded[0] & 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);
|
||||||
|
}
|
||||||
|
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33));
|
||||||
|
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65));
|
||||||
|
ECDSASignature sig = new ECDSASignature(r, s);
|
||||||
|
byte[] messageBytes = formatMessageForSigning(message);
|
||||||
|
// Note that the C++ code doesn't actually seem to specify any character encoding. Presumably it's whatever
|
||||||
|
// JSON-SPIRIT hands back. Assume UTF-8 for now.
|
||||||
|
Sha256Hash messageHash = Sha256Hash.twiceOf(messageBytes);
|
||||||
|
boolean compressed = false;
|
||||||
|
if(header >= 31) {
|
||||||
|
compressed = true;
|
||||||
|
header -= 4;
|
||||||
|
}
|
||||||
|
int recId = header - 27;
|
||||||
|
ECKey key = ECKey.recoverFromSignature(recId, sig, messageHash, compressed);
|
||||||
|
if(key == null) {
|
||||||
|
throw new SignatureException("Could not recover public key from signature");
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper around {@link ECKey#signedMessageToKey(String, String)}. If the key derived from the
|
||||||
|
* signature is not the same as this one, throws a SignatureException.
|
||||||
|
*/
|
||||||
|
public void verifyMessage(String message, String signatureBase64) throws SignatureException {
|
||||||
|
ECKey key = ECKey.signedMessageToKey(message, signatureBase64);
|
||||||
|
if(!key.pub.equals(pub)) {
|
||||||
|
throw new SignatureException("Signature did not match for message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the recovery ID, a byte with value between 0 and 3, inclusive, that specifies which of 4 possible
|
||||||
|
* curve points was used to sign a message. This value is also referred to as "v".
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if no recovery ID can be found.
|
||||||
|
*/
|
||||||
|
public byte findRecoveryId(Sha256Hash hash, ECDSASignature sig) {
|
||||||
|
byte recId = -1;
|
||||||
|
for(byte i = 0; i < 4; i++) {
|
||||||
|
ECKey k = ECKey.recoverFromSignature(i, sig, hash, isCompressed());
|
||||||
|
if(k != null && k.pub.equals(pub)) {
|
||||||
|
recId = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(recId == -1) {
|
||||||
|
throw new RuntimeException("Could not construct a recoverable key. This should never happen.");
|
||||||
|
}
|
||||||
|
return recId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <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.
|
||||||
|
* @param compressed Whether or not the original pubkey was compressed.
|
||||||
|
* @return An ECKey containing only the public part, or null if recovery wasn't possible.
|
||||||
|
*/
|
||||||
|
public static ECKey recoverFromSignature(int recId, ECDSASignature sig, Sha256Hash message, boolean compressed) {
|
||||||
|
if(recId < 0) {
|
||||||
|
throw new IllegalArgumentException("recId must be positive");
|
||||||
|
}
|
||||||
|
if(sig.r.signum() < 0 || sig.s.signum() < 0) {
|
||||||
|
throw new IllegalArgumentException("Signature values r and s must both be positive");
|
||||||
|
}
|
||||||
|
if(message == null) {
|
||||||
|
throw new IllegalArgumentException("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 = message.toBigInteger();
|
||||||
|
// 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);
|
||||||
|
return ECKey.fromPublicOnly(q, compressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a 32 byte array containing the private key.
|
* Returns a 32 byte array containing the private key.
|
||||||
* @throws MissingPrivateKeyException if the private key bytes are missing/encrypted.
|
* @throws MissingPrivateKeyException if the private key bytes are missing/encrypted.
|
||||||
|
@ -931,4 +1125,27 @@ public class ECKey implements EncryptableItem {
|
||||||
return Byte.toUnsignedInt(a) - Byte.toUnsignedInt(b);
|
return Byte.toUnsignedInt(a) - Byte.toUnsignedInt(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The string that prefixes all text messages signed using Bitcoin keys. */
|
||||||
|
private static final String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n";
|
||||||
|
private static final byte[] BITCOIN_SIGNED_MESSAGE_HEADER_BYTES = BITCOIN_SIGNED_MESSAGE_HEADER.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Given a textual message, returns a byte buffer formatted as follows:</p>
|
||||||
|
* <p>{@code [24] "Bitcoin Signed Message:\n" [message.length as a varint] message}</p>
|
||||||
|
*/
|
||||||
|
private static byte[] formatMessageForSigning(String message) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.length);
|
||||||
|
bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES);
|
||||||
|
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
VarInt size = new VarInt(messageBytes.length);
|
||||||
|
bos.write(size.encode());
|
||||||
|
bos.write(messageBytes);
|
||||||
|
return bos.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e); // Cannot happen.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue