赞
踩
所里有个项目客户端是Java开发的,服务端是C开发的,之间使用了SM2算法进行密钥交换。
Java端是在网上找的一个比较流行的基于BC的SM2实现(https://github.com/PopezLotado/SM2Java),依赖的bcprov-jdk15on,版本1.56。C端是用的OpenSSL。
服务端和客户端联调时发现了很多问题,SM2算法的公钥加解密一直没法调通,签名验签也不通,但Java应用加密的数据可以自己解密成功,C应用加密的数据自己也可以解密成功。
我们分析了一下二者之间的差异:
从Java端encrypt的实现来看,不难看出,输出格式为C1C2C3的二进制数据拼接。注意这个C1采用了Encoded进行编码
public byte[] encrypt(String input, ECPoint publicKey) { // byte[] C1Buffer; // byte[] C2; // byte[] C3; //省略 C1Buffer = C1.getEncoded(false); //省略 /* 8 输出密文 C=C1 || C2 || C3 */ byte[] encryptResult = new byte[C1Buffer.length + C2.length + C3.length]; System.arraycopy(C1Buffer, 0, encryptResult, 0, C1Buffer.length); System.arraycopy(C2, 0, encryptResult, C1Buffer.length, C2.length); System.arraycopy(C3, 0, encryptResult, C1Buffer.length + C2.length, C3.length); if (debug) { System.out.print("密文: "); printHexString(encryptResult); } return encryptResult;
通过查看C端OpenSSL接口实现,它使用i2d将结果转化为了ASN1编码输出。
typedef struct ECIES_st { BIGNUM *x; /** X of C1 point on the affine coordinates */ BIGNUM *y; /** y of C1 point on the affine coordinates */ ASN1_OCTET_STRING *C3; /** Hash 32 bytes*/ ASN1_OCTET_STRING *C2; /** encrypted data */ }ECIES; unsigned char *ECIES_public_encrypt(const unsigned char *src,size_t slen,EC_KEY *eckey,size_t *dlen) { ECIES *ec=NULL; //... ec=ECIES_do_public_encrypt(src,slen,eckey); //... eclen=i2d_ECIES(ec,&ret); /... return ret; }
一个是ASN1编码后的密文,另一个是二进制拼凑的密文,联调自然无法通过。这是C和Java在SM2公钥加解密算法实现中的一处不同。我们可以在Java端将C1C2C3转换为标准C1C3C2的ASN1编码输出。
加密结果转换部分的代码实现如下:
ASN1Integer x = new ASN1Integer(C1.getXCoord().toBigInteger()); ASN1Integer y = new ASN1Integer(C1.getYCoord().toBigInteger()); DEROctetString derDig = new DEROctetString(C3); DEROctetString derEnc = new DEROctetString(C2); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(x); v.add(y); v.add(derDig); v.add(derEnc); DERSequence seq = new DERSequence(v); byte[] ret = null; try { ret = seq.getEncoded(); }catch (Exception e){ e.printStackTrace(); } return ret;
相应的在解密部分,我们提前将ASN1密文数据转换为原实现中C1C2C3拼接的方式,要注意的是原实现中C1使用encode进行了编码。
public String decrypt2(byte[] encryptData2, BigInteger privateKey) { byte[] encryptData; try{ ASN1InputStream aIn = new ASN1InputStream(encryptData2); ASN1Sequence seq = (ASN1Sequence)aIn.readObject(); BigInteger x = ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); BigInteger y = ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); byte[] c3 = ASN1OctetString.getInstance(seq.getObjectAt(2)).getOctets(); byte[] c2 = ASN1OctetString.getInstance(seq.getObjectAt(3)).getOctets(); ECPoint p = curve.validatePoint(x, y); //原实现中的c1是进行了encode编码的,为了正常解密,这里加一次编码转换 byte[] c1b = p.getEncoded(false); ByteArrayOutputStream os = new ByteArrayOutputStream(); os.write(c1b); os.write(c2); os.write(c3); encryptData = os.toByteArray(); }catch (Exception e){ e.printStackTrace(); return null; } //省略
Java算法中将大数转换为二进制数组使用了BigIneger.toByteArray(),与OpenSSL中BIGNUM不同的是,在大数的最高二进制位为1时,BigIneger.toByteArray()会额外的在返回结果前加一个字符‘\0’,标准SM2算法的计算过程中并不会使用到这个额外的字符,Java端SM2算法的实现没有考虑到这个问题。
如下代码,Java的SM2实现中,加密过程中计算C3这一步,如果不对toByteArray()的返回做处理,那SM3 Hash时偶尔就会引入额外的字符。这将导致和C端不一样的结果。
/* 7 计算C3 = Hash(x2 || M || y2) */
byte[] C3 = sm3hash(kpb.getXCoord().toBigInteger().toByteArray(), inputBuffer,
kpb.getYCoord().toBigInteger().toByteArray());
另外,此处Hash计算的数据应该是个定长数据,toByteArray()返回结果的高位要补0,这都是该Java版本SM2实现中没有考虑到的地方。我们自己实现几个函数来完成这个功能:
public byte[] p2bx(ECPoint point){ int size = (curve.getFieldSize() + 7) / 8; byte[] xb = point.getXCoord().toBigInteger().toByteArray(); if(xb.length > size){ byte[] tmp = xb; xb = new byte[size]; System.arraycopy(tmp, tmp.length - size, xb, 0, size); } byte[] ret = new byte[size]; Arrays.fill(ret, (byte)0); System.arraycopy(xb, 0, ret, size - xb.length, xb.length); return ret; } public byte[] p2by(ECPoint point){ int size = (curve.getFieldSize() + 7) / 8; byte[] yb = point.getYCoord().toBigInteger().toByteArray(); if(yb.length > size){ byte[] tmp = yb; yb = new byte[size]; System.arraycopy(tmp, yb.length - size, yb, 0, size); } byte[] ret = new byte[size]; Arrays.fill(ret, (byte)0); System.arraycopy(yb, 0, ret, size - yb.length, yb.length); return ret; }
并将以上C3计算部分替换为
byte[] C3 = sm3hash(p2bx(kpb), inputBuffer,p2by(kpb));
对应解密部分的代码也做如下替换:
/* 替换前
byte[] u = sm3hash(dBC1.getXCoord().toBigInteger().toByteArray(), M,
dBC1.getYCoord().toBigInteger().toByteArray());
*/
byte[] u = sm3hash(p2bx(dBC1), M,p2by(dBC1));
如下代码所示,Java的SM2实现中滥用了ECPoint的encode方法,比如在加密过程中计算C1时将返回结果进行了不必要的encode。不过我们前面已经在ASN1转换的时候注意了这个问题。
/* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
ECPoint C1 = G.multiply(k);
C1Buffer = C1.getEncoded(false);
最要命的是在计算t值时,标准SM2算法要求KDF的输入是对[k]PB即(x2,y2)进行大端二进制数拼接,而这个Java实现却是对(x2,y2)进行了encode编码,如下:
/* 4 计算 [k]PB = (x2, y2) */
kpb = publicKey.multiply(k).normalize();
/* 5 计算 t = KDF(x2||y2, klen) */
byte[] kpbBytes = kpb.getEncoded(false);
t = KDF(kpbBytes, inputBuffer.length);
我们重新编写一个函数来替代encode
public byte[] p2bytes(ECPoint point){ int size = (curve.getFieldSize() + 7) / 8; byte[] xb = point.getXCoord().toBigInteger().toByteArray(); byte[] yb = point.getYCoord().toBigInteger().toByteArray(); if(xb.length > size){ byte[] tmp = xb; xb = new byte[size]; System.arraycopy(tmp, tmp.length - size, xb, 0, size); } if(yb.length > size){ byte[] tmp = yb; yb = new byte[size]; System.arraycopy(tmp, tmp.length - size, yb, 0, size); } byte[] ret = new byte[size*2]; Arrays.fill(ret, (byte)0); System.arraycopy(xb, 0, ret, size - xb.length, xb.length); System.arraycopy(yb, 0, ret, size + size - yb.length, yb.length); return ret; }
然后[k]PB的计算做一下修改:
/* 4 计算 [k]PB = (x2, y2) */
kpb = publicKey.multiply(k).normalize();
/* 5 计算 t = KDF(x2||y2, klen) */
byte[] kpbBytes = p2bytes(kpb);
对应解密的地方也要做一下修改:
/* 计算t = KDF(x2 || y2, klen) */
//修改前
//byte[] dBC1Bytes = dBC1.getEncoded(false);
byte[] dBC1Bytes = p2bytes(dBC1);
int klen = encryptData.length - 65 - DIGEST_LENGTH;
byte[] t = KDF(dBC1Bytes, klen);
OpenSSL实现中默认z值为1,所以原Java的SM2加密实现中必须实现对C1的归一化处理,修改如下:
/* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
C1 = G.multiply(k).normalize();
if (debug) {
System.out.print("C1: ");
printHexString(p2bytes(C1));
}
公钥数据也要进行归一化
public byte[] encrypt2(String input, ECPoint publicKey) {
ECPoint C1 = null;
publicKey = publicKey.normalize();
我们在main中加入一段测试代码,记录下java加密后的密文,使用采用GmSSL的c端进行解密,通过!
ECPoint publicKey = sm02.importPublicKeyFromDERCert("xxx\\cert-sm2.der");
BigInteger privateKey = sm02.importPrivateKeyDER("xxx\\key-sm2.der");
String encStr = "测试加密";
int ct = 0;
byte[] encbytes = sm02.encrypt(encStr, publicKey);
String decbytes = sm02.decrypt(encbytes, privateKey);
if(!decbytes.equals(encStr))
System.out.println("测试失败");
-----------------加密解密----------------- E6B58BE8AF95E58AA0E5AF8661616161616161616161613132336161616262 k: 238411EA6041271ABFCCD923693ECD8DB9F281A5D324F788AA1C148E846E8AD4 C1: A09346F82CF7811B4F20EA6D9162A8E17015FE7E45F7E3C0D3A2D788D585CC207921FF77563D74E7F63208E2084D61551CB0F8F910D32E10583CE9E93E8FCE7A kpb: D7387C2A1DE31C64C551E1E38723D097F98A3C6A1283FED01D6059EDE25BC1159CA26FBC4B7C9BB0B9646554A62AE189D8F80D95C15ABA30CC4C1079C121077C C2: A3A792A89FFECB372935308EE6E7B30E83ADE6FA5722431F753958A7BC2F31 C3: 1A4FEE8EA158923FA2876225B2E32A7B8FFE537092E08C3035FB49507AE1674C encryptData2 length: 139 C1: A09346F82CF7811B4F20EA6D9162A8E17015FE7E45F7E3C0D3A2D788D585CC207921FF77563D74E7F63208E2084D61551CB0F8F910D32E10583CE9E93E8FCE7A Disconnected from the target VM, address: '127.0.0.1:42148', transport: 'socket' dBC1: D7387C2A1DE31C64C551E1E38723D097F98A3C6A1283FED01D6059EDE25BC1159CA26FBC4B7C9BB0B9646554A62AE189D8F80D95C15ABA30CC4C1079C121077C M: E6B58BE8AF95E58AA0E5AF8661616161616161616161613132336161616262 M = 测试加密aaaaaaaaaaa123aaabb C3: 1A4FEE8EA158923FA2876225B2E32A7B8FFE537092E08C3035FB49507AE1674C 解密成功 测试成功
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。