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?
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.