This article describes how to use X.509 certificates to connect MQTT-based devices with IoT Platform and receive messages. In this example, the ./demos/mqtt_x509_auth_demo.c sample code file is used.

Background information

Step 1: Initialize a client

  1. Add header files.
    #include "aiot_state_api.h"
    #include "aiot_sysdep_api.h"
    #include "aiot_mqtt_api.h"
  2. Add the underlying dependency and configure the log output feature.
        aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);
        aiot_state_set_logcb(demo_state_logcb);
  3. Call the aiot_mqtt_init operation to create an MQTT client instance and initialize the default parameters.
        mqtt_handle = aiot_mqtt_init();
        if (mqtt_handle == NULL) {
            printf("aiot_mqtt_init failed\n");
            return -1;
        }

Step 2: Configure required features

Call the aiot_mqtt_setopt operation to configure the following items:

  1. Set connection parameters.
  2. Configure a callback to obtain device identity information.
  3. Configure callbacks to monitor status and receive messages.

For more information about the parameters of the operation, see aiot_mqtt_option_t.

  1. Set connection parameters.
    • Sample code
      const char client_cert[] = {
          "-----BEGIN CERTIFICATE-----\r\n"
          "MIIDiDCCAnCgAwIBAgIIAJ3GD7c2860wDQYJKoZIhvcNAQELBQAwUzEoMCYGA1UE\r\n"
          ... 
          ...
          "v4aDacYavCH03JXKQ6zWpAwnwLcYrbW7XdhtDrqFCj+v6VJ6NDZaTGEW3/I=\r\n"
          "-----END CERTIFICATE-----\r\n"
      };
      
      const char client_private_key[] = {
      
          "-----BEGIN RSA PRIVATE KEY-----\r\n"
          "MIIEowIBAAKCAQEApyRaelm4b4sKOlqBywOIR4RIJrYEfNtYIAofMIkkwnClrqgh\r\n"
          ...    
          ...
          "mPw5JEAkNBy6wOWepJ9Tv1wY8yFEzV2dVsx3P93p5P3UdZb4M7i0\r\n"
          "-----END RSA PRIVATE KEY-----\r\n"
      };
          ...
      int main(int argc, char *argv[])
      {
          int32_t     res = STATE_SUCCESS;
          void       *mqtt_handle = NULL;
          char       *host = "x509.itls.cn-shanghai.aliyuncs.com";
      
          uint16_t    port = 1883; 
          aiot_sysdep_network_cred_t cred; 
      
          char *product_key       = "";
          char *device_name       = "";
          char *device_secret     = "";
      
          ...
          /* The security credential structure. To establish a TLS connection, specify the CA certificate in this structure. */
          aiot_sysdep_network_cred_t cred;
      
          /* Create a security credential for the SDK to establish a TLS connection. */
          memset(&cred, 0, sizeof(aiot_sysdep_network_cred_t));
          cred.option = AIOT_SYSDEP_NETWORK_CRED_SVRCERT_CA;  /* Verify the MQTT broker by using the RSA certificate. */
          cred.max_tls_fragment = 16384; /* The fragment can be up to 16 KB in length. Other optional values include 4 KB, 2 KB, 1 KB, and 0.5 KB. */
          cred.sni_enabled = 1;                               /* The Server Name Indicator (SNI) extension is supported when you establish a TLS connection. */
          cred.x509_server_cert = ali_ca_crt;                 /* The RSA root certificate that is used to verify the MQTT broker. */
          cred.x509_server_cert_len = strlen(ali_ca_crt);     /* The length of the RSA root certificate. */
      
          /* TODO: When you use an X.509 certificate for two-way authentication, you must add the following code to set a security credential. */
          cred.x509_client_cert = client_cert;
          cred.x509_client_cert_len = strlen(client_cert);
          cred.x509_client_privkey = client_private_key;
          cred.x509_client_privkey_len = strlen(client_private_key);
      
          /* Set the security credential of the connection. */
          aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_NETWORK_CRED, (void *)&cred);
      
          ...
      }
    • Parameters:
      Parameter Example Description
      client_cert[]
          "-----BEGIN CERTIFICATE-----\r\n"
          "MIIDiDCCAnCgAwIBAgIIAJ3GD7c2860wDQYJKoZIhvcNAQELBQAwUzEoMCYGA1UE\r\n"
          ... 
          ...
          "v4aDacYavCH03JXKQ6zWpAwnwLcYrbW7XdhtDrqFCj+v6VJ6NDZaTGEW3/I=\r\n"
          "-----END CERTIFICATE-----\r\n"
      The X.509 certificate information of the device.

      On the Device Details page of the IoT Platform console, click Download next to X.509 Certificate to download the certificate information. After you decompress the certificate file, replace the value of this parameter with the information in the .cer file in the format of the example value.

      The certificate information consists of multiple lines of strings. The ellipsis (…) in the sample code indicates the omitted strings. Add "to the beginning and \r\n" to the end of each string.

      client_private_key[]
          "-----BEGIN RSA PRIVATE KEY-----\r\n"
          "MIIEowIBAAKCAQEApyRaelm4b4sKOlqBywOIR4RIJrYEfNtYIAofMIkkwnClrqgh\r\n"
          ... 
          ...
          "mPw5JEAkNBy6wOWepJ9Tv1wY8yFEzV2dVsx3P93p5P3UdZb4M7i0\r\n"
          "-----END RSA PRIVATE KEY-----\r\n"
      The private key of the X.509 certificate.

      On the Device Details page of the IoT Platform console, click Download next to X.509 Certificate to download the certificate information. After you decompress the certificate file, replace the value of this parameter with the information in the .key file in the format of the example value.

      The private key consists of multiple lines of strings. The ellipsis (…) in the sample code indicates the omitted strings. Add "to the beginning and \r\n" to the end of each string.

      host x509.itls.cn-shanghai.aliyuncs.com The endpoint. Format: x509.itls.${YourRegionId}.aliyuncs.com.

      Replace ${YourRegionId} with the ID of the region in which the device is connected to IoT Platform. For more information, see Regions and zones.

      port 1883 The port number.
      product_key "" If you use X.509 certificates for authentication, specify an empty string for each of these parameters.
      device_name ""
      device_secret ""
  2. Configure a callback to obtain device identity information.
    After a device uses an X.509 certificate for authentication and establishes a connection with IoT Platform, IoT Platform sends a message that contains a ProductKey and DeviceName to the device. You must configure a callback to receive the message. The callback is called to save these parameters to a specified location for subsequent use.

    You can customize the demo_get_device_info function. In this example, the message is parsed and printed.

    • Sample code
      
      static void demo_get_device_info(const char *topic, uint16_t topic_len, const char *payload, uint32_t payload_len)
      {
          const char *target_topic = "/ext/auth/identity/response";
          char *p_product_key = NULL;
          uint32_t product_key_len = 0;
          char *p_device_name = NULL;
          uint32_t device_name_len = 0;
          int32_t res = STATE_SUCCESS;
      
          if (topic_len != strlen(target_topic) || memcmp(topic, target_topic, topic_len) != 0) {
              return;
          }
      
          /* TODO: The core_json_value operation in the SDK is specified as an example. 
      
                   You must replace the operation with an operation from a JSON parsing library such as cJSON to process payloads.
          */
          res = core_json_value(payload, payload_len, "productKey", strlen("productKey"), &p_product_key, &product_key_len);
          if (res < 0) {
              return;
          }
          res = core_json_value(payload, payload_len, "deviceName", strlen("deviceName"), &p_device_name, &device_name_len);
          if (res < 0) {
              return;
          }
      
          if (g_product_key == NULL) {
              g_product_key = malloc(product_key_len + 1);
              if (NULL == g_product_key) {
                  return;
              }
      
              memset(g_product_key, 0, product_key_len + 1);
              memcpy(g_product_key, p_product_key, product_key_len);
          }
          if (g_device_name == NULL) {
              g_device_name = malloc(device_name_len + 1);
              if (NULL == g_product_key) {
                  return;
              }
      
              memset(g_device_name, 0, device_name_len + 1);
              memcpy(g_device_name, p_device_name, device_name_len);
          }
      
          printf("device productKey: %s\r\n", g_product_key);
          printf("device deviceName: %s\r\n", g_device_name);
      }
    • Note:

      IoT Platform uses the /ext/auth/identity/response topic to issue a ProductKey and DeviceName to the device. Payload format:

      {
          "productKey":"***",
          "deviceName":"***"
      }
  3. Configure callbacks to monitor status and receive messages.
    1. Specify the callbacks to monitor status.
      • Sample code
         int main(int argc, char *argv[])
        {
            ...
            ...
        
            /* Specify the default callback to receive MQTT messages.  */
            aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_RECV_HANDLER, (void *)demo_mqtt_default_recv_handler);
            /* Specify the callback to handle MQTT events.  */
            aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_EVENT_HANDLER, (void *)demo_mqtt_event_handler);
            ...
            ...
        }
                                      
      • Parameters:
        Parameter Example Description
        AIOT_MQTTOPT_RECV_HANDLER demo_mqtt_default_recv_handler When a message is received, the callback is called to perform the required operations.
        AIOT_MQTTOPT_EVENT_HANDLER demo_mqtt_event_handler When the status of device connection changes, the callback is called to perform the required operations.
    2. Define the callback to monitor status.
      Notice
      • Do not specify time-consuming logic to handle events. Otherwise, threads that are used to receive packets may be blocked.
      • Connection status changes include network exceptions, automatic reconnections, and disconnections.
      • To handle connection status changes, you can modify the code as needed in the TODO section.

      /* The callback to handle MQTT events. When the connection is created, recovered, or closed, the callback is called. For information about the event definition, see core/aiot_mqtt_api.h.  */
      void demo_mqtt_event_handler(void *handle, const aiot_mqtt_event_t *event, void *userdata)
      {
          switch (event->type) {
              /* Call the aiot_mqtt_connect operation to establish a connection with the MQTT broker.  */
              case AIOT_MQTTEVT_CONNECT: {
                  printf("AIOT_MQTTEVT_CONNECT\n");
                  /* TODO: Specify the processing logic after the connection is established. Do not call time-consuming functions that may block threads.  */
              }
              break;
      
              /* If a disconnection error occurs due to a network exception, the SDK automatically initiates a request to reconnect with the MQTT broker.  */
              case AIOT_MQTTEVT_RECONNECT: {
                  printf("AIOT_MQTTEVT_RECONNECT\n");
                  /* TODO: Specify the processing logic after the connection is re-established. Do not call time-consuming functions that may block threads.  */
              }
              break;
      
              /* A disconnection error occurs due to a network exception. The underlying read or write operation fails. A heartbeat response is not obtained from the MQTT broker.  */
              case AIOT_MQTTEVT_DISCONNECT: {
                  char *cause = (event->data.disconnect == AIOT_MQTTDISCONNEVT_NETWORK_DISCONNECT) ? ("network disconnect") :
                                ("heartbeat disconnect");
                  printf("AIOT_MQTTEVT_DISCONNECT: %s\n", cause);
                  /* TODO: Specify the logic to process disconnection issues. Do not call time-consuming functions that may block threads.  */
              }
              break;
      
              default: {
      
              }
          }
      }
                                      
    3. Define the callback to receive messages.
      Notice
      • Do not specify time-consuming logic to process messages. Otherwise, threads that are used to receive packets may be blocked.
      • To process received messages, you can modify the code as needed in the TODO section.
      
      /* The default callback to process MQTT messages. When the SDK receives messages from the MQTT broker and you do not configure callbacks, the following operation is called.  */
      void demo_mqtt_default_recv_handler(void *handle, const aiot_mqtt_recv_t *packet, void *userdata)
      {
          switch (packet->type) {
              case AIOT_MQTTRECV_HEARTBEAT_RESPONSE: {
                  printf("heartbeat response\n");
                  /* TODO: Specify the logic to process heartbeat responses from the MQTT broker. In most cases, this logic is not required.  */
              }
              break;
      
              case AIOT_MQTTRECV_SUB_ACK: {
                  printf("suback, res: -0x%04X, packet id: %d, max qos: %d\n",
                         -packet->data.sub_ack.res, packet->data.sub_ack.packet_id, packet->data.sub_ack.max_qos);
                  /* TODO: Specify the logic to process the responses of the MQTT broker to subscription requests. In most cases, this logic is not required.  */
              }
              break;
      
              case AIOT_MQTTRECV_PUB: {
                  printf("pub, qos: %d, topic: %.*s\n", packet->data.pub.qos, packet->data.pub.topic_len, packet->data.pub.topic);
                  printf("pub, payload: %.*s\n", packet->data.pub.payload_len, packet->data.pub.payload);
                  /* TODO: Specify the logic to process the messages that are sent from the MQTT broker.  */
              }
              break;
      
              case AIOT_MQTTRECV_PUB_ACK: {
                  printf("puback, packet id: %d\n", packet->data.pub_ack.packet_id);
                  /* TODO: Specify the logic to process the responses of the MQTT broker to QoS 1 messages. In most cases, this logic is not required.  */
              }
              break;
      
              default: {
      
              }
          }
      }

