这篇帖子,不会过多解释X509证书的基础理论知识,也不会介绍太多SSL/TLS的基本信息,重点介绍如何用java实现SSL协议需要的X509规范的证书。
之前的博文,介绍过用openssl创建证书,并配合EMQ进行发布订阅的工作逻辑,基于openssl创建证书和秘钥,还算是比较简便的,然后,基于java创建证书的过程,就有些许的小不方便,能找到的公开资料并不是太多,看到的都是基于keytool指令进行构建的介绍,但是呢,这种方案,对于我们的物联网安全应用,似乎不是很和谐。于是,啃了一段时间的java.security的相关资料(系统的也没有,都是比较零散的),最终还是折腾出了基本所需的创建证书及导入证书等基本操作方案。
用java代码创建证书的逻辑,相对比较的灵活,比openssl灵活,整个过程完全可以由自己控制,遵循SSL规范,证书需要什么参数,逐个配置进去即可,有些参数是非必须的,比如extension参数,有很多可以不用。
X509证书规范,是需要CA签发intermediate certificate,然后由intermediate certificate签发下级证书,最终创建出leaf certificate或者叫用户证书。证书和私钥是配对使用的,证书中含有公钥,公钥加密的数据需要对应的私钥解密,私钥加密的数据需要对应的公钥进行解密,这些基本信息,或者叫基本理论,需要具备。
接下来,需要介绍如何通过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,下面也给出来,以享读者。
exportCrt:
/** * 将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:
/** * 导出私钥内容到文件中,以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(); } } } }
上述代码中,存在几个参数:
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:HOWTOemqtt-sslself1java>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 FilesJavajdk1.8.0_121jrelib t.jar!sunsecurity oolskeytoolCertAndKeyGen.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的相关配置,请参考我前面的博客文章,或者有其他详细内容需要探讨的话,可以关注我的博客,和我互动。