# 密钥生成

先看代码实现,再解析注意事项

/**
 * SM2 密钥对的生成
 */
@Test
public void testGenKeypair() {
    // 获取国密曲线
    X9ECParameters gmParameters = GMNamedCurves.getByName("sm2p256v1");
    // 构造Domain参数
    ECDomainParameters gmDomainParameters = new ECDomainParameters(gmParameters.getCurve(),
            gmParameters.getG(), gmParameters.getN());

    ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
    keyPairGenerator.init(new ECKeyGenerationParameters(gmDomainParameters, new SecureRandom()));

    // 生成密钥对
    AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair();
    ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) keyPair.getPrivate();
    ECPublicKeyParameters ecpub = (ECPublicKeyParameters) keyPair.getPublic();

    // 注意这里需要用到BigIntegers.asUnsignedByteArray将私钥转换为二进制数组,并且指定二进制数组的长度为32字节
    // 如果不这样做,遇到大数最高字节超出byte表示范围的(如0xF0),ecpriv.getD().toByteArray()这样转出来的二进
    // 制数组长度会是33字节,下标为0的数据为0x00,即补了一字节的0。而对于大数长度不满32字节的呢,转出来的二进制数组
    // 长度也不是32字节,这不符合一般的应用要求(一般我们认为SM2的私钥就是32字节长度256bit的一个大数)
    System.out.println("私钥:" + Hex.toHexString(BigIntegers.asUnsignedByteArray(32, ecpriv.getD())));

    // 压缩公钥即为:yTile || X
    System.out.println("压缩公钥:" + Hex.toHexString(ecpub.getQ().getEncoded(true)));

    // 未压缩公钥即为:PC || X || Y,其中PC = 0x04,有些文档里公钥写64字节,其实就是省略了PC这一个字节
    System.out.println("公钥:" + Hex.toHexString(ecpub.getQ().getEncoded(false)));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

密钥对的生成其实不难,很多第一次接触SM2或对其了解不深的,最容易搞错的就是私钥转成二进制或十六进制字符串的时候,会取错值。在Java中,BigInteger的toByteArray方法,会根据大数的实际值进行处理,通常的我们希望得到的私钥是一个32字节的byte数组,如果大数小于249比特,toByteArray转出来的byte数组就会小于32字节。如果大数的最高一字节大于0x7F的(也即最高位为1),toByteArray转出来的就会是33字节,第一字节为0,用来确保它是无符号大数。所以,要想得到正确的值,就必须用BigIntegers.asUnsignedByteArray(32, your_bigInteger_here),这个方法会处理小于249比特及超过255比特的大数,出来的值固定是32字节。

注意

压缩公钥:调用ECPoint的getEncoded(true)方法,即可得到33字节压缩公钥
非压缩公钥:调用ECPoint的getEncoded(false)方法,即可得到65字节的非压缩公钥,第一个字节固定为0x04,如果要得到64字节公钥,则忽略getEncoded(false)返回的第一个字节即可。

# 创建私钥

/**
 * 从十六进制字符串或二进制数组中创建一个SM2私钥参数
 * 无论是后续加解密,还是签名验签,都会用到ECPrivateKeyParameters
 */
