1、发现问题:
客户用“SHA256withECDSA”生成的签名,我们用openssl算法的验签接口有些可以通过,有些没法通过。
2、自发验证-方法一:
通过php的脚本验证这个问题,代码如下:
//验证签名 $algo = "SHA256"; // 304402205E4799D8E8199184FB7613000781EE5D7AE2E27121733E8B391FD33C51BD80C7022039EAF55B1C3958F39764D64EC18945E1A63AB7DA2DD9D6D1BA9F1BC95FDDCCEB $signature = "MEQCIF5HmdjoGZGE+3YTAAeB7l164uJxIXM+izkf0zxRvYDHAiA56vVbHDlY85dk1k7BiUXhpjq32i3Z1tG6nxvJX93M6w=="; $pubKeyContent="-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMrpFAuH2QXyMLYE3vlA3CfHDPhyp\nnEMu/FPajy/woq1MSHcwHeu0Ui0PczdzJ5qLcLo6srJjVG8WqgvXn2eryA==\n-----END PUBLIC KEY-----"; $pKey=openssl_get_publickey($pubKeyContent); // 3930303034303031303032353030303120062317084014952068DB5C0989EEB00F1F128BBC990DF9C1D0CCAFFDEA3973021F945AF8B2589C955EF3181F8CA05B3059301306072A8648CE3D020106082A8648CE3D03010703420004B29046B2F93A2BFCF497390FEF8E1BFE0C34FA89D3A45AF925A22571B1C1B5E7A7409A73A7F8F9C1A61D3F9FF284926819EAEBF2E6D4968A46F112B0811A4E0501A10100 $message="OTAwMDQwMDEwMDI1MDAwMSAGIxcIQBSVIGjbXAmJ7rAPHxKLvJkN+cHQzK/96jlzAh+UWviyWJyVXvMYH4ygWzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLKQRrL5Oiv89Jc5D++OG/4MNPqJ06Ra+SWiJXGxwbXnp0Cac6f4+cGmHT+f8oSSaBnq6/Lm1JaKRvESsIEaTgUBoQEA"; $success = openssl_verify(base64_decode($message), base64_decode($signature), $pKey, $algo); if ($success === -1) { $return = openssl_error_string(); } elseif ($success === 1) { $return = "Verification signature success"; } else { $return = openssl_error_string(); } echo "\r\n 1-return : ($success):($return) \r\n"; //验证签名 $algo = "SHA256"; //304402209903A6794AF9E758295E486064FC6ECB63F62E5F04F66579E99A83800D32CB0902203984B7421A2E337388C782E2FF86F23D9EF313BA5D73E598D6BE2F0B99B4FE69 $signature = "MEQCIJkDpnlK+edYKV5IYGT8bstj9i5fBPZleemag4ANMssJAiA5hLdCGi4zc4jHguL/hvI9nvMTul1z5ZjWvi8LmbT+aQ=="; $pubKeyContent="-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMrpFAuH2QXyMLYE3vlA3CfHDPhyp\nnEMu/FPajy/woq1MSHcwHeu0Ui0PczdzJ5qLcLo6srJjVG8WqgvXn2eryA==\n-----END PUBLIC KEY-----"; $pKey=openssl_get_publickey($pubKeyContent); //3930303034303031303032353030303120062311462329072068DB5C0989EEB00F1F128BBC990DF9C1D0CCAFFDEA3973021F945AF8B2589C955EF2CC8F8CA05B3059301306072A8648CE3D020106082A8648CE3D03010703420004C507CABC29EAA162A33A14D190BA3100204D82E6D3F20E54BCD73A3FD78F657B67BB2E783692CB8D6A75516257633148CBA492DCE19B32410DEE1B62EAE1DAAB01A10100 $message="OTAwMDQwMDEwMDI1MDAwMSAGIxFGIykHIGjbXAmJ7rAPHxKLvJkN+cHQzK/96jlzAh+UWviyWJyVXvLMj4ygWzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMUHyrwp6qFiozoU0ZC6MQAgTYLm0/IOVLzXOj/Xj2V7Z7sueDaSy41qdVFiV2MxSMukktzhmzJBDe4bYurh2qsBoQEA"; $success = openssl_verify(base64_decode($message), base64_decode($signature), $pKey, $algo); if ($success === -1) { $return = openssl_error_string(); } elseif ($success === 1) { $return = "Verification signature success"; } else { $return = openssl_error_string(); } echo "\r\n 2-return : ($success):($return) \r\n";
执行结果如下:
结论:php的验证接口表现和c调用openssl的表现一致,两者都依赖openssl开源库
3、自发验证-方法二:
java的ecdsa算法验签接口:
ECDSAUtil.java
package ecdsa; import javax.crypto.KeyAgreement; import javax.xml.bind.DatatypeConverter; import java.security.*; import java.security.spec.ECGenParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; /** * ECCDSA加签验签工具类 * @author Administrator * */ public class ECDSAUtil { private static final String SIGNALGORITHMS = "SHA256withECDSA"; private static final String ALGORITHM = "EC"; private static final String SECP256K1 = "secp256k1"; public static void main(final String[] args) throws Exception { // 生成公钥私钥 final KeyPair keyPair1 = getKeyPair(); final PublicKey publicKey1 = keyPair1.getPublic(); final PrivateKey privateKey1 = keyPair1.getPrivate(); //密钥转16进制字符串 final String publicKey = HexUtil.encodeHexString(publicKey1.getEncoded()); final String privateKey = HexUtil.encodeHexString(privateKey1.getEncoded()); //String privateKey2 = HexUtil.encodeHexString(bytesToHex(pubkeyhex)); System.out.println("生成公钥:"+publicKey); System.out.println("生成私钥:"+privateKey); //16进制字符串转密钥对象 final PrivateKey privateKey2 = getPrivateKey(privateKey); final PublicKey publicKey2 = getPublicKey(publicKey); //加签验签 final String data="需要签名的数据\b23\b21\r\n"; final String signECDSA = signECDSA(privateKey2, data); System.out.println("signECDSA:"+signECDSA); System.out.println("data: "+data); final boolean verifyECDSA = verifyECDSA(publicKey2, signECDSA, data); System.out.println("验签结果:"+verifyECDSA); verifyecdsa_sign1("第一个二维码"); verifyecdsa_sign2("第二个二维码"); } public static void verifyecdsa_sign(final String data, final String pubkey, final String sign) throws Exception { System.out.println("验证公钥:"+pubkey); System.out.println("验证数据:"+data); System.out.println("验证签名:"+sign); final PublicKey publicKey = getPublicKey(pubkey); boolean verifyECDSA = verifyECDSA_jxl(publicKey, sign, data); System.out.println("验签结果:"+verifyECDSA); } public static void verifyecdsa_sign1(final String sttitle) throws Exception { System.out.println("验证--------------------------------------------"+sttitle ); final String datas = "3930303034303031303032353030303120062317084014952068DB5C0989EEB00F1F128BBC990DF9C1D0CCAFFDEA3973021F945AF8B2589C955EF3181F8CA05B3059301306072A8648CE3D020106082A8648CE3D03010703420004B29046B2F93A2BFCF497390FEF8E1BFE0C34FA89D3A45AF925A22571B1C1B5E7A7409A73A7F8F9C1A61D3F9FF284926819EAEBF2E6D4968A46F112B0811A4E0501A10100"; final String pubkey = "3059301306072A8648CE3D020106082A8648CE3D0301070342000432BA4502E1F6417C8C2D8137BE503709F1C33E1CA99C432EFC53DA8F2FF0A2AD4C4877301DEBB4522D0F733773279A8B70BA3AB2B263546F16AA0BD79F67ABC8"; final String sign = "304402205E4799D8E8199184FB7613000781EE5D7AE2E27121733E8B391FD33C51BD80C7022039EAF55B1C3958F39764D64EC18945E1A63AB7DA2DD9D6D1BA9F1BC95FDDCCEB"; verifyecdsa_sign(datas, pubkey, sign); } public static void verifyecdsa_sign2(final String sttitle) throws Exception { System.out.println("验证--------------------------------------------"+sttitle ); final String datas = "3930303034303031303032353030303120062311462329072068DB5C0989EEB00F1F128BBC990DF9C1D0CCAFFDEA3973021F945AF8B2589C955EF2CC8F8CA05B3059301306072A8648CE3D020106082A8648CE3D03010703420004C507CABC29EAA162A33A14D190BA3100204D82E6D3F20E54BCD73A3FD78F657B67BB2E783692CB8D6A75516257633148CBA492DCE19B32410DEE1B62EAE1DAAB01A10100"; final String pubkey = "3059301306072A8648CE3D020106082A8648CE3D0301070342000432BA4502E1F6417C8C2D8137BE503709F1C33E1CA99C432EFC53DA8F2FF0A2AD4C4877301DEBB4522D0F733773279A8B70BA3AB2B263546F16AA0BD79F67ABC8"; final String sign = "304402209903A6794AF9E758295E486064FC6ECB63F62E5F04F66579E99A83800D32CB0902203984B7421A2E337388C782E2FF86F23D9EF313BA5D73E598D6BE2F0B99B4FE69"; verifyecdsa_sign(datas, pubkey, sign); } /** * 加签 * @param privateKey 私钥 * @param data 数据 * @return */ public static String signECDSA(final PrivateKey privateKey, final String data) { final String result = ""; try { //执行签名 final Signature signature = Signature.getInstance(SIGNALGORITHMS); signature.initSign(privateKey); signature.update(data.getBytes()); final byte[] sign = signature.sign(); return HexUtil.encodeHexString(sign); } catch (final Exception e) { e.printStackTrace(); } return result; } /** * 验签 * @param publicKey 公钥 * @param signed 签名 * @param data 数据 * @return */ public static boolean verifyECDSA_jxl(final PublicKey publicKey, final String signed, final String data) { try { //验证签名 final Signature signature = Signature.getInstance(SIGNALGORITHMS); signature.initVerify(publicKey); final byte[] datahex = HexUtil.decode(data); signature.update(datahex); final byte[] hex = HexUtil.decode(signed); final boolean bool = signature.verify(hex); System.out.println("验证:" + bool); return bool; } catch (Exception e) { e.printStackTrace(); } return false; } /** * 验签 * @param publicKey 公钥 * @param signed 签名 * @param data 数据 * @return */ public static boolean verifyECDSA(final PublicKey publicKey, final String signed, final String data) { try { //验证签名 final Signature signature = Signature.getInstance(SIGNALGORITHMS); signature.initVerify(publicKey); signature.update(data.getBytes()); final byte[] hex = HexUtil.decode(signed); final boolean bool = signature.verify(hex); System.out.println("验证:" + bool); return bool; } catch (final Exception e) { e.printStackTrace(); } return false; } /** * 从string转private key * @param key 私钥的字符串 * @return * @throws Exception */ public static PrivateKey getPrivateKey(final String key) throws Exception { final byte[] bytes = DatatypeConverter.parseHexBinary(key); final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); final KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePrivate(keySpec); } /** * 从string转publicKey * @param key 公钥的字符串 * @return * @throws Exception */ public static PublicKey getPublicKey(final String key) throws Exception { final byte[] bytes = DatatypeConverter.parseHexBinary(key); final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); final KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePublic(keySpec); } /** * 生成密钥对 * @return * @throws Exception */ public static KeyPair getKeyPair() throws Exception { final ECGenParameterSpec ecSpec = new ECGenParameterSpec(SECP256K1); final KeyPairGenerator kf = KeyPairGenerator.getInstance(ALGORITHM); kf.initialize(ecSpec, new SecureRandom()); final KeyPair keyPair = kf.generateKeyPair(); return keyPair; } }
HexUtil.java
package ecdsa; /** * 16进制字符串与byte数组转换 * @author Administrator * */ public final class HexUtil { private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; public HexUtil() { } /** * byte数组转16进制字符串 * @param bytes * @return */ public static String encodeHexString(byte[] bytes) { int nBytes = bytes.length; char[] result = new char[2 * nBytes]; int j = 0; byte[] var4 = bytes; int var5 = bytes.length; for(int var6 = 0; var6 < var5; ++var6) { byte aByte = var4[var6]; result[j++] = HEX[(240 & aByte) >>> 4]; result[j++] = HEX[15 & aByte]; } return new String(result); } /** * 16进制字符串转byte数组 * @param s 字符串 * @return */ public static byte[] decode(CharSequence s) { int nChars = s.length(); if (nChars % 2 != 0) { throw new IllegalArgumentException("Hex-encoded string must have an even number of characters"); } else { byte[] result = new byte[nChars / 2]; for(int i = 0; i < nChars; i += 2) { int msb = Character.digit(s.charAt(i), 16); int lsb = Character.digit(s.charAt(i + 1), 16); if (msb < 0 || lsb < 0) { throw new IllegalArgumentException("Detected a Non-hex character at " + (i + 1) + " or " + (i + 2) + " position"); } result[i / 2] = (byte)(msb << 4 | lsb); } return result; } } }
验证结果:两套数据验签都是通过的
4、通过调试openssl源码可知
签名的数据:
304402209903A6794AF9E758295E486064FC6ECB63F62E5F04F66579E99A83800D32CB0902203984B7421A2E337388C782E2FF86F23D9EF313BA5D73E598D6BE2F0B99B4FE69
在 ossl_ecdsa_verify 接口中校验DER签名序列时保存,究其原因,原签名数据和 i2d_ECDSA_SIG 转换的签名数据不一致如下:
30450221009903A6794AF9E758295E486064FC6ECB63F62E5F04F66579E99A83800D32CB0902203984B7421A2E337388C782E2FF86F23D9EF313BA5D73E598D6BE2F0B99B4FE69
两者对比差异:
附带DER签名序列的规则
//Sig = (R, S)
//签名序列化(DER)
//解锁脚本序列化之后:
//3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301
/包含以下9个元素:
//● 0x30表示DER序列的开始
//● 0x45 - 序列的长度(69字节)
//● 0x02 - 一个整数值
//● 0x21 - 整数的长度(33字节)
//● R-00884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb
//● 0x02 - 接下来是一个整数
//● 0x20 - 整数的长度(32字节)
//● S-4b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813
//● 后缀(0x01)指示使用的哈希的类型(SIGHASH_ALL)
5、为了项目能顺利使用,最快速的处理方法(统一格式化签名数据格式)
通过下面的接口,统一签名的数据,如下:
/**********************************************************************************************//** * @fn static int Alg_Statistical_ECDSA_DER_Sign_format(const uint8_t *pucSigInBuf, int iSigInLen, uint8_t *pucSigOutBuf, int *piSigOutLen); * @brief 统一ECDSA的签名数据格式(java计算的签名格式有时候不统一) * * @date 2020年6月28日 11:20:32 * @param [in] pucSigInBuf * @param [in] iSigInLen * @param [out] pucSigOutBuf * @param [out] piSigOutLen * @return - 0 成功 * - -1 失败 ***************************************************************************************************/ static int Alg_Statistical_ECDSA_DER_Sign_format(const uint8_t *pucSigInBuf, int iSigInLen, uint8_t *pucSigOutBuf, int *piSigOutLen) { ECDSA_SIG *s; uint8_t *pucDestSign = NULL; int iDestSignLen = -1; int iRet = -1; Comm_logh("IN-SIGN", pucSigInBuf, iSigInLen); s = ECDSA_SIG_new(); if (s == NULL) { return iRet; } if (d2i_ECDSA_SIG(&s, &pucSigInBuf, iSigInLen) == NULL) { return iRet; } /* Ensure signature uses DER and doesn't have trailing garbage */ iDestSignLen = i2d_ECDSA_SIG(s, &pucDestSign); *piSigOutLen = iDestSignLen; memcpy(pucSigOutBuf, pucDestSign, *piSigOutLen); Comm_logh("OUT-SIGN", pucSigOutBuf, *piSigOutLen); OPENSSL_free(pucDestSign); ECDSA_SIG_free(s); return (0); }
验证结果:
备注:关于DER的规则,为什么java和c处理的格式有部分一样,有部分不一样,这块没做深的研究,项目时间比较紧张,待有空了研究下补充此部分差异