美文网首页IoT
SSL自签名证书双向认证实践

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

作者: 国服最坑开发 | 来源:发表于2023-06-18 17:29 被阅读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输出方式。

    相关文章

      网友评论

        本文标题:SSL自签名证书双向认证实践

        本文链接:https://www.haomeiwen.com/subject/xzajydtx.html