MQTT研究之EMQ:【JAVA代碼構建X509證書】

這篇帖子,不會過多解釋X509證書的基礎理論知識,也不會介紹太多SSL/TLS的基本信息,重點介紹如何用java實現SSL協議須要的X509規範的證書。java

 

以前的博文,介紹過用openssl建立證書,並配合EMQ進行發佈訂閱的工做邏輯,基於openssl建立證書和祕鑰,還算是比較簡便的,而後,基於java建立證書的過程,就有些許的小不方便,能找到的公開資料並非太多,看到的都是基於keytool指令進行構建的介紹,可是呢,這種方案,對於咱們的物聯網安全應用,彷佛不是很和諧。因而,啃了一段時間的java.security的相關資料(系統的也沒有,都是比較零散的),最終仍是折騰出了基本所需的建立證書導入證書等基本操做方案。node

 

用java代碼建立證書的邏輯,相對比較的靈活,比openssl靈活,整個過程徹底能夠由本身控制,遵循SSL規範,證書須要什麼參數,逐個配置進去便可,有些參數是非必須的,好比extension參數,有不少能夠不用。算法

 

X509證書規範,是須要CA簽發intermediate certificate,而後由intermediate certificate簽發下級證書,最終建立出leaf certificate或者叫用戶證書。證書和私鑰是配對使用的,證書中含有公鑰,公鑰加密的數據須要對應的私鑰解密,私鑰加密的數據須要對應的公鑰進行解密,這些基本信息,或者叫基本理論,須要具有apache

 

接下來,須要介紹如何經過java建立證書以及導出導入證書的代碼實現,以及遇到的一些問題,或者說注意事項。本博文的介紹,是基於RSA算法進行代碼展現的。安全

 

1. 建立根證書服務器

    /**
     * 建立根證書, 並保存根證書到指定路徑的文件中, 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=taikang,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
     * @throws NoSuchAlgorithmException
     * @throws NoSuchProviderException
     * @throws InvalidKeyException
     * @throws IOException
     * @throws CertificateException
     * @throws SignatureException
     * @throws UnrecoverableKeyException
     * @return 私鑰和證書對的map對象
     */
    public static HashMap<PrivateKey, X509Certificate> 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("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension((new KeyIdentifier(publicKey)).getIdentifier()));
            certExts.set("AuthorityKeyIdentifier", new AuthorityKeyIdentifierExtension(new KeyIdentifier(publicKey), null, null));
            certExts.set("BasicConstraints", new BasicConstraintsExtension(false,true,0));

            //配置證書的有效期,並生成根證書(自簽名證書)
            X509Certificate certificate = cak.getSelfCertificate(subject, new Date(),validDays * 24L * 60L * 60L, certExts);

            HashMap<PrivateKey, X509Certificate> rootCA = new HashMap<PrivateKey, X509Certificate>();
            rootCA.put(privateKey, certificate);

            exportCrt(certificate, rootCACrtPath);
            exportKey(privateKey, rootCAKeyPath);

//            String rootPath = "E:\\2018\\IOT\\MQTT\\javassl\\jsseRoot.keystore";
//            String rootPfxPath = "E:\\2018\\IOT\\MQTT\\javassl\\jsseRoot.pfx";
//            /**
//             * 經過下面的指令,能夠將keystore裏面的內容轉爲DER格式的證書jsseRoot.cer
//             * keytool -export -alias rootCA -storepass abcdef -file jsseRoot.cer -keystore jsseRoot.keystore
//             *
//             * 經過下面的指令,能夠將DER格式的證書轉化爲OPENSSL默認支持的PEM證書:
//             * openssl x509 -inform der -in jsseRoot.cer -out jsseRoot.pem
//             */
//            saveJks("rootCA", privateKey, "abcdef", new Certificate[]{certificate}, rootPath);
//
//            /**
//             * 經過下面的指令,能夠獲取證書的私鑰
//             * openssl pkcs12 -in jsseRoot.pfx -nocerts -nodes -out jsseRoot.key
//             */
//            savePfx("rootCA", privateKey, "abcdef", new Certificate[]{certificate}, rootPfxPath);
            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;
    }

