Python pycryptodome与Java sshtools的Ed25519签名不匹配问题
Ed25519跨语言签名不匹配问题:Python(pycryptodome)与Java(sshtools)验证失败
问题背景
Python端使用pycryptodome库,基于OpenSSH格式的Ed25519私钥签名数据;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(); } } }
测试情况
当消息为json.dumps({"key": "value"})时:
- Python生成的签名:
5Sdt3bIKFbLBhbZ2JLzQP+8MNX6/uzFtxHTkBa/UIpBbjtwKfNu+wfcMHmxksQkmzI5OMhEpY46hVlkM0P5nAA== - Java生成的签名:
xHgYq8/nUYOkpbGzCsUkei9Vw0O1/XKoYZlLAbsUPpQF3cTMQ96ROL/ZHSH+cUUNJlmTI2Qb2thAU3kEqvdHBQ== - Java验证Python签名时返回
false
密钥格式:
- 私钥:
-----BEGIN OPENSSH PRIVATE KEY-----<suff>-----END OPENSSH PRIVATE KEY----- - 公钥:
ssh-ed25519 <stuff>
签名不匹配的核心原因
1. 消息预处理不一致
Python代码对原始消息执行了message.replace(" ", ""),移除了所有空格;但Java代码直接使用Gson生成的带空格JSON字符串(如{"key": "value"}),两者的原始输入数据完全不同,导致后续哈希结果不一致。
2. 签名的哈希与格式逻辑差异
- Python侧:对预处理后的消息做SHA-512哈希,然后直接对哈希值执行纯Ed25519签名(输出64字节的R+S格式签名,无额外封装)。
- Java侧:
- 未对齐消息预处理,直接计算SHA-512哈希;
- 使用sshtools的高层
sign/verifySignature方法,这些方法会自动对传入数据添加SSH签名格式封装(包含算法标识ssh-ed25519和长度前缀),且内部会对传入数据再执行一次Ed25519标准的SHA-512哈希,相当于双重哈希+格式封装,最终生成的签名格式和内容都与Python完全不匹配。
适配Python逻辑的Java验证解决方案
修改Java验证代码,对齐Python的预处理、哈希和签名验证逻辑:
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.SshKeyUtils; import com.sshtools.common.ssh.components.SshPublicKey; import com.sshtools.common.ssh.components.jce.SshEd25519PublicKey; public class StackOverflow { private static final Gson gson = new Gson(); public static boolean verifyV1Signature(String message, String signature) { try { // 1. 对齐Python的消息预处理:移除所有空格 String processedMessage = message.replaceAll(" ", ""); byte[] messageBytes = processedMessage.getBytes(StandardCharsets.UTF_8); // 2. 计算SHA-512哈希,与Python保持一致 MessageDigest digest = MessageDigest.getInstance("SHA-512"); byte[] hashBytes = digest.digest(messageBytes); // 3. 读取公钥并转为Ed25519专用类型 SshPublicKey publicKey = SshKeyUtils.getPublicKey(new File("public.pub")); if (!(publicKey instanceof SshEd25519PublicKey)) { throw new IllegalArgumentException("不是Ed25519格式的公钥"); } SshEd25519PublicKey ed25519PublicKey = (SshEd25519PublicKey) publicKey; // 4. 解码Python的纯Ed25519签名(64字节R+S) Base64.Decoder decoder = Base64.getDecoder(); byte[] signatureBytes = decoder.decode(signature); // 5. 使用底层方法直接验证:签名的是SHA-512哈希值,跳过SSH格式封装 boolean isVerified = ed25519PublicKey.verifySignatureRaw(signatureBytes, hashBytes); System.out.println("signature is valid: " + isVerified); return isVerified; } catch (Exception e) { e.printStackTrace(); return false; } } 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); } }
修改说明
- 添加消息空格移除逻辑,对齐Python的预处理步骤;
- 使用
SshEd25519PublicKey的verifySignatureRaw方法,直接验证纯Ed25519签名,跳过SSH格式封装; - 传入Python签名的目标数据(即预处理后消息的SHA-512哈希值),确保验证逻辑与Python完全一致。
内容的提问来源于stack exchange,提问作者ATK
相关产品推荐
相关产品推荐

