使用 OpenSSL 实现基于证书的双向认证


本文帮助你快速入门,使用 OpenSSL 实现设备与 EnOS 云端之间的基于证书的双向安全连接。

前提条件


本步骤的前提是你已参考 将智能设备连接至 EnOS 云端将非智能设备通过 EnOS Edge 连接至 EnOS 云端,完成创建模型、产品和设备步骤。在创建时,注意本教程的以下特殊配置:


创建 EnOS Edge 产品


在创建 EnOS Edge 产品时需要启用 证书双向认证机制,默认证书有效期和最长证书有效期使用默认值即可。如下图所示:


../_images/edge_ssl.png


逆变器产品不需要开启 证书双向认证机制,因为逆变器是作为子设备由 EnOS Edge 代理连接 EnOS 云端,只需要 Edge 与 cloud 进行基于证书的双向认证即可。


创建 EnOS Edge 设备


基于以上产品创建网关类型设备 Edge01_Certificate。如下图所示:

../_images/edge01_certificate.png


在设备列表中点击 查看 view,记下 Edge01_Certificate 的设备三元组(Product Key,Device Key 和 Device Secret),将用于创建证书请求文件。


创建子设备


参见 将智能设备连接至 EnOS 云端 的步骤 3,创建逆变器设备,为设备配置以下值:

  • 基本信息

    • 产品:Inverter_Product

    • 设备名称:INV002

    • 时区/城市:UTC+14:00

  • 属性信息

    • 逆变器类型:1:String

    • 组件容量:20

步骤 1:创建证书签名请求文件


需要创建的私钥名称为:edge.key。需要创建的 CSR 文件的文件名为:edge.csr。创建的具体步骤,参见 创建证书签名申请(CSR)

步骤 2:调用 EnOS API 申请证书


在生成 edge.csr 以后,调用 EnOS API 申请证书。

  1. 在 POM 中添加如下的 Maven 依赖,以便使用 EnOS API:

    <dependency>
     <groupId>com.envisioniot</groupId>
     <artifactId>enos-dm-api-pojo</artifactId>
     <version>0.2.23</version>
    </dependency>
    
  2. 使用如下代码调用 EnOS API 申请证书:

import com.envision.apim.poseidon.config.PConfig;
import com.envision.apim.poseidon.core.Poseidon;
import com.envisioniot.enos.connect_service.v2_1.cert.ApplyCertificateRequest;
import com.envisioniot.enos.connect_service.v2_1.cert.ApplyCertificateResponse;
import com.envisioniot.enos.connect_service.vo.DeviceIdentifier;


import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;



public class applyCert {

  // ISSUE_AUTHORITY表示证书类型,可选值有: RSA 和 ECC
   public static final String ISSUE_AUTHORITY = "ECC";
  // VALID_DAY表示证书有效期
   public static final Integer VALID_DAY = 300;

  /**
   * 设备认证所需信息,这些参数依次分别表示EnOS Edge设备的device key, product key
   */
   public static final String DEVICE_KEY = "xIhSzeP6lh";
   public static final String PRODUCT_KEY = "rHbNES3c";

   /**
    * 在EnOS上注册的,用于调用API的应用的Access key和secret key,可以在 “EnOS应用门户 > 开发者控制台 > 应用注册” 获取
    */
   public static final String ACCESS_KEY = "2e0b21b7-1b51-46a5-b501-c3824331b8df";
   public static final String SECRET_KEY = "46e715d8-3c38-4cb6-9d65-b0bdad1ffe89";
  // API网关URL,在应用门户开发者控制台的右上角,点击 帮助 > 环境信息 查询
   public static final String API_GATEWAY_URL = "https://apim-cn5.envisioniot.com";
  /**
   * OU ID,鼠标悬停在EnOS应用门户开发者控制台的左上角OU名称处可以获得
   */
   public static final String ORG_ID = "o15724268424841";

   /**
    * 以下参数用于保存生成的各种文件:
    * SAVE_CSR_FILE_PATH 用于保存生成的CSR文件
    * SAVE_DEVICE_CERT_FILE_PATH 用于保存申请到的设备证书
    * SAVE_ROOT_CERT_FILE_PATH 用于保存根证书
    */
   public static final String SAVE_CSR_FILE_PATH = "edge.csr";
   public static final String SAVE_DEVICE_CERT_FILE_PATH = "edge.pem";
   public static final String SAVE_ROOT_CERT_FILE_PATH = "cacert.pem";

