IoT

SSL自签名证书双向认证实践

2023-06-18  本文已影响0人  国服最坑开发

0x00 TLDR;

使用 openssl, nginx, curl 进行双向自签名认证实验
本机/etc/hosts 配置单机域名: 127.0.0.1 demo.cc

术语

在对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 小结

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输出方式。

上一篇下一篇

猜你喜欢

热点阅读