這裏涉及到的兩個重要的函數,exportCrt以及exportKey,下面也給出來,以享讀者。session

exportCrt:dom

    /**
     * 將JAVA建立的證書內容導出到文件, 基於BASE64轉碼了。
     *
     *
     * @param devCrt 設備證書對象
     * @param crtPath 設備證書存儲路徑
     */
    public static void exportCrt(Certificate devCrt, String crtPath) {
        BASE64Encoder base64Crt = new BASE64Encoder();
        FileOutputStream fosCrt = null;
        try {
            fosCrt = new FileOutputStream(new File(crtPath));
            String cont = BEGIN_CERTIFICATE + NEW_LINE;
            fosCrt.write(cont.getBytes());
            base64Crt.encodeBuffer(devCrt.getEncoded(), fosCrt);
            cont = END_CERTIFICATE;
            fosCrt.write(cont.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fosCrt != null) {
                try {
                    fosCrt.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

exportKey:eclipse

   /**
     * 導出私鑰內容到文件中,以base64編碼。
     * 注意,java生成的私鑰文件默認是PKCS#8的格式,加載的時候,要注意對應關係。
     *
     * @param key
     * @param keyPath
     */
    public static void exportKey(PrivateKey key, String keyPath) {
        BASE64Encoder base64Crt = new BASE64Encoder();
        FileOutputStream fosKey = null;
        try {
            fosKey = new FileOutputStream(new File(keyPath));
            String cont = BEGIN_RSA_PRIVATE_KEY + NEW_LINE;
            fosKey.write(cont.getBytes());
            base64Crt.encodeBuffer(key.getEncoded(), fosKey);
            cont = END_RSA_PRIVATE_KEY;
            fosKey.write(cont.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fosKey != null) {
                try {
                    fosKey.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

上述代碼中,存在幾個參數:ide

private static final String NEW_LINE = System.getProperty("line.separator");
/** 證書摘要及簽名算法組 */
public static final String MSG_DIGEST_SIGN_ALGO = "SHA256withRSA";

/** 在將java生成的證書導出到文件的時候,須要將下面兩行信息對應的添加到證書內容的頭部後尾部 */
private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";

/** 在將java生成的私鑰導出到文件的時候,須要將下面兩行信息對應的添加到私鑰內容的頭部後尾部 */
private static final String BEGIN_RSA_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
private static final String END_RSA_PRIVATE_KEY = "-----END PRIVATE KEY-----";

 

2. 建立用戶證書,也就是leaf certificate.

證書建立過程,有很重要的一個基本理論,就是簽發證書的簽發者私鑰用來對申請證書者的公鑰的摘要進行簽名(Signature)。

    /**
     * 建立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 cert 新建立獲得的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 = 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();

            /*
             * 以上是證書的基本信息 若是要添加用戶擴展信息 則比較麻煩 首先要肯定version必須是v3不然不行 而後按照如下步驟
             *
             */
            exts.set("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension((new KeyIdentifier(publicKey)).getIdentifier()));
            exts.set("AuthorityKeyIdentifier", new AuthorityKeyIdentifierExtension(new KeyIdentifier(ca.getPublicKey()), null, null));
            exts.set("BasicConstraints", new BasicConstraintsExtension(false,false,0));
            x509CertInfo.set("extensions", 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;
    }

注意,這裏我遇到了些問題,在研究如何建立用戶證書的邏輯中,上述代碼紅色部分,很值得注意,X509CertInfo.ALGORITHM_ID這個值的設定,必須和後面證書籤名過程當中用到的算法配置信息一致,不然會出現錯誤。

例如:

1. AlgorithmId algorithmId = new AlgorithmId(AlgorithmId.SHA256_oid);
2.
cert.sign(caPrivateKey, sginAlgo);

將上述代碼中紅色部分的代碼,對應修改爲上面的代碼, 獲得的證書內容將是下面的樣子(openssl打開):
[root@mq2 new]# openssl x509 -in dev003.crt -text 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1552444704 (0x5c886d20)
    Signature Algorithm: sha256
        Issuer: C=CN, ST=Hubei, L=Wuhan, O=Taikang, OU=TKcloud, CN=iotca
        Validity
            Not Before: Mar 13 02:38:24 2019 GMT
            Not After : Mar 10 02:38:24 2029 GMT
        Subject: C=CN, ST=Hubei, L=Wuhan, OU=TKCloud, O=TaiKang, CN=IOTPlatform
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:af:11:aa:cf:65:b8:15:7f:d7:d6:66:f3:20:6d:
                    9f:ed:b5:f7:d6:71:52:09:40:42:75:dd:b0:26:6b:
                    be:70:8b:3e:ec:3e:63:1a:83:02:13:42:76:ea:5f:
                    76:24:b5:e1:a8:b2:16:84:74:b8:27:04:bb:fc:d5:
                    93:ab:ef:de:94:82:8e:ae:3f:a0:53:4e:2f:12:5e:
                    a2:cd:10:00:7f:b2:7f:8c:4b:e5:62:ed:32:95:48:
                    b4:04:ba:40:4a:8e:43:78:d3:09:f3:31:49:09:e8:
                    c9:5b:7b:aa:88:25:44:d4:0a:d6:97:b6:13:f6:81:
                    be:e1:78:a0:34:a5:01:6b:4e:49:12:3e:b5:0b:85:
                    56:d4:bf:8b:b6:46:6f:32:d3:21:28:96:04:27:43:
                    ce:73:d8:07:b9:1d:05:55:9b:f0:8e:32:62:a3:11:
                    6d:e7:55:be:a7:06:96:15:e5:65:e6:3e:59:3d:5b:
                    a2:c0:91:dd:dd:7c:19:a8:a3:5c:4e:7e:96:64:48:
                    17:50:8c:28:09:2e:51:33:bb:73:83:99:dc:1c:40:
                    0e:1d:be:3b:e1:22:8f:a1:33:c7:5e:78:02:65:d6:
                    1d:19:24:8e:22:f7:bd:a7:98:23:47:34:3f:89:46:
                    39:29:46:ec:80:c5:ec:fb:0e:b2:e7:f5:6b:8a:93:
                    46:c1
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier: 
                keyid:F9:B0:44:83:66:08:68:54:3A:EB:E2:86:F6:28:42:EA:E6:F7:20:6F
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Subject Key Identifier: 
                FD:3D:EA:57:38:26:85:46:61:07:B6:B9:0E:0E:57:4C:4B:5F:13:D8
    Signature Algorithm: sha256WithRSAEncryption
         5c:dd:3c:c0:9a:01:e2:bb:f6:15:30:23:fa:96:17:df:25:ef:
         30:4a:3b:e7:f9:ad:10:28:32:45:a8:1e:4d:ef:ad:75:e8:3d:
         2b:4b:c9:cf:7e:d6:7d:23:c5:a0:da:8e:d0:6f:c6:49:9f:6a:
         05:25:50:25:dd:55:8c:f4:3a:40:7d:32:a2:ca:2f:4a:88:69:
         20:2d:3d:7b:9a:4a:01:8a:36:c0:8e:fc:0e:ac:e9:d7:ce:9b:
         34:e7:98:c7:eb:21:a7:b1:ab:88:e0:3e:60:47:6a:4e:0e:34:
         34:b0:b0:94:af:7f:f3:fd:e2:1c:06:c0:81:f7:29:11:eb:f4:
         e0:44:5c:4d:d1:e0:ef:75:db:34:f4:3e:b2:b0:fb:6d:f5:52:
         85:d7:d6:85:77:cd:04:bc:fd:7d:02:41:13:9b:5d:ad:b6:fc:
         57:6b:9e:5d:b3:3f:70:5f:a4:ee:fc:cf:d9:eb:92:1b:54:57:
         8b:94:5e:c2:d8:dc:78:03:fe:1c:eb:70:92:81:1f:00:e1:39:
         4d:4a:09:e1:18:65:87:e4:8c:4f:16:dc:1b:5c:6a:bc:9a:a6:
         f5:1f:21:db:db:3d:12:34:f9:df:54:71:30:67:98:cd:9b:5d:
         de:2b:73:88:83:02:c9:1b:b4:74:b6:63:73:d8:8d:ea:19:46:
         83:f0:c5:b3
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIEXIhtIDANBglghkgBZQMEAgEFADBhMQswCQYDVQQGEwJD
TjEOMAwGA1UECBMFSHViZWkxDjAMBgNVBAcTBVd1aGFuMRAwDgYDVQQKEwdUYWlr
YW5nMRAwDgYDVQQLEwdUS2Nsb3VkMQ4wDAYDVQQDEwVpb3RjYTAeFw0xOTAzMTMw
MjM4MjRaFw0yOTAzMTAwMjM4MjRaMGcxCzAJBgNVBAYTAkNOMQ4wDAYDVQQIEwVI
dWJlaTEOMAwGA1UEBxMFV3VoYW4xEDAOBgNVBAsTB1RLQ2xvdWQxEDAOBgNVBAoT
B1RhaUthbmcxFDASBgNVBAMTC0lPVFBsYXRmb3JtMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEArxGqz2W4FX/X1mbzIG2f7bX31nFSCUBCdd2wJmu+cIs+
7D5jGoMCE0J26l92JLXhqLIWhHS4JwS7/NWTq+/elIKOrj+gU04vEl6izRAAf7J/
jEvlYu0ylUi0BLpASo5DeNMJ8zFJCejJW3uqiCVE1ArWl7YT9oG+4XigNKUBa05J
Ej61C4VW1L+LtkZvMtMhKJYEJ0POc9gHuR0FVZvwjjJioxFt51W+pwaWFeVl5j5Z
PVuiwJHd3XwZqKNcTn6WZEgXUIwoCS5RM7tzg5ncHEAOHb474SKPoTPHXngCZdYd
GSSOIve9p5gjRzQ/iUY5KUbsgMXs+w6y5/VripNGwQIDAQABo00wSzAfBgNVHSME
GDAWgBT5sESDZghoVDrr4ob2KELq5vcgbzAJBgNVHRMEAjAAMB0GA1UdDgQWBBT9
PepXOCaFRmEHtrkODldMS18T2DANBgkqhkiG9w0BAQsFAAOCAQEAXN08wJoB4rv2
FTAj+pYX3yXvMEo75/mtECgyRageTe+tdeg9K0vJz37WfSPFoNqO0G/GSZ9qBSVQ
Jd1VjPQ6QH0yosovSohpIC09e5pKAYo2wI78Dqzp186bNOeYx+shp7GriOA+YEdq
Tg40NLCwlK9/8/3iHAbAgfcpEev04ERcTdHg73XbNPQ+srD7bfVShdfWhXfNBLz9
fQJBE5tdrbb8V2ueXbM/cF+k7vzP2euSG1RXi5RewtjceAP+HOtwkoEfAOE5TUoJ
4Rhlh+SMTxbcG1xqvJqm9R8h29s9EjT531RxMGeYzZtd3itziIMCyRu0dLZjc9iN
6hlGg/DFsw==
-----END CERTIFICATE-----

java代碼System.out.println()後的結果是:

[
[
  Version: V3
  Subject: CN=IOTPlatform, O=TaiKang, OU=TKCloud, L=Wuhan, ST=Hubei, C=CN
  Signature Algorithm: SHA-256, OID = 2.16.840.1.101.3.4.2.1

  Key:  Sun RSA public key, 2048 bits
  modulus: 22100415403461147769703305213448236435291091599500372576393207568700349231600053929377169253271708264086922062489915587742680699568147166292657405024687142663679862653326108966347003350382759105760873634692726436212445868677275254417601217934492671984075705736834665269777196054445727622919125252620280460414683757317803758405278316493298836245123113715548182249172360433950772288124650422206474078655361586976243735010284588656799317146969758352186734270791703268656883332820356976996092034036342017749761274195668094128310252837941203275081210268951855256858877697644483450150126554222919162374395071829414430066369
  public exponent: 65537
  Validity: [From: Wed Mar 13 10:38:24 CST 2019,
               To: Sat Mar 10 10:38:24 CST 2029]
  Issuer: CN=iotca, OU=TKcloud, O=Taikang, L=Wuhan, ST=Hubei, C=CN
  SerialNumber: [    5c886d20]

Certificate Extensions: 3
[1]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: F9 B0 44 83 66 08 68 54   3A EB E2 86 F6 28 42 EA  ..D.f.hT:....(B.
0010: E6 F7 20 6F                                        .. o
]
]

[2]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen:0
]

[3]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: FD 3D EA 57 38 26 85 46   61 07 B6 B9 0E 0E 57 4C  .=.W8&.Fa.....WL
0010: 4B 5F 13 D8                                        K_..
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 5C DD 3C C0 9A 01 E2 BB   F6 15 30 23 FA 96 17 DF  \.<.......0#....
0010: 25 EF 30 4A 3B E7 F9 AD   10 28 32 45 A8 1E 4D EF  %.0J;....(2E..M.
0020: AD 75 E8 3D 2B 4B C9 CF   7E D6 7D 23 C5 A0 DA 8E  .u.=+K.....#....
0030: D0 6F C6 49 9F 6A 05 25   50 25 DD 55 8C F4 3A 40  .o.I.j.%P%.U..:@
0040: 7D 32 A2 CA 2F 4A 88 69   20 2D 3D 7B 9A 4A 01 8A  .2../J.i -=..J..
0050: 36 C0 8E FC 0E AC E9 D7   CE 9B 34 E7 98 C7 EB 21  6.........4....!
0060: A7 B1 AB 88 E0 3E 60 47   6A 4E 0E 34 34 B0 B0 94  .....>`GjN.44...
0070: AF 7F F3 FD E2 1C 06 C0   81 F7 29 11 EB F4 E0 44  ..........)....D
0080: 5C 4D D1 E0 EF 75 DB 34   F4 3E B2 B0 FB 6D F5 52  \M...u.4.>...m.R
0090: 85 D7 D6 85 77 CD 04 BC   FD 7D 02 41 13 9B 5D AD  ....w......A..].
00A0: B6 FC 57 6B 9E 5D B3 3F   70 5F A4 EE FC CF D9 EB  ..Wk.].?p_......
00B0: 92 1B 54 57 8B 94 5E C2   D8 DC 78 03 FE 1C EB 70  ..TW..^...x....p
00C0: 92 81 1F 00 E1 39 4D 4A   09 E1 18 65 87 E4 8C 4F  .....9MJ...e...O
00D0: 16 DC 1B 5C 6A BC 9A A6   F5 1F 21 DB DB 3D 12 34  ...\j.....!..=.4
00E0: F9 DF 54 71 30 67 98 CD   9B 5D DE 2B 73 88 83 02  ..Tq0g...].+s...
00F0: C9 1B B4 74 B6 63 73 D8   8D EA 19 46 83 F0 C5 B3  ...t.cs....F....

]

keytool工具打開出現異常

E:\HOWTO\emqtt-ssl\self1\java>keytool -printcert -file dev003.crt
keytool 錯誤: java.lang.Exception: 沒法解析輸入

 

利用這種錯誤算法配置生成的證書,paho驗證時遇到下面的錯誤:

MqttException (0) - javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
    at org.eclipse.paho.client.mqttv3.internal.ExceptionHelper.createMqttException(ExceptionHelper.java:38)
    at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:715)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule.start(SSLNetworkModule.java:108)
    at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:701)
    ... 7 more

 

 

若將代碼配置成正確的樣子(以下):

AlgorithmId algorithmId = AlgorithmId.get(sginAlgo);
cert.sign(caPrivateKey, sginAlgo);

此時獲得的證書System.out.println()的結果是:

[
[
  Version: V3
  Subject: CN=IOTPlatform, O=TaiKang, OU=TKCloud, L=Wuhan, ST=Hubei, C=CN
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 2048 bits
  modulus: 24432147221753587234121698847557506215403000746362314743770473106244605156442048925471100863946024447744245247173143906374961637860133770351758954245624337633019840391612056971788053431344923912732756572547230066439048623717883548779197575248990533448640138554865164802391917653810705837231800178512454525858904683774761641844185822612274130202485583585437704579954701614249147763086827463785056404478747314726644724031516663902877405922164586802284780016485886812601548277739378779321593704405375065851220610582543746898662211429458672678390419016572208511891890989713985833494745283809342794778356154442311205558287
  public exponent: 65537
  Validity: [From: Wed Mar 13 10:49:38 CST 2019,
               To: Sat Mar 10 10:49:38 CST 2029]
  Issuer: CN=iotca, OU=TKcloud, O=Taikang, L=Wuhan, ST=Hubei, C=CN
  SerialNumber: [    5c886fc2]

Certificate Extensions: 3
[1]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: F9 B0 44 83 66 08 68 54   3A EB E2 86 F6 28 42 EA  ..D.f.hT:....(B.
0010: E6 F7 20 6F                                        .. o
]
]

[2]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen:0
]

[3]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 5D 48 8C FB B3 1C 23 00   16 34 A0 3B 74 3B C5 94  ]H....#..4.;t;..
0010: 4C 80 01 00                                        L...
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 33 D2 F1 08 E8 E9 FE 53   67 B2 9F 5F 59 3B D5 6F  3......Sg.._Y;.o
0010: 62 81 D4 39 07 91 98 F0   C7 0A 60 52 30 D8 0C 9F  b..9......`R0...
0020: 5B 16 04 76 38 42 08 9D   90 78 C5 04 1C BE 51 3C  [..v8B...x....Q<
0030: 38 A9 49 AE 7A 50 8F C9   CE 83 70 64 AE 81 03 D2  8.I.zP....pd....
0040: 4D 14 1E 74 A6 EB 33 14   7C 08 E4 C0 2F 8F 7D 55  M..t..3...../..U
0050: C5 F8 F4 FE D4 A1 B3 5D   D1 07 CB 7C 8A 2E 6D 1B  .......]......m.
0060: 09 ED C9 A1 8A 5D 51 81   C2 B3 4E 91 5D DB B1 36  .....]Q...N.]..6
0070: D3 F4 A3 62 5D 87 02 50   E4 CA 01 53 2E C1 A1 3D  ...b]..P...S...=
0080: 77 BD AF 47 A3 6D 3E EC   CC 79 9B 8F A8 05 10 5F  w..G.m>..y....._
0090: F1 D3 A2 E7 72 DF 3A 15   02 FA 15 C2 6E B2 1C DA  ....r.:.....n...
00A0: 29 07 20 1C 11 0B F3 EF   96 98 9B 93 6B D5 5C A9  ). .........k.\.
00B0: 88 77 B1 AA 08 D0 9B E5   FF 5F 66 9C BF B5 D9 20  .w......._f.... 
00C0: 6B E8 85 C4 50 7C 58 2A   89 76 86 7A A0 F6 FE 2C  k...P.X*.v.z...,
00D0: 11 B0 64 47 3E 43 F2 E5   CD DD 6C 56 ED 71 A4 1D  ..dG>C....lV.q..
00E0: DA B1 81 AA 57 13 EB B8   02 85 73 E7 D2 87 B4 90  ....W.....s.....
00F0: 00 27 03 5A E0 4A 74 39   78 A3 CF 6E 07 A7 F5 AE  .'.Z.Jt9x..n....

]

也就是說,正確的算法配置方法,是證書算法配置和簽名用到的算法必須是一致的。

關於這個配置,咱們能夠看看java代碼(jdk)中的關於生成自簽名證書的函數(C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar!\sun\security\tools\keytool\CertAndKeyGen.class),能夠佐證這個邏輯:

public X509Certificate getSelfCertificate(X500Name var1, Date var2, long var3, CertificateExtensions var5) throws CertificateException, InvalidKeyException, SignatureException, NoSuchAlgorithmException, NoSuchProviderException {
        try {
            Date var7 = new Date();
            var7.setTime(var2.getTime() + var3 * 1000L);
            CertificateValidity var8 = new CertificateValidity(var2, var7);
            X509CertInfo var9 = new X509CertInfo();
            var9.set("version", new CertificateVersion(2));
            var9.set("serialNumber", new CertificateSerialNumber((new Random()).nextInt() & 2147483647));
            AlgorithmId var10 = AlgorithmId.get(this.sigAlg);
            var9.set("algorithmID", new CertificateAlgorithmId(var10));
            var9.set("subject", var1);
            var9.set("key", new CertificateX509Key(this.publicKey));
            var9.set("validity", var8);
            var9.set("issuer", var1);
            if (var5 != null) {
                var9.set("extensions", var5);
            }

            X509CertImpl var6 = new X509CertImpl(var9);
            var6.sign(this.privateKey, this.sigAlg);
            return var6;
        } catch (IOException var11) {
            throw new CertificateEncodingException("getSelfCert: " + var11.getMessage());
        }
    }

 

3. 證書和私鑰導入

   /**
     * 獲得私鑰, 記得這個文件是相似PEM格式的問題,須要將文件頭部的----BEGIN和尾部的----END信息去掉
     *
     * @param privateKey 密鑰字符串(通過base64編碼)
     * @throws Exception
     */
    public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
        //經過PKCS#8編碼的Key指令得到私鑰對象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        if(privateKey.startsWith(BEGIN_RSA_PRIVATE_KEY)) {
            int bidx = BEGIN_RSA_PRIVATE_KEY.length();
            privateKey = privateKey.substring(bidx);
        }
        if (privateKey.endsWith(END_RSA_PRIVATE_KEY)) {
            int eidx = privateKey.indexOf(END_RSA_PRIVATE_KEY);
            privateKey = privateKey.substring(0, eidx);
        }
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
        return key;
    }
    /**
     * 從通過base64轉化後的證書文件中構建證書對象,是一個標準的X509證書,
     *
     * 且很是重要的是,文件頭部含有-----BEGIN CERTIFICATE-----
     * 文件的尾部含有 -----END CERTIFICATE-----
     * 若沒有上述頭和尾部,證書驗證的時候會報certificate_unknown。
     *
     * @param crtFile 通過base64處理的證書文件
     * @return X509的證書
     */
    public static X509Certificate getCertficate(File crtFile) {
        //這個客戶端證書,是用來發送給服務端的,準備作雙向驗證用的。
        CertificateFactory cf;
        X509Certificate cert = null;
        FileInputStream crtIn = null;
        try {
            cf = CertificateFactory.getInstance(X509);
            crtIn = new FileInputStream(crtFile);
            cert = (X509Certificate) cf.generateCertificate(crtIn);
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(crtIn != null){
                try {
                    crtIn.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return cert;
    }

導入證書和私鑰的地方,須要注意,java代碼中有個奇怪的現象,對應私鑰的導入,私鑰文件內容(字符串)中,不能含有相似BEGIN PRIVATE KEY的頭和尾部信息,才能正確的導入並生成PrivateKey對象,可是呢,對應Certificate文件內容(字符串)導入並建立證書對象的時候,文件內容中必需要含有BEGIN CERTIFICATE之類的頭部信息,不然導入證書失敗

 

關於從文件導入證書和私鑰並構建java對象,驗證MQTT環境下,與服務端創建鏈接的邏輯,給出一個demo:

package com.taikang.iot.scc.ca;

import com.taikang.iot.scc.utils.MySSL;
import org.apache.commons.io.FileUtils;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.concurrent.ScheduledExecutorService;

/**
 * @Author: chengsh05
 * @Date: 2019/3/8 15:22
 */
public class JsseConsumer {

    public static final String HOST = "ssl://10.95.197.3:8883";
    public static final String TOPIC1 = "taikang/rulee";
    private static final String clientid = "dev001";
    private MqttClient client;
    private MqttConnectOptions options;
    private String userName = "shihuc";    //非必須
    private String passWord = "shihuc";  //非必須
    @SuppressWarnings("unused")
    private ScheduledExecutorService scheduler;
    private String sslPemPath = "E:\\HOWTO\\emqtt-ssl\\self1\\";
    private String sslCerPath = "E:\\HOWTO\\emqtt-ssl\\self1\\java\\";

    private void start() {
        try {
            // host爲主機名,clientid即鏈接MQTT的客戶端ID,通常以惟一標識符表示,MemoryPersistence設置clientid的保存形式,默認爲之內存保存
            client = new MqttClient(HOST, clientid, new MemoryPersistence());
            // MQTT的鏈接設置
            options = new MqttConnectOptions();

 String devCrtCtx = FileUtils.readFileToString(new File(sslCerPath,"dev003.crt"), MySSL.CHARSET); String devKeyCtx = FileUtils.readFileToString(new File(sslCerPath,"dev003.key"), MySSL.CHARSET); X509Certificate devCrt = MySSL.getCertficate(devCrtCtx); PrivateKey devKey = MySSL.getPrivateKey(devKeyCtx); 
            String rootCACrtPath = "E:\\HOWTO\\emqtt-ssl\\self1\\java\\rootCA0.crt";
            String rootCAKeyPath = "E:\\HOWTO\\emqtt-ssl\\self1\\java\\ca0.key";

            CertificateFactory cAf = CertificateFactory.getInstance("X.509");
            FileInputStream caIn = new FileInputStream(rootCACrtPath);
            X509Certificate ca = (X509Certificate) cAf.generateCertificate(caIn);
            PrivateKey caKey = (PrivateKey) MySSL.getPrivateKey(new File(rootCAKeyPath));

            SSLSocketFactory factory = MySSL.getSSLSocketFactory(ca, devCrt, devKey, "shihuc");
            options.setSocketFactory(factory);


            // 設置是否清空session,這裏若是設置爲false表示服務器會保留客戶端的鏈接記錄,設置爲true表示每次鏈接到服務器都以新的身份鏈接
            options.setCleanSession(false);
            // 設置鏈接的用戶名
            options.setUserName(userName);
            // 設置鏈接的密碼
            options.setPassword(passWord.toCharArray());
            // 設置超時時間 單位爲秒
            options.setConnectionTimeout(10);
            // 設置會話心跳時間 單位爲秒 服務器會每隔1.5*20秒的時間向客戶端發送個消息判斷客戶端是否在線,但這個方法並無重連的機制
            options.setKeepAliveInterval(20);
            // 設置重連機制
            options.setAutomaticReconnect(false);
            // 設置回調
            client.setCallback(new BuizCallback());
            MqttTopic topic = client.getTopic(TOPIC1);
            //setWill方法,若是項目中須要知道客戶端是否掉線能夠調用該方法。設置最終端口的通知消息
            //options.setWill(topic, "close".getBytes(), 2, true);//遺囑
            client.connect(options);
            //訂閱消息
            int[] Qos  = {1};
            String[] topic1 = {TOPIC1};
            client.subscribe(topic1, Qos);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws MqttException {
        System.setProperty("javax.net.debug", "ssl,handshake");
        JsseConsumer client = new JsseConsumer();
        client.start();
    }
}

 

關於其中paho的相關配置,請參考我前面的博客文章,或者有其餘詳細內容須要探討的話,能夠關注個人博客,和我互動。

相關文章
相關標籤/搜索