   private static void applyCertToDevice() throws IOException {
      // 读取生成的csr文件
       String certificateRequest = readFile(SAVE_CSR_FILE_PATH);
      // 设置绑定证书设备请求的参数
       ApplyCertificateRequest applyCertificateRequest = createApplyCertParam(certificateRequest);
      // 利用EnOS API发起请求
       ApplyCertificateResponse certRsp =
               Poseidon.config(PConfig.init().appKey(ACCESS_KEY).appSecret(SECRET_KEY).debug())
                       .url(API_GATEWAY_URL)
                       .getResponse(applyCertificateRequest, ApplyCertificateResponse.class);
      // 获得设备的证书和根证书
       if (certRsp.success()) {
          // 保存设备的证书
           saveFile(certRsp.getData().getCert(), SAVE_DEVICE_CERT_FILE_PATH);
          // 保存设备证书的根证书
           saveFile(certRsp.getData().getCaCert(), SAVE_ROOT_CERT_FILE_PATH);
       }
   }
  /**
   * 读取CSR文件
   */
   private static String readFile(String path) {
       try (InputStream inputStream = new FileInputStream(path);
            Reader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader reader = new BufferedReader(inputStreamReader)) {
           StringBuilder sb = new StringBuilder();
           String line;
           while ((line = reader.readLine()) != null) {
               sb.append(line).append("\n");
           }
           return sb.toString();
       } catch (IOException e) {
           e.printStackTrace();
       }
       return "";
   }


   private static ApplyCertificateRequest createApplyCertParam(String certificateRequest) {
       ApplyCertificateRequest applyCertificateRequest = new ApplyCertificateRequest();
       applyCertificateRequest.setCsr(certificateRequest);
      /*
       * 请确定ISSUE_AUTHORITY和你生成csr对应的颁发方是一致的
       */
       applyCertificateRequest.setIssueAuthority(ISSUE_AUTHORITY);
      /*
       * 请确保证书的有效天数满足小于所属产品规定的最大有效期
       */
       applyCertificateRequest.setValidDay(VALID_DAY);
      /*
       * 设备信息
       */
       DeviceIdentifier deviceIdentifier = new DeviceIdentifier();
      /*
       * 使用以下任意一个参数或参数组合以指定设备:
       * ASSET_ID
       * PRODUCT_KEY + DEVICE_KEY
       * 本示例代码使用了 PRODUCT_KEY + DEVICE_KEY
       */
       deviceIdentifier.setKey(PRODUCT_KEY, DEVICE_KEY);
       applyCertificateRequest.setDevice(deviceIdentifier);
       applyCertificateRequest.setOrgId(ORG_ID);
       return applyCertificateRequest;
   }
   public static void main(String[] args) throws NoSuchAlgorithmException, CertificateException, SignatureException, InvalidKeyException, IOException, KeyStoreException {
      //生成证书
       applyCertToDevice();
  }

  /*
   * 保存申请到的证书
   */
   public static void saveFile(String fileContent, String filePath) throws IOException {
       File fp = new File(filePath);
       try (OutputStream os = new FileOutputStream(fp)) {
           os.write(fileContent.getBytes());
       }
   }
 }

步骤 3:生成设备连接 EnOS 云端的 JKS 文件


按照以下步骤使用 keytool 生成 edge.jks 文件。keytool 是 Java 原生 JDK 的工具,其路径为 %JAVA_HOME%\bin\keytool

a. 查看文件


[root@DemoMachine cert] ll
total 12
-rw-r--r-- 1 root root 1395 Nov 28 19:51 cacert.pem
-rw-r--r-- 1 root root 1858 Nov 28 19:51 edge.key
-rw-r--r-- 1 root root 1416 Nov 28 20:08 edge.pem

b. 将证书与私钥导出为 .p12 文件


[root@DemoMachine cert] openssl pkcs12 -export -in edge.pem -inkey edge.key -out edge.p12 -name edge -CAfile cacert.pem -caname cacert
Enter pass phrase for edge.key:
Enter Export Password:
Verifying - Enter Export Password:

c. 查看生成的 .p12 文件


[root@DemoMachine cert] ll
total 16
-rw-r--r-- 1 root root 1395 Nov 28 19:51 cacert.pem
-rw-r--r-- 1 root root 1858 Nov 28 19:51 edge.key
-rw-r--r-- 1 root root 2654 Nov 28 20:19 edge.p12
-rw-r--r-- 1 root root 1416 Nov 28 20:08 edge.pem

d. 导入 .p12 文件至密钥库


[root@DemoMachine cert] keytool -importkeystore -deststorepass 123456 -destkeypass 123456 -destkeystore edge.jks -srckeystore edge.p12 -srcstoretype PKCS12 -srcstorepass 123456 -alias edge
Importing keystore edge.p12 to edge.jks...

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore edge.jks -destkeystore edge.jks -deststoretype pkcs12".

