使用Java语言生成X.509 V3证书
1. X.509 V3证书
X.509 是公钥证书的格式标准, 广泛用于 TLS/SSL 安全通信或者其他需要认证的环境中。 X.509 证书可以由 CA(Certificate Authority,数字证书认证机构)颁发,也可以自签名产生。
X.509 证书中主要含有公钥、身份信息、签名信息和有效性信息等信息。 这些信息用于构建一个验证公钥的体系,用来保证客户端得到的公钥正是它期望的公钥。
- 公钥 : 非对称密码中的公钥。公钥证书的目的就是为了在互联网上分发公钥。
- 身份信息 : 公钥对应的私钥持有者的信息,域名以及用途等。
- 签名信息 : 对公钥进行签名的信息,提供公钥的验证链。可以是 CA
的签名或者是自签名,不同之处在于CA证书的根证书大都内置于操作系统或者浏览器中,而自签名证书的公钥验证链则需要自己维护(手动导入到操作系统中或者再验证流程中单独提供自签名的根证书)。 - 有效性信息:证书的有效时间区间,以及 CRL(证书吊销列表)等相关信息。
X.509 证书的标准规范RFC5280中详细描述了证书的 Encoding Format(编码格式)和 Structure(证书结构)。
1.1 编码格式
- DER格式 : 二进制格式。
- PEM格式 : ASCII 文本格式。在 DER 格式或者其他二进制数据的基础上,使用 base64 编码为 ASCII 文本,以便于在仅支持 ASCII
的环境中使用二进制的 DER 编码的数据。
1.2 X.509证书结构
Certificate
Version Number
Serial Number
Signature Algorithm ID
Issuer Name
Validity period
Not Before
Not After
Subject name
Subject Public Key Info
Public Key Algorithm
Subject Public Key
Issuer Unique Identifier (optional)
Subject Unique Identifier (optional)
Extensions (optional)
...
Certificate Signature Algorithm
Certificate Signature
1.3 X.509 证书相关文件常见的扩展名
- .pem : 隐私增强型电子邮件格式(缩写:PEM)格式,通常是由证书的 DER 二进制 Base64 编码得出。(最常用)。
- .key : PEM 格式的私钥文件。
- .pub : PEM 格式的公钥文件。
- .crt : PEM 格式的公钥证书文件,也可能是 DER。
- .cer : DER 格式的公钥证书文件,也可能是 PEM。
- .crs : PEM 格式的 CSR 文件,也可能是 DER。
- .p12 – PKCS#12 格式,包含证书的同时可能还包含私钥。
- .pfx – PFX,PKCS#12 之前的格式(通常用PKCS#12格式,比如由互联网信息服务产生的 PFX 文件)。
提示: 目前一般使用pem格式的证书。 大多数系统会根据证书文件中的内容来识别具体的格式,而不依赖于扩展名。
更多信息请参考:维基百科X.509
2. 使用Java生成X.509 V3 证书
2.1 引入依赖
如果使用 Maven 项目,则需要在 pom.xml 中引入下面的依赖:
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
bouncycastle 是一个 JCE Provider,实现了常用的加密算法,是一个加密算法库,同时提供了 X.509 证书生成的一些帮助类。
2.2 代码实现
下面是一个例子:
import org.apache.commons.io.FileUtils;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Date;
public class App {
public static void main(String[] args) throws OperatorCreationException, IOException {
X500Name subject = generateSubject("CN", "Beijing", "Beijing", "", "", "www.test.com");
KeyPair keyPair = generateRsaKeyPair(2048);
assert keyPair != null;
PublicKey aPublic = keyPair.getPublic();
PrivateKey aPrivate = keyPair.getPrivate();
byte[] privateKeyEncode = aPrivate.getEncoded();
String privateKeyStr = Base64.getEncoder().encodeToString(privateKeyEncode);
String privateKeyFileContent = "" +
"-----BEGIN RSA PRIVATE KEY-----\n" +
lf(privateKeyStr, 64) +
"-----END RSA PRIVATE KEY-----";
FileUtils.write(new File("C:\\Users\\Administrator\\Desktop\\test1.key"), privateKeyFileContent,
StandardCharsets.UTF_8);
long currTimestamp = System.currentTimeMillis();
X500Name issuer = subject;
X509v3CertificateBuilder x509v3CertificateBuilder = new JcaX509v3CertificateBuilder(
issuer, BigInteger.valueOf(System.currentTimeMillis()),
new Date(currTimestamp), new Date(currTimestamp + (long) 365 * 24 * 60 * 60 * 1000),
subject, aPublic);
JcaContentSignerBuilder sha256WITHRSA = new JcaContentSignerBuilder("SHA256WITHRSA");
ContentSigner contentSigner = sha256WITHRSA.build(aPrivate);
X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder.build(contentSigner);
Certificate certificate = x509CertificateHolder.toASN1Structure();
byte[] encoded = certificate.getEncoded();
String certStr = Base64.getEncoder().encodeToString(encoded);
String certFileContent = "" +
"-----BEGIN CERTIFICATE-----\n" +
lf(certStr, 64) +
"-----END CERTIFICATE-----";
FileUtils.write(new File("C:\\Users\\Administrator\\Desktop\\test1.pem"), certFileContent,
StandardCharsets.UTF_8);
}
public static X500Name generateSubject(String C, String ST, String L,
String O, String OU, String CN) {
X500NameBuilder x500NameBuilder = new X500NameBuilder();
x500NameBuilder.addRDN(BCStyle.C, C);
x500NameBuilder.addRDN(BCStyle.ST, ST);
x500NameBuilder.addRDN(BCStyle.L, L);
x500NameBuilder.addRDN(BCStyle.O, O);
x500NameBuilder.addRDN(BCStyle.OU, OU);
x500NameBuilder.addRDN(BCStyle.CN, CN);
return x500NameBuilder.build();
}
public static KeyPair generateRsaKeyPair(int keySize) {
try {
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
rsa.initialize(keySize);
return rsa.generateKeyPair();
} catch (NoSuchAlgorithmException ignore) {
}
return null;
}
public static String lf(String str, int lineLength) {
assert str != null;
assert lineLength > 0;
StringBuilder sb = new StringBuilder();
char[] chars = str.toCharArray();
int n = 0;
for (char aChar : chars) {
sb.append(aChar);
n++;
if (n == lineLength) {
n = 0;
sb.append("\n");
}
}
if (n != 0)
sb.append("\n");
return sb.toString();
}
}
运行上面的代码,将生成两个文件:test1.pem(证书)和test1.key(私钥)。 如果使用 windows 系统,请更改 test1.pem 后缀为 .cert,然后双击该文件即可打开。
可以看到证书中的一些信息。
|