背景消息设备证书是由CA根证书签发给客户端设备使用的数字证书,用于客户端和服务端连接时,服务端对客户端进行安全认证。认证通过后服务端和客户端可基于证书内的加密密钥进行安全通信,若认证不通过则服务端拒绝客户端接入。使用设备证书认证时,必须保证签发该设备证书的CA证书已在MQTT服务端中注册。客户端设备使用设备证书进行接入认证时,服务端会根据已注册的CA证书验证设备证书是否正确,若CA证书和设备证书匹配成功,则客户端认证通过,且系统会将该设备证书自动注册到服务端中。双向SSLTLS安全连接 作为基于现代密码学公钥算法的安全协议,TLSSSL能在计算机通讯网络上保证传输安全,很多MQTTBroker内置对TLSSSL的支持,包括支持单双向认证、X。509证书、负载均衡SSL等多种安全认证。SSLTLS安全优势强认证。用TLS建立连接的时候,通讯双方可以互相检查对方的身份。在实践中,很常见的一种身份检查方式是检查对方持有的X。509数字证书。这样的数字证书通常是由一个授信机构颁发的,不可伪造。保证机密性。TLS通讯的每次会话都会由会话密钥加密,会话密钥由通讯双方协商产生。任何第三方都无法知晓通讯内容。即使一次会话的密钥泄露,并不影响其他会话的安全性。完整性。加密通讯中的数据很难被篡改而不被发现。SSLTLS协议 TLSSSL协议下的通讯过程分为两部分,第一部分是握手协议。握手协议的目的是鉴别对方身份并建立一个安全的通讯通道。握手完成之后双方会协商出接下来使用的密码套件和会话密钥;第二部分是record协议,record和其他数据传输协议非常类似,会携带内容类型,版本,长度和荷载等信息,不同的是它所携带的信息是加密了的。 下面的图片描述了TLSSSL握手协议的过程,从客户端的hello一直到服务器的finished完成握手。有兴趣的同学可以找更详细的资料看。 SSLTLS证书准备 在双向认证中,一般都使用自签名证书的方式来生成服务端和客户端证书,因此本文就以自签名证书为例。 通常来说,我们需要数字证书来保证TLS通讯的强认证。数字证书的使用本身是一个三方协议,除了通讯双方,还有一个颁发证书的受信第三方,有时候这个受信第三方就是一个CA。和CA的通讯,一般是以预先发行证书的方式进行的。也就是在开始TLS通讯的时候,我们需要至少有2个证书,一个CA的,一个MQTT服务端的,MQTT服务端的证书由CA颁发,并用CA的证书验证。 在这里,我们假设您的系统已经安装了OpenSSL。使用OpenSSL附带的工具集就可以生成我们需要的证书了。已安装OpenSSLv1。1。1i或以上版本。 使用OpenSSL创建生成CA证书、服务器、客户端证书及密钥生成CA证书生成服务器证书生成客户端证书对于SSL单向认证:服务器需要CA证书、server证书、server私钥,客户端需要CA证。对于SSL双向认证:服务器需要CA证书、server证书、server私钥,客户端需要CA证书,client证书、client私钥。各类证书与密钥文件后缀的解释 总得来说这些文件都与X。509证书和密钥文件有关,从文件编码上分,只有两大类: PEM格式:使用Base64ASCII进行编码的纯文本格式 DER格式:二机制格式 而CRT,CER,KEY这几种证书和密钥文件,它们都有自己的schema,在存储为物理文件时,既可以是PEM格式,也可以DER格式。 CER:一般用于windows的证书文件格式 CRT:一般用于Linux的证书,包含公钥和主体信息 KEY:一般用于密钥,特别是私钥,与证书一一配对 打个比方:CER,CRT,KEY相当于论文,说明书等,有规定好的行文格式与规范,而PEM和DER相当于txt格式还是word格式。 CSR:CertificateSigningRequest,即证书签名请求文件。证书申请者在生成私钥的同时也生成证书请求文件。把CSR文件提交给证书颁发机构后,证书颁发机构使用其根证书私钥签名就生成了证书公钥文件,也就是颁发给用户的证书。证书生成1。生成CA证书1。创建CA证书私钥opensslgenrsaoutca。key20482。请求证书证数各参数含义如下C国家(CountryName)ST省份(StateorProvinceName)L城市(LocalityName)O公司(OrganizationName)OU部门(OrganizationalUnitName)CN产品名(CommonName)emailAddress邮箱(EmailAddress)opensslreqnewsha256keyca。keyoutca。csrsubjCCNSTSZLSZOC。X。LOUC。X。LCNCAemailAddress123456test。com3。自签署证书opensslx509reqdays36500sha256extensionsv3casignkeyca。keyinca。csroutca。crt2。生成服务端证书1。创建服务器私钥 opensslgenrsaoutserver。key2048新建openssl。cnf文件,reqdistinguishedname:根据情况进行修改,altnames:BROKERADDRESS修改为EMQX服务器实际的IP或DNS地址,例如:IP。1127。0。0。1,或DNS。1broker。xxx。com注意:IP和DNS二者保留其一即可,如果已购买域名,只需保留DNS并修改为你所使用的域名地址。〔req〕defaultbits2048distinguishednamereqdistinguishednamereqextensionsreqextx509extensionsv3reqpromptno〔reqdistinguishedname〕countryNameCNstateOrProvinceNameSZorganizationNameC。X。LorganizationalUnitNameC。X。LcommonNameservice〔reqext〕subjectAltNamealtnames〔v3req〕subjectAltNamealtnames〔altnames〕IP。1127。0。0。1IP。2192。168。5。2492。请求证书opensslreqnewsha256keyserver。keyconfigopenssl。cnfoutserver。csr3。使用CA证书签署服务器证书opensslx509reqinserver。csrCAca。crtCAkeyca。keyCAcreateserialoutserver。crtdays3650sha256extensionsv3reqextfileopenssl。cnf4。验证服务端证书opensslverifyCAfileca。crtserver。crt5查看服务端证书opensslx509noouttextinserver。crt6。Netty需要支持PKCS8格式读取私钥opensslpkcs8topk8nocryptinserver。keyoutpkcs8key。pem 注:错误日志也很明确的打印了:atsun。security。pkcs。PKCS8Key。decode(PKCS8Key。java:351),采用PKCS8无法解析证书。这是因为部分MQTTbroker使用的是netty,netty默认使用PKCS8格式对证书进行解析,然而我们使用openssl生成的服务端server。key是PKCS1格式的,所以MQTTbroker采用PKCS8无法对证书进行解析。问题处理 对证书进行格式转行,将PKCS1格式转换成PKCS8即可。 证书格式区别:PKCS1的文件头格式BEGINRSAPRIVATEKEYPKCS8的文件头格式BEGINPRIVATEKEY生成客户端证书1。生成客户端私钥opensslgenrsaoutclient。key20482。请求证书opensslreqnewsha256keyclient。keyoutclient。csrsubjCCNSTSZLSZOC。X。LOUC。X。LCNCLIENTemailAddress123456test。com3。使用CA证书签署客户端证书opensslx509reqdays36500sha256extensionsv3reqCAca。cerCAkeyca。keyCAserialca。srlCAcreateserialinclient。csroutclient。crt4。验证服务端证书opensslverifyCAfileca。crtclient。crt5。查看服务端证书opensslx509noouttextinclient。crt证书转换CRT转为PEM。key转换成。pem:opensslrsainserver。keyoutserverkey。pem。crt转换成。pem:opensslx509inserver。crtoutserver。pemoutformPEM 既然PEM与DER只是编码格式上的不同,那么不管是证书还是密钥,都可以随意转换为想要的格式:PEM转DERopensslx509outformderinserver。pemoutserver。derDER专PEMopensslx509informderinserver。deroutserver。crt 注:也可以直接生成PEM格式的证书,生成方式和CRT一样 区别:生成CA证书opensslreqx509newnodeskeyca。keysha256days3650outca。pem生成服务端证书opensslx509reqinserver。csrCAca。pemCAkeyca。keyCAcreateserialoutserver。pemdays3650sha256extensionsv3reqextfileopenssl。cnf生成客户端证书opensslx509reqdays3650inclient。csrCAca。pemCAkeyca。keyCAcreateserialoutclient。pemSSLTLS双向连接的启用及验证1。EMQX 在EMQX中mqtt:ssl的默认监听端口为8883。 将前文中通过OpenSSL工具生成的server。crt、server。key及ca。crt文件拷贝到EMQX的etccerts目录下,并参考如下配置修改emqx。conf:listener。ssl。nameistheIPaddressandportthattheMQTTSSLValue:IP:PortPortlistener。ssl。external8883PathtothefilecontainingtheusersprivatePEMencodedkey。Value:Filelistener。ssl。external。keyfileetccertsserver。keyPathtoafilecontainingtheusercertificate。Value:Filelistener。ssl。external。certfileetccertsemqx。pemPathtothefilecontainingPEMencodedCAcertificates。TheCAcertificatesValue:Filelistener。ssl。external。cacertfileetccertsca。pemAserveronlydoesx509pathvalidationinmodeverifypeer,asitthensendsacertificaterequesttotheclient(thismessageisnotsentiftheverifyoptionisverifynone)。Value:verifypeerverifynonelistener。ssl。external。verifyverifypeer2。MQTT连接测试 参照下图在MQTTX中创建MQTT客户端(Host输入框里的127。0。0。1需替换为实际的EMQX服务器IP) 此时Certificate一栏需要选择Selfsigned,并携带自签名证书中生成的ca。pem文件,客户端证书client。pem和客户端密钥client。key文件。 点击Connect按钮,连接成功后,如果能正常执行MQTT发布订阅操作,则SSL双向连接认证配置成功。 2。SMQTT双向向认证配置smqtt:tcp:MQTT配置ssl:ssl配置enable:true开关key:C:UsersAdministratorDesktopfsdownloadpkcs8key。pem指定ssl文件默认系统生成crt:C:UsersAdministratorDesktopfsdownloadserver。crt指定ssl文件默认系统生成ca:C:UsersAdministratorDesktopfsdownloadca。crtMQTT连接测试 此时Certificate一栏需要选择Selfsigned,并携带自签名证书中生成的ca。pem文件,客户端证书client。pem和客户端密钥client。key文件。 点击Connect按钮,连接成功后,如果能正常执行MQTT发布订阅操作,则SSL双向连接认证配置成功。 MQTTJava客户端库 EclipsePahoJavaClient(opensnewwindow)是用Java编写的MQTT客户端库(MQTTJavaClient),可用于JVM或其他Java兼容平台(例如Android)。 EclipsePahoJavaClient提供了MqttAsyncClient和MqttClient异步和同步API。通过Maven安装PahoJavadependencygroupIdorg。eclipse。pahogroupIdorg。eclipse。paho。client。mqttv3artifactIdversion1。2。2versiondependencydependencygroupIdorg。bouncycastlegroupIdbcpkixjdk15onartifactIdversion1。70versiondependencyPahoJava使用示例 Java体系中PahoJava是比较稳定、广泛应用的MQTT客户端库,本示例包含Java语言的PahoJava连接SMQTTBroker,并进行消息收发完整代码: MqttConnectimportlombok。DauthorC。X。Ldate20221024002416:29descriptionDatapublicclassMqttConnect{根证书路径privateStringCACRTPATHC:UsersAdministratorDesktopfsdownloadca。设备crt证书路径privateStringDEVICECERTPATHC:UsersAdministratorDesktopfsdownloadclient。设备key证书路径privateStringDEVICEPEMPATHC:UsersAdministratorDesktopfsdownloadclient。mqtt代理服务器地址privateStringhostssl:127。0。0。1:1883;设备idprivateStringclientId;privatebooleancleanS设备密码privateSprivateStringuserN} SSLUtilsimportorg。bouncycastle。jce。provider。BouncyCastlePimportorg。bouncycastle。openssl。PEMKeyPimportorg。bouncycastle。openssl。PEMPimportorg。bouncycastle。openssl。jcajce。JcaPEMKeyCimportjava。io。;importjava。security。KeyPimportjava。security。KeySimportjava。security。Simportjava。security。cert。CertificateFimportjava。security。cert。X509Cimportjavax。net。ssl。KeyManagerFimportjavax。net。ssl。SSLCimportjavax。net。ssl。SSLSocketFimportjavax。net。ssl。TrustManagerFauthorCharleydate20221205descriptionpublicclassSSLUtils{publicstaticSSLSocketFactorygetSingleSocketFactory(InputStreamcaCrtFileInputStream)throwsException{Security。addProvider(newBouncyCastleProvider());X509CertificatecaCBufferedInputStreambisnewBufferedInputStream(caCrtFileInputStream);CertificateFactorycfCertificateFactory。getInstance(X。509);while(bis。available()0){caCert(X509Certificate)cf。generateCertificate(bis);}KeyStorecaKsKeyStore。getInstance(KeyStore。getDefaultType());caKs。load(null,null);caKs。setCertificateEntry(certcertificate,caCert);TrustManagerFactorytmfTrustManagerFactory。getInstance(TrustManagerFactory。getDefaultAlgorithm());tmf。init(caKs);SSLContextsslContextSSLContext。getInstance(TLSv1。2);sslContext。init(null,tmf。getTrustManagers(),null);returnsslContext。getSocketFactory();}publicstaticSSLSocketFactorygetSocketFactory(finalStringcaCrtFile,finalStringcrtFile,finalStringkeyFile,finalStringpassword)throwsException{Security。addProvider(newBouncyCastleProvider());loadCAcertificateX509CertificatecaCFileInputStreamfisnewFileInputStream(caCrtFile);BufferedInputStreambisnewBufferedInputStream(fis);CertificateFactorycfCertificateFactory。getInstance(X。509);while(bis。available()0){caCert(X509Certificate)cf。generateCertificate(bis);}loadclientcertificatebisnewBufferedInputStream(newFileInputStream(crtFile));X509Cwhile(bis。available()0){cert(X509Certificate)cf。generateCertificate(bis);}loadclientprivatekeyPEMParserpemParsernewPEMParser(newFileReader(keyFile));ObjectobjectpemParser。readObject();JcaPEMKeyConverterconverternewJcaPEMKeyConverter()。setProvider(BC);KeyPairkeyconverter。getKeyPair((PEMKeyPair)object);pemParser。close();CAcertificateisusedtoauthenticateserverKeyStorecaKsKeyStore。getInstance(KeyStore。getDefaultType());caKs。load(null,null);caKs。setCertificateEntry(cacertificate,caCert);TrustManagerFactorytmfTrustManagerFactory。getInstance(X509);tmf。init(caKs);clientkeyandcertificatesaresenttoserver,soitcanauthenticateKeyStoreksKeyStore。getInstance(KeyStore。getDefaultType());ks。load(null,null);ks。setCertificateEntry(certificate,cert);ks。setKeyEntry(privatekey,key。getPrivate(),password。toCharArray(),newjava。security。cert。Certificate〔〕{cert});KeyManagerFactorykmfKeyManagerFactory。getInstance(KeyManagerFactory。getDefaultAlgorithm());kmf。init(ks,password。toCharArray());finally,createSSLsocketfactorySSLContextcontextSSLContext。getInstance(TLSv1。2);context。init(kmf。getKeyManagers(),tmf。getTrustManagers(),null);returncontext。getSocketFactory();}} MqttServiceTestimportcom。demo。smqtt。vo。MqttCimportcom。demo。xl。utils。SSLUimportorg。eclipse。paho。client。mqttv3。;importorg。slf4j。Limportorg。slf4j。LoggerFimportjavax。net。ssl。SSLSocketFimportstaticorg。eclipse。paho。client。mqttv3。MqttConnectOptions。MQTTVERSION311;authorCharleydate20221205descriptionpublicclassMqttServiceTest{privatestaticLoggerlogLoggerFactory。getLogger(MqttServiceTest。class);消息级别privatefinalstaticintQOS0;publicstaticMqttClientcreateMqtt(MqttConnectmqttConnect)throwsException{MqttClientclientnewMqttClient(mqttConnect。getHost(),mqttConnect。getClientId());MqttConnectOptionsconnOptsnewMqttConnectOptions();connOpts。setUserName(mqttConnect。getUserName());connOpts。setPassword(mqttConnect。getPassword()。toCharArray());connOpts。setCleanSession(mqttConnect。isCleanSession());connOpts。setKeepAliveInterval(90);connOpts。setAutomaticReconnect(true);connOpts。setMqttVersion(MQTTVERSION311);SSLSocketFactoryfactorySSLUtils。getSocketFactory(mqttConnect。getCACRTPATH(),mqttConnect。getDEVICECERTPATH(),mqttConnect。getDEVICEPEMPATH(),);connOpts。setSocketFactory(factory);client。connect(connOpts);log。info(mqtt({})connectsuccess,mqttConnect。getClientId());}publicstaticvoidsubscribe(MqttClientclient,Stringtopic,StringclientId){try{创建MqttClientif(!client。isConnected()){log。error(mqtt({})subisdisconnect,clientId);client。connect();log。error(mqtt({})iserror);}client。setCallback(newMqttCallback(){OverridepublicvoidconnectionLost(Throwablearg0){log。error(connectionLost:arg0。getMessage());}OverridepublicvoidmessageArrived(Stringtopic,MqttMessagemessage)throwsException{log。info(recivemessage{},newString(message。getPayload()));}OverridepublicvoiddeliveryComplete(IMqttDeliveryTokentoken){log。info(deliveryisComplete:token。isComplete()anddeliveryresponse:token。getResponse());}});client。subscribe(topic,QOS);}catch(Exceptione){log。error(e。getMessage());}}publicstaticvoidpublish(MqttClientclient,Stringmsg,Stringtopic,StringclientId)throwsMqttException{if(!client。isConnected()){log。error(mqtt({})connectiserror,clientId);client。connect();log。error(mqtt({})connectreconnect,clientId);}MqttTopicmqttTopicclient。getTopic(topic);MqttMessagemessagenewMqttMessage(msg。getBytes());message。setQos(QOS);mqttTopic。publish(message);log。info(MQTTUtil({})Sendtopic:topicMessage:msg,clientId);}publicstaticvoidmain(String〔〕args)throwsException{MqttConnectmqttConnectnewMqttConnect();mqttConnect。setClientId(123456);MqttClientmqttClientcreateMqtt(mqttConnect);订阅消息subscribe(mqttClient,testhello,123456);发布消息publish(mqttClient,sayhello,testhello,123456);}} 测试结果: