У меня есть библиотека Python, которая использует библиотеку pycryptodome
для подписи данных с использованием алгоритма Ed25519 с использованием закрытого ключа ED25519 формата openssh. Затем подпись необходимо проверить в приложении Java с использованием библиотеки sshtools
с соответствующим открытым ключом. Однако проверка подписи не удалась.
Ограничение: важно читать закрытые/открытые ключи из файлов. Я не могу изменить код Python и/или используемые ключи.
Для отладки я написал реализацию для генерации подписи на Java, а также для проверки подписи, сгенерированной Python. Однако оба приходят разными.
Моя реализация Python для подписи данных выглядит следующим образом:
from Crypto.Hash import SHA512
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
import base64
import json
def generate_signature_v1(message):
message = message.replace(" ", "")
h = SHA512.new(message.encode("utf-8"))
with open("private", "r") as f:
key = ECC.import_key(f.read())
signer = eddsa.new(key, "rfc8032")
signature = signer.sign(h)
str_signature = base64.standard_b64encode(signature).decode("utf-8")
return str_signature
Моя реализация Java для генерации и проверки подписи.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.io.File;
import java.io.IOException;
import com.google.gson.Gson;
import com.sshtools.common.publickey.InvalidPassphraseException;
import com.sshtools.common.publickey.SshKeyUtils;
import com.sshtools.common.ssh.components.SshKeyPair;
import com.sshtools.common.ssh.components.SshPrivateKey;
import com.sshtools.common.ssh.components.SshPublicKey;
public class StackOverflow {
private static final Gson gson = new Gson();
public static boolean verifyV1Signature(String message, String signature) {
try {
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] hash = digest.digest(messageBytes);
// read public key
SshPublicKey readPublicKey = SshKeyUtils.getPublicKey(new File("public.pub"));
// verify signature
Base64.Decoder decoder = Base64.getDecoder();
byte[] signatureDecoded = decoder.decode(signature);
boolean isVerified = readPublicKey.verifySignature(signatureDecoded, hash);
System.out.println("signature is valid: " + isVerified);
return isVerified;
} catch (Exception e) {
return false;
}
}
public static String generateV1Signature(String message)
throws NoSuchAlgorithmException, IOException, InvalidPassphraseException {
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] hash = digest.digest(messageBytes);
// create signature
SshKeyPair readKeyPair = SshKeyUtils.getPrivateKey(new File("private"));
SshPrivateKey readPrivateKey = readKeyPair.getPrivateKey();
byte[] signature = readPrivateKey.sign(hash);
Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(signature);
}
public static void main(String[] args) {
Map<String, String> data = new HashMap<>();
data.put("key", "value");
String message = gson.toJson(data);
String pythonSignature = "5Sdt3bIKFbLBhbZ2JLzQP+8MNX6/uzFtxHTkBa/UIpBbjtwKfNu+wfcMHmxksQkmzI5OMhEpY46hVlkM0P5nAA= = ";
verifyV1Signature(message, pythonSignature);
try {
String javaSignature = generateV1Signature(message);
System.out.println(javaSignature);
} catch (NoSuchAlgorithmException | IOException | InvalidPassphraseException e) {
e.printStackTrace();
}
}
}
Запуск кода Python для сообщения json.dumps({"key": "value"})
дает 5Sdt3bIKFbLBhbZ2JLzQP+8MNX6/uzFtxHTkBa/UIpBbjtwKfNu+wfcMHmxksQkmzI5OMhEpY46hVlkM0P5nAA==
Запуск Java-кода дает xHgYq8/nUYOkpbGzCsUkei9Vw0O1/XKoYZlLAbsUPpQF3cTMQ96ROL/ZHSH+cUUNJlmTI2Qb2thAU3kEqvdHBQ==
, а проверка не удалась.
Ключ private
выглядит как -----BEGIN OPENSSH PRIVATE KEY-----<suff>-----END OPENSSH PRIVATE KEY-----
, а открытый ключ выглядит как ssh-ed25519 <stuff>
.
Почему подпись не совпадает? Я тоже попробовал bouncycastle
, но подпись все равно не совпадает.
Обе реализации не требуют хеширования с помощью SHA512, это происходит неявно. Учитывая это, для меня оба кода предоставляют одну и ту же подпись для идентичных входных данных. Вам следует упростить свой тестовый пример: используйте простое сообщение (а не строку JSON, где проблемы с форматированием могут возникнуть сверху) и проверьте, действительно ли в обоих кодах одно и то же сообщение передается функции подписи. Если проблема не устранена, опубликуйте тестовые данные: непродуктивную пару ключей, сообщение и сгенерированные подписи.
@ Топако, ты прав. Это сработало после того, как я удалил ручное хеширование с помощью SHA512. Это также хорошо сработало для json, используя json
в Python и Gson
в Java. Я углубился в реализацию pycryptodome
, и кажется, что если переданное сообщение уже является hash
, pycryptodome
добавляет какое-то жестко закодированное сообщение к хеш-дайджесту, а затем снова вычисляет хэш. Итак, похоже, именно это и стало причиной проблемы. Справочный код: метод sign
в файле Crypto/Signature/eddsa.py
. Пожалуйста, не стесняйтесь опубликовать решение, чтобы я мог принять его.
Поскольку вы передаете хеш-объект в коде Python в sign()
, вы используете Ed25519ph вместо Ed25519 (отсюда и жестко закодированный префикс), более подробную информацию см. в моем ответе.
Различные подписи возникают из-за непреднамеренного использования разных алгоритмов подписи: в коде Python применяется Ed25519ph, в коде Java — Ed25519.
Кроме того, в коде Java подписывается не сообщение, а хеш SHA512 сообщения.
PyCryptodome поддерживает Ed25519 (PureEdDSA) и Ed25519ph (HashEdDSA), s. вот . Для Ed25519 сообщение должно передаваться как байтовый объект, для Ed25519ph — как хэш-объект.
Поскольку Ed25519 должен использоваться для подписи, сообщение должно быть передано как байтовый объект, т. е. оно должно быть применено:
...
signature = signer.sign(message.encode("utf-8"))
...
Добавленное жестко закодированное сообщение, которое вы упомянули в комментарии, является особенностью Ed25519ph, более подробную информацию см. RFC 8032, главы 4, 5.
Кроме того, в Java-коде необходимо убрать явное хеширование:
byte[] signature = readPrivateKey.sign(messageBytes);
В общем, хеширование с помощью SHA512 является частью алгоритма Ed25519 и выполняется неявно; сообщение не должно быть явно хешировано с помощью SHA512 (в противном случае хеш SHA512 сообщения, а не само сообщение, будет подписан Ed25519).
Благодаря этим изменениям оба кода создают одну и ту же подпись для одних и тех же входных данных.
Вот пример использования этого конкретного алгоритма в Java. Лично я не знаю достаточно, чтобы сказать, правильно ли это, но это может помочь. Howtodoinjava.com/java15/java-eddsa-example