• MQTT研究之EMQ:【JAVA代码构建X509证书【续集】】


    openssl创建私钥,获取公钥,创建证书都是比较简单的,就几个指令,很快就可以搞定,之所以说简单,是因为证书里面的基本参数配置不需要我们组装,只需要将命令行里面需要的几个参数配置进去即可。但是呢,用java代码,原生创建证书,其实需要我们了解的内容就要稍微多点,去填充创建证书里面的所需要的参数,逐行填充。

    openssl证书的格式默认是PEM的,即Privacy Enhanced Mail,说白了,就是将创建后的证书元素数据经过Base64编码,然后添加类似----BEGIN CERTIFICATE----、----END CERTIFICATE----这样的头和尾,是ASCII编码的纯字符串格式。

    现在完整的介绍一下java创建PEM证书的逻辑。都遵循X509的协议,创建证书,和openssl逻辑有点不同的是,java创建证书不需要构建CSR(Certificate Sign Request)这个步骤,同样需要创建私钥/公钥,创建CA证书,构建设备证书。

    本博文是对前叙的博文的一个补充 MQTT研究之EMQ:【JAVA代码构建X509证书】,内容涉及的话题相同,但是角度有些不一样。

    1. 私钥/公钥创建

    /**
         * 创建私钥和公钥的数据,以一个map的形式返回。
         *
         * @param keySize 私钥的长度
         * @param keyAlgo 创建私钥的算法,例如RSA,DSA等
         * @return map 私钥和公钥对信息
         */
        public static Map<String, String> createKeys(int keySize, String keyAlgo){
            //为RSA算法创建一个KeyPairGenerator对象
            KeyPairGenerator kpg;
            try{
                kpg = KeyPairGenerator.getInstance(keyAlgo);
            }catch(NoSuchAlgorithmException e) {
                throw new IllegalArgumentException("No such algorithm-->[" + keyAlgo + "]");
            }
    
            //初始化KeyPairGenerator对象,密钥长度
            kpg.initialize(keySize);
            //生成密匙对
            KeyPair keyPair = kpg.generateKeyPair();
            //得到公钥
            Key publicKey = keyPair.getPublic();
            String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
            //得到私钥
            Key privateKey = keyPair.getPrivate();
            String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
            Map<String, String> keyPairMap = new HashMap<String, String>();
            keyPairMap.put(PUBLIC_KEY, publicKeyStr);
            keyPairMap.put(PRIVATE_KEY, privateKeyStr);
            return keyPairMap;
        }

    2. 创建CA自签名证书

    /**
         * 创建根证书, 并保存根证书到指定路径的文件中, crt和key分开存储文件。
         * 创建SSL根证书的逻辑,很重要,此函数调用频次不高,创建根证书,也就是自签名证书。
         *
         * @param algorithm 私钥安全算法,e.g. RSA
         * @param keySize 私钥长度,越长越安全,RSA要求不能小于512, e.g. 2048
         * @param digestSignAlgo 信息摘要以及签名算法 e.g. SHA256withRSA
         * @param subj 证书所有者信息描述,e.g. CN=iotp,OU=tkcloud,O=TanKang,L=wuhan,S=hubei,C=CN
         * @param validDays 证书有效期天数,e.g. 3650即10年
         * @param rootCACrtPath 根证书所要存入的全路径,e.g. /opt/certs/iot/rootCA.crt
         * @param rootCAKeyPath 根证书对应秘钥key所要存入的全路径,e.g. /opt/certs/iot/rootCA.key
         * @return 私钥和证书对的map对象
         */
        public static HashMap<String, Object> createRootCA(String algorithm, int keySize, String digestSignAlgo,
                                                   String subj, long validDays, String rootCACrtPath, String rootCAKeyPath) {
    
            //参数分别为 公钥算法 签名算法 providerName(因为不知道确切的 只好使用null 既使用默认的provider)
            CertAndKeyGen cak = null;
            try {
                cak = new CertAndKeyGen(algorithm, digestSignAlgo,null);
                //生成一对key 参数为key的长度 对于rsa不能小于512
                cak.generate(keySize);
                cak.setRandom(new SecureRandom());
    
                //证书拥有者subject的描述name
                X500Name subject = new X500Name(subj);
    
                //给证书配置扩展信息
                PublicKey publicKey = cak.getPublicKey();
                PrivateKey privateKey = cak.getPrivateKey();
                CertificateExtensions certExts = new CertificateExtensions();
                certExts.set(SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension((new KeyIdentifier(publicKey)).getIdentifier()));
                certExts.set(AuthorityKeyIdentifierExtension.NAME, new AuthorityKeyIdentifierExtension(new KeyIdentifier(publicKey), null, null));
                //设置是否根证书
                BasicConstraintsExtension bce = new BasicConstraintsExtension(true, -1);
                certExts.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(false, bce.getExtensionValue()));
    
                //配置证书的有效期,并生成根证书(自签名证书)
                X509Certificate certificate = cak.getSelfCertificate(subject, new Date(),validDays * 24L * 60L * 60L, certExts);
    
                HashMap<String, Object> rootCA = new HashMap<>();
                rootCA.put("key", privateKey);
                rootCA.put("crt", certificate);
    
                exportCrt(certificate, rootCACrtPath);
                exportKey(privateKey, rootCAKeyPath);
    
                return rootCA;
    
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchProviderException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (SignatureException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return null;
        }

    3. 创建用户证书

    /**
         * 创建X509的证书, 由ca证书完成签名。
         *
         * subject,issuer都遵循X500Principle规范,
         * 即: X500Principal由可分辨名称表示,例如“CN = Duke,OU = JavaSoft,O = Sun Microsystems,C = US”。
         *
         * @param ca 根证书对象
         * @param caKey CA证书对应的私钥对象
         * @param publicKey 待签发证书的公钥对象
         * @param subj 证书拥有者的主题信息,签发者和主题拥有者名称都转写X500Principle规范,格式:CN=country,ST=state,L=Locality,OU=OrganizationUnit,O=Organization
         * @param validDays 证书有效期天数
         * @param sginAlgo 证书签名算法, e.g. SHA256withRSA
         *
         * @return security 新创建得到的X509证书
         */
        public static X509Certificate createUserCert(X509Certificate ca, PrivateKey caKey, PublicKey publicKey, String subj, long validDays, String sginAlgo)  {
    
            //获取ca证书
            X509Certificate caCert = ca;
    
            X509CertInfo x509CertInfo = new X509CertInfo();
    
            try {
                //设置证书的版本号
                x509CertInfo.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
    
                //设置证书的序列号,基于当前时间计算
                x509CertInfo.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber((int) (System.currentTimeMillis() / 1000L)));
    
                /**
                 * 下面这个设置算法ID的代码,是错误的,会导致证书验证失败,但是报错不是很明确。 若将生成的证书存为keystore,让后keytool转换
                 * 会出现异常。
                 *
                 * 重点: AlgorithmId的参数设置要和后面的证书签名中用到的算法信息一致。
                 *
                 * AlgorithmId algorithmId = new AlgorithmId(AlgorithmId.SHA256_oid);
                 */
                AlgorithmId algorithmId = AlgorithmId.get(sginAlgo);
                x509CertInfo.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algorithmId));
    
                //设置证书的签发者信息
                X500Name issuer = new X500Name(caCert.getIssuerX500Principal().toString());
                x509CertInfo.set(X509CertInfo.ISSUER, issuer);
    
                //设置证书的拥有者信息
                X500Name subject = new X500Name(subj);
                x509CertInfo.set(X509CertInfo.SUBJECT, subject);
    
                //设置证书的公钥
                x509CertInfo.set(X509CertInfo.KEY, new CertificateX509Key(publicKey));
    
                //设置证书有效期
                Date beginDate = new Date();
                Date endDate = new Date(beginDate.getTime() + validDays * 24 * 60 * 60 * 1000L);
                CertificateValidity cv = new CertificateValidity(beginDate, endDate);
                x509CertInfo.set(X509CertInfo.VALIDITY, cv);
    
                CertificateExtensions exts = new CertificateExtensions();
    
                exts.set(SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension((new KeyIdentifier(publicKey)).getIdentifier()));
                exts.set(AuthorityKeyIdentifierExtension.NAME, new AuthorityKeyIdentifierExtension(new KeyIdentifier(ca.getPublicKey()), null, null));
                exts.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(false,false,-1));
                x509CertInfo.set(CertificateExtensions.NAME, exts);
    
            } catch (CertificateException cee) {
                cee.printStackTrace();
            } catch (IOException eio) {
                eio.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
    
            // 获取CA私钥
            PrivateKey caPrivateKey = caKey;
            //用CA的私钥给当前证书进行签名,获取最终的下游证书(证书链的下一节点)
            X509CertImpl cert = new X509CertImpl(x509CertInfo);
            try {
                cert.sign(caPrivateKey, sginAlgo);
            } catch (InvalidKeyException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException | SignatureException e3) {
                e3.printStackTrace();
            }
            return cert;
        }

    4. demo程序
    1) 从根证书开始创建,包含自签名证书,设备证书以及服务节点证书

    private static void demoGenFull(String basePath, String devName, String emqHost) throws InvalidKeySpecException, NoSuchAlgorithmException {
            String subjCA = "CN=IOTPlatform,OU=TanKang,O=TKCloud,L=Wuhan,S=Hubei,C=CN";
            String rootCACrtPath = basePath + "sccCA0.crt";
            String rootCAKeyPath = basePath + "sccCA0.key";
    
            String subjDev = "OU=TanKang,O=TKCloud,L=Wuhan,ST=Hubei,C=CN,CN=IOTDevice" + devName;
            String devCrtPath = basePath + "sccDev" + devName + ".crt";
            String devKeyPath = basePath + "sccDev" + devName + ".key";
    
            String subjEmq = "OU=TanKang,O=TKCloud,L=Wuhan,ST=Hubei,C=CN,CN=" + emqHost;
            String emqCommName = emqHost.replace(".", "-");
            String emqCrtPath = basePath + "sccEmq" + emqCommName + ".crt";
            String emqKeyPath = basePath + "sccEmq" + emqCommName + ".key";
    
            /**
             * 创建根证书,即自签名证书
             */
            HashMap<String, Object> rootCA = MySSL.createRootCA("RSA",2048, MSG_DIGEST_SIGN_ALGO,  subjCA, 3650);
            X509Certificate caCrt = (X509Certificate) rootCA.get(CERTIFICATE);
            PrivateKey caKey = (PrivateKey)rootCA.get(PRIVATE_KEY);
            MySSL.exportCrt(caCrt, rootCACrtPath);
            MySSL.exportKey(caKey, rootCAKeyPath);
    
            /**
             * 创建公钥和私钥对,然后基于自签名证书签发设备证书,即客户端证书
             */
            Map<String, String> keyDev = MySSL.createKeys(2048, RSA_ALGORITHM);
            PublicKey devPubKey = MySSL.getPublicKey(keyDev.get(PUBLIC_KEY));
            PrivateKey devPriKey = MySSL.getPrivateKey(keyDev.get(PRIVATE_KEY));
            X509Certificate devCrt = MySSL.createUserCert(caCrt, caKey, devPubKey, subjDev, 3650, MSG_DIGEST_SIGN_ALGO);
            MySSL.exportCrt(devCrt, devCrtPath);
            MySSL.exportKey(devPriKey, devKeyPath);
    
            /**
             * 创建公钥和私钥对,然后基于自签名证书签发EMQ证书,即服务端证书。
             */
            Map<String, String> keyEmq = MySSL.createKeys(2048, RSA_ALGORITHM);
            PublicKey emqPubKey = MySSL.getPublicKey(keyEmq.get(PUBLIC_KEY));
            PrivateKey emqPriKey = MySSL.getPrivateKey(keyEmq.get(PRIVATE_KEY));
            X509Certificate emqCrt = MySSL.createUserCert(caCrt, caKey, emqPubKey, subjEmq, 3650, MSG_DIGEST_SIGN_ALGO);
            MySSL.exportCrt(emqCrt, emqCrtPath);
            MySSL.exportKey(emqPriKey, emqKeyPath);
        }

    2) 根证书已经创建了,通过加载根证书的方式,签发设备证书以及服务端节点证书

    private static void demoGenUserCertWithExistedCA(String basePath, String devName, String emqHost) throws InvalidKeySpecException, NoSuchAlgorithmException {
            String rootCACrtPath = basePath + "sccCA0.crt";
            String rootCAKeyPath = basePath + "sccCA0.key";
    
            String subjDev = "OU=TanKang,O=TKCloud,L=Wuhan,ST=Hubei,C=CN,CN=IOTDevice" + devName;
            String devCrtPath = basePath + "sccDev" + devName + ".crt";
            String devKeyPath = basePath + "sccDev" + devName + ".key";
    
            String subjEmq = "OU=TanKang,O=TKCloud,L=Wuhan,ST=Hubei,C=CN,CN=" + emqHost;
            String emqCommName = emqHost.replace(".", "-");
            String emqCrtPath = basePath + "sccEmq" + emqCommName + ".crt";
            String emqKeyPath = basePath + "sccEmq" + emqCommName + ".key";
    
            /**
             * 从指定的文件加载构建根证书以及对应的私钥
             */
            X509Certificate caCrt = MySSL.getCertficate(new File(rootCACrtPath));
            PrivateKey caKey = MySSL.getPrivateKey(new File(rootCAKeyPath));
    
            /**
             * 创建公钥和私钥对,然后基于自签名证书签发设备证书,即客户端证书
             */
            Map<String, String> keyDev = MySSL.createKeys(2048, RSA_ALGORITHM);
            PublicKey devPubKey = MySSL.getPublicKey(keyDev.get(PUBLIC_KEY));
            PrivateKey devPriKey = MySSL.getPrivateKey(keyDev.get(PRIVATE_KEY));
            X509Certificate devCrt = MySSL.createUserCert(caCrt, caKey, devPubKey, subjDev, 3650, MSG_DIGEST_SIGN_ALGO);
            MySSL.exportCrt(devCrt, devCrtPath);
            MySSL.exportKey(devPriKey, devKeyPath);
    
            /**
             * 创建公钥和私钥对,然后基于自签名证书签发EMQ证书,即服务端证书。
             */
            Map<String, String> keyEmq = MySSL.createKeys(2048, RSA_ALGORITHM);
            PublicKey emqPubKey = MySSL.getPublicKey(keyEmq.get(PUBLIC_KEY));
            PrivateKey emqPriKey = MySSL.getPrivateKey(keyEmq.get(PRIVATE_KEY));
            X509Certificate emqCrt = MySSL.createUserCert(caCrt, caKey, emqPubKey, subjEmq, 3650, MSG_DIGEST_SIGN_ALGO);
            MySSL.exportCrt(emqCrt, emqCrtPath);
            MySSL.exportKey(emqPriKey, emqKeyPath);
        }

    其中,通过文件重构私钥的函数getPrivateKey(File file)的函数如下:

       /**
         * 利用开源的工具类BC解析私钥,例如openssl私钥文件格式为pem,需要去除页眉页脚后才能被java读取
         *
         * @param file 私钥文件
         * @return 私钥对象
         */
        public static PrivateKey getPrivateKey(File file) {
            if (file == null) {
                return null;
            }
            PrivateKey privKey = null;
            PemReader pemReader = null;
            try {
                pemReader = new PemReader(new FileReader(file));
                PemObject pemObject = pemReader.readPemObject();
                byte[] pemContent = pemObject.getContent();
                //支持从PKCS#1或PKCS#8 格式的私钥文件中提取私钥, PKCS#1的私钥,主要是openssl默认生成的编码格式
                if (pemObject.getType().endsWith("RSA PRIVATE KEY")) {
                    /*
                     * 取得私钥  for PKCS#1
                     * openssl genrsa 默认生成的私钥就是PKCS1的编码
                     */
                    org.bouncycastle.asn1.pkcs.RSAPrivateKey asn1PrivateKey = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(pemContent);
                    RSAPrivateKeySpec rsaPrivateKeySpec = new RSAPrivateKeySpec(asn1PrivateKey.getModulus(), asn1PrivateKey.getPrivateExponent());
                    KeyFactory keyFactory= KeyFactory.getInstance(RSA_ALGORITHM);
                    privKey= keyFactory.generatePrivate(rsaPrivateKeySpec);
                } else if (pemObject.getType().endsWith("PRIVATE KEY")) {
                    /*
                     * java创建的私钥,默认是PKCS#8格式
                     *
                     * 通过openssl pkcs8 -topk8转换为pkcs8,例如(-nocrypt不做额外加密操作):
                     * openssl pkcs8 -topk8 -in pri.key -out pri8.key -nocrypt
                     *
                     * 取得私钥 for PKCS#8
                     */
                    PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(pemContent);
                    KeyFactory kf = KeyFactory.getInstance(RSA_ALGORITHM);
                    privKey = kf.generatePrivate(privKeySpec);
                }
            } catch (FileNotFoundException e) {
                logger.error("read private key fail,the reason is the file not exist");
                e.printStackTrace();
            } catch (IOException e) {
                logger.error("read private key fail,the reason is :"+e.getMessage());
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                logger.error("read private key fail,the reason is :"+e.getMessage());
                e.printStackTrace();
            } catch (InvalidKeySpecException e) {
                logger.error("read private key fail,the reason is :"+e.getMessage());
                e.printStackTrace();
            }  finally {
                try {
                    if (pemReader != null) {
                        pemReader.close();
                    }
                } catch (IOException e) {
                    logger.error(e.getMessage());
                }
            }
            return privKey;
        }

    这个补充的博文,不做过多解释,所有的代码,都浅显易懂,创建的证书等文件,通过openssl工具,当做PEM格式的文件进行查看或者其他操作,都是可以的。若有什么不清楚,请关注我的博客,给我留言,一起探讨!

  • 相关阅读:
    程序猿神吐槽,说出那些苦逼的日子!
    java设计模式演示样例
    DP Leetcode
    标准差(standard deviation)和标准误差(standard error)你能解释清楚吗?
    移动开发 Native APP、Hybrid APP和Web APP介绍
    Oracle cloud control 12c 的启动与关闭
    Ubuntu文件的复制、移动和删除命令
    Android中集成第三方库的方法和问题
    腾讯QQ企业邮箱POP3/SMTP设置
    又发现支付宝和淘宝的设计漏洞
  • 原文地址:https://www.cnblogs.com/shihuc/p/10642284.html
Copyright © 2020-2023  润新知