e. 查看 jks 文件


[root@DemoMachine cert] ll
total 20
-rw-r--r-- 1 root root 1395 Nov 28 19:51 cacert.pem
-rw-r--r-- 1 root root 2356 Nov 28 20:20 edge.jks
-rw-r--r-- 1 root root 1858 Nov 28 19:51 edge.key
-rw-r--r-- 1 root root 2654 Nov 28 20:19 edge.p12
-rw-r--r-- 1 root root 1416 Nov 28 20:08 edge.pem

f. 检查 jks 文件含有一个证书条目(trusted certificate entry)


[root@DemoMachine cert] keytool -list --keystore edge.jks
Enter keystore password:
Keystore type: jks
Keystore provider: SUN

Your keystore contains 1 entry

edge, Nov 28, 2018, PrivateKeyEntry,
Certificate fingerprint (SHA1): 38:16:5A:1F:1D:68:44:44:FE:56:1A:84:36:31:85:CB:14:5B:9C:5E

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore edge.jks -destkeystore edge.jks -deststoretype pkcs12".

g. 导入 cacert 根证书至密钥库


[root@DemoMachine cert]# keytool -import -trustcacerts -alias cacert -file cacert.pem -keystore edge.jks -storepass 123456
Owner: EMAILADDRESS=ca@eniot.io, CN=EnOS CA, OU=EnOS CA, O=EnOS, L=Shanghai, ST=Shanghai, C=CN
Issuer: EMAILADDRESS=ca@eniot.io, CN=EnOS CA, OU=EnOS CA, O=EnOS, L=Shanghai, ST=Shanghai, C=CN
Serial number: 8c54a99157c8ef28
Valid from: Mon Nov 19 18:20:27 CST 2018 until: Thu Nov 16 18:20:27 CST 2028
Certificate fingerprints:
     MD5:  4E:BF:2A:53:85:1E:21:97:70:72:AD:DF:A5:79:51:3F
     SHA1: 96:BC:6B:F0:15:CD:BB:03:52:12:A2:C6:C4:BD:20:69:71:4A:75:C2
     SHA256: 81:B0:E3:01:D3:2B:48:E7:CF:CC:BC:07:9A:AD:49:74:EF:92:97:A1:D4:46:E2:4E:56:94:14:32:A7:09:FA:9F
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3

Extensions:

#1: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: AE 4F F7 AF A7 19 7B 0B   AE 2E 79 0F B4 7B E5 AE  .O........y.....
0010: 8C F4 54 0D                                        ..T.
]
]

#2: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:true
  PathLen:2147483647
]

#3: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: AE 4F F7 AF A7 19 7B 0B   AE 2E 79 0F B4 7B E5 AE  .O........y.....
0010: 8C F4 54 0D                                        ..T.
]
]

Trust this certificate? [no]:  yes
Certificate was added to keystore

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore edge.jks -destkeystore edge.jks -deststoretype pkcs12".

h. 检查 jks 文件含有两个证书条目(trusted certificate entry)


[root@DemoMachine cert]# keytool -list --keystore edge.jks
Enter keystore password:
Keystore type: jks
Keystore provider: SUN

Your keystore contains 2 entries

cacert, Nov 28, 2018, trustedCertEntry,
Certificate fingerprint (SHA1): 96:BC:6B:F0:15:CD:BB:03:52:12:A2:C6:C4:BD:20:69:71:4A:75:C2
edge, Nov 28, 2018, PrivateKeyEntry,
Certificate fingerprint (SHA1): 38:16:5A:1F:1D:68:44:44:FE:56:1A:84:36:31:85:CB:14:5B:9C:5E

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore edge.jks -destkeystore edge.jks -deststoretype pkcs12".
[root@DemoMachine cert]#

步骤 4:使用 SDK 连接设备至 EnOS


你可以选择使用 MQTT SDK 或 HTTP SDK 连接。

MQTT SDK


在 Java 项目的 pom.xml 文件中添加 MQTT SDK 的依赖,版本最低必须为 2.2.5,如下所示:

<dependency>
    <groupId>com.envisioniot</groupId>
    <artifactId>enos-mqtt</artifactId>
    <version>2.2.16</version>
</dependency>


参考 快速入门:将非智能设备通过 edge 连接至 EnOS 云端 的步骤 5,但将步骤 5 中 “EnOS Edge 上线” 示例代码替换成下面这段:

