Search code examples
javaopensslbouncycastleecdsa

Java's BouncyCastle doesn't always verify OpenSSL ECDSA signature


I sign text using OpenSSL (in C++) however my Java program doesn't always validate signed messages (only ~1 out of 5 gets verified). Interestingly https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html doesn't verify any of them:

Curve name: secp256k1 Signature algorithm: SHA256withECDSA

privateKey

431313701ec60d303fa7d027d5f1579eaa57f0e870b23e3a25876e61bed2caa3

publicKey

035bcefc4a6ca257e394e82c20027db2af368474afb8917273713644f11a7cecb3

Failed:

text to sign=
    pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867163229

signature=
    3044022061dff8e39f9324b0794ec2c58abda971898f694ca980baf3c2a4045a9048b441022054a2fb8ef3d383fd7eeb31425dba440e2fd2053778d4ab3725046385c7845cff0000

Successful:

text to sign=
    pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867122614

signature=
    3046022100f200d0fb9e86a16bd46ee2dd11f1840a436d0a5c6823001a516e975a44906fcf022100d062a60611fc0f21d81fa3140741c8b6e650fff33d2c48aef69a3a40d7c7b3ca

Java

private static final String SHA256WITH_ECDSA = "SHA256withECDSA";

public static boolean isValidSignature(PublicKey pub, byte[] dataToVerify, byte[] signature) {

    try {

        Signature sign = Signature.getInstance(SHA256WITH_ECDSA, BouncyCastleProvider.PROVIDER_NAME);

        sign.initVerify(pub);

        sign.update(dataToVerify);

        return sign.verify(signature);

    } catch (Exception e) {
        log.error("Error: " + e.getMessage());
    }

    return false;

}

C++

std::vector<unsigned char> utils::crypto::sign(std::string& private_key_58, std::string& message) {

    auto priv_bytes = utils::base58::decode_base(private_key_58);

    auto digest = utils::crypto::sha256(message);

    auto key = utils::crypto::ec_new_keypair(priv_bytes);

    auto signature = ECDSA_do_sign(digest.data(), digest.size(), key);

    auto der_len = ECDSA_size(key);
    auto der = (uint8_t*) calloc(der_len, sizeof(uint8_t));
    auto der_copy = der;
    i2d_ECDSA_SIG(signature, &der_copy);

    std::vector<unsigned char> s (der, der+der_len);

    return s;

}

std::vector<unsigned char> utils::crypto::sha256(std::string& str) {

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, str.c_str(), str.size());
    SHA256_Final(hash, &sha256);

    std::vector<unsigned char> data(hash, hash + SHA256_DIGEST_LENGTH);

    return data;

}

EC_KEY *utils::crypto::ec_new_keypair(std::vector<unsigned char>& priv_bytes) {

    EC_KEY *key = nullptr;
    BIGNUM *priv = nullptr;
    BN_CTX *ctx = nullptr;
    const EC_GROUP *group = nullptr;
    EC_POINT *pub = nullptr;

    key = EC_KEY_new_by_curve_name(NID_secp256k1);

    if (!key) {
        std::cerr << "Can't generate curve secp256k1\n";
        std::abort();
    }

    priv = BN_new();
    BN_bin2bn(priv_bytes.data(), 32, priv);
    EC_KEY_set_private_key(key, priv);

    ctx = BN_CTX_new();
    BN_CTX_start(ctx);

    group = EC_KEY_get0_group(key);
    pub = EC_POINT_new(group);
    EC_POINT_mul(group, pub, priv, NULL, NULL, ctx);
    EC_KEY_set_public_key(key, pub);

    EC_POINT_free(pub);
    BN_CTX_end(ctx);
    BN_CTX_free(ctx);
    BN_clear_free(priv);

    return key;
}

Solution

  • Neardupes ECDSA signature length and how to specify signature length for java.security.Signature sign method (and more links there)

    ASN.1 DER encoding is variable size for all but certain very limited data, and in particular for ECDSA (or DSA) signatures. ECDSA_size returns the maximum length possible for the given key, but each actual signature may be either that length or shorter, depending on the binary representations of the values r and s in the signature, which for your purposes can be treated essentially as random numbers.

    In cases where an actual signature is shorter than ECDSA_size you still encode the entire buffer and pass it to your Java; notice the two bytes of zero (0000 in hex) at the end of your 'failed' example? A DER decoder can ignore trailing garbage, and when I test such a case on older BouncyCastle and SunEC providers it actually works okay, but it fails for me starting at BouncyCastle 1.54 -- with a rather clear exception, java.security.SignatureException: error decoding signature bytes. -- and SunEC starting at 8u121 with cause or exception similar to java.security.SignatureException: Invalid encoding for signature.

    Many implementations have recently made DER decoding stricter, after some successful attacks on 'lax' encodings, including the secp256k1 signatures in Bitcoin -- see https://bitcoin.stackexchange.com/questions/51706/what-can-be-changed-in-signed-bitcoin-transaction and https://en.bitcoin.it/wiki/Transaction_malleability . This is mentioned in the Oracle Java 8u121 release notes item "More checks added to DER encoding parsing code" although I don't see anything similar for Bouncy.

    Since secp256k1 is a Certicom/X9 'prime' (Fp) curve group, its cofactor is 1 and its order is very close to the underlying field size which in turn is very close to 256 bits which is a multiple of 8, so signatures in this group will DER-encode to the maximum length (and work) almost exactly 1/4 (25%) of the time; the rest of the time they will fail.

    The official and best solution is to use the updated value in the pointer, here der_copy, output by (any) i2d* routine, to determine the length of the encoding, and use that length. If you can't handle variable length for some reason, you can transmit the whole buffer but then truncate it before passing to BouncyCastle (or SunEC) by using 2+signature[1] as the valid length -- but not if you change to a curve larger than about 480 bits; above that it is different and more complicated.