JuneAndGreen / sm-crypto

国密算法js版
MIT License
894 stars 245 forks source link

js加签,java验签始终不通过 #85

Closed zhangYiFeng closed 1 year ago

zhangYiFeng commented 1 year ago

java方要求签名是r+"#"+s格式,可是执行sm2.doSignature,der为false,得到 r+s整串后,按r前64拆分,s后64,手动拼接成r+”#“+s,java方始终验签不通过。

JuneAndGreen commented 1 year ago

java 侧也请尽量参考此文档实现:https://github.com/JuneAndGreen/sm-crypto/blob/master/docs/SM2%E6%A4%AD%E5%9C%86%E6%9B%B2%E7%BA%BF%E5%85%AC%E9%92%A5%E5%AF%86%E7%A0%81%E7%AE%97%E6%B3%95.pdf 然后看看标准文档中的用例是否可以通过?

zhangYiFeng commented 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

zhangYiFeng commented 1 year ago
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;
}
changhr2013 commented 1 year ago

我给你个 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
zhangYiFeng commented 1 year ago

感谢 @changhr2013 兄台尽心解答。但现在问题是对方是行方,是不会去修改代码以求兼容能解js。他们的验签方法如我四楼贴的代码,先取到我的签名串,然后根据#号拆分成数组,再放进自己编写的签名类。然后传给验签方法。 所以我只能尽量的去兼容对方代码,但签名内容,格式,我都检查了,都是一致的

changhr2013 commented 1 year ago

@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

因此,目前我能推断的几个可能的排查方向是:

zhangYiFeng commented 1 year ago

@changhr2013 再次感谢您的指导解惑。

一直拿我们js代码与他们新demo的java代码作比较。后来追踪到可疑点,做签名前,会先对报文做一下sm4加密,各自的 sm4加密后的密文稍稍不一致。虽然输出都是base64,但对方java加密 后,每76个字符带一个\n,而我们js是不带\n的。因为之前跟他们老版本调通过,他们老demo也是不带\n的,就想着看看他们老demo和新demo有什么区别。发现了老demo如下图,密文结果又做了字符处理,而新demo没有。

image