@Test
public void testCreatePrivK() {
    // 获取国密曲线
    X9ECParameters gmParameters = GMNamedCurves.getByName("sm2p256v1");
    // 构造Domain参数
    ECDomainParameters gmDomainParameters = new ECDomainParameters(gmParameters.getCurve(),
            gmParameters.getG(), gmParameters.getN());

    try {
        // 创建无符号大数
        BigInteger sm2D = new BigInteger(1,
                Hex.decode("b8b08eae2876ef4e24bc7b3e95373b39246cdcce58aaf6cdaf42874369ba1ff3"));

        // 创建SM2私钥,ECPrivateKeyParameters实例创建时,会去校验大数是否符合SM2曲线的要求
        ECPrivateKeyParameters ecpriv = new ECPrivateKeyParameters(sm2D, gmDomainParameters);
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }

    try {
        // 测试一个非法的私钥
        BigInteger sm2D = new BigInteger(1,
                Hex.decode("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54124"));

        ECPrivateKeyParameters ecpriv = new ECPrivateKeyParameters(sm2D, gmDomainParameters);
        Assert.fail("大数不在[1, n - 1]范围");
    }catch (Exception ex) {
        Assert.assertEquals("Scalar is not in the interval [1, n - 1]", ex.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里主要注意,新建BigInteger实例的时候,第一个参数为1,表示要创建一个无符号大数就行了。

# 创建公钥

/**
 * 从十六进制字符串或二进制数组中创建一个SM2公钥参数
 * 无论是后续加解密,还是签名验签,都会用到ECPublicKeyParameters
 */
@Test
public void testCreatePubK() {
    // 获取国密曲线
    X9ECParameters gmParameters = GMNamedCurves.getByName("sm2p256v1");
    // 构造Domain参数
    ECDomainParameters gmDomainParameters = new ECDomainParameters(gmParameters.getCurve(),
            gmParameters.getG(), gmParameters.getN());

    try {
        // 从压缩公钥中创建点
        ECPoint sm2Q = gmDomainParameters.getCurve().decodePoint(
                Hex.decode("03aa8644b5ffafe526a6ed5dbeb09a1743b919f078da457536bc3c381d4ada6801"));

        // 跟私钥一样,在创建ECPublicKeyParameters实例的时候,会去校验点是否符合SM2曲线要求
        ECPublicKeyParameters ecpub = new ECPublicKeyParameters(sm2Q, gmDomainParameters);

        // 这里跟未压缩的公钥进行比较
        Assert.assertEquals(Hex.toHexString(ecpub.getQ().getEncoded(false)),
                "04aa8644b5ffafe526a6ed5dbeb09a1743b919f078da457536bc3c381d4ada6801d57ff1f6acc7547121bfb36e5eda717c2be60bcd2542e1d1857924b6a5f11bfb");
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }

    try {
        // 从未压缩公钥中创建点,decodePoint会校验点是否合法
        ECPoint sm2Q = gmDomainParameters.getCurve().decodePoint(
                Hex.decode("04aa8644b5ffafe526a6ed5dbeb09a1743b919f078da457536bc3c381d4ada6801d57ff1f6acc7547121bfb36e5eda717c2be60bcd2542e1d1857924b6a5f11bfb"));

        // 跟私钥一样,在创建ECPublicKeyParameters实例的时候,会去校验点是否符合SM2曲线要求
        ECPublicKeyParameters ecpub = new ECPublicKeyParameters(sm2Q, gmDomainParameters);

        // 这里跟压缩的公钥进行比较
        Assert.assertEquals(Hex.toHexString(ecpub.getQ().getEncoded(true)),
                "03aa8644b5ffafe526a6ed5dbeb09a1743b919f078da457536bc3c381d4ada6801");
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }

    try {
        // 从未压缩公钥中创建点,decodePoint会校验点是否合法
        ECPoint sm2Q = gmDomainParameters.getCurve().decodePoint(
                Hex.decode("0432C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A2"));

        // 跟私钥一样,在创建ECPublicKeyParameters实例的时候,会去校验点是否符合SM2曲线要求
        ECPublicKeyParameters ecpub = new ECPublicKeyParameters(sm2Q, gmDomainParameters);

        Assert.fail("无效点");
    }catch (Exception ex) {
        Assert.assertEquals("Invalid point coordinates", ex.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

没啥需要特别注意的地方,像公钥合法性检查,BouncyCastle该处理的都处理了。

# 从私钥中获取公钥

/**
 * 从私钥中获取公钥
 */
@Test
public void testGetPubKFromPrivK() {
    // 获取国密曲线
    X9ECParameters gmParameters = GMNamedCurves.getByName("sm2p256v1");
    // 构造Domain参数
    ECDomainParameters gmDomainParameters = new ECDomainParameters(gmParameters.getCurve(),
            gmParameters.getG(), gmParameters.getN());
    try {
        // 创建无符号大数
        BigInteger sm2D = new BigInteger(1,
                Hex.decode("b8b08eae2876ef4e24bc7b3e95373b39246cdcce58aaf6cdaf42874369ba1ff3"));

        // 创建SM2私钥,ECPrivateKeyParameters实例创建时,会去校验大数是否符合SM2曲线的要求
        ECPrivateKeyParameters ecpriv = new ECPrivateKeyParameters(sm2D, gmDomainParameters);

        // 从私钥中获取公钥
        ECPoint sm2Q = gmDomainParameters.getG().multiply(sm2D);

        // 创建SM2公钥参数,在创建ECPublicKeyParameters实例的时候,会去校验点是否符合SM2曲线要求
        ECPublicKeyParameters ecpub = new ECPublicKeyParameters(sm2Q, gmDomainParameters);

        // 与预期的压缩公钥进行比较
        Assert.assertEquals("02a9036e0289d9fa6d566cd0500807e3cba1ce14ba9b58bfbbef00b4b8d502ed72",
                Hex.toHexString(ecpub.getQ().getEncoded(true)));
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

其实就是拿私钥去与SM2曲线中的参数G相乘,即可得到公钥