本文以SpringBoot为开发框架,基于BouncyCastle加密组件、Commons 系列工具包,实现了ECC(椭圆曲线密码学) 算法下的文件签名与验签功能,核心采用 SHA256withECDSA 签名算法,配套完成了密钥的初始化、签名生成、验签验证的全流程开发,同时提供了 OpenSSL 生成 P-256 椭圆曲线密钥对(DER 私钥 + PEM 公钥)的完整命令。
使用场景
该实现基于 ECC 椭圆曲线算法,相比 RSA 算法具有密钥长度短、加密效率高、安全性强的特点,其签名验签能力可广泛应用于对数据完整性、不可篡改性、身份真实性有要求的业务场景,核心适用场景包括:
- 文件传输安全:如 FTP / 文件服务器的文件更新、下载场景,对传输的文件(如 zip 包、定制化文件 vbf)生成签名,接收方验签确认文件未被篡改、来源合法;
- 分布式系统文件同步:微服务、分布式集群间的配置文件、静态资源同步,通过签名验签保证同步文件的完整性,避免节点间文件不一致;
- 定制化业务文件校验:如自研业务格式文件(如vbf 文件)的生成、分发,为文件添加唯一签名,接收方通过验签验证文件有效性;
- API 接口参数防篡改:对接口的核心参数(如文件路径、业务标识)生成签名,接口接收方验签,防止参数在传输过程中被恶意修改(如本文模拟接口对文件路径签名的场景)。
此外,该实现支持灵活替换算法(如切换为 SHA256withRSA),只需修改配置文件参数即可适配 RSA 签名验签场景,具备良好的扩展性,可适配更多加密算法的业务需求。
1. 引入依赖
xml
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.68</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
2. yaml配置
yaml
signature:
privateKey: sign/signature-private.der
publicKey: sign/signature-public.pem
algorithm: SHA256withECDSA
keyFactory: EC
provider: BC
3. 配置文件
java
@Data
@ConfigurationProperties(prefix = "signature")
@Configuration
public class SignatureProperties {
/**
* 签名私钥
*/
private String privateKey;
/**
* 公钥
*/
private String publicKey;
/**
* signatureInfo.put("sun.security.provider.DSA$RawDSA", TRUE);
* signatureInfo.put("sun.security.provider.DSA$SHA1withDSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$MD2withRSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$MD5withRSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$SHA1withRSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$SHA256withRSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$SHA384withRSA", TRUE);
* signatureInfo.put("sun.security.rsa.RSASignature$SHA512withRSA", TRUE);
* signatureInfo.put("com.sun.net.ssl.internal.ssl.RSASignature", TRUE);
* signatureInfo.put("sun.security.pkcs11.P11Signature", TRUE);
* 签名算法 (SHA256withECDSA、SHA256withRSA)
*/
private String algorithm;
/**
* key算法(RSA、EC)
*/
private String keyFactory;
private String provider;
}
4. vbf文件签名key初始化
java
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* vbf文件签名key初始化
*/
@Slf4j
@Configuration
public class SignatureConfig {
@Resource
private SignatureProperties signatureProperties;
@Bean
public PrivateKey GetPrivateKey() throws IOException {
String privateKey = signatureProperties.getPrivateKey();
String keyFactory = signatureProperties.getKeyFactory();
Security.addProvider(new BouncyCastleProvider());
ClassPathResource classPathResource = new ClassPathResource(privateKey);
// File keystore = new ClassPathResource("sample.jks").getFile();
File file = new File(privateKey);
if (!file.exists()) {
FileUtils.copyInputStreamToFile(classPathResource.getInputStream(), file);
}
try {
byte[] pk = FileUtils.readFileToByteArray(new File(privateKey));
PKCS8EncodedKeySpec priSpec = new PKCS8EncodedKeySpec(pk);
KeyFactory kf = KeyFactory.getInstance(keyFactory);
log.info("初始化密钥:{}", privateKey);
return kf.generatePrivate(priSpec);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@Bean
public PublicKey getPemPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String publicKey = signatureProperties.getPublicKey();
String keyFactory = signatureProperties.getKeyFactory();
ClassPathResource classPathResource = new ClassPathResource(publicKey);
// File keystore = new ClassPathResource("sample.jks").getFile();
File file = new File(publicKey);
if (!file.exists()) {
FileUtils.copyInputStreamToFile(classPathResource.getInputStream(), file);
}
String b64pk = FileUtils.readFileToString(new File(publicKey), "UTF-8");
b64pk = b64pk.replace("-----BEGIN PUBLIC KEY-----", "");
b64pk = b64pk.replace("-----END PUBLIC KEY-----", "");
byte[] pk = Base64.decode(b64pk);
X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pk);
KeyFactory kf = KeyFactory.getInstance(keyFactory);
log.info("初始化公钥:{}",publicKey);
log.info("初始化keyFactory:{}", keyFactory);
return kf.generatePublic(pubSpec);
}
}
5. 签名和验签
java
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
/**
* 签名和验签
*/
@Slf4j
@Service
public class ECDSASignUtils {
@Resource
private PrivateKey privateKey;
@Resource
private PublicKey publicKey;
@Resource
private SignatureProperties signatureProperties;
public static final int BLOCK_SIZE = 4 * 1024;
/**
* 使用默认key进行签名
*/
public String signature(InputStream inputStream) {
String s = Hex.encodeHexString(signature(inputStream, privateKey));
log.info("对文件签名:{}", s);
return s;
}
/**
* 使用默认key进行签名
*/
public String signature(String filePath) {
try {
FileInputStream inputStream = new FileInputStream(filePath);
return signature(inputStream);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* 验证签名
*/
public boolean verify(String signatureStr, String filePath) throws FileNotFoundException {
String publicKey = signatureProperties.getPublicKey();
File file = ResourceUtils.getFile(publicKey);
try {
return verify(Hex.decodeHex(signatureStr), filePath, file.getAbsolutePath());
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public boolean verify(String signatureStr, InputStream filePath) throws FileNotFoundException {
File file = ResourceUtils.getFile(signatureProperties.getPublicKey());
try {
return verify(Hex.decodeHex(signatureStr), filePath, file.getAbsolutePath());
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* 验证签名
*/
public boolean verify(byte[] signature, String filePath) {
try {
File file = ResourceUtils.getFile(signatureProperties.getPublicKey());
return verify(signature, filePath, file.getAbsolutePath());
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private byte[] signature(InputStream inputStream, PrivateKey privateKey) {
String algorithm = signatureProperties.getAlgorithm();
String provider = signatureProperties.getProvider();
Signature decode;
try {
if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
decode = Signature.getInstance("SHA256withECDSA", "BC");
} else {
decode = Signature.getInstance(algorithm, provider);
}
decode.initSign(privateKey);
log.info("签名: {},算法:{}", algorithm, provider);
byte[] buff = new byte[BLOCK_SIZE];
int read = inputStream.read(buff, 0, BLOCK_SIZE);
while (read > -1) {
decode.update(buff, 0, read);
read = inputStream.read(buff, 0, BLOCK_SIZE);
}
inputStream.close();
return decode.sign();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private boolean verify(byte[] signature, String filePath, String pubKeyPath) {
String algorithm = signatureProperties.getAlgorithm();
String provider = signatureProperties.getProvider();
Signature decode;
try {
if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
decode = Signature.getInstance("SHA256withECDSA", "BC");
} else {
decode = Signature.getInstance(algorithm, provider);
}
log.info("pubKeyPath: {}", pubKeyPath);
log.info("验证:{},签名算法:{}", algorithm, provider);
decode.initVerify(publicKey);
File vbf = new File(filePath);
byte[] buff = new byte[BLOCK_SIZE];
try (InputStream inputStream = new FileInputStream(vbf)) {
int read = inputStream.read(buff, 0, BLOCK_SIZE);
while (read > -1) {
decode.update(buff, 0, read);
read = inputStream.read(buff, 0, BLOCK_SIZE);
}
} catch (Exception e) {
throw e;
}
return decode.verify(signature);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private boolean verify(byte[] signature, InputStream inputStream, String pubKeyPath) {
String algorithm = signatureProperties.getAlgorithm();
String provider = signatureProperties.getProvider();
Signature decode;
try {
if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
decode = Signature.getInstance("SHA256withECDSA", "BC");
} else {
decode = Signature.getInstance(algorithm, provider);
}
log.info("pubKeyPath: {}", pubKeyPath);
log.info("验证:{},签名算法:{}", algorithm, provider);
decode.initVerify(publicKey);
byte[] buff = new byte[BLOCK_SIZE];
try {
int read = inputStream.read(buff, 0, BLOCK_SIZE);
while (read > -1) {
decode.update(buff, 0, read);
read = inputStream.read(buff, 0, BLOCK_SIZE);
}
} catch (Exception e) {
throw e;
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return decode.verify(signature);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
6. 模拟接口
java
@RestController
@Slf4j
public class DemoController {
@Resource
private ECDSASignUtils ecdsaSignUtils;
@GetMapping("/updateFile")
public ResponseEntity<String> updateFile(String zipFilePath) {
String signature = ecdsaSignUtils.signature(new ByteArrayInputStream(zipFilePath.getBytes()));
return ResponseEntity.ok(signature);
}
}
7. OpenSSL生成P-256椭圆曲线密钥(DER私钥+PEM公钥)
7.1. 前置准备
-
安装 OpenSSL
-
Windows:官网下载安装,或用 Git Bash 自带的 openssl
-
Mac:brew install openssl
-
Linux:sudo apt install openssl
-
-
创建存放密钥的文件夹
bash
# 创建 sign 文件夹(必须先建,否则命令报错)
mkdir sign
7.2. 执行命令
bash
# 1. 生成 prime256v1 椭圆曲线私钥(PEM格式,中间文件)
openssl ecparam -genkey -name prime256v1 -noout -out sign/signature-private.pem
# 2. 转换为 PKCS8 标准的 DER 格式私钥(最终使用的私钥)
openssl pkcs8 -topk8 -nocrypt -in sign/signature-private.pem -outform DER -out sign/signature-private.der
# 3. 从私钥导出 PEM 格式公钥(最终使用的公钥)
openssl ec -in sign/signature-private.pem -pubout -out sign/signature-public.pem
7.3. 命令作用
| 命令 | 作用 |
|---|---|
| ecparam -genkey -name prime256v1 | 生成 secp256r1/prime256v1 椭圆曲线密钥(最常用的 ECC 算法) |
| pkcs8 -topk8 -outform DER | 把私钥转为 标准 PKCS8 + DER 二进制格式(程序/硬件常用) |
| ec -pubout | 从私钥提取 公钥,输出 PEM 文本格式 |
7.4. 生成的 3 个文件说明
-
signature-private.pem:临时中间文件,可以删除
-
signature-private.der:最终私钥(二进制 DER 格式,用于签名)
-
signature-public.pem:最终公钥(文本 PEM 格式,用于验签)
7.5. 验证密钥是否正确
- 查看公钥内容
bash
cat sign/signature-public.pem
正常输出格式:
bash
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
- 查看私钥信息(验证格式)
bash
openssl ec -in sign/signature-private.der -inform DER -text -noout
显示 prime256v1 曲线信息,就说明密钥完全正常。