You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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侧:
    1. 未对齐消息预处理,直接计算SHA-512哈希;
    2. 使用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);
    }
}

修改说明

  1. 添加消息空格移除逻辑,对齐Python的预处理步骤;
  2. 使用SshEd25519PublicKey的verifySignatureRaw方法,直接验证纯Ed25519签名,跳过SSH格式封装;
  3. 传入Python签名的目标数据(即预处理后消息的SHA-512哈希值),确保验证逻辑与Python完全一致。

内容的提问来源于stack exchange,提问作者ATK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:35:55