Step 3: Establish a connection

Call aiot_mqtt_connect operation send an authentication and connection request to IoT Platform.

/* Establish an MQTT connection with IoT Platform. */  */
    res = aiot_mqtt_connect(mqtt_handle);
    if (res < STATE_SUCCESS) {
        /* Release resources of the MQTT instance if the MQTT connection fails to be established.  */
        aiot_mqtt_deinit(&mqtt_handle);
        printf("aiot_mqtt_connect failed: -0x%04X\n", -res);
        return -1;
    }

Step 4: Enable the keep-alive thread

Call the aiot_mqtt_process operation to send a heartbeat message to the MQTT broker and re-send the QoS 1 messages for which no responses are generated. This way, a persistent connection is enabled.

  1. Enable the keep-alive thread.
        res = pthread_create(&g_mqtt_process_thread, NULL, demo_mqtt_process_thread, mqtt_handle);
        if (res < 0) {
            printf("pthread_create demo_mqtt_process_thread failed: %d\n", res);
            return -1;
        }
  2. Configure the function to manage the keep-alive thread.
    void *demo_mqtt_process_thread(void *args)
    {
        int32_t res = STATE_SUCCESS;
    
        while (g_mqtt_process_thread_running) {
            res = aiot_mqtt_process(args);
            if (res == STATE_USER_INPUT_EXEC_DISABLED) {
                break;
            }
            sleep(1);
        }
        return NULL;
    }