public static boolean IS_ECC_CONNECT = true; //使用RSA证书时,该参数值为false;使用ECC证书时,值为true
public static final String DEVICE_KEY = "yourDeviceKey";
public static final String PRODUCT_KEY = "yourProductKey";
public static final String DEVICE_SECRET = "yourDeviceSecret";
public static final String JKS_PASSWORD = "yourJksPassword";
public static final String JKS_FILE_NAME = "edge.jks";
public static final String SSL_CONNECT_URL = "ssl://MqttBrokerUrl:18883"; // MQTT broker URL,点击应用门户开发者控制台右上角的 帮助 > 环境信息 查询


private static void connectEnos() {
    DefaultProfile defaultProfile = new DefaultProfile(SSL_CONNECT_URL, PRODUCT_KEY, DEVICE_KEY,DEVICE_SECRET);
    // 设置连接属性
    defaultProfile.setConnectionTimeout(60).setKeepAlive(180).setAutoReconnect(false)
            .setSSLSecured(true)
            // 设置双向认证,设备的jks文件路径和读取密码
            .setSSLJksPath(JKS_FILE_NAME, JKS_PASSWORD)
            // 设置是否是ECC证书连接 EnOS 云端
            .setEccConnect(IS_ECC_CONNECT);
    final MqttClient mqttClient = new MqttClient(defaultProfile);
    mqttClient.connect(new ConnCallback() {
        @Override
        public void connectComplete(boolean reconnect) {
            System.out.println("connect success");
        }

        @Override
        public void connectLost(Throwable cause) {
            System.out.println("connect lost");
        }

        @Override
        public void connectFailed(Throwable cause) {
            System.out.println("onConnectFailed : " + cause);
        }
    });
}

HTTP SDK


在 Java 项目的 pom.xml 文件中添加 HTTP-SDK 的依赖,版本最低必须为 0.1.9,如下所示:

<dependency>
    <groupId>com.envisioniot</groupId>
    <artifactId>enos-http</artifactId>
    <version>0.2.1</version>
</dependency>


参考 快速入门:将非智能设备通过 edge 连接至 EnOS 云端 的步骤 5,但将步骤 5 中 “EnOS Edge 上线” 示例代码替换成下面这段:

public class HttpBiDirectionalAuthenticate {
    // EnOS HTTP Broker URL, which can be obtained from Environment Information page in EnOS Console
    // ssl port 8443
    static final String BROKER_URL = "https://broker_url:8443/";

    // Device credentials, which can be obtained from Device Details page in EnOS Console
    static final String PRODUCT_KEY = "productKey";
    static final String DEVICE_KEY = "deviceKey";
    static final String DEVICE_SECRET = "deviceSecret";

    private static String jksPath = "jskPath";
    private static String jksPassword = "jskPassword";

    /** Ecc cert flag
     * if use ECC certificate, chose true
     * if use RSA certificate, chose false */
    static final boolean IS_ECC_CONNECT = false;

    public static void main(String[] args) throws EnvisionException {
        // construct a static device credential via ProductKey, DeviceKey and DeviceSecret
        StaticDeviceCredential credential = new StaticDeviceCredential(
                PRODUCT_KEY, DEVICE_KEY, DEVICE_SECRET);

        // construct a http connection
        SessionConfiguration configuration = SessionConfiguration
                .builder()
                .lifetime(30_000)
                .sslSecured(true)
                .isEccConnect(IS_ECC_CONNECT)
                .jksPath(jksPath)
                .jksPassword(jksPassword)
                .build();

        HttpConnection connection = new HttpConnection.Builder(BROKER_URL, credential)
                .sessionConfiguration(configuration)
                .build();

        MeasurepointPostRequest request = buildMeasurepointPostRequest();

        try
        {
            MeasurepointPostResponse response = connection.publish(request, null);
            System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(response));
        } catch (EnvisionException | IOException e)
        {
            e.printStackTrace();
        }
    }

    private static MeasurepointPostRequest buildMeasurepointPostRequest()
    {
        // Measurepoints are defined in the Model
        return MeasurepointPostRequest.builder()
                .addMeasurePoint("Int_value", 100)
                .addMeasurePoint("DI_value_01", 5)
                .build();
    }
}

步骤 5:启动示例程序


完成步骤 4 的代码替换操作后,运行更新后的示例代码。

步骤 6:检查设备连接状态


在运行示例程序以后,EnOS Edge 上线,并添加子设备作为拓扑,代理子设备连接云端。设备连接状态如下图所示:

../_images/device_list.png

步骤 7:查看设备数据


  1. 在设备列表中,找到 INV001 设备,并点击对应的 查看 view 图标,进入 设备详情 页面。

  2. 点击 测点 标签页,找到测点 INV.GenActivePW,点击 查看数据 view,进入 时序洞察 页面,查看测点的最新数据。

  3. 也可以通过 TSDB 数据服务,获取测点的最新数据。