javac#cryptographydigital-signatureecdsa

ECDSA hash signing with Java


I have a Java function that can sign a given hash and a Python script that can perform XAdES signature, embedding the signature into a container and returning an adoc file. There's something wrong with my Java function because when I try to verify the adoc file at https://verifysignature.eu, it fails with the following error message: "Verification of the SignatureValue field with the public key does not match the SignedInfo field (this means that one of these three elements is modified)".

public static String signHash(String hash, String base64PrivateKey) throws Exception {
    try {
        setupBouncyCastle();

        PrivateKey privateKey = JavaSignUtil.importECPrivateKey(base64PrivateKey);

        Signature signature = Signature.getInstance("SHA256withPlain-ECDSA");

        signature.initSign(privateKey);
        signature.update(hash.getBytes());

        // Sign the hash
        byte[] signedHash = signature.sign();

        // Convert the signed hash to Base64 for easy handling (optional)
        String signedHashBase64 = new String(Base64.encodeBase64(signedHash), StandardCharsets.UTF_8);

        return signedHashBase64;
    } catch (Exception e) {
        e.printStackTrace();
        throw e; // TODO: handle the exception
    }
}

private static PrivateKey importECPrivateKey(String base64Key) throws Exception {
    // Decode the Base64 string to get the raw key data
    byte[] keyBytes = Base64.decodeBase64(base64Key.getBytes());

    // Assuming P-256 curve, extract private scalar 'K'
    // First byte is 0x04 and then 64 bytes for X and Y, so K starts at 65th byte
    byte[] kBytes = new byte[32]; // Size of K for P-256
    System.arraycopy(keyBytes, 65, kBytes, 0, 32);
    BigInteger k = new BigInteger(1, kBytes);

    // Specify the curve parameters (example for P-256)
    // Get the parameters for 'secp256r1' curve from BouncyCastle's curve table
    ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
    // Create the private key spec for BouncyCastle
    ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(k, ecSpec);
    KeyFactory kf = KeyFactory.getInstance("ECDSA", "BC");

    // Create the key spec and generate the private key
    return kf.generatePrivate(privateKeySpec);
}

private static void setupBouncyCastle() {
    if (JavaSignUtil.provider != null) { return; } // The provider is already set

    Provider provider = new BouncyCastleProvider();
    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    Security.insertProviderAt(provider, 1);

    JavaSignUtil.provider = provider;
}

I am sure that the Python script works because another developer has tested it with a signature produced in C# and the result passed the verification. I'll also add his signing function here:

void SignHashWithEcdsa()
{
    ECDsa ecdsa = EcdsaPrivateKeyImporter.ImportEcPrivateKey(privateKeyBase64);

    Console.WriteLine("Please insert the hash in base64 format:");
    var hashBase64 = Console.ReadLine();
    var rgbHash = Convert.FromBase64String(hashBase64);
    byte[] signature = ecdsa.SignHash(rgbHash);
    var signatureBase64 = Convert.ToBase64String(signature);
    Console.WriteLine(signatureBase64);
}

public static class EcdsaPrivateKeyImporter
{
    public static ECDsa ImportEcPrivateKey(string base64Key)
    {
        byte[] keyBytes = Convert.FromBase64String(base64Key);

        if (keyBytes.Length != 97 || keyBytes[0] != 0x04)
        {
            throw new ArgumentException("Invalid key format.");
        }

        byte[] kBytes = new byte[32];
        Array.Copy(keyBytes, 65, kBytes, 0, 32);

        byte[] xBytes = new byte[32];
        byte[] yBytes = new byte[32];
        Array.Copy(keyBytes, 1, xBytes, 0, 32);
        Array.Copy(keyBytes, 33, yBytes, 0, 32);

        ECParameters ecParams = new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            D = kBytes,
            Q = new ECPoint
            {
                X = xBytes,
                Y = yBytes
            }
        };

        ECDsa ecdsa = ECDsa.Create(ecParams);
        Console.WriteLine(ecdsa.SignatureAlgorithm);
        
        // Extract public key parameters
        ECParameters publicParams = ecdsa.ExportParameters(false);

        // Concatenate X and Y coordinates
        byte[] publicKeyBytes = new byte[64];
        Array.Copy(publicParams.Q.X, 0, publicKeyBytes, 0, 32);
        Array.Copy(publicParams.Q.Y, 0, publicKeyBytes, 32, 32);

        // Convert to base64 string
        string base64PublicKey = Convert.ToBase64String(publicKeyBytes);
        Console.WriteLine($"Public Key (Base64): {base64PublicKey}");
        
        return ecdsa;
    }
}

The only difference seems to be that the C# code also takes into account the X and Y values, while Java does not. However, I could not find a way of using those values while signing in Java.

How should I change my Java code to make the signature valid?


Solution

  • I was able to solve it by making the following changes.

    Changed this:

    Signature signature = Signature.getInstance("SHA256withECDSA");
    

    to:

    Signature signature = Signature.getInstance("NONEwithECDSA");
    

    Changed this:

    signature.update(hash.getBytes());
    

    to:

    signature.update(Base64.decodeBase64(hash.getBytes()));
    

    Also converted the result (signedHash) to P1363 format via this function:

    private static byte[] toP1363(byte[] asn1EncodedSignature) {
        ASN1Sequence seq = ASN1Sequence.getInstance(asn1EncodedSignature);
        BigInteger r = ((ASN1Integer) seq.getObjectAt(0)).getValue();
        BigInteger s = ((ASN1Integer) seq.getObjectAt(1)).getValue();
        BigInteger n = new SecP256R1Curve().getOrder();
        return PlainDSAEncoding.INSTANCE.encode(n, r, s);
    }
    

    As far as I understand, the solution could be much simpler if I could use Java 11 in my project. This would allow me to use the NONEwithECDSAinP1363Format algorithm.