У меня есть функция Java, которая может подписывать заданный хэш, и скрипт Python, который может выполнять подпись XAdES, встраивая подпись в контейнер и возвращая файл adoc
. Что-то не так с моей функцией Java, потому что когда я пытаюсь проверить файл adoc по адресу https://verifysignature.eu, происходит сбой со следующим сообщением об ошибке: «Проверка поля SignatureValue с помощью открытого ключа не соответствует SignedInfo (это означает, что один из этих трех элементов изменен)".
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;
}
Я уверен, что скрипт Python работает, потому что другой разработчик протестировал его с подписью, созданной на C#, и результат прошел проверку. Я также добавлю сюда его функцию подписи:
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;
}
}
Единственная разница, по-видимому, заключается в том, что код C# также учитывает значения X и Y, а Java — нет. Однако мне не удалось найти способ использовать эти значения при входе в Java.
Как мне изменить свой Java-код, чтобы сделать подпись действительной?
Вы имеете в виду, что метод Signature.sign() неявно вычисляет хеш входных данных, а метод C# ecdsa.SignHash() этого не делает?
... Вы имеете в виду, что метод подпись.sign() неявно вычисляет хеш входных данных... Не signature.sign()
как таковой, а указанный алгоритм SHA256withPlain-ECDSA
.
Применить, например NONEwithECDSAinP1363Format
. Между прочим, на стороне Java вообще не требуется BC; ECDSA поддерживается поставщиком SunEC, например. Java 11: Поставщик SunEC).
О, теперь я понял. Я думаю, что не смогу использовать NONEwithECDSAinP1363Format
, так как не могу использовать Java 11 в своем проекте, но я нашел другое решение. Большое спасибо! Я опубликую ответ ниже.
Рад, что вы нашли решение. Если используются более старые версии, их следует указать в вопросе, поскольку это, конечно, важно для успешного ответа.
Мне удалось решить эту проблему, внеся следующие изменения.
Изменил это:
Signature signature = Signature.getInstance("SHA256withECDSA");
к:
Signature signature = Signature.getInstance("NONEwithECDSA");
Изменил это:
signature.update(hash.getBytes());
к:
signature.update(Base64.decodeBase64(hash.getBytes()));
Также преобразовал результат (signedHash
) в формат P1363 с помощью этой функции:
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);
}
Насколько я понимаю, решение могло бы быть намного проще, если бы я мог использовать Java 11 в своем проекте. Это позволило бы мне использовать алгоритм NONEwithECDSAinP1363Format
.
В коде C# данные (декодируются в Base64 и) подписываются напрямую (без предварительного хеширования), в коде Java они (предположительно в кодировке UTF-8 и) хешируются с помощью SHA-256 перед подписанием.