AWS IoT core supports TLS 1.2: https://docs.aws.amazon.com/iot/latest/developerguide/transport-security.html

Still frustrated by the challenges of getting what should be simple to work:

  • Raspberry Pi Pico W
  • lwIP for TCP/IP
  • mbedTLS for TLS
  • coreMQTT for MQTT

I don’t mind which MQTT client or which TLS library I’m using. But I’m just not having much success. So the next question is: with a given TLS version, and a specific cipher suite, how hard is it to get it to work. I don’t need a general-purpose TLS library for all occasions, just something that would be enough. And to be honest, with the kind of state secrets I’m sending to AWS (sensor temperature in the kitchen), I’m not sure that I care that much about sophisticated side-channel attacks.

So let’s investigate the wonders of TLS v1.2. This post is a long and boring attempt at making sense of TLS.

Tha TLS Handshake

I am mainly going to be using RFC 5246 and some Python to understand what’s going on.

Maybe I’m a bit impatient, but the TLS handshake sounds like a good place to start.

TCP Socket

Create a TCP socket and connect to the endpoint (I’m using the IoT endpoint):

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
    s.connect((ENDPOINT, 8883))
    print(s)
    s.close()

The output:

<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('192.168.4.24', 59993), raddr=('52.211.117.185', 8883)>

Seems reasonable.

ClientHello

The ClientHello message is defined as:

      struct {
          ProtocolVersion client_version;
          Random random;
          SessionID session_id;
          CipherSuite cipher_suites<2..2^16-2>;
          CompressionMethod compression_methods<1..2^8-1>;
          select (extensions_present) {
              case false:
                  struct {};
              case true:
                  Extension extensions<0..2^16-1>;
          };
      } ClientHello;

For this exercise:

  • The protocol version is TLS 1.2 AKA version 3.3 (two bytes, 3 and 3)
  • The random structure contains the current time (4 bytes from time.time()) and 28 random bytes (from secrets.token_bytes)
  • The initial session_id is empty because it comes from the server and so I don’t have a value for it
  • The cipher suite contains a single entry, just TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 for now. The RFC says about ECDHE_RSA: “RSA public key; the certificate MUST allow the key to be used for signing (the digitalSignature bit MUST be set if the key usage extension is present) with the signature scheme and hash algorithm that will be employed in the server key exchange message.” Since the certificate uses RSA with SHA256, the cipher suite must also use the same algorithms (that’s my understanding, I could be wrong).
  • Compression method is null
  • No extension (at least for now, maybe later for MQTT)

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 is defined in https://www.rfc-editor.org/rfc/rfc5289.

Building this message, I’m getting:

01 00 00 25 03 03 63 d6 bf 6c 52 b0 4d c0 54 8f
67 7b 29 b0 f1 04 30 55 65 5b 2f 12 6d 18 99 22
5c 0d be 1e 82 8f c0 2f 00

