如何在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
相关产品推荐
相关产品推荐