Step 5: Enable the thread to receive messages

Call the aiot_mqtt_recv operation to receive MQTT messages from the broker. The required operations are performed by using the callback to receive messages. If a disconnection and then an automatic reconnection occur, the required operations are performed by using the callback to handle events.

  1. Enable the thread to receive messages.
    
        res = pthread_create(&g_mqtt_recv_thread, NULL, demo_mqtt_recv_thread, mqtt_handle);
        if (res < 0) {
            printf("pthread_create demo_mqtt_recv_thread failed: %d\n", res);
            return -1;
        }
                                        
  2. Configure the function to manage the thread.
    void *demo_mqtt_recv_thread(void *args)
    {
        int32_t res = STATE_SUCCESS;
    
        while (g_mqtt_recv_thread_running) {
            res = aiot_mqtt_recv(args);
            if (res < STATE_SUCCESS) {
                if (res == STATE_USER_INPUT_EXEC_DISABLED) {
                    break;
                }
                sleep(1);
            }
        }
        return NULL;
    }

Step 6: Subscribe to a topic

Call the aiot_mqtt_sub operation to subscribe to a specified topic.

  • Sample code
        {
            char *sub_topic = "/a18wP******/LightSwitch/user/get";
    
            res = aiot_mqtt_sub(mqtt_handle, sub_topic, NULL, 1, NULL);
            if (res < 0) {
                printf("aiot_mqtt_sub failed, res: -0x%04X\n", -res);
                return -1;
            }
        }
    Note After you configure the sample code, delete the annotation symbols on both sides of the code.
  • Parameters:
    Parameter Example Description
    sub_topic /a18wP******/LightSwitch/user/get
    The topic that has the Subscribe permission.
    • a18wP****** indicates the ProductKey of the device.
    • LightSwitch indicates the DeviceName of the device.

    In this example, the default custom topic is used.

    The device can receive messages from IoT Platform by using this topic.

    For more information about topics, see What is a topic?.

