Java SM2实现 与 OpenSSL SM2 实现的对接
所里有个项目客户端是Java开发的,服务端是C开发的,之间使用了SM2算法进行密钥交换。Java端是在网上找的一个比较流行的基于BC的SM2实现(https://github.com/PopezLotado/SM2Java),依赖的bcprov-jdk15on,版本1.56。C端是用的OpenSSL。服务端和客户端联调时发现了很多问题,SM2算法的公钥加解密一直没法调通,签名验签也不通,但Ja..
所里有个项目客户端是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;
}
//省略
BigInteger的toByteArray()
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));
ECPoint的encoded和decodePoint
如下代码所示,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);
ECPoint的归一化
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
解密成功
测试成功
更多推荐
所有评论(0)