diff --git a/chsm-common/pom.xml b/chsm-common/pom.xml index ad67cd8..207ee15 100644 --- a/chsm-common/pom.xml +++ b/chsm-common/pom.xml @@ -32,10 +32,6 @@ com.baomidou mybatis-plus-boot-starter - - javax.persistence - javax.persistence-api - org.bouncycastle diff --git a/chsm-common/src/main/java/com/sunyard/chsm/mapper/ApplicationMapper.java b/chsm-common/src/main/java/com/sunyard/chsm/mapper/ApplicationMapper.java new file mode 100644 index 0000000..480b87a --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/mapper/ApplicationMapper.java @@ -0,0 +1,13 @@ +package com.sunyard.chsm.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sunyard.chsm.model.entity.Application; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author liulu + * @since 2024/10/29 + */ +@Mapper +public interface ApplicationMapper extends BaseMapper { +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/mapper/KeyCsrMapper.java b/chsm-common/src/main/java/com/sunyard/chsm/mapper/KeyCsrMapper.java new file mode 100644 index 0000000..c2a4a39 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/mapper/KeyCsrMapper.java @@ -0,0 +1,20 @@ +package com.sunyard.chsm.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sunyard.chsm.model.entity.KeyCsr; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author liulu + * @since 2024/10/22 + */ +@Mapper +public interface KeyCsrMapper extends BaseMapper { + + default KeyCsr selectBySubject(String dn) { + return selectOne( + new LambdaQueryWrapper().eq(KeyCsr::getSubject, dn) + ); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/mapper/SpKeyRecordMapper.java b/chsm-common/src/main/java/com/sunyard/chsm/mapper/SpKeyRecordMapper.java index 788a552..a7c9582 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/mapper/SpKeyRecordMapper.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/mapper/SpKeyRecordMapper.java @@ -1,13 +1,28 @@ package com.sunyard.chsm.mapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.sunyard.chsm.model.entity.KeyRecord; import org.apache.ibatis.annotations.Mapper; +import java.time.LocalDateTime; + /** * @author liulu * @since 2024/10/22 */ @Mapper public interface SpKeyRecordMapper extends BaseMapper { + + default KeyRecord selectUsedByKeyId(Long id) { + LocalDateTime now = LocalDateTime.now(); + return selectOne( + new LambdaQueryWrapper() + .eq(KeyRecord::getKeyId, id) + .lt(KeyRecord::getEffectiveTime, now) + .gt(KeyRecord::getExpiredTime, now) + ); + } + + } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/model/Subject.java b/chsm-common/src/main/java/com/sunyard/chsm/model/Subject.java new file mode 100644 index 0000000..7322272 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/model/Subject.java @@ -0,0 +1,105 @@ +package com.sunyard.chsm.model; + +import lombok.Data; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.NotEmpty; +import java.util.Arrays; +import java.util.List; + +/** + * @author liulu + * @since 2024/10/29 + */ +@Data +public class Subject { + + private static final List keys = Arrays.asList("C", "ST", "L", "O", "OU", "CN"); + + private static final String COMMA = ","; + + /** + * 通用名 + */ + @NotEmpty(message = "通用名不能为空") + private String commonName; + /** + * 国家 + */ + private String country; + /** + * 省份 + */ + private String province; + /** + * 城市 + */ + private String city; + /** + * 组织 + */ + private String org; + /** + * 部门 + */ + private String orgUnit; + + + public String getDN() { + Assert.notNull(commonName, "通用名不能为空"); + StringBuilder builder = new StringBuilder(); + if (StringUtils.hasText(country)) { + builder.append("C=").append(country).append(COMMA); + } + if (StringUtils.hasText(province)) { + builder.append("ST=").append(province).append(COMMA); + } + if (StringUtils.hasText(city)) { + builder.append("L=").append(city).append(COMMA); + } + if (StringUtils.hasText(org)) { + builder.append("O=").append(org).append(COMMA); + } + if (StringUtils.hasText(orgUnit)) { + builder.append("OU=").append(orgUnit).append(COMMA); + } + if (StringUtils.hasText(commonName)) { + builder.append("CN=").append(commonName).append(COMMA); + } + builder.deleteCharAt(builder.lastIndexOf(COMMA)); + return builder.toString(); + } + + public static Subject fromDN(String dn) { + Assert.hasText(dn, "dn不能为空值"); + Subject subject = new Subject(); + String[] splits = dn.trim().split(","); + for (String split : splits) { + String trim = split.trim(); + + if (trim.startsWith("C=")) { + subject.setCountry(trim.split("=")[1]); + } + if (trim.startsWith("ST=")) { + subject.setProvince(trim.split("=")[1]); + } + if (trim.startsWith("L=")) { + subject.setCity(trim.split("=")[1]); + } + if (trim.startsWith("O=")) { + subject.setOrg(trim.split("=")[1]); + } + if (trim.startsWith("OU=")) { + subject.setOrgUnit(trim.split("=")[1]); + } + if (trim.startsWith("CN=")) { + subject.setCommonName(trim.split("=")[1]); + } + } + Assert.notNull(subject.commonName, "通用名不能为空"); + return subject; + } + + +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/model/entity/Application.java b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/Application.java new file mode 100644 index 0000000..8b5b4e2 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/Application.java @@ -0,0 +1,30 @@ +package com.sunyard.chsm.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author liulu + * @since 2024/10/29 + */ +@Data +@TableName("sp_application") +public class Application { + + private Long id; + private String name; + private String bindService; + private String status; + // 32位uuid + private String appKey; + // 32位uuid + private String appSecrete; + private Long creatorId; + + private String remark; + private LocalDateTime createTime; + private LocalDateTime updateTime; + +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyCsr.java b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyCsr.java new file mode 100644 index 0000000..cb1d270 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyCsr.java @@ -0,0 +1,29 @@ +package com.sunyard.chsm.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author liulu + * @since 2024/10/22 + */ +@Data +@TableName("sp_key_csr") +public class KeyCsr { + + private Long id; + private Long applicationId; + private Long keyId; + private Long keyRecordId; + private String subject; + private String keyData; + private String pubKey; + private String csrTxt; + + private String remark; + private LocalDateTime createTime; + private LocalDateTime updateTime; + +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyInfo.java b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyInfo.java index 1ec9bd6..2a9ba57 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyInfo.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/model/entity/KeyInfo.java @@ -1,9 +1,9 @@ package com.sunyard.chsm.model.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import javax.persistence.Transient; import java.time.LocalDateTime; import java.util.List; @@ -33,7 +33,7 @@ public class KeyInfo { private LocalDateTime createTime; private LocalDateTime updateTime; - @Transient + @TableField(exist = false) private List records; diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/BCSdfApiService.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/BCSdfApiService.java index 037afab..3801c33 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/BCSdfApiService.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/BCSdfApiService.java @@ -4,22 +4,18 @@ package com.sunyard.chsm.sdf; import com.sunyard.chsm.sdf.model.EccKey; import com.sunyard.chsm.sdf.model.EccPriKey; import com.sunyard.chsm.sdf.model.EccPubKey; +import com.sunyard.chsm.utils.gm.BCSM2Utils; +import com.sunyard.chsm.utils.gm.BCSM3Utils; import lombok.SneakyThrows; -import org.bouncycastle.asn1.gm.GMNamedCurves; -import org.bouncycastle.asn1.gm.GMObjectIdentifiers; -import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.digests.SM3Digest; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.util.BigIntegers; import org.springframework.stereotype.Service; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.SecureRandom; /** @@ -40,17 +36,8 @@ public class BCSdfApiService implements SdfApiService { @SneakyThrows @Override public EccKey genKeyPairEcc() { - - // 获取SM2参数 - X9ECParameters sm2Params = GMNamedCurves.getByOID(GMObjectIdentifiers.sm2p256v1); - ECParameterSpec sm2Spec = new ECParameterSpec(sm2Params.getCurve(), sm2Params.getG(), sm2Params.getN(), sm2Params.getH()); - - // 创建密钥对生成器 - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME); - keyPairGenerator.initialize(sm2Spec); - // 生成密钥对 - KeyPair keyPair = keyPairGenerator.generateKeyPair(); + KeyPair keyPair = BCSM2Utils.generateKeyPair(); BCECPublicKey pubKey = (BCECPublicKey) keyPair.getPublic(); BCECPrivateKey priKey = (BCECPrivateKey) keyPair.getPrivate(); byte[] x = pubKey.getQ().getXCoord().getEncoded(); @@ -84,11 +71,17 @@ public class BCSdfApiService implements SdfApiService { @Override public byte[] hash(byte[] pucData) { - SM3Digest digest = new SM3Digest(); - digest.update(pucData, 0, pucData.length); - byte[] hash = new byte[digest.getDigestSize()]; - digest.doFinal(hash, 0); - return hash; + return BCSM3Utils.hash(pucData); + } + + @Override + public byte[] encryptByMKNoPadding(byte[] data) { + return data; + } + + @Override + public byte[] decryptByMKNoPadding(byte[] data) { + return data; } } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/SdfApiService.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/SdfApiService.java index b9f081a..bed16b9 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/SdfApiService.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/SdfApiService.java @@ -54,4 +54,7 @@ public interface SdfApiService { byte[] hash(byte[] pucData); + byte[] encryptByMKNoPadding(byte[] data); + + byte[] decryptByMKNoPadding(byte[] data); } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCECUtils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCECUtils.java new file mode 100644 index 0000000..884f00d --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCECUtils.java @@ -0,0 +1,526 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X962Parameters; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9ECPoint; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.generators.ECKeyPairGenerator; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyGenerationParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * 这个工具类的方法,也适用于其他基于BC库的ECC算法 + */ +public class BCECUtils { + private static final String ALGO_NAME_EC = "EC"; + private static final String PEM_STRING_PUBLIC = "PUBLIC KEY"; + private static final String PEM_STRING_ECPRIVATEKEY = "EC PRIVATE KEY"; + + /** + * 生成ECC密钥对 + * + * @return ECC密钥对 + */ + public static AsymmetricCipherKeyPair generateKeyPairParameter( + ECDomainParameters domainParameters, SecureRandom random) { + ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters(domainParameters, + random); + ECKeyPairGenerator keyGen = new ECKeyPairGenerator(); + keyGen.init(keyGenerationParams); + return keyGen.generateKeyPair(); + } + + public static KeyPair generateKeyPair(ECDomainParameters domainParameters, SecureRandom random) + throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + ECParameterSpec parameterSpec = new ECParameterSpec(domainParameters.getCurve(), domainParameters.getG(), + domainParameters.getN(), domainParameters.getH()); + kpg.initialize(parameterSpec, random); + return kpg.generateKeyPair(); + } + + public static int getCurveLength(ECKeyParameters ecKey) { + return getCurveLength(ecKey.getParameters()); + } + + public static int getCurveLength(ECDomainParameters domainParams) { + return (domainParams.getCurve().getFieldSize() + 7) / 8; + } + + public static byte[] fixToCurveLengthBytes(int curveLength, byte[] src) { + if (src.length == curveLength) { + return src; + } + + byte[] result = new byte[curveLength]; + if (src.length > curveLength) { + System.arraycopy(src, src.length - result.length, result, 0, result.length); + } else { + System.arraycopy(src, 0, result, result.length - src.length, src.length); + } + return result; + } + + /** + * @param dHex 十六进制字符串形式的私钥d值,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + String dHex, ECDomainParameters domainParameters) { + return createECPrivateKeyParameters(ByteUtils.fromHexString(dHex), domainParameters); + } + + /** + * @param dBytes 字节数组形式的私钥d值,如果是SM2算法,应该是32字节 + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters(byte[] dBytes) { + return createECPrivateKeyParameters(new BigInteger(1, dBytes), BCSM2Utils.DOMAIN_PARAMS); + } + + /** + * @param dBytes 字节数组形式的私钥d值,如果是SM2算法,应该是32字节 + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + byte[] dBytes, ECDomainParameters domainParameters) { + return createECPrivateKeyParameters(new BigInteger(1, dBytes), domainParameters); + } + + /** + * @param d 大数形式的私钥d值 + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + BigInteger d, ECDomainParameters domainParameters) { + return new ECPrivateKeyParameters(d, domainParameters); + } + + /** + * 根据EC私钥构造EC公钥 + * + * @param priKey ECC私钥参数对象 + * @return + */ + public static ECPublicKeyParameters buildECPublicKeyByPrivateKey(ECPrivateKeyParameters priKey) { + ECDomainParameters domainParameters = priKey.getParameters(); + ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), priKey.getD()); + return new ECPublicKeyParameters(q, domainParameters); + } + + /** + * @param x 大数形式的公钥x分量 + * @param y 大数形式的公钥y分量 + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + BigInteger x, BigInteger y, ECCurve curve, ECDomainParameters domainParameters) { + return createECPublicKeyParameters(x.toByteArray(), y.toByteArray(), curve, domainParameters); + } + + /** + * @param xHex 十六进制形式的公钥x分量,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param yHex 十六进制形式的公钥y分量,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + String xHex, String yHex, ECCurve curve, ECDomainParameters domainParameters) { + return createECPublicKeyParameters(ByteUtils.fromHexString(xHex), ByteUtils.fromHexString(yHex), + curve, domainParameters); + } + + public static ECPublicKeyParameters createECPublicKeyParameters(byte[] xBytes, byte[] yBytes) { + return createECPublicKeyParameters(xBytes, yBytes, BCSM2Utils.CURVE, BCSM2Utils.DOMAIN_PARAMS); + } + + /** + * @param xBytes 十六进制形式的公钥x分量,如果是SM2算法,应该是32字节 + * @param yBytes 十六进制形式的公钥y分量,如果是SM2算法,应该是32字节 + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link BCSM2Utils#DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + byte[] xBytes, byte[] yBytes, ECCurve curve, ECDomainParameters domainParameters) { + final byte uncompressedFlag = 0x04; + int curveLength = getCurveLength(domainParameters); + xBytes = fixToCurveLengthBytes(curveLength, xBytes); + yBytes = fixToCurveLengthBytes(curveLength, yBytes); + byte[] encodedPubKey = new byte[1 + xBytes.length + yBytes.length]; + encodedPubKey[0] = uncompressedFlag; + System.arraycopy(xBytes, 0, encodedPubKey, 1, xBytes.length); + System.arraycopy(yBytes, 0, encodedPubKey, 1 + xBytes.length, yBytes.length); + return new ECPublicKeyParameters(curve.decodePoint(encodedPubKey), domainParameters); + } + + public static ECPrivateKeyParameters convertPrivateKeyToParameters(BCECPrivateKey ecPriKey) { + ECParameterSpec parameterSpec = ecPriKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + return new ECPrivateKeyParameters(ecPriKey.getD(), domainParameters); + } + + public static ECPublicKeyParameters convertPublicKeyToParameters(BCECPublicKey ecPubKey) { + ECParameterSpec parameterSpec = ecPubKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + return new ECPublicKeyParameters(ecPubKey.getQ(), domainParameters); + } + + public static BCECPublicKey createPublicKeyFromSubjectPublicKeyInfo(SubjectPublicKeyInfo subPubInfo) + throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeySpecException, IOException { + return BCECUtils.convertX509ToECPublicKey(subPubInfo.toASN1Primitive().getEncoded(ASN1Encoding.DER)); + } + + /** + * 将ECC私钥转换为PKCS8标准的字节流 + * + * @param priKey + * @param pubKey 可以为空,但是如果为空的话得到的结果OpenSSL可能解析不了 + * @return + */ + public static byte[] convertECPrivateKeyToPKCS8( + ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) { + ECDomainParameters domainParams = priKey.getParameters(); + ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(), + domainParams.getN(), domainParams.getH()); + BCECPublicKey publicKey = null; + if (pubKey != null) { + publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec, + BouncyCastleProvider.CONFIGURATION); + } + BCECPrivateKey privateKey = new BCECPrivateKey(ALGO_NAME_EC, priKey, publicKey, + spec, BouncyCastleProvider.CONFIGURATION); + return privateKey.getEncoded(); + } + + /** + * 将PKCS8标准的私钥字节流转换为私钥对象 + * + * @param pkcs8Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + */ + public static BCECPrivateKey convertPKCS8ToECPrivateKey(byte[] pkcs8Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException { + PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(pkcs8Key); + KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + return (BCECPrivateKey) kf.generatePrivate(peks); + } + + /** + * 将PKCS8标准的私钥字节流转换为PEM + * + * @param encodedKey + * @return + * @throws IOException + */ + public static String convertECPrivateKeyPKCS8ToPEM(byte[] encodedKey) throws IOException { + return convertEncodedDataToPEM(PEM_STRING_ECPRIVATEKEY, encodedKey); + } + + /** + * 将PEM格式的私钥转换为PKCS8标准字节流 + * + * @param pemString + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeyPEMToPKCS8(String pemString) throws IOException { + return convertPEMToEncodedData(pemString); + } + + /** + * 将ECC私钥转换为SEC1标准的字节流 + * openssl d2i_ECPrivateKey函数要求的DER编码的私钥也是SEC1标准的, + * 这个工具函数的主要目的就是为了能生成一个openssl可以直接“识别”的ECC私钥. + * 相对RSA私钥的PKCS1标准,ECC私钥的标准为SEC1 + * + * @param priKey + * @param pubKey + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeyToSEC1( + ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) throws IOException { + byte[] pkcs8Bytes = convertECPrivateKeyToPKCS8(priKey, pubKey); + PrivateKeyInfo pki = PrivateKeyInfo.getInstance(pkcs8Bytes); + ASN1Encodable encodable = pki.parsePrivateKey(); + ASN1Primitive primitive = encodable.toASN1Primitive(); + byte[] sec1Bytes = primitive.getEncoded(); + return sec1Bytes; + } + + /** + * 将SEC1标准的私钥字节流恢复为PKCS8标准的字节流 + * + * @param sec1Key + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeySEC1ToPKCS8(byte[] sec1Key) throws IOException { + /** + * 参考org.bouncycastle.asn1.pkcs.PrivateKeyInfo和 + * org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey,逆向拼装 + */ + X962Parameters params = getDomainParametersFromName(BCSM2Utils.JDK_EC_SPEC, false); + ASN1OctetString privKey = new DEROctetString(sec1Key); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(0)); //版本号 + v.add(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params)); //算法标识 + v.add(privKey); + DERSequence ds = new DERSequence(v); + return ds.getEncoded(ASN1Encoding.DER); + } + + /** + * 将SEC1标准的私钥字节流转为BCECPrivateKey对象 + * + * @param sec1Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + * @throws IOException + */ + public static BCECPrivateKey convertSEC1ToBCECPrivateKey(byte[] sec1Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException { + PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(convertECPrivateKeySEC1ToPKCS8(sec1Key)); + KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + return (BCECPrivateKey) kf.generatePrivate(peks); + } + + /** + * 将SEC1标准的私钥字节流转为ECPrivateKeyParameters对象 + * openssl i2d_ECPrivateKey函数生成的DER编码的ecc私钥是:SEC1标准的、带有EC_GROUP、带有公钥的, + * 这个工具函数的主要目的就是为了使Java程序能够“识别”openssl生成的ECC私钥 + * + * @param sec1Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + */ + public static ECPrivateKeyParameters convertSEC1ToECPrivateKey(byte[] sec1Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException { + BCECPrivateKey privateKey = convertSEC1ToBCECPrivateKey(sec1Key); + return convertPrivateKeyToParameters(privateKey); + } + + /** + * 将ECC公钥对象转换为X509标准的字节流 + * + * @param pubKey + * @return + */ + public static byte[] convertECPublicKeyToX509(ECPublicKeyParameters pubKey) { + ECDomainParameters domainParams = pubKey.getParameters(); + ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(), + domainParams.getN(), domainParams.getH()); + BCECPublicKey publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec, + BouncyCastleProvider.CONFIGURATION); + return publicKey.getEncoded(); + } + + /** + * 将X509标准的公钥字节流转为公钥对象 + * + * @param x509Bytes + * @return + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static BCECPublicKey convertX509ToECPublicKey(byte[] x509Bytes) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec eks = new X509EncodedKeySpec(x509Bytes); + KeyFactory kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME); + return (BCECPublicKey) kf.generatePublic(eks); + } + + /** + * 将X509标准的公钥字节流转为PEM + * + * @param encodedKey + * @return + * @throws IOException + */ + public static String convertECPublicKeyX509ToPEM(byte[] encodedKey) throws IOException { + return convertEncodedDataToPEM(PEM_STRING_PUBLIC, encodedKey); + } + + /** + * 将PEM格式的公钥转为X509标准的字节流 + * + * @param pemString + * @return + * @throws IOException + */ + public static byte[] convertECPublicKeyPEMToX509(String pemString) throws IOException { + return convertPEMToEncodedData(pemString); + } + + /** + * copy from BC + * + * @param genSpec + * @return + */ + public static X9ECParameters getDomainParametersFromGenSpec(ECGenParameterSpec genSpec) { + return getDomainParametersFromName(genSpec.getName()); + } + + /** + * copy from BC + * + * @param curveName + * @return + */ + public static X9ECParameters getDomainParametersFromName(String curveName) { + X9ECParameters domainParameters; + try { + if (curveName.charAt(0) >= '0' && curveName.charAt(0) <= '2') { + ASN1ObjectIdentifier oidID = new ASN1ObjectIdentifier(curveName); + domainParameters = ECUtil.getNamedCurveByOid(oidID); + } else { + if (curveName.indexOf(' ') > 0) { + curveName = curveName.substring(curveName.indexOf(' ') + 1); + domainParameters = ECUtil.getNamedCurveByName(curveName); + } else { + domainParameters = ECUtil.getNamedCurveByName(curveName); + } + } + } catch (IllegalArgumentException ex) { + domainParameters = ECUtil.getNamedCurveByName(curveName); + } + return domainParameters; + } + + /** + * copy from BC + * + * @param ecSpec + * @param withCompression + * @return + */ + public static X962Parameters getDomainParametersFromName( + java.security.spec.ECParameterSpec ecSpec, boolean withCompression) { + X962Parameters params; + + if (ecSpec instanceof ECNamedCurveSpec) { + ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec) ecSpec).getName()); + if (curveOid == null) { + curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec) ecSpec).getName()); + } + params = new X962Parameters(curveOid); + } else if (ecSpec == null) { + params = new X962Parameters(DERNull.INSTANCE); + } else { + ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve()); + + X9ECParameters ecP = new X9ECParameters( + curve, + new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), withCompression), + ecSpec.getOrder(), + BigInteger.valueOf(ecSpec.getCofactor()), + ecSpec.getCurve().getSeed()); + + //// 如果是1.62或更低版本的bcprov-jdk15on应该使用以下这段代码,因为高版本的EC5Util.convertPoint没有向下兼容 + /* + X9ECParameters ecP = new X9ECParameters( + curve, + EC5Util.convertPoint(curve, ecSpec.getGenerator(), withCompression), + ecSpec.getOrder(), + BigInteger.valueOf(ecSpec.getCofactor()), + ecSpec.getCurve().getSeed()); + */ + + params = new X962Parameters(ecP); + } + + return params; + } + + private static String convertEncodedDataToPEM(String type, byte[] encodedData) throws IOException { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + PemWriter pWrt = new PemWriter(new OutputStreamWriter(bOut)); + try { + PemObject pemObj = new PemObject(type, encodedData); + pWrt.writeObject(pemObj); + } finally { + pWrt.close(); + } + return new String(bOut.toByteArray()); + } + + private static byte[] convertPEMToEncodedData(String pemString) throws IOException { + ByteArrayInputStream bIn = new ByteArrayInputStream(pemString.getBytes()); + PemReader pRdr = new PemReader(new InputStreamReader(bIn)); + try { + PemObject pemObject = pRdr.readPemObject(); + return pemObject.getContent(); + } finally { + pRdr.close(); + } + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM2Utils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM2Utils.java new file mode 100644 index 0000000..48aa84b --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM2Utils.java @@ -0,0 +1,606 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.engines.SM2Engine.Mode; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.ECFieldFp; +import java.security.spec.EllipticCurve; + +public class BCSM2Utils extends GMBaseUtil { + ////////////////////////////////////////////////////////////////////////////////////// + /* + * 以下为SM2推荐曲线参数 + */ + public static final SM2P256V1Curve CURVE = new SM2P256V1Curve(); + public final static BigInteger SM2_ECC_P = CURVE.getQ(); + public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger(); + public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger(); + public final static BigInteger SM2_ECC_N = CURVE.getOrder(); + public final static BigInteger SM2_ECC_H = CURVE.getCofactor(); + public final static BigInteger SM2_ECC_GX = new BigInteger( + "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16); + public final static BigInteger SM2_ECC_GY = new BigInteger( + "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16); + public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY); + public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT, + SM2_ECC_N, SM2_ECC_H); + public static final int CURVE_LEN = BCECUtils.getCurveLength(DOMAIN_PARAMS); + ////////////////////////////////////////////////////////////////////////////////////// + + public static final EllipticCurve JDK_CURVE = new EllipticCurve(new ECFieldFp(SM2_ECC_P), SM2_ECC_A, SM2_ECC_B); + public static final java.security.spec.ECPoint JDK_G_POINT = new java.security.spec.ECPoint( + G_POINT.getAffineXCoord().toBigInteger(), G_POINT.getAffineYCoord().toBigInteger()); + public static final java.security.spec.ECParameterSpec JDK_EC_SPEC = new java.security.spec.ECParameterSpec( + JDK_CURVE, JDK_G_POINT, SM2_ECC_N, SM2_ECC_H.intValue()); + + ////////////////////////////////////////////////////////////////////////////////////// + + public static final int SM3_DIGEST_LENGTH = 32; + + /** + * 生成ECC密钥对 + * + * @return ECC密钥对 + */ + public static AsymmetricCipherKeyPair generateKeyPairParameter() { + SecureRandom random = new SecureRandom(); + return BCECUtils.generateKeyPairParameter(DOMAIN_PARAMS, random); + } + + /** + * 生成ECC密钥对 + * + * @return + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws InvalidAlgorithmParameterException + */ + public static KeyPair generateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + SecureRandom random = new SecureRandom(); + return BCECUtils.generateKeyPair(DOMAIN_PARAMS, random); + } + + /** + * 只获取私钥里的d值,32字节 + * + * @param privateKey + * @return + */ + public static byte[] getRawPrivateKey(BCECPrivateKey privateKey) { + return fixToCurveLengthBytes(privateKey.getD().toByteArray()); + } + + /** + * 只获取公钥里的XY分量,64字节 + * + * @param publicKey + * @return 64字节数组 + */ + public static byte[] getRawPublicKey(BCECPublicKey publicKey) { + byte[] src65 = publicKey.getQ().getEncoded(false); + byte[] rawXY = new byte[CURVE_LEN * 2];//SM2的话这里应该是64字节 + System.arraycopy(src65, 1, rawXY, 0, rawXY.length); + return rawXY; + } + + /** + * @param pubKey 公钥 + * @param srcData 原文 + * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException { + ECPublicKeyParameters pubKeyParameters = BCECUtils.convertPublicKeyToParameters(pubKey); + return encrypt(Mode.C1C3C2, pubKeyParameters, srcData); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param pubKey 公钥 + * @param srcData 原文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(Mode mode, BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException { + ECPublicKeyParameters pubKeyParameters = BCECUtils.convertPublicKeyToParameters(pubKey); + return encrypt(mode, pubKeyParameters, srcData); + } + + /** + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(ECPublicKeyParameters pubKeyParameters, byte[] srcData) + throws InvalidCipherTextException { + return encrypt(Mode.C1C3C2, pubKeyParameters, srcData); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(Mode mode, ECPublicKeyParameters pubKeyParameters, byte[] srcData) + throws InvalidCipherTextException { + SM2Engine engine = new SM2Engine(mode); + ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParameters, new SecureRandom()); + engine.init(true, pwr); + return engine.processBlock(srcData, 0, srcData.length); + } + + /** + * @param priKey 私钥 + * @param sm2Cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException { + ECPrivateKeyParameters priKeyParameters = BCECUtils.convertPrivateKeyToParameters(priKey); + return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param priKey 私钥 + * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(Mode mode, BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException { + ECPrivateKeyParameters priKeyParameters = BCECUtils.convertPrivateKeyToParameters(priKey); + return decrypt(mode, priKeyParameters, sm2Cipher); + } + + /** + * @param priKeyParameters 私钥 + * @param sm2Cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher) + throws InvalidCipherTextException { + return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param priKeyParameters 私钥 + * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(Mode mode, ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher) + throws InvalidCipherTextException { + SM2Engine engine = new SM2Engine(mode); + engine.init(false, priKeyParameters); + return engine.processBlock(sm2Cipher, 0, sm2Cipher.length); + } + + /** + * 分解SM2密文 + * + * @param cipherText 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + * @throws Exception + */ + public static SM2Cipher parseSM2Cipher(byte[] cipherText) throws Exception { + int curveLength = BCECUtils.getCurveLength(DOMAIN_PARAMS); + return parseSM2Cipher(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipherText); + } + + /** + * 分解SM2密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param cipherText 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + */ + public static SM2Cipher parseSM2Cipher(Mode mode, byte[] cipherText) throws Exception { + int curveLength = BCECUtils.getCurveLength(DOMAIN_PARAMS); + return parseSM2Cipher(mode, curveLength, SM3_DIGEST_LENGTH, cipherText); + } + + /** + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipherText 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + * @throws Exception + */ + public static SM2Cipher parseSM2Cipher( + int curveLength, int digestLength, byte[] cipherText) throws Exception { + return parseSM2Cipher(Mode.C1C3C2, curveLength, digestLength, cipherText); + } + + /** + * 分解SM2密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipherText 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + */ + public static SM2Cipher parseSM2Cipher(Mode mode, int curveLength, int digestLength, + byte[] cipherText) throws Exception { + byte[] c1 = new byte[curveLength * 2 + 1]; + byte[] c2 = new byte[cipherText.length - c1.length - digestLength]; + byte[] c3 = new byte[digestLength]; + + System.arraycopy(cipherText, 0, c1, 0, c1.length); + if (mode == Mode.C1C2C3) { + System.arraycopy(cipherText, c1.length, c2, 0, c2.length); + System.arraycopy(cipherText, c1.length + c2.length, c3, 0, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(cipherText, c1.length, c3, 0, c3.length); + System.arraycopy(cipherText, c1.length + c3.length, c2, 0, c2.length); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + SM2Cipher result = new SM2Cipher(); + result.setC1(c1); + result.setC2(c2); + result.setC3(c3); + result.setCipherText(cipherText); + return result; + } + + /** + * DER编码密文 + * + * @param cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return DER编码后的密文 + * @throws IOException + */ + public static byte[] encodeSM2CipherToDER(byte[] cipher) throws Exception { + int curveLength = BCECUtils.getCurveLength(DOMAIN_PARAMS); + return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipher); + } + + /** + * DER编码密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 按指定mode DER编码后的密文 + * @throws Exception + */ + public static byte[] encodeSM2CipherToDER(Mode mode, byte[] cipher) throws Exception { + int curveLength = BCECUtils.getCurveLength(DOMAIN_PARAMS); + return encodeSM2CipherToDER(mode, curveLength, SM3_DIGEST_LENGTH, cipher); + } + + /** + * DER编码密文 + * + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 默认输出按C1C3C2编码的结果 + * @throws IOException + */ + public static byte[] encodeSM2CipherToDER(int curveLength, int digestLength, byte[] cipher) + throws Exception { + return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, digestLength, cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 按指定mode DER编码后的密文 + * @throws Exception + */ + public static byte[] encodeSM2CipherToDER(Mode mode, int curveLength, int digestLength, byte[] cipher) + throws Exception { + + byte[] c1x = new byte[curveLength]; + byte[] c1y = new byte[curveLength]; + byte[] c2 = new byte[cipher.length - c1x.length - c1y.length - 1 - digestLength]; + byte[] c3 = new byte[digestLength]; + + int startPos = 1; + System.arraycopy(cipher, startPos, c1x, 0, c1x.length); + startPos += c1x.length; + System.arraycopy(cipher, startPos, c1y, 0, c1y.length); + startPos += c1y.length; + if (mode == Mode.C1C2C3) { + System.arraycopy(cipher, startPos, c2, 0, c2.length); + startPos += c2.length; + System.arraycopy(cipher, startPos, c3, 0, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(cipher, startPos, c3, 0, c3.length); + startPos += c3.length; + System.arraycopy(cipher, startPos, c2, 0, c2.length); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + ASN1Encodable[] arr = new ASN1Encodable[4]; + // c1x,c1y的第一个bit可能为1,这个时候要确保他们表示的大数一定是正数,所以new BigInteger符号强制设为正。 + arr[0] = new ASN1Integer(new BigInteger(1, c1x)); + arr[1] = new ASN1Integer(new BigInteger(1, c1y)); + if (mode == Mode.C1C2C3) { + arr[2] = new DEROctetString(c2); + arr[3] = new DEROctetString(c3); + } else if (mode == Mode.C1C3C2) { + arr[2] = new DEROctetString(c3); + arr[3] = new DEROctetString(c2); + } + DERSequence ds = new DERSequence(arr); + return ds.getEncoded(ASN1Encoding.DER); + } + + /** + * 解码DER密文 + * + * @param derCipher 默认输入按C1C3C2顺序DER编码的密文 + * @return 输出按C1C3C2排列的字节数组,C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + */ + public static byte[] decodeDERSM2Cipher(byte[] derCipher) throws Exception { + return decodeDERSM2Cipher(Mode.C1C3C2, derCipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param derCipher 根据mode输入C1C2C3或C1C3C2顺序DER编码后的密文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws Exception + */ + public static byte[] decodeDERSM2Cipher(Mode mode, byte[] derCipher) throws Exception { + ASN1Sequence as = DERSequence.getInstance(derCipher); + byte[] c1x = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] c1y = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + // c1x,c1y可能因为大正数的补0规则在第一个有效字节前面插了一个(byte)0,变成33个字节,在这里要修正回32个字节去 + c1x = fixToCurveLengthBytes(c1x); + c1y = fixToCurveLengthBytes(c1y); + byte[] c3; + byte[] c2; + if (mode == Mode.C1C2C3) { + c2 = ((DEROctetString) as.getObjectAt(2)).getOctets(); + c3 = ((DEROctetString) as.getObjectAt(3)).getOctets(); + } else if (mode == Mode.C1C3C2) { + c3 = ((DEROctetString) as.getObjectAt(2)).getOctets(); + c2 = ((DEROctetString) as.getObjectAt(3)).getOctets(); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + int pos = 0; + byte[] cipherText = new byte[1 + c1x.length + c1y.length + c2.length + c3.length]; + final byte uncompressedFlag = 0x04; + cipherText[0] = uncompressedFlag; + pos += 1; + System.arraycopy(c1x, 0, cipherText, pos, c1x.length); + pos += c1x.length; + System.arraycopy(c1y, 0, cipherText, pos, c1y.length); + pos += c1y.length; + if (mode == Mode.C1C2C3) { + System.arraycopy(c2, 0, cipherText, pos, c2.length); + pos += c2.length; + System.arraycopy(c3, 0, cipherText, pos, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(c3, 0, cipherText, pos, c3.length); + pos += c3.length; + System.arraycopy(c2, 0, cipherText, pos, c2.length); + } + return cipherText; + } + + /** + * 签名 + * + * @param priKey 私钥 + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(BCECPrivateKey priKey, byte[] srcData) throws CryptoException { + ECPrivateKeyParameters priKeyParameters = BCECUtils.convertPrivateKeyToParameters(priKey); + return sign(priKeyParameters, null, srcData); + } + + /** + * 签名 + * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes() + * + * @param priKeyParameters 私钥 + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] srcData) throws CryptoException { + return sign(priKeyParameters, null, srcData); + } + + /** + * 私钥签名 + * + * @param priKey 私钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(BCECPrivateKey priKey, byte[] withId, byte[] srcData) throws CryptoException { + ECPrivateKeyParameters priKeyParameters = BCECUtils.convertPrivateKeyToParameters(priKey); + return sign(priKeyParameters, withId, srcData); + } + + /** + * 签名 + * + * @param priKeyParameters 私钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 源数据 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] withId, byte[] srcData) + throws CryptoException { + SM2Signer signer = new SM2Signer(); + CipherParameters param = null; + ParametersWithRandom pwr = new ParametersWithRandom(priKeyParameters, new SecureRandom()); + if (withId != null) { + param = new ParametersWithID(pwr, withId); + } else { + param = pwr; + } + signer.init(true, param); + signer.update(srcData, 0, srcData.length); + return signer.generateSignature(); + } + + /** + * 将DER编码的SM2签名解码成64字节的纯R+S字节流 + * + * @param derSign + * @return 64字节数组,前32字节为R,后32字节为S + */ + public static byte[] decodeDERSM2Sign(byte[] derSign) { + ASN1Sequence as = DERSequence.getInstance(derSign); + byte[] rBytes = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] sBytes = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + //由于大数的补0规则,所以可能会出现33个字节的情况,要修正回32个字节 + rBytes = fixToCurveLengthBytes(rBytes); + sBytes = fixToCurveLengthBytes(sBytes); + byte[] rawSign = new byte[rBytes.length + sBytes.length]; + System.arraycopy(rBytes, 0, rawSign, 0, rBytes.length); + System.arraycopy(sBytes, 0, rawSign, rBytes.length, sBytes.length); + return rawSign; + } + + /** + * 把64字节的纯R+S字节数组编码成DER编码 + * + * @param rawSign 64字节数组形式的SM2签名值,前32字节为R,后32字节为S + * @return DER编码后的SM2签名值 + * @throws IOException + */ + public static byte[] encodeSM2SignToDER(byte[] rawSign) throws IOException { + //要保证大数是正数 + BigInteger r = new BigInteger(1, extractBytes(rawSign, 0, 32)); + BigInteger s = new BigInteger(1, extractBytes(rawSign, 32, 32)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + return new DERSequence(v).getEncoded(ASN1Encoding.DER); + } + + /** + * 验签 + * + * @param pubKey 公钥 + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return + */ + public static boolean verify(BCECPublicKey pubKey, byte[] srcData, byte[] sign) { + ECPublicKeyParameters pubKeyParameters = BCECUtils.convertPublicKeyToParameters(pubKey); + return verify(pubKeyParameters, null, srcData, sign); + } + + /** + * 验签 + * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes() + * + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return 验签成功返回true,失败返回false + */ + public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] srcData, byte[] sign) { + return verify(pubKeyParameters, null, srcData, sign); + } + + /** + * 验签 + * + * @param pubKey 公钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return + */ + public static boolean verify(BCECPublicKey pubKey, byte[] withId, byte[] srcData, byte[] sign) { + ECPublicKeyParameters pubKeyParameters = BCECUtils.convertPublicKeyToParameters(pubKey); + return verify(pubKeyParameters, withId, srcData, sign); + } + + /** + * 验签 + * + * @param pubKeyParameters 公钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return 验签成功返回true,失败返回false + */ + public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] withId, byte[] srcData, byte[] sign) { + SM2Signer signer = new SM2Signer(); + CipherParameters param; + if (withId != null) { + param = new ParametersWithID(pubKeyParameters, withId); + } else { + param = pubKeyParameters; + } + signer.init(false, param); + signer.update(srcData, 0, srcData.length); + return signer.verifySignature(sign); + } + + private static byte[] extractBytes(byte[] src, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(src, offset, result, 0, result.length); + return result; + } + + private static byte[] fixToCurveLengthBytes(byte[] src) { + if (src.length == CURVE_LEN) { + return src; + } + + byte[] result = new byte[CURVE_LEN]; + if (src.length > CURVE_LEN) { + System.arraycopy(src, src.length - result.length, result, 0, result.length); + } else { + System.arraycopy(src, 0, result, result.length - src.length, src.length); + } + return result; + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM3Utils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM3Utils.java new file mode 100644 index 0000000..9f03640 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM3Utils.java @@ -0,0 +1,58 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Arrays; + +public class BCSM3Utils extends GMBaseUtil { + + /** + * 计算SM3摘要值 + * + * @param srcData 原文 + * @return 摘要值,对于SM3算法来说是32字节 + */ + public static byte[] hash(byte[] srcData) { + SM3Digest digest = new SM3Digest(); + digest.update(srcData, 0, srcData.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + return hash; + } + + /** + * 验证摘要 + * + * @param srcData 原文 + * @param sm3Hash 摘要值 + * @return 返回true标识验证成功,false标识验证失败 + */ + public static boolean verify(byte[] srcData, byte[] sm3Hash) { + byte[] newHash = hash(srcData); + if (Arrays.equals(newHash, sm3Hash)) { + return true; + } else { + return false; + } + } + + /** + * 计算SM3 Mac值 + * + * @param key key值,可以是任意长度的字节数组 + * @param srcData 原文 + * @return Mac值,对于HMac-SM3来说是32字节 + */ + public static byte[] hmac(byte[] key, byte[] srcData) { + KeyParameter keyParameter = new KeyParameter(key); + SM3Digest digest = new SM3Digest(); + HMac mac = new HMac(digest); + mac.init(keyParameter); + mac.update(srcData, 0, srcData.length); + byte[] result = new byte[mac.getMacSize()]; + mac.doFinal(result, 0); + return result; + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM4Utils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM4Utils.java new file mode 100644 index 0000000..d8663e2 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/BCSM4Utils.java @@ -0,0 +1,191 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.engines.SM4Engine; +import org.bouncycastle.crypto.macs.CBCBlockCipherMac; +import org.bouncycastle.crypto.macs.GMac; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.paddings.BlockCipherPadding; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; + +public class BCSM4Utils extends GMBaseUtil { + public static final String ALGORITHM_NAME = "SM4"; + public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding"; + public static final String ALGORITHM_NAME_ECB_NOPADDING = "SM4/ECB/NoPadding"; + public static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding"; + public static final String ALGORITHM_NAME_CBC_NOPADDING = "SM4/CBC/NoPadding"; + + /** + * SM4算法目前只支持128位(即密钥16字节) + */ + public static final int DEFAULT_KEY_SIZE = 128; + + public static byte[] generateKey() throws NoSuchAlgorithmException, NoSuchProviderException { + return generateKey(DEFAULT_KEY_SIZE); + } + + public static byte[] generateKey(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException { + KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME); + kg.init(keySize, new SecureRandom()); + return kg.generateKey().getEncoded(); + } + + public static byte[] encrypt_ECB_Padding(byte[] key, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data); + } + + public static byte[] decrypt_ECB_Padding(byte[] key, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_ECB_NoPadding(byte[] key, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data); + } + + public static byte[] decrypt_ECB_NoPadding(byte[] key, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.DECRYPT_MODE, key); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_CBC_Padding(byte[] key, byte[] iv, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, key, iv); + return cipher.doFinal(data); + } + + public static byte[] decrypt_CBC_Padding(byte[] key, byte[] iv, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_CBC_NoPadding(byte[] key, byte[] iv, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.ENCRYPT_MODE, key, iv); + return cipher.doFinal(data); + } + + public static byte[] decrypt_CBC_NoPadding(byte[] key, byte[] iv, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(cipherText); + } + + public static byte[] doCMac(byte[] key, byte[] data) throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidKeyException { + Key keyObj = new SecretKeySpec(key, ALGORITHM_NAME); + return doMac("SM4-CMAC", keyObj, data); + } + + public static byte[] doGMac(byte[] key, byte[] iv, int tagLength, byte[] data) { + org.bouncycastle.crypto.Mac mac = new GMac(new GCMBlockCipher(new SM4Engine()), tagLength * 8); + return doMac(mac, key, iv, data); + } + + /** + * 默认使用PKCS7Padding/PKCS5Padding填充的CBCMAC + * + * @param key + * @param iv + * @param data + * @return + */ + public static byte[] doCBCMac(byte[] key, byte[] iv, byte[] data) { + SM4Engine engine = new SM4Engine(); + org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, new PKCS7Padding()); + return doMac(mac, key, iv, data); + } + + /** + * @param key + * @param iv + * @param padding 可以传null,传null表示NoPadding,由调用方保证数据必须是BlockSize的整数倍 + * @param data + * @return + * @throws Exception + */ + public static byte[] doCBCMac(byte[] key, byte[] iv, BlockCipherPadding padding, byte[] data) throws Exception { + SM4Engine engine = new SM4Engine(); + if (padding == null) { + if (data.length % engine.getBlockSize() != 0) { + throw new Exception("if no padding, data length must be multiple of SM4 BlockSize"); + } + } + org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, padding); + return doMac(mac, key, iv, data); + } + + + private static byte[] doMac(org.bouncycastle.crypto.Mac mac, byte[] key, byte[] iv, byte[] data) { + CipherParameters cipherParameters = new KeyParameter(key); + mac.init(new ParametersWithIV(cipherParameters, iv)); + mac.update(data, 0, data.length); + byte[] result = new byte[mac.getMacSize()]; + mac.doFinal(result, 0); + return result; + } + + private static byte[] doMac(String algorithmName, Key key, byte[] data) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); + mac.init(key); + mac.update(data); + return mac.doFinal(); + } + + private static Cipher generateECBCipher(String algorithmName, int mode, byte[] key) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidKeyException { + Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); + Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME); + cipher.init(mode, sm4Key); + return cipher; + } + + private static Cipher generateCBCCipher(String algorithmName, int mode, byte[] key, byte[] iv) + throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, + NoSuchProviderException, NoSuchPaddingException { + Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); + Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + cipher.init(mode, sm4Key, ivParameterSpec); + return cipher; + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/GMBaseUtil.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/GMBaseUtil.java new file mode 100644 index 0000000..a44a47e --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/GMBaseUtil.java @@ -0,0 +1,11 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; + +public class GMBaseUtil { + static { + Security.addProvider(new BouncyCastleProvider()); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2Cipher.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2Cipher.java new file mode 100644 index 0000000..ef6b194 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2Cipher.java @@ -0,0 +1,55 @@ +package com.sunyard.chsm.utils.gm; + +public class SM2Cipher { + /** + * ECC密钥 + */ + private byte[] c1; + + /** + * 真正的密文 + */ + private byte[] c2; + + /** + * 对(c1+c2)的SM3-HASH值 + */ + private byte[] c3; + + /** + * SM2标准的密文,即(c1+c2+c3) + */ + private byte[] cipherText; + + public byte[] getC1() { + return c1; + } + + public void setC1(byte[] c1) { + this.c1 = c1; + } + + public byte[] getC2() { + return c2; + } + + public void setC2(byte[] c2) { + this.c2 = c2; + } + + public byte[] getC3() { + return c3; + } + + public void setC3(byte[] c3) { + this.c3 = c3; + } + + public byte[] getCipherText() { + return cipherText; + } + + public void setCipherText(byte[] cipherText) { + this.cipherText = cipherText; + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2PreprocessSigner.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2PreprocessSigner.java new file mode 100644 index 0000000..e479f63 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/SM2PreprocessSigner.java @@ -0,0 +1,294 @@ +package com.sunyard.chsm.utils.gm; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.DSAKCalculator; +import org.bouncycastle.crypto.signers.RandomDSAKCalculator; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECConstants; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECMultiplier; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * 有的国密需求是用户可以自己做预处理,签名验签只是对预处理的结果进行签名和验签 + */ +public class SM2PreprocessSigner implements ECConstants { + private static final int DIGEST_LENGTH = 32; // bytes + + private final DSAKCalculator kCalculator = new RandomDSAKCalculator(); + private Digest digest = null; + + private ECDomainParameters ecParams; + private ECPoint pubPoint; + private ECKeyParameters ecKey; + private byte[] userID; + + /** + * 初始化 + * + * @param forSigning true表示用于签名,false表示用于验签 + * @param param + */ + public void init(boolean forSigning, CipherParameters param) { + init(forSigning, new SM3Digest(), param); + } + + /** + * 初始化 + * + * @param forSigning true表示用于签名,false表示用于验签 + * @param digest SM2算法的话,一般是采用SM3摘要算法 + * @param param + * @throws RuntimeException + */ + public void init(boolean forSigning, Digest digest, CipherParameters param) throws RuntimeException { + CipherParameters baseParam; + + if (digest.getDigestSize() != DIGEST_LENGTH) { + throw new RuntimeException("Digest size must be " + DIGEST_LENGTH); + } + this.digest = digest; + + if (param instanceof ParametersWithID) { + baseParam = ((ParametersWithID) param).getParameters(); + userID = ((ParametersWithID) param).getID(); + } else { + baseParam = param; + userID = Hex.decode("31323334353637383132333435363738"); // the default value + } + + if (forSigning) { + if (baseParam instanceof ParametersWithRandom) { + ParametersWithRandom rParam = (ParametersWithRandom) baseParam; + + ecKey = (ECKeyParameters) rParam.getParameters(); + ecParams = ecKey.getParameters(); + kCalculator.init(ecParams.getN(), rParam.getRandom()); + } else { + ecKey = (ECKeyParameters) baseParam; + ecParams = ecKey.getParameters(); + kCalculator.init(ecParams.getN(), CryptoServicesRegistrar.getSecureRandom()); + } + pubPoint = createBasePointMultiplier().multiply(ecParams.getG(), ((ECPrivateKeyParameters) ecKey).getD()).normalize(); + } else { + ecKey = (ECKeyParameters) baseParam; + ecParams = ecKey.getParameters(); + pubPoint = ((ECPublicKeyParameters) ecKey).getQ(); + } + } + + /** + * 预处理,辅助方法 + * ZA=H256(ENT LA ∥ IDA ∥ a ∥ b ∥ xG ∥yG ∥ xA ∥ yA)。 + * M=ZA ∥ M; + * e = Hv(M) + * + * @return + */ + public byte[] preprocess(byte[] m) { + return preprocess(m, 0, m.length); + } + + + public byte[] preprocess(byte[] m, int off, int len) { + byte[] z = getZ(userID); + digest.update(z, 0, z.length); + digest.update(m, off, len); + byte[] eHash = new byte[DIGEST_LENGTH]; + digest.doFinal(eHash, 0); + return eHash; + } + + public boolean verifySignature(byte[] eHash, byte[] signature) { + try { + BigInteger[] rs = derDecode(signature); + if (rs != null) { + return verifySignature(eHash, rs[0], rs[1]); + } + } catch (IOException e) { + } + + return false; + } + + public void reset() { + digest.reset(); + } + + public byte[] generateSignature(byte[] eHash) throws CryptoException { + BigInteger n = ecParams.getN(); + BigInteger e = calculateE(eHash); + BigInteger d = ((ECPrivateKeyParameters) ecKey).getD(); + + BigInteger r, s; + + ECMultiplier basePointMultiplier = createBasePointMultiplier(); + + // 5.2.1 Draft RFC: SM2 Public Key Algorithms + do // generate s + { + BigInteger k; + do // generate r + { + // A3 + k = kCalculator.nextK(); + + // A4 + ECPoint p = basePointMultiplier.multiply(ecParams.getG(), k).normalize(); + + // A5 + r = e.add(p.getAffineXCoord().toBigInteger()).mod(n); + } + while (r.equals(ZERO) || r.add(k).equals(n)); + + // A6 + BigInteger dPlus1ModN = d.add(ONE).modInverse(n); + + s = k.subtract(r.multiply(d)).mod(n); + s = dPlus1ModN.multiply(s).mod(n); + } + while (s.equals(ZERO)); + + // A7 + try { + return derEncode(r, s); + } catch (IOException ex) { + throw new CryptoException("unable to encode signature: " + ex.getMessage(), ex); + } + } + + private boolean verifySignature(byte[] eHash, BigInteger r, BigInteger s) { + BigInteger n = ecParams.getN(); + + // 5.3.1 Draft RFC: SM2 Public Key Algorithms + // B1 + if (r.compareTo(ONE) < 0 || r.compareTo(n) >= 0) { + return false; + } + + // B2 + if (s.compareTo(ONE) < 0 || s.compareTo(n) >= 0) { + return false; + } + + // B3 eHash + + // B4 + BigInteger e = calculateE(eHash); + + // B5 + BigInteger t = r.add(s).mod(n); + if (t.equals(ZERO)) { + return false; + } + + // B6 + ECPoint q = ((ECPublicKeyParameters) ecKey).getQ(); + ECPoint x1y1 = ECAlgorithms.sumOfTwoMultiplies(ecParams.getG(), s, q, t).normalize(); + if (x1y1.isInfinity()) { + return false; + } + + // B7 + BigInteger expectedR = e.add(x1y1.getAffineXCoord().toBigInteger()).mod(n); + + return expectedR.equals(r); + } + + private byte[] digestDoFinal() { + byte[] result = new byte[digest.getDigestSize()]; + digest.doFinal(result, 0); + + reset(); + + return result; + } + + private byte[] getZ(byte[] userID) { + digest.reset(); + + addUserID(digest, userID); + + addFieldElement(digest, ecParams.getCurve().getA()); + addFieldElement(digest, ecParams.getCurve().getB()); + addFieldElement(digest, ecParams.getG().getAffineXCoord()); + addFieldElement(digest, ecParams.getG().getAffineYCoord()); + addFieldElement(digest, pubPoint.getAffineXCoord()); + addFieldElement(digest, pubPoint.getAffineYCoord()); + + byte[] result = new byte[digest.getDigestSize()]; + + digest.doFinal(result, 0); + + return result; + } + + private void addUserID(Digest digest, byte[] userID) { + int len = userID.length * 8; + digest.update((byte) (len >> 8 & 0xFF)); + digest.update((byte) (len & 0xFF)); + digest.update(userID, 0, userID.length); + } + + private void addFieldElement(Digest digest, ECFieldElement v) { + byte[] p = v.getEncoded(); + digest.update(p, 0, p.length); + } + + protected ECMultiplier createBasePointMultiplier() { + return new FixedPointCombMultiplier(); + } + + protected BigInteger calculateE(byte[] message) { + return new BigInteger(1, message); + } + + protected BigInteger[] derDecode(byte[] encoding) + throws IOException { + ASN1Sequence seq = ASN1Sequence.getInstance(ASN1Primitive.fromByteArray(encoding)); + if (seq.size() != 2) { + return null; + } + + BigInteger r = ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); + BigInteger s = ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); + + byte[] expectedEncoding = derEncode(r, s); + if (!Arrays.constantTimeAreEqual(expectedEncoding, encoding)) { + return null; + } + + return new BigInteger[]{r, s}; + } + + protected byte[] derEncode(BigInteger r, BigInteger s) + throws IOException { + + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + return new DERSequence(v).getEncoded(ASN1Encoding.DER); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/BCSM2CertUtils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/BCSM2CertUtils.java new file mode 100644 index 0000000..b9c6ce6 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/BCSM2CertUtils.java @@ -0,0 +1,160 @@ +package com.sunyard.chsm.utils.gm.cert; + + +import com.sunyard.chsm.utils.gm.BCECUtils; +import com.sunyard.chsm.utils.gm.BCSM2Utils; +import com.sunyard.chsm.utils.gm.GMBaseUtil; +import org.bouncycastle.asn1.pkcs.ContentInfo; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.operator.InputDecryptorProvider; +import org.bouncycastle.pkcs.PKCS12PfxPdu; +import org.bouncycastle.pkcs.PKCS12SafeBag; +import org.bouncycastle.pkcs.PKCS12SafeBagFactory; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.NoSuchProviderException; +import java.security.cert.CertPath; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; + +public class BCSM2CertUtils extends GMBaseUtil { + public static BCECPublicKey getBCECPublicKey(X509Certificate sm2Cert) { + ECPublicKey pubKey = (ECPublicKey) sm2Cert.getPublicKey(); + ECPoint q = pubKey.getQ(); + ECParameterSpec parameterSpec = new ECParameterSpec(BCSM2Utils.CURVE, BCSM2Utils.G_POINT, + BCSM2Utils.SM2_ECC_N, BCSM2Utils.SM2_ECC_H); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(q, parameterSpec); + return new BCECPublicKey(pubKey.getAlgorithm(), pubKeySpec, + BouncyCastleProvider.CONFIGURATION); + } + + /** + * 校验证书 + * + * @param issuerPubKey 从颁发者CA证书中提取出来的公钥 + * @param cert 待校验的证书 + * @return + */ + public static boolean verifyCertificate(BCECPublicKey issuerPubKey, X509Certificate cert) { + try { + cert.verify(issuerPubKey, BouncyCastleProvider.PROVIDER_NAME); + } catch (Exception ex) { + return false; + } + return true; + } + + public static X509Certificate getX509Certificate(String certFilePath) throws IOException, CertificateException, + NoSuchProviderException { + try (InputStream is = Files.newInputStream(Paths.get(certFilePath))) { + return getX509Certificate(is); + } + } + + public static X509Certificate getX509Certificate(byte[] certBytes) throws CertificateException, + NoSuchProviderException { + ByteArrayInputStream bais = new ByteArrayInputStream(certBytes); + return getX509Certificate(bais); + } + + public static X509Certificate getX509Certificate(InputStream is) throws CertificateException, + NoSuchProviderException { + CertificateFactory cf = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME); + return (X509Certificate) cf.generateCertificate(is); + } + + public static CertPath getCertificateChain(String certChainPath) throws IOException, CertificateException, + NoSuchProviderException { + try (InputStream is = Files.newInputStream(Paths.get(certChainPath))) { + return getCertificateChain(is); + } + } + + public static CertPath getCertificateChain(byte[] certChainBytes) throws CertificateException, + NoSuchProviderException { + ByteArrayInputStream bais = new ByteArrayInputStream(certChainBytes); + return getCertificateChain(bais); + } + + public static byte[] getCertificateChainBytes(CertPath certChain) throws CertificateEncodingException { + return certChain.getEncoded("PKCS7"); + } + + public static CertPath getCertificateChain(InputStream is) throws CertificateException, NoSuchProviderException { + CertificateFactory cf = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME); + return cf.generateCertPath(is, "PKCS7"); + } + + public static CertPath getCertificateChain(List certs) throws CertificateException, + NoSuchProviderException { + CertificateFactory cf = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME); + return cf.generateCertPath(certs); + } + + public static X509Certificate getX509CertificateFromPfx(byte[] pfxDER, String passwd) throws Exception { + InputDecryptorProvider inputDecryptorProvider = new JcePKCSPBEInputDecryptorProviderBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passwd.toCharArray()); + PKCS12PfxPdu pfx = new PKCS12PfxPdu(pfxDER); + + ContentInfo[] infos = pfx.getContentInfos(); + if (infos.length != 2) { + throw new Exception("Only support one pair ContentInfo"); + } + + for (int i = 0; i != infos.length; i++) { + if (infos[i].getContentType().equals(PKCSObjectIdentifiers.encryptedData)) { + PKCS12SafeBagFactory dataFact = new PKCS12SafeBagFactory(infos[i], inputDecryptorProvider); + PKCS12SafeBag[] bags = dataFact.getSafeBags(); + X509CertificateHolder certHoler = (X509CertificateHolder) bags[0].getBagValue(); + return BCSM2CertUtils.getX509Certificate(certHoler.getEncoded()); + } + } + + throw new Exception("Not found X509Certificate in this pfx"); + } + + public static BCECPublicKey getPublicKeyFromPfx(byte[] pfxDER, String passwd) throws Exception { + return BCSM2CertUtils.getBCECPublicKey(getX509CertificateFromPfx(pfxDER, passwd)); + } + + public static BCECPrivateKey getPrivateKeyFromPfx(byte[] pfxDER, String passwd) throws Exception { + InputDecryptorProvider inputDecryptorProvider = new JcePKCSPBEInputDecryptorProviderBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passwd.toCharArray()); + PKCS12PfxPdu pfx = new PKCS12PfxPdu(pfxDER); + + ContentInfo[] infos = pfx.getContentInfos(); + if (infos.length != 2) { + throw new Exception("Only support one pair ContentInfo"); + } + + for (int i = 0; i != infos.length; i++) { + if (!infos[i].getContentType().equals(PKCSObjectIdentifiers.encryptedData)) { + PKCS12SafeBagFactory dataFact = new PKCS12SafeBagFactory(infos[i]); + PKCS12SafeBag[] bags = dataFact.getSafeBags(); + PKCS8EncryptedPrivateKeyInfo encInfo = (PKCS8EncryptedPrivateKeyInfo) bags[0].getBagValue(); + PrivateKeyInfo info = encInfo.decryptPrivateKeyInfo(inputDecryptorProvider); + return BCECUtils.convertPKCS8ToECPrivateKey(info.getEncoded()); + } + } + + throw new Exception("Not found Private Key in this pfx"); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CertSNAllocator.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CertSNAllocator.java new file mode 100644 index 0000000..76f93e3 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CertSNAllocator.java @@ -0,0 +1,7 @@ +package com.sunyard.chsm.utils.gm.cert; + +import java.math.BigInteger; + +public interface CertSNAllocator { + BigInteger nextSerialNumber() throws Exception; +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CommonCertUtils.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CommonCertUtils.java new file mode 100644 index 0000000..91e7ac1 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/CommonCertUtils.java @@ -0,0 +1,69 @@ +package com.sunyard.chsm.utils.gm.cert; + +import com.sunyard.chsm.utils.gm.cert.exception.InvalidX500NameException; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +import java.security.PrivateKey; +import java.util.Iterator; +import java.util.Map; + +public class CommonCertUtils { + /** + * 如果不知道怎么填充names,可以查看org.bouncycastle.asn1.x500.style.BCStyle这个类, + * names的key值必须是BCStyle.DefaultLookUp中存在的(可以不关心大小写) + * + * @param names + * @return + * @throws InvalidX500NameException + */ + public static X500Name buildX500Name(Map names) throws InvalidX500NameException { + if (names == null || names.size() == 0) { + throw new InvalidX500NameException("names can not be empty"); + } + try { + X500NameBuilder builder = new X500NameBuilder(); + Iterator itr = names.entrySet().iterator(); + BCStyle x500NameStyle = (BCStyle) BCStyle.INSTANCE; + Map.Entry entry; + while (itr.hasNext()) { + entry = (Map.Entry) itr.next(); + ASN1ObjectIdentifier oid = x500NameStyle.attrNameToOID((String) entry.getKey()); + builder.addRDN(oid, (String) entry.getValue()); + } + return builder.build(); + } catch (Exception ex) { + throw new InvalidX500NameException(ex.getMessage(), ex); + } + } + + public static PKCS10CertificationRequest createCSR(X500Name subject, SM2PublicKey pubKey, PrivateKey priKey, + String signAlgo) throws OperatorCreationException { + PKCS10CertificationRequestBuilder csrBuilder = new JcaPKCS10CertificationRequestBuilder(subject, pubKey); + ContentSigner signerBuilder = new JcaContentSignerBuilder(signAlgo) + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(priKey); + return csrBuilder.build(signerBuilder); + } + + public static AlgorithmIdentifier findSignatureAlgorithmIdentifier(String algoName) { + DefaultSignatureAlgorithmIdentifierFinder sigFinder = new DefaultSignatureAlgorithmIdentifierFinder(); + return sigFinder.find(algoName); + } + + public static AlgorithmIdentifier findDigestAlgorithmIdentifier(String algoName) { + DefaultDigestAlgorithmIdentifierFinder digFinder = new DefaultDigestAlgorithmIdentifierFinder(); + return digFinder.find(findSignatureAlgorithmIdentifier(algoName)); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/FileSNAllocator.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/FileSNAllocator.java new file mode 100644 index 0000000..4d54f86 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/FileSNAllocator.java @@ -0,0 +1,48 @@ +package com.sunyard.chsm.utils.gm.cert; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.math.BigInteger; + +public class FileSNAllocator implements CertSNAllocator { + private static final String SN_FILENAME = "sn.dat"; + private static String snFilePath; + + static { + ClassLoader loader = FileSNAllocator.class.getClassLoader(); + snFilePath = loader.getResource(SN_FILENAME).getPath(); + } + + public synchronized BigInteger nextSerialNumber() throws Exception { + BigInteger sn = readSN(); + writeSN(sn.add(BigInteger.ONE)); + return sn; + } + + private BigInteger readSN() throws IOException { + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(snFilePath, "r"); + byte[] data = new byte[(int) raf.length()]; + raf.read(data); + String snStr = new String(data); + return new BigInteger(snStr); + } finally { + if (raf != null) { + raf.close(); + } + } + } + + private void writeSN(BigInteger sn) throws IOException { + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(snFilePath, "rw"); + raf.writeBytes(sn.toString(10)); + } finally { + if (raf != null) { + raf.close(); + } + } + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/RandomSNAllocator.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/RandomSNAllocator.java new file mode 100644 index 0000000..b7b7939 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/RandomSNAllocator.java @@ -0,0 +1,78 @@ +/* + * + * This is simplified version of the RandomSerialNumberGenerator from the project + * https://github.com/xipki/xipki. + */ + +package com.sunyard.chsm.utils.gm.cert; + +import java.math.BigInteger; +import java.security.SecureRandom; + +/** + * Random serial number generator. + * + * This class is thread safe. + * + * @author Lijun Liao + */ +public class RandomSNAllocator implements CertSNAllocator { + + /** + * The highest bit is always set to 1, so the effective bit length is bitLen - 1. To ensure that + * at least 64 bit entropy, bitLen must be at least 65. + */ + private final static int MIN_SERIALNUMBER_SIZE = 65; + + /** + * Since serial number should be positive and maximal 20 bytes, the maximal value of bitLen is + * 159. + */ + private final static int MAX_SERIALNUMBER_SIZE = 159; + + private static int[] AND_MASKS = new int[] {0xFF, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F}; + + private static int[] OR_MASKS = new int[] {0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40}; + + private final SecureRandom random; + + private final int bitLen; + + /** + * Constructor with the bitLen = 65. + */ + public RandomSNAllocator() { + this(MIN_SERIALNUMBER_SIZE); + } + + /** + * Constructor with the specification of bitLen. + * @param bitLen bit length of the serial number. The highest bit is always set to 1, so the + * effective bit length is bitLen - 1. Valid value is [65, 159]. + */ + public RandomSNAllocator(int bitLen) { + if (bitLen < MIN_SERIALNUMBER_SIZE || bitLen > MAX_SERIALNUMBER_SIZE) { + throw new IllegalArgumentException(String.format( + "%s may not be out of the range [%d, %d]: %d", + "bitLen", MIN_SERIALNUMBER_SIZE, MAX_SERIALNUMBER_SIZE, bitLen)); + } + + this.random = new SecureRandom(); + this.bitLen = bitLen; + } + + @Override + public BigInteger nextSerialNumber() { + final byte[] rdnBytes = new byte[(bitLen + 7) / 8]; + final int ci = bitLen % 8; + + random.nextBytes(rdnBytes); + if (ci != 0) { + rdnBytes[0] = (byte) (rdnBytes[0] & AND_MASKS[ci]); + } + rdnBytes[0] = (byte) (rdnBytes[0] | OR_MASKS[ci]); + + return new BigInteger(1, rdnBytes); + } + +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PrivateKey.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PrivateKey.java new file mode 100644 index 0000000..9f95d08 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PrivateKey.java @@ -0,0 +1,81 @@ +package com.sunyard.chsm.utils.gm.cert; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.jcajce.provider.config.ProviderConfiguration; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.io.IOException; +import java.security.spec.ECParameterSpec; + +public class SM2PrivateKey extends BCECPrivateKey { + private transient DERBitString sm2PublicKey; + private boolean withCompression; + + public SM2PrivateKey(BCECPrivateKey privateKey, BCECPublicKey publicKey) { + super(privateKey.getAlgorithm(), privateKey); + this.sm2PublicKey = getSM2PublicKeyDetails(new SM2PublicKey(publicKey.getAlgorithm(), publicKey)); + this.withCompression = false; + } + + @Override + public void setPointFormat(String style) { + withCompression = !("UNCOMPRESSED".equalsIgnoreCase(style)); + } + + /** + * Return a PKCS8 representation of the key. The sequence returned + * represents a full PrivateKeyInfo object. + * + * @return a PKCS8 representation of the key. + */ + @Override + public byte[] getEncoded() { + ECParameterSpec ecSpec = getParams(); + ProviderConfiguration configuration = BouncyCastleProvider.CONFIGURATION; + ASN1Encodable params = SM2PublicKey.ID_SM2_PUBKEY_PARAM; + + int orderBitLength; + if (ecSpec == null) { + orderBitLength = ECUtil.getOrderBitLength(configuration, null, this.getS()); + } else { + orderBitLength = ECUtil.getOrderBitLength(configuration, ecSpec.getOrder(), this.getS()); + } + + PrivateKeyInfo info; + org.bouncycastle.asn1.sec.ECPrivateKey keyStructure; + + if (sm2PublicKey != null) { + keyStructure = new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, this.getS(), sm2PublicKey, params); + } else { + keyStructure = new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, this.getS(), params); + } + + try { + info = new PrivateKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params), keyStructure); + + return info.getEncoded(ASN1Encoding.DER); + } catch (IOException e) { + return null; + } + } + + private DERBitString getSM2PublicKeyDetails(SM2PublicKey pub) { + try { + SubjectPublicKeyInfo info = SubjectPublicKeyInfo.getInstance(ASN1Primitive.fromByteArray(pub.getEncoded())); + + return info.getPublicKeyData(); + } catch (IOException e) { // should never happen + return null; + } + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PublicKey.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PublicKey.java new file mode 100644 index 0000000..1433f04 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2PublicKey.java @@ -0,0 +1,44 @@ +package com.sunyard.chsm.utils.gm.cert; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X9ECPoint; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.util.KeyUtil; + +public class SM2PublicKey extends BCECPublicKey { + public static final ASN1ObjectIdentifier ID_SM2_PUBKEY_PARAM = new ASN1ObjectIdentifier("1.2.156.10197.1.301"); + + private boolean withCompression; + + public SM2PublicKey(BCECPublicKey key) { + super(key.getAlgorithm(), key); + this.withCompression = false; + } + + public SM2PublicKey(String algorithm, BCECPublicKey key) { + super(algorithm, key); + this.withCompression = false; + } + + @Override + public byte[] getEncoded() { + ASN1OctetString p = ASN1OctetString.getInstance( + new X9ECPoint(getQ(), withCompression).toASN1Primitive()); + + // stored curve is null if ImplicitlyCa + SubjectPublicKeyInfo info = new SubjectPublicKeyInfo( + new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, ID_SM2_PUBKEY_PARAM), + p.getOctets()); + + return KeyUtil.getEncodedSubjectPublicKeyInfo(info); + } + + @Override + public void setPointFormat(String style) { + withCompression = !("UNCOMPRESSED".equalsIgnoreCase(style)); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java new file mode 100644 index 0000000..943d781 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java @@ -0,0 +1,280 @@ +package com.sunyard.chsm.utils.gm.cert; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +public class SM2X509CertMaker { + + private static enum CertLevel { + RootCA, + SubCA, + EndEntity + } // class CertLevel + + public static final String SIGN_ALGO_SM3WITHSM2 = "SM3withSM2"; + + private long certExpire; + private X500Name issuerDN; + private CertSNAllocator snAllocator; + private KeyPair issuerKeyPair; + + /** + * @param issuerKeyPair 证书颁发者的密钥对。 + * 其实一般的CA的私钥都是要严格保护的。 + * 一般CA的私钥都会放在加密卡/加密机里,证书的签名由加密卡/加密机完成。 + * 这里仅是为了演示BC库签发证书的用法,所以暂时不作太多要求。 + * @param certExpire 证书有效时间,单位毫秒 + * @param issuer 证书颁发者信息 + * @param snAllocator 维护/分配证书序列号的实例,证书序列号应该递增且不重复 + */ + public SM2X509CertMaker(KeyPair issuerKeyPair, long certExpire, X500Name issuer, + CertSNAllocator snAllocator) { + this.issuerKeyPair = issuerKeyPair; + this.certExpire = certExpire; + this.issuerDN = issuer; + this.snAllocator = snAllocator; + } + + /** + * 生成根CA证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeRootCACert(byte[] csr) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + return makeCertificate(CertLevel.RootCA, null, csr, usage, null); + } + + /** + * 生成SubCA证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeSubCACert(byte[] csr) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + return makeCertificate(CertLevel.SubCA, 0, csr, usage, null); + } + + /** + * 生成SSL用户证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeSSLEndEntityCert(byte[] csr) + throws Exception { + return makeEndEntityCert(csr, + new KeyPurposeId[] {KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}); + } + + /** + * 生成用户证书 + * + * @param csr CSR + * @param extendedKeyUsages 扩展指数用途。 + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeEndEntityCert(byte[] csr, + KeyPurposeId[] extendedKeyUsages) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyAgreement + | KeyUsage.dataEncipherment | KeyUsage.keyEncipherment); + return makeCertificate(CertLevel.EndEntity, null, csr, usage, extendedKeyUsages); + } + + /** + * @param isCA 是否是颁发给CA的证书 + * @param keyUsage 证书用途 + * @param csr CSR + * @return + * @throws Exception + */ + private X509Certificate makeCertificate(CertLevel certLevel, Integer pathLenConstrain, + byte[] csr, KeyUsage keyUsage, KeyPurposeId[] extendedKeyUsages) + throws Exception { + if (certLevel == CertLevel.EndEntity) { + if (keyUsage.hasUsages(KeyUsage.keyCertSign)) { + throw new IllegalArgumentException( + "keyusage keyCertSign is not allowed in EndEntity Certificate"); + } + } + + PKCS10CertificationRequest request = new PKCS10CertificationRequest(csr); + SubjectPublicKeyInfo subPub = request.getSubjectPublicKeyInfo(); + + PrivateKey issPriv = issuerKeyPair.getPrivate(); + PublicKey issPub = issuerKeyPair.getPublic(); + + X500Name subject = request.getSubject(); + String email = null; + String commonName = null; + /* + * RFC 5280 §4.2.1.6 Subject + * Conforming implementations generating new certificates with + * electronic mail addresses MUST use the rfc822Name in the subject + * alternative name extension (Section 4.2.1.6) to describe such + * identities. Simultaneous inclusion of the emailAddress attribute in + * the subject distinguished name to support legacy implementations is + * deprecated but permitted. + */ + RDN[] rdns = subject.getRDNs(); + List newRdns = new ArrayList<>(rdns.length); + for (int i = 0; i < rdns.length; i++) { + RDN rdn = rdns[i]; + + AttributeTypeAndValue atv = rdn.getFirst(); + ASN1ObjectIdentifier type = atv.getType(); + if (BCStyle.EmailAddress.equals(type)) { + email = IETFUtils.valueToString(atv.getValue()); + } else { + if (BCStyle.CN.equals(type)) { + commonName = IETFUtils.valueToString(atv.getValue()); + } + newRdns.add(rdn); + } + } + + List subjectAltNames = new LinkedList<>(); + if (email != null) { + subject = new X500Name(newRdns.toArray(new RDN[0])); + subjectAltNames.add( + new GeneralName(GeneralName.rfc822Name, + new DERIA5String(email, true))); + } + + boolean selfSignedEECert = false; + switch (certLevel) { + case RootCA: + if (issuerDN.equals(subject)) { + subject = issuerDN; + } else { + throw new IllegalArgumentException("subject != issuer for certLevel " + CertLevel.RootCA); + } + break; + case SubCA: + if (issuerDN.equals(subject)) { + throw new IllegalArgumentException( + "subject MUST not equals issuer for certLevel " + certLevel); + } + break; + default: + if (issuerDN.equals(subject)) { + selfSignedEECert = true; + subject = issuerDN; + } + } + + BigInteger serialNumber = snAllocator.nextSerialNumber(); + Date notBefore = new Date(); + Date notAfter = new Date(notBefore.getTime() + certExpire); + X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder( + issuerDN, serialNumber, + notBefore, notAfter, + subject, subPub); + + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + v3CertGen.addExtension(Extension.subjectKeyIdentifier, false, + extUtils.createSubjectKeyIdentifier(subPub)); + if (certLevel != CertLevel.RootCA && !selfSignedEECert) { + v3CertGen.addExtension(Extension.authorityKeyIdentifier, false, + extUtils.createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(issPub.getEncoded()))); + } + + // RFC 5280 §4.2.1.9 Basic Constraints: + // Conforming CAs MUST include this extension in all CA certificates + // that contain public keys used to validate digital signatures on + // certificates and MUST mark the extension as critical in such + // certificates. + BasicConstraints basicConstraints; + if (certLevel == CertLevel.EndEntity) { + basicConstraints = new BasicConstraints(false); + } else { + basicConstraints = pathLenConstrain == null + ? new BasicConstraints(true) : new BasicConstraints(pathLenConstrain.intValue()); + } + v3CertGen.addExtension(Extension.basicConstraints, true, basicConstraints); + + // RFC 5280 §4.2.1.3 Key Usage: When present, conforming CAs SHOULD mark this extension as critical. + v3CertGen.addExtension(Extension.keyUsage, true, keyUsage); + + if (extendedKeyUsages != null) { + ExtendedKeyUsage xku = new ExtendedKeyUsage(extendedKeyUsages); + v3CertGen.addExtension(Extension.extendedKeyUsage, false, xku); + + boolean forSSLServer = false; + for (KeyPurposeId purposeId : extendedKeyUsages) { + if (KeyPurposeId.id_kp_serverAuth.equals(purposeId)) { + forSSLServer = true; + break; + } + } + + if (forSSLServer) { + if (commonName == null) { + throw new IllegalArgumentException("commonName must not be null"); + } + GeneralName name = new GeneralName(GeneralName.dNSName, + new DERIA5String(commonName, true)); + subjectAltNames.add(name); + } + } + + if (!subjectAltNames.isEmpty()) { + v3CertGen.addExtension(Extension.subjectAlternativeName, false, + new GeneralNames(subjectAltNames.toArray(new GeneralName[0]))); + } + + JcaContentSignerBuilder contentSignerBuilder = makeContentSignerBuilder(issPub); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(v3CertGen.build(contentSignerBuilder.build(issPriv))); + cert.verify(issPub); + + return cert; + } + + private JcaContentSignerBuilder makeContentSignerBuilder(PublicKey issPub) throws Exception { + if (issPub.getAlgorithm().equals("EC")) { + JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(SIGN_ALGO_SM3WITHSM2); + contentSignerBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME); + return contentSignerBuilder; + } + throw new Exception("Unsupported PublicKey Algorithm:" + issPub.getAlgorithm()); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/exception/InvalidX500NameException.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/exception/InvalidX500NameException.java new file mode 100644 index 0000000..92f5874 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/exception/InvalidX500NameException.java @@ -0,0 +1,21 @@ +package com.sunyard.chsm.utils.gm.cert.exception; + +public class InvalidX500NameException extends Exception { + private static final long serialVersionUID = 3192247087539921768L; + + public InvalidX500NameException() { + super(); + } + + public InvalidX500NameException(String message) { + super(message); + } + + public InvalidX500NameException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidX500NameException(Throwable cause) { + super(cause); + } +} diff --git a/chsm-web-manage/pom.xml b/chsm-web-manage/pom.xml index 8e53a91..426e23d 100644 --- a/chsm-web-manage/pom.xml +++ b/chsm-web-manage/pom.xml @@ -49,6 +49,10 @@ jedis + + javax.persistence + javax.persistence-api + com.dm DmJdbcDriver diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/ApplicationController.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/ApplicationController.java new file mode 100644 index 0000000..22fc89e --- /dev/null +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/ApplicationController.java @@ -0,0 +1,8 @@ +package com.sunyard.chsm.controller; + +/** + * @author liulu + * @since 2024/10/29 + */ +public class ApplicationController { +} diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/KeyInfoAsymController.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/KeyInfoAsymController.java index 8def252..af7d71e 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/KeyInfoAsymController.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/controller/KeyInfoAsymController.java @@ -21,6 +21,7 @@ import javax.validation.Valid; /** * 非对称密钥管理接口 + * * @author liulu * @since 2024/10/28 */ @@ -71,5 +72,14 @@ public class KeyInfoAsymController { .body(new ByteArrayResource(content)); } + @PostMapping("/createCsr") + public R createCsr(KeyInfoDTO.CreateCsr createCsr) { + + String csr = keyInfoService.createCsr(createCsr); + + KeyInfoDTO.CreateCSRResp resp = new KeyInfoDTO.CreateCSRResp(); + resp.setCsr(csr); + return R.data(resp); + } } diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/dto/KeyInfoDTO.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/dto/KeyInfoDTO.java index 5488025..f947839 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/dto/KeyInfoDTO.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/dto/KeyInfoDTO.java @@ -1,6 +1,7 @@ package com.sunyard.chsm.dto; import com.sunyard.chsm.model.PageQuery; +import com.sunyard.chsm.model.Subject; import lombok.Data; import lombok.EqualsAndHashCode; @@ -118,5 +119,21 @@ public abstract class KeyInfoDTO { } + @EqualsAndHashCode(callSuper = true) + @Data + public static class CreateCsr extends Subject { + /** + * 密钥id + */ + @NotNull(message = "密钥id不能为空") + private Long id; + } + + @Data + public static class CreateCSRResp { + // 证书请求内容 + private String csr; + } + } diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/KeyInfoService.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/KeyInfoService.java index c47fbd5..a265bae 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/KeyInfoService.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/KeyInfoService.java @@ -22,6 +22,8 @@ public interface KeyInfoService { String recoveryKey(InputStream is); + String createCsr(KeyInfoDTO.CreateCsr createCsr); + void enableKey(List ids); void disableKey(List ids); diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyInfoServiceImpl.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyInfoServiceImpl.java index 5f82d17..6b4dac9 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyInfoServiceImpl.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyInfoServiceImpl.java @@ -9,9 +9,11 @@ import com.sunyard.chsm.dto.KeyInfoDTO; import com.sunyard.chsm.enums.KeyCategory; import com.sunyard.chsm.enums.KeyStatus; import com.sunyard.chsm.enums.KeyUsage; +import com.sunyard.chsm.mapper.KeyCsrMapper; import com.sunyard.chsm.mapper.KeyInfoMapper; import com.sunyard.chsm.mapper.KeyTemplateMapper; import com.sunyard.chsm.mapper.SpKeyRecordMapper; +import com.sunyard.chsm.model.entity.KeyCsr; import com.sunyard.chsm.model.entity.KeyInfo; import com.sunyard.chsm.model.entity.KeyRecord; import com.sunyard.chsm.model.entity.KeyTemplate; @@ -19,9 +21,23 @@ import com.sunyard.chsm.sdf.SdfApiService; import com.sunyard.chsm.sdf.model.EccKey; import com.sunyard.chsm.service.KeyInfoService; import com.sunyard.chsm.utils.JsonUtils; +import com.sunyard.chsm.utils.gm.BCECUtils; +import com.sunyard.chsm.utils.gm.cert.CommonCertUtils; +import com.sunyard.chsm.utils.gm.cert.SM2PublicKey; +import com.sunyard.chsm.utils.gm.cert.SM2X509CertMaker; import com.sunyard.ssp.common.exception.SspwebException; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,12 +51,14 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -55,6 +73,8 @@ import java.util.stream.Collectors; @Service public class KeyInfoServiceImpl implements KeyInfoService { + @Resource + private KeyCsrMapper keyCsrMapper; @Resource private KeyInfoMapper keyInfoMapper; @Resource @@ -210,18 +230,20 @@ public class KeyInfoServiceImpl implements KeyInfoService { if (KeyCategory.SYM_KEY.getCode().equals(info.getKeyType())) { byte[] symKey = sdfApiService.generateRandom(16); - record.setKeyData(Hex.encodeHexString(symKey)); - String checkHash = Hex.encodeHexString(sdfApiService.hash(symKey)); + byte[] encSymKey = sdfApiService.encryptByMKNoPadding(symKey); + record.setKeyData(Hex.toHexString(encSymKey)); + String checkHash = Hex.toHexString(sdfApiService.hash(symKey)); record.setCheckValue(checkHash); } else { EccKey eccKey = sdfApiService.genKeyPairEcc(); byte[] d = eccKey.getPriKey().getD(); - record.setKeyData(Hex.encodeHexString(d)); - String checkHash = Hex.encodeHexString(sdfApiService.hash(d)); + byte[] encD = sdfApiService.encryptByMKNoPadding(d); + record.setKeyData(Hex.toHexString(encD)); + String checkHash = Hex.toHexString(sdfApiService.hash(d)); record.setCheckValue(checkHash); byte[] pubKeyBytes = eccKey.getPubKey().getPubKeyBytes(); - record.setPubKey(Hex.encodeHexString(pubKeyBytes)); + record.setPubKey(Hex.toHexString(pubKeyBytes)); } return record; } @@ -254,7 +276,7 @@ public class KeyInfoServiceImpl implements KeyInfoService { public String recoveryKey(InputStream is) { int suc = 0, count = 0, err = 0, exd = 0; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))){ + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { String line; while (true) { try { @@ -290,6 +312,75 @@ public class KeyInfoServiceImpl implements KeyInfoService { return String.format("恢复完成,共%d条数据,跳过已经存在的密钥%d条,恢复成功%d条,解析失败%d条", count, exd, suc, err); } + @Override + public String createCsr(KeyInfoDTO.CreateCsr createCsr) { + + String subject = createCsr.getDN(); + KeyCsr exist = keyCsrMapper.selectBySubject(subject); + if (Objects.nonNull(exist)) { + if (Objects.equals(exist.getKeyId(), createCsr.getId())) { + return exist.getCsrTxt(); + } + throw new IllegalArgumentException("此证书主题已经被使用!"); + } + KeyInfo keyInfo = keyInfoMapper.selectById(createCsr.getId()); + Assert.notNull(keyInfo, "密钥不存在!"); + LocalDateTime now = LocalDateTime.now(); + Assert.isTrue(KeyCategory.ASYM_KEY.getCode().equals(keyInfo.getKeyType()) + && KeyStatus.ENABLED.getCode().equals(keyInfo.getStatus()) + && now.isAfter(keyInfo.getEffectiveTime()) + && now.isBefore(keyInfo.getExpiredTime()), + "只能选择已启用的非对称密钥生成证书请求"); + KeyRecord record = spKeyRecordMapper.selectUsedByKeyId(keyInfo.getId()); + Assert.notNull(record, "数据异常,没有在使用中的密钥"); + + X500Name dn = new X500Name(subject); + + byte[] xy = Hex.decode(record.getPubKey()); + byte[] x = Arrays.copyOfRange(xy, 0, 32); + byte[] y = Arrays.copyOfRange(xy, 32, 64); + ECPublicKeyParameters pubKeyParam = BCECUtils.createECPublicKeyParameters(x, y); + + byte[] priKeyBytes = sdfApiService.decryptByMKNoPadding(Hex.decode(record.getKeyData())); +// byte[][] pri18 = LangUtils.splitAverage(priKeyBytes); + ECPrivateKeyParameters priKeyParam = BCECUtils.createECPrivateKeyParameters(priKeyBytes); + ECDomainParameters domainParams = priKeyParam.getParameters(); + ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(), domainParams.getN(), domainParams.getH()); + BCECPublicKey pubKey = new BCECPublicKey("EC", pubKeyParam, spec, BouncyCastleProvider.CONFIGURATION); + BCECPrivateKey priKey = new BCECPrivateKey("EC", priKeyParam, pubKey, spec, BouncyCastleProvider.CONFIGURATION); +// + SM2PublicKey sm2SubPub = new SM2PublicKey(pubKey.getAlgorithm(), pubKey); + String csrTxt; + try { + byte[] csr = CommonCertUtils.createCSR(dn, sm2SubPub, priKey, SM2X509CertMaker.SIGN_ALGO_SM3WITHSM2).getEncoded(); + + PemObject pem = new PemObject("CERTIFICATE REQUEST", csr); + StringWriter str = new StringWriter(); + PemWriter pemWriter = new PemWriter(str); + pemWriter.writeObject(pem); + pemWriter.close(); + str.close(); + + csrTxt = str.toString(); + } catch (Exception e) { + log.error("生成CSR异常", e); + throw new IllegalArgumentException("生成证书请求异常"); + } + KeyCsr csr = new KeyCsr(); + csr.setId(IdWorker.getId()); + csr.setApplicationId(keyInfo.getApplicationId()); + csr.setKeyId(keyInfo.getId()); + csr.setKeyRecordId(record.getId()); + csr.setPubKey(record.getPubKey()); + csr.setKeyData(record.getKeyData()); + csr.setSubject(subject); + csr.setCreateTime(LocalDateTime.now()); + csr.setCsrTxt(csrTxt); + keyCsrMapper.insert(csr); + return csrTxt; + } + + @Override public void enableKey(List ids) { if (CollectionUtils.isEmpty(ids)) { diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyTemplateServiceImpl.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyTemplateServiceImpl.java index 5c0851e..51dda9c 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyTemplateServiceImpl.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/service/impl/KeyTemplateServiceImpl.java @@ -42,13 +42,13 @@ public class KeyTemplateServiceImpl implements KeyTemplateService { @Override public Page selectPageList(KeyTemplateDTO.Query query) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(StringUtils.hasText(query.getKeyType()), KeyTemplate::getKeyType, query.getKeyType()) - .orderByDesc(KeyTemplate::getCreateTime); IPage page = keyTemplateMapper.selectPage( new Page<>(query.getPageNumber(), query.getPageSize()), - wrapper); + new LambdaQueryWrapper() + .eq(StringUtils.hasText(query.getKeyType()), KeyTemplate::getKeyType, query.getKeyType()) + .orderByDesc(KeyTemplate::getCreateTime) + ); List records = page.getRecords(); if (CollectionUtils.isEmpty(records)) { return new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); @@ -145,7 +145,7 @@ public class KeyTemplateServiceImpl implements KeyTemplateService { .map(KeyUsage::valueOf) .collect(Collectors.toList()); - if (KeyCategory.SYM_KEY == keyCategory ) { + if (KeyCategory.SYM_KEY == keyCategory) { Assert.isTrue(!usageList.contains(KeyUsage.SIGN_VERIFY), "对称密钥不能用于签名验签"); } else { Assert.isTrue(!usageList.contains(KeyUsage.HMAC), "非对称密钥不能用于计算HMac"); diff --git a/doc/ssp_dm.sql b/doc/ssp_dm.sql new file mode 100644 index 0000000..56cd9e1 --- /dev/null +++ b/doc/ssp_dm.sql @@ -0,0 +1,148 @@ + +-- 密码设备 +CREATE TABLE sp_device ( + id BIGINT NOT NULL COMMENT 'id', + name VARCHAR(255) COMMENT '名称', + device_number VARCHAR(255) COMMENT '编号', + manufacturer VARCHAR(255) COMMENT '制造厂商', + manufacturer_model VARCHAR(255) COMMENT '制造厂商型号', + service_ip VARCHAR(30) COMMENT '服务ip', + service_port INT COMMENT '服务端口', + manage_ip VARCHAR(30) COMMENT '管理ip', + manage_port INT COMMENT '管理端口', + access_credentials VARCHAR(1000) COMMENT '访问凭证', + status VARCHAR(25) DEFAULT '' COMMENT '设备状态', + group_id BIGINT NOT NULL DEFAULT 0 COMMENT '设备组id', + group_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '设备组名称', + weight INT DEFAULT 1 COMMENT '负载时权重', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +CREATE TABLE sp_device_group ( + id BIGINT NOT NULL COMMENT 'id', + name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '服务名称', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 密码服务 +CREATE TABLE sp_crypto_service ( + id BIGINT NOT NULL COMMENT 'id', + name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '服务名称', + device_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '设备组id', + device_group_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '设备组名称', + status VARCHAR(50) NOT NULL DEFAULT '' COMMENT '状态', + creator_id BIGINT COMMENT '创建者id', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 密码服务api +CREATE TABLE sp_crypto_service_api ( + id BIGINT NOT NULL COMMENT 'id', + crypto_service_id BIGINT NOT NULL COMMENT '密码服务id', + api_group VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'API分组', + api_code VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'API标识', + api_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'API名称', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 业务应用 +CREATE TABLE sp_application ( + id BIGINT NOT NULL COMMENT 'id', + name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '应用名称', + bind_service VARCHAR(1020) NOT NULL DEFAULT '' COMMENT '密码服务 ,分隔', + app_key VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'app_key', + app_secret VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'app_secret', + status VARCHAR(50) NOT NULL DEFAULT '' COMMENT '状态', + creator_id BIGINT COMMENT '创建者id', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 密钥模版 +CREATE TABLE sp_key_template ( + id BIGINT NOT NULL COMMENT 'id', + code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '编号', + name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '名称', + key_type VARCHAR(30) NOT NULL DEFAULT '' COMMENT '密钥类型', + key_alg VARCHAR(30) NOT NULL DEFAULT '' COMMENT '密钥算法', + key_length INT NOT NULL DEFAULT 0 COMMENT '密钥长度', + key_usage INT NOT NULL DEFAULT 0 COMMENT '密钥用途', + check_alg VARCHAR(30) NOT NULL DEFAULT '' COMMENT '校验算法', + check_value VARCHAR(255) NOT NULL DEFAULT '' COMMENT '校验值', + valid_time INT NOT NULL DEFAULT 0 COMMENT '有效期', + valid_unit VARCHAR(30) NOT NULL DEFAULT '' COMMENT '有效期时间单位', + start_after_create_time INT NOT NULL DEFAULT 0 COMMENT '创建后多长时间生效', + start_after_create_unit VARCHAR(30) NOT NULL DEFAULT '' COMMENT '创建后多长时间生效时间单位', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 密钥信息 +CREATE TABLE sp_key_info ( + id BIGINT NOT NULL COMMENT 'id', + application_id BIGINT NOT NULL COMMENT '应用id', + key_template_id BIGINT NOT NULL COMMENT '模版id', + code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '编号', + key_type VARCHAR(30) NOT NULL DEFAULT '' COMMENT '密钥分类', + key_alg VARCHAR(30) NOT NULL DEFAULT '' COMMENT '密钥算法', + key_length INT NOT NULL DEFAULT 0 COMMENT '密钥长度', + key_usage INT NOT NULL DEFAULT 0 COMMENT '密钥用途', + status VARCHAR(30) NOT NULL DEFAULT '' COMMENT '密钥状态', + check_alg VARCHAR(30) NOT NULL DEFAULT '' COMMENT '校验算法', + check_value VARCHAR(255) NOT NULL DEFAULT '' COMMENT '校验值', + effective_time TIMESTAMP COMMENT '启用时间', + expired_time TIMESTAMP COMMENT '停用时间', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 密钥记录 +CREATE TABLE sp_key_record ( + id BIGINT NOT NULL COMMENT 'id', + key_id BIGINT NOT NULL COMMENT '密钥id', + key_index VARCHAR(100) NOT NULL DEFAULT '' COMMENT '密钥索引', + key_data VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密钥密文', + pub_key VARCHAR(400) NOT NULL DEFAULT '' COMMENT '公钥', + check_alg VARCHAR(30) NOT NULL DEFAULT '' COMMENT '校验算法', + check_value VARCHAR(255) NOT NULL DEFAULT '' COMMENT '校验值', + effective_time TIMESTAMP COMMENT '生效时间', + expired_time TIMESTAMP COMMENT '过期时间', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); + +-- 证书请求记录 +CREATE TABLE sp_key_csr ( + id BIGINT NOT NULL COMMENT 'id', + application_id BIGINT NOT NULL COMMENT '应用id', + key_id BIGINT NOT NULL COMMENT '密钥id', + key_record_id BIGINT NOT NULL COMMENT '密钥记录id', + subject VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'DN', + key_data VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密钥密文', + pub_key VARCHAR(400) NOT NULL DEFAULT '' COMMENT '公钥', + csr_txt VARCHAR(2000) COMMENT '证书', + remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + PRIMARY KEY (id) +); \ No newline at end of file