Подписание хеша ECDSA с помощью Java

У меня есть функция 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-код, чтобы сделать подпись действительной?

В коде C# данные (декодируются в Base64 и) подписываются напрямую (без предварительного хеширования), в коде Java они (предположительно в кодировке UTF-8 и) хешируются с помощью SHA-256 перед подписанием.

Topaco 09.07.2024 13:58

Вы имеете в виду, что метод Signature.sign() неявно вычисляет хеш входных данных, а метод C# ecdsa.SignHash() этого не делает?

Terlan Ismayilsoy 09.07.2024 15:06

... Вы имеете в виду, что метод подпись.sign() неявно вычисляет хеш входных данных... Не signature.sign() как таковой, а указанный алгоритм SHA256withPlain-ECDSA.

Topaco 09.07.2024 15:12

Применить, например NONEwithECDSAinP1363Format. Между прочим, на стороне Java вообще не требуется BC; ECDSA поддерживается поставщиком SunEC, например. Java 11: Поставщик SunEC).

Topaco 09.07.2024 15:33

О, теперь я понял. Я думаю, что не смогу использовать NONEwithECDSAinP1363Format, так как не могу использовать Java 11 в своем проекте, но я нашел другое решение. Большое спасибо! Я опубликую ответ ниже.

Terlan Ismayilsoy 10.07.2024 08:00

Рад, что вы нашли решение. Если используются более старые версии, их следует указать в вопросе, поскольку это, конечно, важно для успешного ответа.

Topaco 10.07.2024 08:35
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
6
103
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мне удалось решить эту проблему, внеся следующие изменения.

Изменил это:

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.

Другие вопросы по теме