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

如何在secp256k1的ECDSA签名中嵌入并提取文本?

实现方案:将原文本与secp256k1签名绑定传输

这个需求其实很常见——带宽受限下要把签名和原文本绑定传输,不用单独发文本。不过得先明确:secp256k1的签名本身是固定结构的(r,s)值,没法直接把可变长度的文本塞进签名里。我们得换个思路:把原文本+签名打包成一个可解析的二进制结构,客户端收到后拆分出文本和签名,再验证签名是否对应这个文本的哈希。

下面是基于Java+BouncyCastle的具体实现方案,和你给出的代码片段兼容:

服务器端:签名+打包文本与签名

首先完成签名逻辑,再将文本和签名打包成二进制流(用长度前缀保证解析正确性,最省带宽):

import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import java.nio.ByteBuffer;
import java.math.BigInteger;
import java.security.SecureRandom;

public class ServerSigner {
    // 初始化secp256k1曲线参数
    static final X9ECParameters curve = org.bouncycastle.asn1.sec.SECNamedCurves.getByName("secp256k1");
    static final ECDomainParameters domain = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH());

    public static byte[] signAndPackage(String originalText, ECPrivateKeyParameters privateKey) throws Exception {
        // 1. 计算文本的SHA-256哈希
        SHA256Digest digest = new SHA256Digest();
        byte[] textBytes = originalText.getBytes("UTF-8");
        digest.update(textBytes, 0, textBytes.length);
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);

        // 2. 用secp256k1私钥签名哈希
        ECDSASigner signer = new ECDSASigner();
        signer.init(true, privateKey);
        BigInteger[] rs = signer.generateSignature(hash);
        BigInteger r = rs[0];
        BigInteger s = rs[1];

        // 3. 将r和s转成固定32字节的大端字节数组(不足补0)
        byte[] rBytes = toFixedLengthBytes(r, 32);
        byte[] sBytes = toFixedLengthBytes(s, 32);
        byte[] signature = new byte[64];
        System.arraycopy(rBytes, 0, signature, 0, 32);
        System.arraycopy(sBytes, 0, signature, 32, 32);

        // 4. 打包:4字节文本长度(大端) + 文本字节 + 64字节签名
        ByteBuffer buffer = ByteBuffer.allocate(4 + textBytes.length + 64);
        buffer.putInt(textBytes.length);
        buffer.put(textBytes);
        buffer.put(signature);

        return buffer.array();
    }

    // 将BigInteger转成固定长度的大端字节数组,不足前面补0
    private static byte[] toFixedLengthBytes(BigInteger num, int length) {
        byte[] bytes = num.toByteArray();
        if (bytes.length == length) {
            return bytes;
        }
        byte[] fixedBytes = new byte[length];
        if (bytes.length < length) {
            // 补0在前面
            System.arraycopy(bytes, 0, fixedBytes, length - bytes.length, bytes.length);
        } else {
            // 如果长度超过,只取后面length字节(因为BigInteger的toByteArray可能带符号位前缀)
            System.arraycopy(bytes, bytes.length - length, fixedBytes, 0, length);
        }
        return fixedBytes;
    }
}

客户端:解析+验证签名

客户端收到打包后的字节流,先拆分出原文本和签名,再验证签名是否对应文本的哈希:

import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import java.nio.ByteBuffer;
import java.math.BigInteger;

public class ClientVerifier {
    // 和服务器用相同的曲线参数
    static final X9ECParameters curve = org.bouncycastle.asn1.sec.SECNamedCurves.getByName("secp256k1");
    static final ECDomainParameters domain = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH());

    public static String verifyAndExtract(byte[] packagedData, ECPublicKeyParameters publicKey) throws Exception {
        // 1. 解析打包数据
        ByteBuffer buffer = ByteBuffer.wrap(packagedData);
        int textLength = buffer.getInt();
        byte[] textBytes = new byte[textLength];
        buffer.get(textBytes);
        byte[] signature = new byte[64];
        buffer.get(signature);

        // 2. 提取r和s
        byte[] rBytes = new byte[32];
        byte[] sBytes = new byte[32];
        System.arraycopy(signature, 0, rBytes, 0, 32);
        System.arraycopy(signature, 32, sBytes, 0, 32);
        BigInteger r = new BigInteger(1, rBytes); // 1表示正整数
        BigInteger s = new BigInteger(1, sBytes);

        // 3. 计算文本的SHA-256哈希
        SHA256Digest digest = new SHA256Digest();
        digest.update(textBytes, 0, textBytes.length);
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);

        // 4. 验证签名
        ECDSASigner signer = new ECDSASigner();
        signer.init(false, publicKey);
        boolean isValid = signer.verifySignature(hash, r, s);

        if (!isValid) {
            throw new SecurityException("签名验证失败,数据可能被篡改");
        }

        // 5. 返回提取的原文本
        return new String(textBytes, "UTF-8");
    }
}

关键注意事项

  • 哈希一致性:服务器和客户端必须使用相同的哈希算法(这里用SHA-256,是secp256k1的标准搭配)
  • 签名编码:r和s必须转成固定32字节的大端字节数组,避免解析时长度不一致
  • 公钥预存:客户端需要事先持有服务器的公钥,如果带宽允许也可以把公钥加入打包结构,但预存更省带宽
  • 二进制打包:用ByteBuffer的长度前缀是最省带宽的方式,比JSON/XML等文本序列化格式更高效

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 12:34:51