1. 前言
本篇博客是工作经验总结,如果您发现此篇博客有疏漏或有待改进之处,欢迎评论区交流。
2. CRL简介
CRL (Certificate Revocation List) 证书吊销列表。是CA机构维护的一个已经被吊销的证书序列号列表,浏览器需要定时更新这个列表,浏览器在验证证书合法性的时候也会在证书吊销列表中查询此证书是否已经被吊销,如果被吊销了那这个证书也是不可信的。可以看出,这个列表随着被吊销证书的增加而增加,列表会越来越大,浏览器还需要定时更新,实时性也比较差。所以就有了OSCP。
OSCP (Online Certificate Status Protocol) 在线证书状态协议。这个协议就是解决了 CRL 列表越来越大和实时性差的问题而生的。有了这个协议,浏览器就可以不用定期更新CRL了,在验证证书的时候直接去CA服务器实时校验一下证书有没有被吊销就可以,是解决了CRL的问题,但是每次都要去CA服务器上校验也会很慢,在网络环境较差的时候或者跨国访问的时候,体验就非常差了,OCSP虽然解决了CRL的问题但是性能却很差。
3. 代码实现
3.1. 代码功能介绍
1. 生成RSA密钥对(当成是CA密钥对);
2. 根据1中的CA密钥对,自签一个CA根证书;
3. 生成另一对RSA密钥对(当成是用户密钥对);
4. 通过CA私钥和CA公钥证书签发一个用户公钥证书;
5. 通过CA私钥和CA公钥证书生成一个CRL文件;
6. 把第4步中的用户公钥证书添加到吊销列表CRL文件中;
7. 解析CRL文件,遍历CRL文件里面被吊销的证书列表;
8. 判断用户证书是否被吊销。
3.2. 需要引入的包
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency>
3.3. 代码实现
代码的功能和 3.2 中是对应的,这里就不过多解释代码逻辑了。
package com.example.cert.cert;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Objects;
import java.util.Set;
/**
* <p> 1. 生成RSA密钥对(当成是CA密钥对);
*
* <p> 2. 根据1中的CA密钥对,自签一个CA根证书;
*
* <p> 3. 生成另一对RSA密钥对(当成是用户密钥对);
*
* <p> 4. 通过CA私钥和CA公钥证书签发一个用户公钥证书;
*
* <p> 5. 通过CA私钥和CA公钥证书生成一个CRL文件;
*
* <p> 6. 把第4步中的用户公钥证书添加到吊销列表CRL文件中;
*
* <p> 7. 解析CRL文件,遍历CRL文件里面被吊销的证书列表;
*
* <p> 8. 判断用户证书是否被吊销。
*
* <p> todo:
* <p> 签发证书时添加一些扩展项
* <p> 给CRL文件里面添加要被撤销的证书时,如果这个证书不是通过根证书签发的,可以添加进去吗
*/
public class CertAndCrl {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) throws Exception {
// 1. 生成CA密钥对
KeyPair caKeyPair = generateRSAKeyPair();
saveToPemFile("ca_private_key.pem", caKeyPair.getPrivate());
saveToPemFile("ca_public_key.pem", caKeyPair.getPublic());
// 2. 自签CA根证书
X509Certificate caCertificate = generateSelfSignedCertificate(caKeyPair, "CN=CA");
saveToPemFile("ca_certificate.pem", caCertificate);
// 3. 生成用户密钥对
KeyPair userKeyPair = generateRSAKeyPair();
saveToPemFile("user_private_key.pem", userKeyPair.getPrivate());
saveToPemFile("user_public_key.pem", userKeyPair.getPublic());
// 4. 签发用户证书
X509Certificate userCertificate = signUserCertificate(caKeyPair.getPrivate(), caCertificate, userKeyPair.getPublic(), "CN=User");
saveToPemFile("user_certificate.pem", userCertificate);
// 5. 生成CRL文件
X509CRL crl = generateCRL(caCertificate, caKeyPair.getPrivate());
saveToPemFile("crl.pem", crl);
// 6. 将用户证书添加到CRL列表中
crl = revokeCertificate(crl, caKeyPair.getPrivate(), userCertificate);
saveToPemFile("updated_crl.pem", crl);
// 7. 解析CRL文件
parseCrl("updated_crl.pem");
// 8. 判断用户证书是否被吊销
boolean isRevoked = isCertificateRevoked("user_certificate.pem", "updated_crl.pem");
if (isRevoked) {
System.out.println("用户证书已被吊销!");
} else {
System.out.println("用户证书未被吊销!");
}
}
public static void parseCrl(String crlPath) throws IOException, CertificateException, CRLException {
FileInputStream fileInputStream = new FileInputStream(crlPath);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509CRL crl = (X509CRL) certificateFactory.generateCRL(fileInputStream);
Set<? extends X509CRLEntry> revokedCertificates = crl.getRevokedCertificates();
String issuerDN = crl.getIssuerDN().toString();
System.out.println("CRL文件的签发者: " + issuerDN);
if (!Objects.isNull(revokedCertificates)) {
for (X509CRLEntry x509CRLEntry : revokedCertificates) {
BigInteger serialNumber = x509CRLEntry.getSerialNumber();
Date revocationDate = x509CRLEntry.getRevocationDate();
java.security.cert.CRLReason revocationReason = x509CRLEntry.getRevocationReason(); // 证书吊销原因
System.out.println("serialNumber: " + serialNumber.toString(16));
System.out.println("revocationDate: " + revocationDate);
System.out.println("revocationReason: " + revocationReason);
System.out.println("============");
}
}
fileInputStream.close();
}
/**
* 判断给定的证书是否被吊销
*
* @param certPath 证书文件路径(PEM 格式)
* @param crlPath CRL 文件路径(PEM 格式)
* @return true 如果证书已被吊销,否则 false
* @throws Exception 读取或解析失败
*/
private static boolean isCertificateRevoked(String certPath, String crlPath) throws Exception {
// 加载证书
X509Certificate certificate = loadCertificate(certPath);
// 加载CRL
X509CRL crl = loadCRL(crlPath);
// 获取吊销列表
Set<?> revokedCertificates = crl.getRevokedCertificates();
if (revokedCertificates == null) {
// 如果 CRL 中没有任何吊销记录,则返回 false
return false;
}
// 检查证书是否在吊销列表中
return revokedCertificates.stream()
.anyMatch(entry -> ((X509CRLEntry) entry).getSerialNumber().equals(certificate.getSerialNumber()));
}
/**
* 加载X509证书
*
* @param certPath 证书文件路径(PEM 格式)
* @return X509Certificate 对象
* @throws Exception 文件读取或解析失败
*/
private static X509Certificate loadCertificate(String certPath) throws Exception {
try (FileInputStream fis = new FileInputStream(certPath)) {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(fis);
}
}
/**
* 加载CRL文件
*
* @param crlPath CRL 文件路径(PEM 格式)
* @return X509CRL 对象
* @throws Exception 文件读取或解析失败
*/
private static X509CRL loadCRL(String crlPath) throws Exception {
try (FileInputStream fis = new FileInputStream(crlPath)) {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509CRL) factory.generateCRL(fis);
}
}
private static X509CRL revokeCertificate(X509CRL crl, PrivateKey caPrivateKey, X509Certificate revokedCertificate) throws Exception {
X509CRLHolder crlHolder = new X509CRLHolder(crl.getEncoded());
X509v2CRLBuilder builder = new X509v2CRLBuilder(crlHolder);
builder.addCRLEntry(revokedCertificate.getSerialNumber(), new Date(), CRLReason.privilegeWithdrawn);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caPrivateKey);
return new JcaX509CRLConverter().setProvider("BC").getCRL(builder.build(signer));
}
private static X509CRL generateCRL(X509Certificate caCertificate, PrivateKey caPrivateKey) throws Exception {
X500Name issuer = new X500Name(caCertificate.getSubjectX500Principal().getName());
Date thisUpdate = new Date();
Date nextUpdate = new Date(thisUpdate.getTime() + 7 * 24 * 60 * 60 * 1000L);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caPrivateKey);
X509v2CRLBuilder builder = new X509v2CRLBuilder(issuer, thisUpdate);
builder.setNextUpdate(nextUpdate);
return new JcaX509CRLConverter().setProvider("BC").getCRL(builder.build(signer));
}
private static X509Certificate signUserCertificate(PrivateKey caPrivateKey, X509Certificate caCertificate, PublicKey userPublicKey, String userDN) throws Exception {
X500Name issuer = new X500Name(caCertificate.getSubjectDN().getName());
X500Name subject = new X500Name(userDN);
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 365 * 24 * 60 * 60 * 1000L);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caPrivateKey);
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, serialNumber, notBefore, notAfter, subject, userPublicKey);
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer));
}
private static X509Certificate generateSelfSignedCertificate(KeyPair keyPair, String subjectDN) throws Exception {
X500Name subject = new X500Name(subjectDN);
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 365 * 24 * 60 * 60 * 1000L);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(subject, serialNumber, notBefore, notAfter, subject, keyPair.getPublic());
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer));
}
private static void saveToPemFile(String filePath, Object obj) throws IOException {
try (JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(new FileWriter(filePath))) {
jcaPEMWriter.writeObject(obj);
jcaPEMWriter.flush();
}
}
private static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}
}