That feels a bit long, let’s see:

  • 01: msg_type (1 - client_hello)
  • 00 00 25: length (37 bytes)
  • 03, 03: protocol version - TLS 1.2
  • 63 d6 bf 6c: gmt_unix_time = 0x63d6bf6c = 1675018092 = Sunday, 29 January 2023 18:48:12
  • 52 b0 … 82 8f: random_bytes[28]
  • c0 2f: cipher suite (TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
  • 00: compression method

Unfortunately, the server is not sending anything. Trying to include the message type and length in the total length doesn’t make any difference. The server is not sending anything.

The key is probably: “Handshake messages are supplied to the TLS record layer, where they are encapsulated within one or more TLSPlaintext structures”.

So TLSPlantext:

   enum {
       change_cipher_spec(20), alert(21), handshake(22),
       application_data(23), (255)
   } ContentType;

   struct {
       ContentType type;
       ProtocolVersion version;
       uint16 length;
       opaque fragment[TLSPlaintext.length];
   } TLSPlaintext;

OK, let’s try again.

16 03 03 00 29 01 00 00 25 03 03 63 d6 c5 0a 8c
d1 37 6d 2e dd d8 6a 5d 38 ea b2 62 0e 9c 73 ea
71 8e 47 82 72 95 25 ad a0 dd f7 c0 2f 00
  • 16: content type (22 = handshake)
  • 03 03: protocol version (TLS 1.2)
  • 00 29: length
  • 01 …: same as before

Still nothing from the server, unfortunately. I’m afraid that the next step is Wireshark.

So… missed so many things:

  1. Vectors are not just dumped into the message. They are preceded by the length of the vector in bytes, encoded using the appropriate number of bytes.
  2. SessionID may be optional, but it’s a vector so at the very least it needs a length.
  3. AWS IoT Core requires the SNI extension. This is defined in https://www.rfc-editor.org/rfc/rfc6066#page-6.

ServerHello

And with this in mind, I’m now getting the following from the server:

16 03 03 00 50 02 00 00 4c 03 03 7d 95 23 b1 32
49 44 72 af f6 1a 85 0f a8 ba ce 05 b1 aa ef 09
02 2e 8d 1d 23 8e e9 8d 76 31 12 20 52 3b c5 20
07 fb 28 86 14 7b 64 28 9a 43 95 87 24 39 b0 8e
f7 ab 1d c6 f3 9b 66 ee 5b 8f 96 2a c0 2f 00 00
04 00 00 00 00

Let’s make sense of this. I’m expecting a TLSPlaintext structure with a Handshake message containing a ServerHello structure. Or possibly multiple TLSPlaintext structures with fragments that make up the Handshake message.

The ServerHello structure is defined as:

      struct {
          ProtocolVersion server_version;
          Random random;
          SessionID session_id;
          CipherSuite cipher_suite;
          CompressionMethod compression_method;
          select (extensions_present) {
              case false:
                  struct {};
              case true:
                  Extension extensions<0..2^16-1>;
          };
      } ServerHello;
  • 16: content type (handshake)
  • 03 03: protocol version (TLS 1.2)
  • 00 50: length
    • 02: message type (server_hello)
    • 00 00 4c: length
    • 03 03: server protocol version (TLS 1.2)
    • 7d 95 … 31 12: 28 random bytes (including timestamp)
    • 20: session id length
    • 52 3b … 96 2a: session id
    • c0 2f: cipher suite (the one I’ve sent)
    • 00: compression method (null)
    • 00 04: number of bytes in the extensions
    • 00 00: extension type (`server_name)
    • 00 00: extension length

I’m not sure why the server_name extension is used with no data. But OK.

What did we get? Hello from the server, a session id and confirmation that the server supports the TLS version (1.2) and the cipher suite (TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256).

Certificate

Everything is good, so next we expect the certificate(s) from the server. I’m guessing that they will be sent inside TLSPlaintext structures.

16 03 03 13 83 0b 00 13 7f 00 13 7c 00 06 14 30
...

The structure is defined as:

      opaque ASN.1Cert<1..2^24-1>;

      struct {
          ASN.1Cert certificate_list<0..2^24-1>;
      } Certificate;
  • 16 03 03 13 83 - as before where length is 0x1383 = 4995 bytes (which is why I did not include the entire message)
  • 0b: certificate
  • 00 13 7f: length (0x137f = 4991 bytes)
  • 00 13 7c: length (0x137c = 4988 bytes) - yes a bit repetitive but that’s how it goes. That’s the length of the certificate list
  • 00 06 14: length (0x614 = 1556 bytes) - the length of the first certificate
  • 30 …: ASN.1 encoded certificate data

I am not going to check the certificates at the moment, just follow the protocol. Parsing and validating the certificates is for another day

ServerKeyExchange

Next we expect the ServerKeyExchange. Things are about to get serious quickly, because in order to make sense of anything, I will have to actually generate a key. But not just yet.

The structure is:

      struct {
          select (KeyExchangeAlgorithm) {
              case dh_anon:
                  ServerDHParams params;
              case dhe_dss:
              case dhe_rsa:
                  ServerDHParams params;
                  digitally-signed struct {
                      opaque client_random[32];
                      opaque server_random[32];
                      ServerDHParams params;
                  } signed_params;
              case rsa:
              case dh_dss:
              case dh_rsa:
                  struct {} ;
                 /* message is omitted for rsa, dh_dss, and dh_rsa */
              /* may be extended, e.g., for ECDH -- see [TLSECC] */
          };
      } ServerKeyExchange;

Clearly not relevant, because we use Elliptic Curve Cryptography. So TLSECC:

        select (KeyExchangeAlgorithm) {
            case ec_diffie_hellman:
                ServerECDHParams    params;
                Signature           signed_params;
        } ServerKeyExchange;

To be honest, the “TLSECC” specification (https://www.rfc-editor.org/rfc/rfc4492) is more complicated than I hoped, because it deals with stuff I have no clue about, like, well, elliptic curves. But nobody’s born knowing elliptic curves, so let’s move on.

The message is quite long (338 bytes), and at this point I’m not really interested. Briefly, it should contain enough information to start the key exchange.

Also, this RFC includes extensions that allow the client to specify what it can support (in terms of curves, whatever they may be), so it’s probably wise to use those extensions.

And So On, And So Forth

At this point, I feel that I have enough information to be able to read and understand the specification. Going into further detail will just be a waste of time.

Next on the agenda is understanding elliptic curve cryptography.