Closed zhangYiFeng closed 1 year ago
public Signature sign(byte[] M, byte[] IDA, byte[] priKey, byte[] pubKey) {
ECPoint publicKey = curve.decodePoint(pubKey);
BigInteger privateKey = new BigInteger(1, priKey);
byte[] ZAdata = ZA(IDA, publicKey);
byte[] M_ = join(ZAdata, M);
BigInteger e = new BigInteger(1, sm3hash(M_));
BigInteger k;
BigInteger r;
do {
do {
k = this.random(n);
ECPoint p1 = G.multiply(k).normalize();
BigInteger x1 = p1.getXCoord().toBigInteger();
r = e.add(x1);
r = r.mod(n);
} while(r.equals(BigInteger.ZERO));
} while(r.add(k).equals(n));
BigInteger s = privateKey.add(BigInteger.ONE).modInverse(n).multiply(k.subtract(r.multiply(privateKey)).mod(n)).mod(n);
return new Signature(r, s);
}
上面是对方java的生成签名方法,应该也是国密标准,最后生成的签名,处理拼接后是
1eaafe3b89059fe245a48b69400ece2d8fe87cdb8b6938592f9a0c3ab178898b#db24c8447c397bb1931fba0bd75fbe68b8cf667784cb0f3dbee47455b27540cf
js不做der,得到的签名串,自己处理拼接如下,r,s都是16进制,长度各64的字符串,但就是验不通过
d9d3debf342e27c1923d56f1d59ce7818611f4f46d682f3fa9574db2b66fa00f#0a3759f7b9e697e81dffeb9a57f2f0b13ceb993abe111c0fb347929a411da9f1
。
public boolean verify(byte[] M, Signature signature, byte[] IDA, byte[] publicKey) {
ECPoint aPublicKey = curve.decodePoint(publicKey);
if (!this.between(signature.r, BigInteger.ONE, n)) {
return false;
} else if (!this.between(signature.s, BigInteger.ONE, n)) {
return false;
} else {
byte[] M_ = join(ZA(IDA, aPublicKey), M);
BigInteger e = new BigInteger(1, sm3hash(M_));
BigInteger t = signature.r.add(signature.s).mod(n);
if (t.equals(BigInteger.ZERO)) {
return false;
} else {
ECPoint p1 = G.multiply(signature.s).normalize();
ECPoint p2 = aPublicKey.multiply(t).normalize();
BigInteger x1 = p1.add(p2).normalize().getXCoord().toBigInteger();
BigInteger R = e.add(x1).mod(n);
return R.equals(signature.r);
}
}
}
上面是对方的验签方法,接收到签名后会用下面方法先处理一下签名字符串,再调用上面方法验签,最后 R.equals(signature.r)为false。
public static Signature fromString(String signStr) {
Signature signature = null;
if (StringUtils.isNotBlank(signStr)) {
try {
String[] signSub = signStr.split("#");
signature = new Signature(new BigInteger(signSub[0], 16), new BigInteger(signSub[1], 16));
} catch (Exception var4) {
var4.printStackTrace();
}
}
return signature;
}
我给你个 sm-crypto 和 BC 库互通的示例,你可以参考下:
sm-crypto 签名/验签:
import {sm2} from "sm-crypto";
const Sm2Crypto = sm2
// 私钥
let privateKeyHex = "15ccdbb178f7f41c4acc7a74d1d35e6dbb3883d2e39559bf7bf74c0daac4d4d6";
// 公钥
let publicKeyHex = "04d7124943876ff89a4bbe3d6f52f446a23868c760475c7993f30dc2fe3281b9a7866dc3644a175e435711ddde50918b46f4d2293a49aa9ffc77005b32f8bdb8dc"
// 签名
let signature = Sm2Crypto.doSignature("hello world", privateKeyHex, {der: false, hash: true});
console.log(signature);
// 验签
let verifyResult = Sm2Crypto.doVerifySignature("hello world", signature, publicKeyHex, {der: false, hash: true});
console.log(verifyResult);
示例输出:
46234cc19854853978807efc964f230cb139328782282a279ad4695792e1b0f15a0c807bf4f73d302cf86fa583b961f181dbbc8775a10487c0ff60537ea13d87
true
Java BC 库验签:
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.signers.PlainDSAEncoding;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class Sm2SignTest {
@Test
public void testSm2SignAndVerify() {
// 待签名的原文
String plainText = "hello world";
byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
// 私钥
String privateKeyHex = "15ccdbb178f7f41c4acc7a74d1d35e6dbb3883d2e39559bf7bf74c0daac4d4d6";
// 公钥
String publicKeyHex = "04d7124943876ff89a4bbe3d6f52f446a23868c760475c7993f30dc2fe3281b9a7866dc3644a175e435711ddde50918b46f4d2293a49aa9ffc77005b32f8bdb8dc";
// js 产生的签名
String jsSignHex = "46234cc19854853978807efc964f230cb139328782282a279ad4695792e1b0f15a0c807bf4f73d302cf86fa583b961f181dbbc8775a10487c0ff60537ea13d87";
final byte[] defaultId = "1234567812345678".getBytes(StandardCharsets.UTF_8);
// 构建 SM2 公钥参数
X9ECParameters sm2CurveParam = ECUtil.getNamedCurveByName("sm2p256v1");
ECPoint qPoint = sm2CurveParam.getCurve().decodePoint(Hex.decode(publicKeyHex));
ECPublicKeyParameters sm2PublicKeyParam = new ECPublicKeyParameters(qPoint, new ECDomainParameters(sm2CurveParam.getCurve(), sm2CurveParam.getG(), sm2CurveParam.getN(), sm2CurveParam.getH(), sm2CurveParam.getSeed()));
SM2Signer sm2Signer = new SM2Signer(PlainDSAEncoding.INSTANCE);
// 构建签名类的初始化参数
ParametersWithID parametersWithID = new ParametersWithID(sm2PublicKeyParam, defaultId);
// 初始化 SM2 签名类
sm2Signer.init(false, parametersWithID);
sm2Signer.update(plainBytes, 0, plainBytes.length);
// 验签
boolean verifyResult = sm2Signer.verifySignature(Hex.decode(jsSignHex));
// 打印验签结果
System.out.println(verifyResult);
}
}
示例输出:
true
感谢 @changhr2013 兄台尽心解答。但现在问题是对方是行方,是不会去修改代码以求兼容能解js。他们的验签方法如我四楼贴的代码,先取到我的签名串,然后根据#号拆分成数组,再放进自己编写的签名类。然后传给验签方法。 所以我只能尽量的去兼容对方代码,但签名内容,格式,我都检查了,都是一致的
@zhangYiFeng 目前能确认的前提是:JS 库与标准的 BC 库是互相兼容的,因此当前 JS 库的实现是没有问题的。
我大致看了一下,你给的代码与标准 BC 库的实现基本是一致的。
因为你给的代码片段信息有限,我只能在你的片段基础上实现了一个类似的逻辑:
public boolean verify(byte[] M, BigInteger r, BigInteger s, byte[] IDA, byte[] publicKey) {
X9ECParameters x9SM2Parameters = ECUtil.getNamedCurveByName("sm2p256v1");
ECPoint aPublicKey = x9SM2Parameters.getCurve().decodePoint(publicKey);
ECPoint G = x9SM2Parameters.getG();
BigInteger n = x9SM2Parameters.getN();
if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(n) >= 0) {
// 相当于 if (!this.between(signature.r, BigInteger.ONE, n))
return false;
} else if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(n) >= 0) {
// 相当于 if (!this.between(signature.s, BigInteger.ONE, n))
return false;
} else {
// 此处做 SM2 的预处理求 eHash
// 相当于 byte eHash = sm3hash(join(ZA(IDA, aPublicKey), M));
// SM2.getPreDataByPublicKey 内是 SM2 的标准预处理逻辑
byte[] eHash = SM2.getPreDataByPublicKey(M, publicKey, IDA);
BigInteger e = new BigInteger(1, eHash);
BigInteger t = r.add(s).mod(n);
if (t.equals(BigInteger.ZERO)) {
return false;
} else {
ECPoint p1 = G.multiply(s).normalize();
ECPoint p2 = aPublicKey.multiply(t).normalize();
BigInteger x1 = p1.add(p2).normalize().getXCoord().toBigInteger();
BigInteger R = e.add(x1).mod(n);
return R.equals(r);
}
}
}
依托于上面的方法我进行了与上面样例中数据一致的验签逻辑:
BigInteger[] rsArr = PlainDSAEncoding.INSTANCE.decode(sm2CurveParam.getN(), Hex.decode(jsSignHex));
boolean verify = verify(plainBytes, rsArr[0], rsArr[1], defaultId, Hex.decode(publicKeyHex));
System.out.println(verify);
得到的示例输出为:
true
因此,目前我能推断的几个可能的排查方向是:
@changhr2013 再次感谢您的指导解惑。
一直拿我们js代码与他们新demo的java代码作比较。后来追踪到可疑点,做签名前,会先对报文做一下sm4加密,各自的 sm4加密后的密文稍稍不一致。虽然输出都是base64,但对方java加密 后,每76个字符带一个\n,而我们js是不带\n的。因为之前跟他们老版本调通过,他们老demo也是不带\n的,就想着看看他们老demo和新demo有什么区别。发现了老demo如下图,密文结果又做了字符处理,而新demo没有。
java方要求签名是r+"#"+s格式,可是执行sm2.doSignature,der为false,得到 r+s整串后,按r前64拆分,s后64,手动拼接成r+”#“+s,java方始终验签不通过。