SSL自签名证书双向认证实践
2023-06-18 本文已影响0人
国服最坑开发
0x00 TLDR;
使用 openssl, nginx, curl 进行双向自签名认证实验
本机/etc/hosts 配置单机域名: 127.0.0.1 demo.cc
术语
- CA:认证机构,浏览器上的绿标是指证书的认证机构是已知的安全的机构,自签的情况下,浏览器一般会提示证书的签发机构不认识,提示不安全。
- key: 证书密钥文件,不用于公网传输,仅用于程序内部计算。
- csr : 请求证书,主要作用就是对待签发的证书进行信息描述,比如要绑定的域名
- crt : 授权证书,签发好的证书主要用于公网下载,给到客户端对数据进行加密,到服务端后由key 进行较验。
- pem: 文本文件,以----- 开头和结尾的证书格式。实测由openssl 生成的crt文件,可以直接拿来当成pem 文件使用。
在对nginx 进行配置的时候,一般只会用到上面的 key、 crt/pem 文件。
0x01 根证书
# 生成 CA 私钥
openssl genrsa -out ca.key 2048
# 生成 CA 证书,有效期 100年,生成过程一路回车
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt
0x02 生成服务端证书
# 生成服务器私钥
openssl genrsa -out server.key 2048
# 生成服务器证书签名请求, 仅在 FQDN 处,设置域名:demo.cc
openssl req -new -key server.key -out server.csr
# 使用 CA 签名服务器证书
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
0x03 生成客户端证书
# 生成客户端私钥
openssl genrsa -out client.key 2048
# 生成客户端证书签名请求, 同上,FQDN 要配置成: demo.cc
openssl req -new -key client.key -out client.csr
# 使用 CA 签名客户端证书
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
0x04 nginx配置
server {
listen 443 ssl;
server_name demo.cc;
ssl_certificate /path/to/server.crt;
ssl_certificate_key /path/to/server.key;
ssl_client_certificate /path/to/ca.crt;
ssl_verify_client on;
location / {
return 200 "hello world";
}
}
0x05 客户端请求
curl --cert /path/to/client.crt --key /path/to/client.key --cacert /path/to/ca.crt https://demo.cc
#返回: hello world
0x06 小结
- 根证书失效的话,所有关联证书全部失效,所以直接干到100年。
- 在对客户端打证书的时候,发现csr那边也要使用和服务端相同的配置。如果在客户端证书的FQDN那里不配置域名的话,双向认证无法通过,会返回 400 错。
0x07 挑战项目:证书链
通过根证书创建一个代理证书,命名为 proxy, 可以尝试使用这个proxy.crt进行服务端和客户端的证书签发。
- 代理认证机构
openssl genrsa -out proxy.key 2048
openssl req -new -x509 -days 3650 -key proxy.key -out proxy.crt
- 服务端
openssl genrsa -out sub-server.key 2048
openssl req -new -key sub-server.key -out sub-server.csr
openssl x509 -req -days 365 -in sub-server.csr -CA proxy.crt -CAkey proxy.key -set_serial 01 -out sub-server.crt
- 客户端
openssl genrsa -out sub-client.key 2048
openssl req -new -key sub-client.key -out sub-client.csr
openssl x509 -req -days 365 -in sub-client.csr -CA proxy.crt -CAkey proxy.key -set_serial 01 -out sub-client.crt
- 验证
curl --cert ./sub-client.crt --key ./sub-client.key --cacert ./proxy.crt https://demo.cc
# hello world
验证结果:通过
0x07 通过Java来实现上述功能
主要使用依赖库bcpkix-jdk15on
来实现,参考代码点这里。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
完整功能实现如下:
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509ExtensionUtils;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
/**
* Utility class for generating self-signed certificates.
*
* @author Mister PKI
*/
@Slf4j
@Service
public final class SelfSignedCertGenerator {
/**
* Generates a self-signed certificate using the BouncyCastle lib.
*/
public static X509Certificate generate(final KeyPair keyPair, final String hashAlgorithm, final String cn, final int days) throws OperatorCreationException, CertificateException, CertIOException {
final Instant now = Instant.now();
final Date notBefore = Date.from(now);
final Date notAfter = Date.from(now.plus(Duration.ofDays(days)));
final ContentSigner contentSigner = new JcaContentSignerBuilder(hashAlgorithm).build(keyPair.getPrivate());
final X500Name x500Name = new X500Name("CN=" + cn);
final X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(x500Name, BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, x500Name, keyPair.getPublic())
.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyId(keyPair.getPublic()))
.addExtension(Extension.authorityKeyIdentifier, false, createAuthorityKeyId(keyPair.getPublic()))
.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner));
}
/**
* Creates the hash value of the public key.
*/
private static SubjectKeyIdentifier createSubjectKeyId(final PublicKey publicKey) throws OperatorCreationException {
final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
final DigestCalculator digCalc = new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
return new X509ExtensionUtils(digCalc).createSubjectKeyIdentifier(publicKeyInfo);
}
/**
* Creates the hash value of the authority public key.
*/
private static AuthorityKeyIdentifier createAuthorityKeyId(final PublicKey publicKey) throws OperatorCreationException {
final SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
final DigestCalculator digCalc = new BcDigestCalculatorProvider().get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
return new X509ExtensionUtils(digCalc).createAuthorityKeyIdentifier(publicKeyInfo);
}
/**
* 在ssl目录下生成root key
*/
public static void generateRootKey() throws NoSuchAlgorithmException, IOException {
// 生成一个私钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
final PublicKey pubKey = keyPair.getPublic();
// X.509
Files.write(Paths.get("ssl/publicKey"), pubKey.getEncoded());
final PrivateKey priKey = keyPair.getPrivate();
// PKCS#8
Files.write(Paths.get("ssl/privateKey"), priKey.getEncoded());
}
/**
* 加载 ssl下的rook key 文件,生成 KeyPair 对象
*/
public static KeyPair loadRootKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
// 读取公钥
byte[] publicKeyBytes = Files.readAllBytes(Paths.get("ssl/publicKey"));
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
// 读取私钥
byte[] privateKeyBytes = Files.readAllBytes(Paths.get("ssl/privateKey"));
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
// 创建 KeyPair 对象
KeyPair keyPair = new KeyPair(publicKey, privateKey);
return keyPair;
}
/**
* 基于已存储的私钥,生成cert证书
*/
public static void generateCert(String domain, int days) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, OperatorCreationException {
final KeyPair keyPair = loadRootKey();
final X509Certificate cert = SelfSignedCertGenerator.generate(keyPair, "SHA256withRSA", domain, days);
final byte[] certBytes = cert.getEncoded();
// 将 DER 编码转换为 Base64
String certPEM = "-----BEGIN CERTIFICATE-----\n";
certPEM += Base64.getMimeEncoder().encodeToString(certBytes);
certPEM += "\n-----END CERTIFICATE-----\n";
// 将 PEM 格式的证书写入文件
Files.write(Paths.get("pub/" + domain + ".pem"), certPEM.getBytes());
}
/**
* 把PrivateKey对象转换为 pem 格式字符串
*/
private static String convertPrivateKeyToPem(PrivateKey priKey) {
// Convert to PEM format
Base64.Encoder encoder = Base64.getMimeEncoder(64, new byte[]{10}); // 64 is the line length
String privateKeyPEM = "-----BEGIN PRIVATE KEY-----\n";
privateKeyPEM += encoder.encodeToString(priKey.getEncoded());
privateKeyPEM += "\n-----END PRIVATE KEY-----\n";
return privateKeyPEM;
}
/**
* 把X509Certificate对象转换为 pem 格式字符串
*/
private static String convertCertToPem(X509Certificate cert) throws CertificateEncodingException {
// write out cert file
final byte[] certBytes = cert.getEncoded();
// 将 DER 编码转换为 Base64
String certPEM = "-----BEGIN CERTIFICATE-----\n";
certPEM += Base64.getMimeEncoder().encodeToString(certBytes);
certPEM += "\n-----END CERTIFICATE-----\n";
return certPEM;
}
/**
* 基于根证书,生成客户端证书
*/
private static void generateClientCert(String domain, int days, String prefix) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, OperatorCreationException {
// root key
final KeyPair rootKeyPair = loadRootKey();
// root cert
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate rootCert = (X509Certificate) cf.generateCertificate(Files.newInputStream(Paths.get("pub/RootCA.pem")));
// generate client key
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();
// generate client cert
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(rootKeyPair.getPrivate());
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
rootCert,
BigInteger.valueOf(System.currentTimeMillis()),
new Date(),
new Date(System.currentTimeMillis() + (long) days * 24 * 60 * 60 * 1000),
new X500Name("CN=" + domain), // replace with your client DN
clientKeyPair.getPublic()
);
X509CertificateHolder certHolder = certBuilder.build(contentSigner);
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
X509Certificate clientCert = certConverter.getCertificate(certHolder);
// write out key file
final String clientKeyContent = convertPrivateKeyToPem(clientKeyPair.getPrivate());
Files.write(Paths.get("pub/" + prefix + "_" + domain + ".key"), clientKeyContent.getBytes());
// write out cert file
final String cert = convertCertToPem(clientCert);
Files.write(Paths.get("pub/" + prefix + "_" + domain + ".pem"), cert.getBytes());
}
public static void main(String[] args) throws NoSuchAlgorithmException, CertificateException, OperatorCreationException, IOException, InvalidKeySpecException {
// 1.生成根Key, 保存到 ssl/privateKey, ssl/publicKey
generateRootKey();
// 2.生成CA证书, 保存到 pub/RootCA.pem, 100年
generateCert("RootCA", 36500);
// 3.生成客户端 Key,Cert : 保存到 pub/server_demo.cc.key, pub/server_demo.cc.pem
generateClientCert("demo.cc", 3650, "server"); // 用于配置 nginx
generateClientCert("demo.cc", 3650, "client"); // 用于配置 curl
log.info("Done");
}
}
执行程序之前,需要在根目录下手动创建 ssl,pub 目录。
这里没有使用 java 的keystore,原理上是一样的,但是为了方便理解。
而是采用了openssl相同的pem输出方式。