Step 7: Send a message

Call the aiot_mqtt_pub operation to send a message to a specified topic.

  • Sample code
         {
            char *pub_topic = "/a18wP******/LightSwitch/user/update";
            char *pub_payload = "{\"id\":\"1\",\"version\":\"1.0\",\"params\":{\"LightSwitch\":0}}";
    
            res = aiot_mqtt_pub(mqtt_handle, pub_topic, (uint8_t *)pub_payload, (uint32_t)strlen(pub_payload), 0);
            if (res < 0) {
                printf("aiot_mqtt_sub failed, res: -0x%04X\n", -res);
                return -1;
            }
        }
    Note After you configure the sample code, delete the annotation symbols on both sides of the code.
  • Parameters:
    Parameter Example Description
    pub_topic /a18wP******/LightSwitch/user/update The topic that has the Publish permission.
    • a18wP****** indicates the ProductKey of the device.
    • LightSwitch indicates the DeviceName of the device.
    The device can send messages to IoT Platform by using this topic.

    For more information about topics, see What is a topic?.

    pub_payload {\"id\":\"1\",\"version\":\"1.0\",\"params\":{\"LightSwitch\":0}} The message that is sent to IoT Platform.

    In this example, a custom topic is used. Therefore, you can customize the message format.

    For more information about message formats, see Data formats.

After an MQTT connection between the device and IoT Platform is established, make sure that the number of messages does not exceed the threshold.

Step 8: Disconnect the device from IoT Platform

Note

MQTT connections are applied to devices that remain persistently connected. You can manually disconnect devices from IoT Platform.

In this example, the main thread is used to set parameters and establish a connection. After the connection is established, you can put the main thread to sleep.

Call the aiot_mqtt_disconnect operation to disconnect the device from IoT Platform.

    res = aiot_mqtt_disconnect(mqtt_handle);
    if (res < STATE_SUCCESS) {
        aiot_mqtt_deinit(&mqtt_handle);
        printf("aiot_mqtt_disconnect failed: -0x%04X\n", -res);
        return -1;
    }

Step 9: Exit the program

Call aiot_mqtt_deinit operation to destroy the MQTT client instance and release resources.
    res = aiot_mqtt_deinit(&mqtt_handle);
    if (res < STATE_SUCCESS) {
        printf("aiot_mqtt_deinit failed: -0x%04X\n", -res);
        return -1;
    }

What to do next

  • After you configure the sample code file, compile the file to generate an executable file. In this example, the ./output/mqtt-x509-auth-demo executable file is generated.

    For more information, see Compilation and running.

  • For more information about running results, see View logs.