As we saw in the last chapter, a modern block cipher will allow two stations to communicate in private across an insecure channel. If both stations are able to agree on a secret key in advance through a secure channel, then establishing an encrypted channel can be straight forward although as we will see later, it is not without its problems. However, in many systems, the use of a “preshared key” is impractical. This means that a session key must be agreed between both stations over the insecure channel. This is a nontrivial problem that is addressed by the Transport Layer Security (TLS) protocol.
Cryptography; Ciphers; Malleability; Security; Encrypt; Decrypt
As we saw in the last chapter, a modern block cipher will allow two stations to communicate in private across an insecure channel. If both stations are able to agree on a secret key in advance through a secure channel, then establishing an encrypted channel can be straight forward although as we will see later, it is not without its problems. However, in many systems, the use of a “preshared key” is impractical. This means that a session key must be agreed between both stations over the insecure channel. This is a nontrivial problem that is addressed by the Transport Layer Security (TLS) protocol (Fig. 5.1).
If we want to communicate with a new station, we need to agree on a session key to be used by a symmetrical cipher. Now we are faced with having to transfer a secret between the two stations with an adversary watching. In this chapter, we will see how this can be achieved with the second class of ciphers called asymmetrical ciphers. We will then have a look at how the broader TLS protocol builds a practical solution to this key distribution problem.
Unlike a symmetrical cipher, a generic asymmetric cipher has two keys. The first is a freely distributed public key, which is used to encrypt data but cannot be used to decrypt any message. The second key is the private key that the owning entity must keep secret. The private key is used to decrypt messages that have been encrypted by the public key (Fig. 5.2).
Apart from the number of keys, there are several big differences between symmetrical and asymmetrical ciphers. First, unlike a symmetrical cipher, we cannot just randomly select encryption keys. In an asymmetrical system, the key pair has to be calculated. While this is only done once, it can be time-consuming, especially if done within the microcontroller. Second, a typical asymmetrical cipher requires a very large key size to achieve the same level of security as a symmetrical cipher. An asymmetrical cipher key will need to be 3072 bits to achieve the same security level as a 128 bit AES cipher and should not be smaller than 2048 bits. This also means that asymmetrical ciphers are much slower to compute than symmetrical ciphers. In a practical system, they are only used to transfer small amounts of data, such as keying material for symmetrical ciphers. Finally, the use of very large keys means that asymmetrical ciphers use very large numbers, which cannot be represented with standard “C” variable types. An asymmetrical cipher will need a dedicated big number library that uses multiprecision integers (MPI) that can support the range of values required. There are a number of asymmetrical cryptosystems in existence, but the majority of communication systems use one of two algorithms; either the RSA cryptosystem or the Diffie Hellman Key Exchange. In this section, we will look at both the RSA and Diffie Hellman systems.
The RSA algorithm was developed by Ron Rivest, Adi Shamir, and Leonard Adleman in 1977. Today, it is the most popular asymmetric algorithm, being used in around 90% of systems. The RSA algorithm is based on the “integer factorization problem.” While it is easy to multiply two primes together, it is very time-consuming to factor a large number into its constituent primes. This allows us to create a one-way function that is easy to compute, but the resulting product is hard to factorize.
The mathematical RSA identity is shown below:
From this, we can derive functions to encrypt and decrypt the plain text:
Here, we can see that the encryption and decryption function is identical. We just need to use the appropriate values for each operation.
The size of the public modulus n is important for two reasons. First, the values for the plaintext n and cyphertext c must be within the integer ring (the modulus) defined by n. Secondly the maximum bit size of the message is equal to the bitsize of n. Finally, the public and private key values e and d must also exist within the integer ring defined by n. For our cryptosystem to be secure, n must contain many key pairs to be invulnerable to brute force attacks. In ever-increasing computing power and advances in factorizing algorithms, a safe value for n needs to be a minimum of 2048 bits.
Unlike a symmetrical cipher, we cannot just pick an encryption key at random. Since the public and private keys are related, they have to be calculated.
RSA key generation uses the following algorithm:
We can select values for p and q using a random number generator then test to see if the numbers are prime. Two methods to do this are the Millar Raban primality test and the Fermat primality test as shown in Fig. 5.3.
We now have to select e, the public exponent. Again, this must be prime and within the cyclic group defined by “n.” This gives us an opportunity to improve the performance of the RSA algorithm. Here, we can select the value 65537. This value is useful because it is prime and is 2^16 + 1 or Hexadecimal 10001. This improves the RSA encryption algorithm's performance by reducing the number of multiplies. While “e” can be a relatively small value, “d” the private key is the same bit size as n (2048 bit), so decryption is many times slower.
This exercise demonstrates using the mbedTLS big number library to implement a demonstration version of the RSA algorithm. The values used in this example are very small and give a weak cipher, which should not be used in any real application.
The bignum library is enabled in mbedTLS_config.h
The example code declares a set of multiprecision integer to be used for the RSA values:
mbedtls_mpi P, E, N,Q,D,M,C, X;
Each value is initialized and Prime values are loaded into P,Q,E and M:
We can then compute the RSA key pair with void computeKeys(void)
This is done by first calculation the Totient:
mbedtls_mpi_mul_mpi( &N, &P, &Q ); mbedtls_mpi_sub_int(&Q, &Q, 1 ); mbedtls_mpi_sub_int(&P, &P, 1 ); mbedtls_mpi_mul_mpi( &L, &P, &Q );
We can then derive the key pair using an inverse modulus calculation, which is part of the bignum library:
mbedtls_mpi_inv_mod( &D, &E, &L );
Once the key pair has been calculated, we can use two functions to encrypt and decrypt plain text:
void computeRSA(void){mbedtls_mpi_exp_mod( &X, &C, &D, &N, NULL ); } void computeMessage(void){mbedtls_mpi_exp_mod( &C, &M, &E, &N, NULL ); }
As you can see, the encrypt and decrypt functions contain the same “mod a log” code but the type of operation depends on which key you use.
In the last exercise, we used the “school book” version of RSA. Even if we used large numbers, this is not a fully fledged cryptosystem. The core RSA algorithm is malleable. In other words, the cypher text can be modified by an attacker, and it will still decrypt to a meaningful value. We can see this by attacking our previous example if we take an integer S and apply this to the cypher text c.
Then, when we decrypt
If S = 2, an attacker has managed to double the value of m and the receiving station will accept this as valid data.
Here, we are taking the value “2” and raising it to the power of the public exponent e and then multiplying the cyphertext:
mbedtls_mpi_init( &S ); mbedtls_mpi_read_string( &S, 16,"00002" ); mbedtls_mpi_exp_mod( &S, &S, &E, &N, NULL ); mbedtls_mpi_mul_mpi( &C, &C, &S ); }
The attacker has successfully modified the encrypted message from 0x55 to double its value 0xAA. There has been a banking error in your favor.
We can overcome this malleability weakness by adding a padding scheme to the plaintext data. There are a number of different padding schemes, but the idea is to place a pattern alongside the plaintext prior to encryption. When we decrypt, we can check the padding to detect any manipulation of the cyphertext. This has the effect of armoring the ciphertext to make it resistant to any malleability attacks.
The original form of padding for the RSA cryptosystem was described in the PKCS1_1.5 document published by the RSA corporation. In this scheme, the total plaintext size is defined by the modulus n. The start of the plaintext contains the padding as a header followed by the data. The header starts with two bytes that define the block padding scheme. For PKCS1_1.5 the header value is set to 0x02. The remainder of the header is filled with random data until the start of the message data with a null byte delimiter between the random values and the message data. There must be a minimum of eight bytes of random data. The format is shown in Fig. 5.6.
While this scheme is still widely used it is subject to a number of protocol attacks, most notably one called Bleichenbacher's Attack.
A more up-to-date alternative to PKCS1_1.5 padding is Optimal Asymmetric Encryption Padding (OAEP). This scheme creates a padded header and encoded message packet but requires much more computation than PKCS1_1.5.
The scheme consists of a two-byte block type field the same as PKCS1_1.5, which for OAEP is set to 0x0000. This is followed by a masked seed field and then the encoded message.
The OAEP encoding uses a Mask Generation Function (MGF()). In most systems, this will be a hashing algorithm, typically SHA-1.
The OAEP padding is calculated as follows:
When using the RSA cipher, the padding scheme is not an option. It must be used to create a working cryptosystem. There are no exceptions.
The RSA functions provided by mbedTLS are shown in Table 5.1, and we will use these functions in the next exercise.
Function | Description |
---|---|
mbedtls_rsa:check_privkey | Check a private key for validity |
mbedtls_rsa:check_pub_priv | Check a key pair for validity |
mbedtls_rsa:check_pubkey | Check a public key for validity |
mbedtls_rsa:copy | Copy a context |
mbedtls_rsa:free | Free the context |
mbedtls_rsa:gen_key | Generate a key pair |
mbedtls_rsa:init | Initialize the context |
mbedtls_rsa:pkcs1_decrypt | Decrypt with PKCS 1 padding |
mbedtls_rsa:pkcs1_encrypt | Encrypt with PKCS 1 padding |
mbedtls_rsa:rsaes_oaep_decrypt | Decrypt with PKCS 1 OAEP padding |
mbedtls_rsa:rsaes_oaep_encrypt | Encrypt with PKCS 1 OAEP padding |
mbedtls_rsa:rsaes_pkcs1_v15_decrypt | Decrypt with PKCS 1.5 padding |
mbedtls_rsa:rsaes_pkcs1_v15_encrypt | Encrypt with PKCS 1.5 padding |
mbedtls_rsa:set_padding | Set the context padding |
In this exercise, we will generate a pair of RSA keys and then use them to encrypt and decrypt a symmetrical encryption key. The keys will be stored in keyfiles within the evaluation boards file system.
To generate the RSA keys, we create a context for the RSA cipher along with contexts for a random number generator and entropy pool.
The example code will initialize the RNG and open files to store the public and private keys.
Then we can initialize the RSA cipher with its context and our choice of padding system.
mbedtls_rsa:init( &rsa, MBEDTLS_RSA_PKCS_V15, 0 );
We can generate the RSA keys with a single function call. This uses random values to search for suitable primes. A weakness in our RNG implementation would result in predictability, which could be exploited by an adversary.
The definitions used in the function call define the required bit size for the private key and the value to be used for the public exponent “e.”
Once generated we can check both keys:
and then use the public key to encrypt an array holding some plain text such as a symmetric cipher encryption key.
mbedtls_rsa:pkcs1_encrypt( &rsa,mbedtls_ctr_drbg_random,NULL,MBEDTLS_RSA_PUBLIC,PT_LEN,rsa:plaintext,rsa:ciphertext )
We can then decrypt the cipher text with the private key and check it matches the original plaintext.
mbedtls_rsa:pkcs1_decrypt( &rsa,mbedtls_ctr_drbg_random,NULL, MBEDTLS_RSA_PRIVATE,&len,rsa:ciphertext,rsa:decrypted,sizeof(rsa:decrypted) );
One inherent problem with RSA is that if the private key is ever compromised, an adversary will be able to decode any previous and future messages that have been encrypted with the public key. There is a second public-key cryptosystem that avoids this problem and provides “perfect forward secrecy.”
Like RSA, the Diffie Hellman (DH) system is based on a mathematical one-way function. While RSA is based on the integer factorization problem, DH is based on the discrete logarithm problem. That is, exponentiation can be used to create a one-way function, and that exponentiation is commutative.
DH uses this identity to create a key exchange protocol. To set up, we need to select a large prime p, then both stations with a large integer alpha to be our domain parameters, which are public values. The value of alpha must be within the cyclic group (modulus integer ring) of p-2.
At the beginning of communication, the master station shares its domain parameters. Then both stations select a session private key, which is a second integer within the cyclic group of p-2. They then both compute their respective public key:
Both sides can now exchange their public keys and can compute a shared secret as follows:
The DH algorithm is not a cryptosystem like RSA. It is used to agree on a shared secret which will be different for every set of domain and session parameters. This means that if any station's private key is compromised, it will be of no use in recalculating the shared secret of other sessions, hence the concept of perfect forward secrecy.
This exercise demonstrates using the mbedTLS big number library to implement a demonstration version of the DH algorithm. Like the RSA, small numbers exercise, the values used in this example are very small and give a weak cipher which should not be used in any real application.
The code initializes a set of MPI numbers and initializes the domain values Alpha and P along with private key values for two stations a and b.
mbedtls_mpi_init( &Pub_A ); mbedtls_mpi_init( &Pub_B ); mbedtls_mpi_init( &a ); mbedtls_mpi_init( &b );
mbedtls_mpi_init( &Alpha ); mbedtls_mpi_init( &P ); mbedtls_mpi_init( &Secret_A ); mbedtls_mpi_init( &Secret_B ); mbedtls_mpi_read_string( &P, 16,"0001D" ); mbedtls_mpi_read_string( &Alpha, 16,"00002"); mbedtls_mpi_read_string( &a, 16,"00005"); mbedtls_mpi_read_string( &b, 16,"0000C");
Using the domain values, we can then compute the public key for each station.
mbedtls_mpi_exp_mod( &Pub_A, &Alpha, &a, &P, NULL ); mbedtls_mpi_exp_mod( &Pub_B, &Alpha, &b, &P, NULL );
By exchanging public keys each station can now compute the shared secret.
mbedtls_mpi_exp_mod( &Secret_A, &Pub_B, &a, &P, NULL ); mbedtls_mpi_exp_mod( &Secret_B, &Pub_A, &b, &P, NULL );
In a practical system, the initial value for the prime p is chosen using a random number generator and one of the prime verifying algorithms we saw with the RSA algorithm. The integers used to compute the public keys should also be selected using a random number generator to prevent the selection of weak values which an adversary could guess (Table 5.2).
Function | Description |
---|---|
mbedtls_dhm_calc_secret | Derive and export the shared secret |
mbedtls_dhm_free | Free the context |
mbedtls_dhm_init | Initialize the context |
mbedtls_dhm_make_params | Setup and write the key server parameters |
mbedtls_dhm_make_public | Create private value and export public value |
mbedtls_dhm_read_params | Read the key server exchange parameters |
mbedtls_dhm_read_public | Read the peer’s public value |
In this exercise, we will configure the evaluation board as a server and allow the PC to connect over a WiFi connection. A client on the PC will then request the DH domain parameters along with its public key. The client will use these values to calculate its public key and will then send this to the server. Both the server and client will calculate a shared secret. The server will use the shared secret to encrypt a text message which is sent to the client for decryption.
The embedded node is designed to run as a server with the address 192.168.0.10. You may need to adjust this to work with your WiFi network. When the PC client starts it will ask you to input the server address.
In the server we first declare the necessary contexts for the DH, AES ciphers and random number generator. We then initialize them as well as starting the local file system.
mbedtls_dhm_context dhm; mbedtls_aes_context aes; mbedtls_dhm_init( &dhm ); mbedtls_aes_init( &aes );
The full example code initializes the filesystem, networking, and random number generator before loading the servers DH parameters from the embedded file system.
f = fopen( "dh_prime.txt", "rb" ) ) == NULL ) mbedtls_mpi_read_file( &dhm.P, 16, f ) mbedtls_mpi_read_file( &dhm.G, 16, f ) fclose( f );
We then calculate the session DH parameters and extract the public parameters into a buffer ready to be sent over the network.
The server then opens a network port and waits for a remote connection.
The client connects to the server then sends its DH public parameter. Next, the client will reply with its public parameter.
The server can then load the local context with the clients public DP parameters and calculate the shared secret.
mbedtls_dhm_read_public( &dhm, buf, dhm.len ); mbedtls_dhm_calc_secret( &dhm,buf,sizeof( buf ),&n,mbedtls_ctr_drbg_random,&ctr_drbg );
Once we have the shared secret, it can be used as the encryption key to wrap the plaintext reply.
mbedtls_aes_setkey_enc( &aes, buf, 256 ); memcpy( buf, PLAINTEXT, 16 ); mbedtls_aes_crypt_ecb( &aes, MBEDTLS_AES_ENCRYPT,buf,buf );
The server will then send the encrypted packet back to the client.
The client-side follows a similar set of API calls to generate the session DH parameters and exchange public values with the server. The major difference is that it creates an ephemeral session key pair and shares the public values with the server.
Once the client has received the servers’ public values, it is able to calculate the same shared secret as the server. When the ciphertext packet arrives from the server, it can use the shared secret to decrypt the message back to the original plaintext.
The two stations will display their progress as they negotiate a secure connection.
Like RSA, the DH algorithm uses very large numbers and needs the use of the mbedTLS “big number library.” However, unlike RSA, the DH algorithm can be implemented with a different style of number line which greatly improves its performance.
So far, all of our asymmetric ciphers are based on the “integer factorization problem” or the “discrete logarithm problem” and a standard (linear) number line. In order to get a good level of security, we need to use very large numbers. This, in turn, results in a large amount of compute time. However, it is also possible to build a discrete logarithm problem by using an alternative number line and arithmetic based on elliptic curves. If you have been brought up on a diet of engineering math, this all looks a bit arbitrary; if you want to take a deeper look at the underlying math, I have listed a number of open source books in the bibliography. Everyone needs an obscure hobby.
Elliptic curve cryptography replaces our standard linear number line with a curve based on the identity.
We can plot the curve as a set of real numbers, which gives us the textbook curve, as shown in Fig. 5.10. An important feature of an EC curve is that it is symmetrical about the X-axis.
For cryptographic use, we want to create a finite field Chapter 4. We first need to create an integer ring, which can be done by adding a modulus operation. If we make p a prime, we can create a cyclic group in which each element is known as a primitive element.
While doing algebraic operations on a curve may seem a bit odd, it is possible to perform addition of two points as shown in Fig. 5.11.
To add two points, P and Q, we bisect them with a line and extend this line until it cuts the curve at a third point. If we then reflect this point across the X-axis, we get a result R = (P + Q).
We can also calculate P + P by extending the tangent of point P until it cuts the curve at a new point. Again this point is reflected across the x-axis to give us a result of 2P, as shown in Fig. 5.12.
For our integer ring to become a cyclic group we also need a neutral element φ such that
On a standard number line, the neutral element would be zero, but on our elliptic curve, it is not clear what we can use. In practice, we can define a point at “plus” infinity along the Y-axis as the neutral element. So we can consider φ to be asymtotic to each point on our curve
If we use our point addition method to calculate − P it turns out that the inverse of P is its mirror across the X-axis as shown in Fig. 5.13.
To create a discrete logarithmic problem on an elliptic curve, we use another property of elliptic curve addition: adding a point P to itself is the equivalent of raising P by a power on a linear number line.
So in effect:
is expressed as
Addition d times
Which can written as
We can then take our base point P and calculate its powers and use these as the basis for a discrete logarithm problem. As we will see in the next exercise, the public key is T and d is the private key.
To create an elliptic curve cryptosystem, we need to define a suitable curve (not easy). In practice, the EC curves supported by mbedTLS come from NIST, the Internet Engineering Task Force or the original researcher, Neil Koblitz. It should be noted that because of their underlying structure, the NIST curves have a higher performance than the IETF Brainpool curves (Table 5.3).
mbedTLS EC curve | Description |
---|---|
MBEDTLS_ECP_DP_NONE | |
MBEDTLS_ECP_DP_SECP192R1 | 192-bits NIST curve |
MBEDTLS_ECP_DP_SECP224R1 | 224-bits NIST curve |
MBEDTLS_ECP_DP_SECP256R1 | 256-bits NIST curve |
MBEDTLS_ECP_DP_SECP384R1 | 384-bits NIST curve |
MBEDTLS_ECP_DP_SECP521R1 | 521-bits NIST curve |
MBEDTLS_ECP_DP_BP256R1 | 256-bits Brainpool curve |
MBEDTLS_ECP_DP_BP384R1 | 384-bits Brainpool curve |
MBEDTLS_ECP_DP_BP512R1 | 512-bits Brainpool curve |
MBEDTLS_ECP_DP_CURVE25519 | Curve25519 |
MBEDTLS_ECP_DP_SECP192K1 | 192-bits “Koblitz” curve |
MBEDTLS_ECP_DP_SECP224K1 | 224-bits “Koblitz” curve |
MBEDTLS_ECP_DP_SECP256K1 | 256-bits “Koblitz” curve |
In this exercise, we replace the standard DH algorithm with the elliptic curve version and compute the server and client keys and shared secret. In the elliptic curve version of the DH algorithm, the public and private keys are points on the elliptic curve. In the code below, the values in the ECDH context are as follows:
The project has code that first creates and initializes a context for both the client and server stages:
mbedtls_ecdh_context ctx_cli, ctx_srv; mbedtls_ecdh_init( &ctx_cli ); mbedtls_ecdh_init( &ctx_srv );
The next few function calls are symmetrical for both the client and server here we will look at the client version.
For the client we first load the common elliptic curve.
We can then generate the public and private keys using a random number as a seed.
The public key MPI is then exported as a binary string to be shared with the server code.
The public key can then be read by the peer, in this case the server.
mbedtls_mpi_read_binary( &ctx_srv.Qp.X, cli_to_srv, 32 )
This process is repeated by the server code.
Once the public keys have been exchanged both the server and the client can compute a shared secret z.
mbedtls_ecdh_compute_shared( &ctx_srv.grp,&ctx_srv.z,&ctx_srv.Qp,&ctx_srv.d,mbedtls_ctr_drbg_random, &ctr_drbg );
Both the server and client will calculate the same shared secret. This exercise is comparable to Exercise 5.2, which uses RSA. Here we can see that the EC version is much faster.
In the last chapter, we saw how to calculate Message Authentication Code (MAC), which can be used to authenticate data through the use of a password. This password has to be shared through a secure channel between all stations that need to communicate.
Unfortunately, this is not a practical approach if we try to establish secure communications between different stations over a public internet connection. Fortunately, we can use public-key cryptography to authenticate data through a digital signing technique.
The RSA approach to signing data is very straight forward and elegant. If we want to sign a message to authenticate it as originating from us, we go through the following 2-step process. First, compute the hash of the plain text we want to sign. Second, we encrypt the hash using the RSA algorithm, but we use our private key. The resulting ciphertext is the signature of our plaintext. We can send the plaintext and signature to any other station along with our public key. The receiving station can authenticate the plain text by decrypting the signature using our public key. This will reveal the per calculated hash of the plaintext. They can now compute the hash of the plaintext, which should match the signature hash. If this is the case, we have confirmed the integrity of the plaintext and also its origin, as the signature could only have been created with our private key (Fig. 5.14).
In practice, the process is slightly more difficult as we have to add padding to the signature to prevent an existential attack. Like the RSA encryption and decryption functions, the different padding strategies are directly implemented by the mbedTLS signing API calls.
In this exercise we will calculate and verify the signature of a file of plaintext data using the RSA algorithm.
At the start of the code, we define each of the RSA public and private key numbers as an ASCII string:
#define RSA_N"9292758453063D803DD603D5E777D788" "8ED1D5BF35786190FA2F23EBC0848AEA" .............. "5E94BB77B07507233A0BC7BAC8F90F79"
We then define a multiprecision integer and initialize it along with the RSA context:
We can then set the RSA public and private keys by reading in each MPI number:
MBEDTLS_MPI_CHK( mbedtls_mpi_read_string( &K, 16, RSA_N ) ); MBEDTLS_MPI_CHK( mbedtls_rsa:import( &rsa,&K,NULL,NULL,NULL,NULL ) );
To create the signature, we first calculate the HASH of the plaintext file:
mbedtls_sha1_ret( rsa:plaintext, PT_LEN, sha1sum )
Then, we compute the signature using the private key and use the pkcs1 padding scheme:
mbedtls_rsa:pkcs1_sign( &rsa,myrand,NULL,MBEDTLS_RSA_PRIVATE,MBEDTLS_MD_SHA1,0, sha1sum_0,rsa:ciphertext );
Once the signature has been computed, a receiving station can recompute the hash and then verify the integrity of the plaintext using the sending stations public key:
mbedtls_rsa:pkcs1_verify( &rsa,NULL,NULL,MBEDTLS_RSA_PUBLIC,MBEDTLS_MD_SHA1,0,sha1sum_1,rsa:ciphertext );
An alternative to signing messages with RSA is the Digital Signature Algorithm, which is a NIST standard FIPS 128 - 3.
The ECDSA algorithm is more complex than RSA and has separate sign and verify algorithms. The ECDSA algorithm can also be implemented using elliptic curve cryptography, which is becoming more widely used for small embedded systems.
To sign a message, we need to select and compute a range of variables as shown in Table 5.4.
Parameter | Description |
---|---|
CURVE | An elliptic curve |
p | Modulus or CURVE |
a b | Coefficients of CURVE |
A | A point which generates a cyclic group of prime order q |
d | A random integer in the range 0 < d < q |
B | Computed as dA |
Public key kpub | p,a,b,A,B |
Private key kpr | D |
Random ephemeral session key ke | Where 0 < ke < q |
x | The message |
To compute a set of keys for ECDSA, we need to select a random integer d and compute B = dA. This will now give us the two keys, as shown in Table 5.4. This process is much faster than generating an RSA key pair.
Signing a message follows the same approach as RSA. We first need to calculate a hash of the data to be signed. The ECDSA algorithm is certified for use with the SHA-1 and SHA-2 algorithms. Once the hash is calculated, we can calculate the final ECDSA signature. The “C” pseudo code is shown below:
where r and s are the signature.
We can then verify the signature with the following calculations:
If xp is the x coordinate of P, the signature is valid if xp = r mod q
In this exercise, we will generate an ECDSA key pair and then compute and verify an ECDSA signature.
We can first select an elliptic curve from our library.
#define ECPARAMS MBEDTLS_ECP_DP_SECP192R1
Then create a context for the signing and verifying algorithms and initialize.
mbedtls_ecdsa:context ctx_sign, ctx_verify; mbedtls_ecdsa:init( &ctx_sign ); mbedtls_ecdsa:init( &ctx_verify );
In addition, we must create a random number generator so that we can generate a key pair.
Before calculating the signature we must first calculate the message hash.
Now we can calculate the signature.
unsigned char sig[512]; mbedtls_ecdsa:write_signature( &ctx_sign,MBEDTLS_MD_SHA256,hash,sizeof( hash ),sig,&sig_len,mbedtls_ctr_drbg_random,&ctr_drbg );
Now we have the signature we can verify it is correct. Our receiving station must setup the same elliptic curve and also copy the signing public key.
Now we can verify the signature:
You will also see that the process of key generation, signing, and verification is significantly faster than the RSA examples.
If we can give our public key to anyone, this enables them to encrypt and send a message that can only be decrypted using our private key. At first sight, it would appear possible to exchange keying material for a symmetrical cipher through a simple request and reply protocol similar to our DH key agreement exercise above. Unfortunately, this naïve use of asymmetrical encryption is prone to many attacks, not least of these being a “Man in the Middle” attack.
If we have two stations trying to establish a secure communication channel, they are traditionally called “Bob” and “Alice.” There is a possibility that a malicious third party can intercept their messages, traditionally called “Mallory” (Fig. 5.15).
In this scenario, when Alice tries to send her public key to Bob, it can be intercepted by Mallory, who substitutes his public key and sends it on to Bob. If Bob tries to send his public key to Alice, Mallory is again able to capture Bob's key and again send his public key to Alice. If Bob or Alice use Mallory's public key to encrypt a message, Mallory will be able to intercept it and decrypt it with his private key and then re-encrypt it with the relevant public key before retransmitting the message. This allows Mallory to read all messages sent between Alice and Bob without either of them being aware of the interception.
In order to prevent a “Man in the Middle” and similar attacks, asymmetrical ciphers are used within a scheme called the public key infrastructure (PKI). This scheme allows two stations to securely exchange public keys. The PKI rests on having a trusted third party called a certificate authority (CA) (Fig. 5.16).
Before they can communicate, Alice and Bob must send their public keys and credentials, including the name of their station. This can be a URL, hostname, or even an IP address. The certificate authority will then create a certificate that contains this information in a standard format. The data in the certificate are signed by the private key of the certificate authority and then returned to their owners. The certificate authority also publishes its own certificate, which contains its public key. The CA’s certificate is available to everyone, and it typically stored within all the participating stations. When Alice and Bob want to establish a secure channel, they can exchange their station certificates. When they receive a certificate, they can ensure its authenticity by using the certificate authorities public key extracted from the CA certificate to validate the signature of the received certificate. Once we have validated the received certificate, we have the public key of the station we want to communicate with. Since the certificate also contains credentials such as a URL or hostname, it is bound to its originating station. Thus, we can be sure that a malicious certificate has not been substituted by a Man in the Middle.
The certificates used in this scheme are held in a format called X.509. In addition to the senders’ public key, the certificate contains a subject field that identifies its origin. This includes the sender's name and location along with a field called the “common name.” The common name field is a text string and does not have a well-defined format. In fact, you can really put just about anything in here as it is just tested for equality by the receiving station. In practice, it will contain either a hostname or a URL. You could put in a raw IP address, but this is considered bad practice and should be avoided (Fig. 5.17).
The X.509 certificate contains additional fields for the X.509 version, a certificate serial number, the issuing certificate authority, and its period of validity as a start and end date. The certificate also contains fields that define the algorithms used to sign the certificate. In order to sign the certificate, the certificate authority will calculate a hash of the certificate fields and then calculate the signature of the certificate by encrypting the hash using an asymmetrical cipher and the certificate authorities private key.
Every station in our system will have the CA’s certificate and hence the CA’s public key. When we receive a new certificate, we can extract the calculated hash value from its signature using the CA’s public key. Next, we can calculate the certificate's hash and compare it to the signature hash. If the two match, then the certificate is valid. To fully trust it, we would next check its period of validity and whether the common name contains the hostname or URL of the station we are trying to communicate with. If we are satisfied with all of this information, then we can start to communicate with the remote station using its public key stored in the certificate.
The mbedTLS library supports certificates being stored in a standard file system and allows them to be stored as arrays in FLASH memory. The “Period of Validity” field in the X.509 certificate gives every certificate a fixed lifetime and an expiration date. If you are designing a system where each node will have a long lifetime, you will likely have to update the certificates held in the IoT device. Since the CA certificate is public information, it does not need to be held in secure storage. This would allow you to either update the certificate directly if it is held in the file system or do a firmware update if the certificate is stored in FLASH as part of the application image.
While a certificate will naturally expire and have to be replaced, it is likely that at some point, we will need to forcibly remove the rights of a node to connect to our system. The Transport Layer Security (TLS) protocol supports two methods of doing this. The first is a certificate revocation list (CRL). Here we can maintain a list of device certificates that are no longer allowed to join our system. The CRL is loaded by the server and acts as a blacklist of prohibited devices. In the broader internet, the certificate authority will maintain a CRL, which can be periodically downloaded to ensure that no revoked device can connect to the server. While CRLs are widely supported, a newer revocation method called online certificate status protocol (OCSP) is becoming the preferred method of checking the status of a certificate. The online certificate status protocol allows a node to directly query the current status of a certificate with the CA before allowing it to connect.
An X.509 certificate is a collection of strings and arrays that can be held as a C object. However, to enable data interchange between many diverse systems, we need to define a standard interchange format. The encoding rules for digital certificates are defined by a telecoms standard called Abstract Syntax Notation One or ASN.1. The ASN.1 standard is defined by the International Telecommunication Union and is an interface description language that is widely used in telecoms and computer networking systems. The ASN.1 standard defines a set of Basic Encoding Rules (BER), which provide a flexible system for encoding data. There is also a subset of rules called the Distinguished Encoding Rules (DER), which define an “unequivocal transfer syntax” or one way to encode and decode a given set of data. An X.509 certificate is encoded using the ASN.1 format with DER encoding. The output of this encoding will be a block of binary data. A certificate may be further encoded into a base 64 format which encodes the binary data as ASCII characters. This ASCII format was originally developed to send binary data as email attachments and is known as Privacy Enhanced Mail or PEM. When we store a certificate, it will either be in a binary format (.der) or an ASCII format (.pem), but either format contains the same information. The ASN.1 format can be described as fiendishly complicated, but fortunately, we do not need to work with it directly. We can use mbedTLS and other tools to generate and parse X.509 certificates.
On the broader internet, there are many clients in the form of PCs, tablets, and mobile phones that want to connect to secure web servers. In this situation, we need the CA to be a trusted third party who can provide certificates to public websites and have their certificate stored in general users' devices. However, if you are going to design a closed system that only contains your own servers and devices, it is possible and usually desirable to be your own certificate authority. This creates a “walled garden” where only authenticated devices can connect to the server. Most commercial IoT cloud systems act as their own CA.
We can further improve the security of the system by issuing certificates to devices. In this case, the server and device exchange and validate each other's certificates. This form of mutual authentication provides a higher level of security and should be used for IoT systems whenever possible. We will set up both server authentication and mutual server/device authentication as we look at the TLS protocol. If you are using a commercial cloud system it should enforce mutual authentication.
In the real world one, CA does not rule them all. There are a small number of root CA's who may sign the certificates for a regional or industry CA. This local CA can, in turn, sign an end-entity certificate. This creates a chain of certificates or certificate path. If you are working with a cloud server or commercial certificate authority, you may be issued a single certificate or a chain of certificates. In the case of a certificate chain, you will need to parse each certificate until you find one you can trust. This is fully managed by the mbedTLS library, so you don't need to develop any further code to parse the certificate chain.
In this exercise, we will look at how to create a set of X.509 certificates which are used to validate the credentials of both servers and clients. We will use XCA, which is an open-source tool to create the certificates. XCA has a graphical user interface which is useful when learning.
The database contains three templates, which we can use to create a set of certificates for our network of IoT devices and servers.
First we will create a “certificate authority” certificate, which will be used to sign the server and client certificates.
Select the source tab This certificate will be our “Root of Trust” and, as such, will be self-signed. The algorithm used to sign the certificate is SHA256. This is the current minimum standard you should use to sign a certificate (Fig. 5.19).
Enter an internal name and a common name. These can be different, but in this case, I have used IoT_CA. The template has set the other distinguished name fields, but you can change them to match your needs.
Here, the type of certificate is configured as “certificate authority” and there is a default time range of 20 years.
This dialog allows you to limit the properties of the certificate. By leaving everything blank, we are not adding any restrictions to our basic certificate. Once you have a working system, you can experiment with these fields.
Here, we will generate a 2048 bit RSA key
The new certificate and key can be viewed in the certificates window (Fig. 5.24).
and the private keys window (Fig. 5.25).
The certificate authorities private key is securely held in the XCA database and must always be kept secret as it is used to sign the server and client certificates.
Here we are using the term Broker as another name for Server. We will see the role of a “broker” in the next chapter.
This time the type of certificate is set to “End Entity” and the life time of the certificate is limited to 10 years.
Now we are going to repeat the process one more time to create a device certificate. This will then give us all the certificate and keys we need to build a secure networked system.
Now in the certificate window, we can see the certificate hierarchy. The IoT_CA certificate is our “Root of Trust,” and it has signed both the broker and the device certificates. If you have the IoT_CA certificate installed, it can be used to check the signature of either the broker or the device certificate and authenticate the data in the certificate.
The certificate information is encoded in “Abstract Syntax Notation 1” (ASIN1) using the “Distinguished Encoding Rules” (DER). Here, the heavy lifting is done for us by XCA. When we save the certificate, the binary data is further transformed into a format called “Privacy Enhanced Mail” (PEM), which is base64 encoded binary with a short additional header.
Now we have three keys stored. We need to export the server and device keys so they can be stored in their respective devices. The certificate authority key must never be exposed outside of the XCA database (Fig. 5.36).
Now in our certs directory, we should have five files: three certificates and two key files (Fig. 5.37). We will use these certificates for server and device authentication later in this chapter.
mbedTLS provides functions to load certificates and keys from static arrays stored in the microcontroller flash memory or from an embedded file system (Table 5.5). The certificates and keys can be stored in DER or PEM format. Keys can be exported and stored in a standard keyfile format that provides password-based encryption with symmetrical ciphers. As we will see in later chapters, the PSA Trusted Firmware creates a Secure Internal Storage volume to hold device secrets. In addition, a Cortex-M33 based microcontroller designed to be an IoT device is likely to provide a key store mechanism, and we will also look at these features in the coming chapters.
Function | Description |
---|---|
mbedtls_x509_crt_parse() | Load a certificate from an array |
mbedtls_x509_crt_parse_file() | Load a certificate from a filesystem |
mbedtls_pk_parse_key() | Load a key from an array with optional password |
mbedtls_pk_parse_keyfile() | Load a key from a filesystem stored as a keyfile with optional password |
Here, we will look at how to load and parse stored certificates within our embedded microcontroller using the standard mbedTLS functions.
As usual, we start by declaring the necessary contexts that we will need in this example.
Here, we have a context for a certificate authority and a client certificate. We also have a public key context to load our private key.
mbedtls_x509_crt cacert; mbedtls_x509_crt clicert; mbedtls_pk_context pkey; mbedtls_x509_crt_init( &clicert );
We can then parse anX.509 certificate.
mbedtls_x509_crt_parse( &clicert,(const unsigned char *) mbedtls_test_cli_crt,mbedtls_test_cli_crt_len );
Since this information is public information, we do not need to store it securely. It can be placed in the microcontroller flash memory as a “C” array.
Alternatively, the certificate can be stored in the embedded file system and loaded from a file.
mbedtls_x509_crt_parse_file (&cacert,"iot_ca.crt");
Once the certificates are loaded, we can verify the validity of the certificates.
Similarly, an RSA private key can be loaded directly from an array stored in flash memory.
mbedtls_pk_parse_key( &pkey,(const unsigned char *) mbedtls_test_srv_key,mbedtls_test_srv_key_len,NULL,0 );
or loaded from a keyfile:
int mbedtls_pk_parse_keyfile(&pkey,&keyfile,&password);
Once loaded we can test the RSA keys:
The code will parse the certificates and display their details to the tera term console window.
Over the last two chapters, we have looked at the essential cryptographic algorithms and the public key infrastructure. We can now look at how to establish a secure communications channel across the internet with the Transport Layer Security (TLS).
The TLS protocol is designed to be encapsulated in Transport Control Protocol packets, which in turn ride inside an Internet Protocol packet to provide a reliable streaming communications channel (Fig. 5.38).
The TLS packet consists of the actual message data prefixed by a header that contains the protocol version, the length of the message data in bytes, and the content type, which is a code for the type of TLS packet being sent (Table 5.6).
When a TLS connection is established, further encrypted packets will be sent in application frames. If there is an error or if the connection needs to be shut down, an Alert frame will be sent. If you are sending infrequent message application packets but need to keep the connection from timing out, it is possible to send a heartbeat frame. When the connection is first negotiated, the server and client exchange a list of available ciphers. During a communication session, it is possible to switch ciphers suites by sending a change cipher spec message. A TLS session will always start with an initial handshake where the client and server authenticate each other and establish a shared secret which can then be used as the basis for a symmetrical encryption key.
The handshake packet has a further code that denotes the handshake content type for each stage of the TLS handshake (Table 5.7).
Message type | Description |
---|---|
0 | Hello Request |
1 | Client Hello |
2 | Server Hello |
4 | New Session Ticket |
11 | Certificate |
12 | Server Key Exchange |
13 | Certificate Request |
14 | Server Hello Done |
15 | Certificate Verify |
16 | Client Key Exchange |
20 | Finished |
At the start of a TLS session, both the client and server will have the CA certificate. In the case of a system that is setup for server authentication, the server will have its own certificate, which has been signed by the CA, and it will also have its own private key (Fig. 5.39).
At the start of the TLS handshake, the client will send a client hello message to the server. The client hello message from the client to the server will contain a random number which is sent in plain text along with a session ID, a list of cipher suites, and compression methods. The cipher suites are predefined groups of asymmetrical and symmetrical ciphers along with hash and MAC algorithms. A small sample set of these cipher suites is shown in Table 5.8.
ID | Cipher suite (key exchange, signing, cipher, hash) |
---|---|
0x2F | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
0x30 | TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 |
0x67 | TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 |
0x69 | TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 |
The server will reply with its own server hello message that also contains a random number again sent in plain text.
The server will next send its X.509 certificate which can be verified by the client using the public key in the CA certificate (Fig. 5.40).
Once the certificate has been validated, it tells the client to authenticate its origin through the certificate common name. In the case of a website, the common name will be a URL that should match the address that the client is expecting to connect to. It is less clear what to use for an IoT device common name. Typical choices are the device IP address or its internal hostname. The certificate also provides the client with the servers’ public key.
Next, the client will generate another random number called a premaster secret, and this is again sent to the server, but this time, it is encrypted and hashed using the servers public key (Fig. 5.41).
Now both the server and client have two agreed random numbers which have been sent in plain text and a further random number that has been exchanged in secret. Using these numbers, we can create a master secret which will be the basis for our symmetrical encryption key. The master secret is derived through a special pseudo-random function (Fig. 5.42).
The two plain text random numbers are concatenated together and passed into the pseudo-random function along with the premaster secret. The output value from the pseudo-random function is then hashed using both an MD5 and an SHA-1 hash algorithm. The two message digests are finally added together modulo32 to provide the final master secret.
Once both sides have agreed on a master secret, the client will send a change cipher spec message to select a cipher suite followed by a finish handshake message. The server will reply with a matching change cipher spec and finish message (Fig. 5.43).
Now both sides will switch to encrypted communication using the chosen cipher suite with the master secret as the agreed key. Your session data will be encrypted with a MAC tag and placed in a TLS application frame (Fig. 5.44).
Now both the server and the client have established a secure communication channel, and all further data packets will be encrypted. If an error occurs, either end can send an alert frame to notify its partner that there is a problem. The alert frame has an alert level field that can be set to a warning level or fatal level and a description field with 31 codes to describe the alert condition (Fig. 5.45). In practice, if an alert frame is sent, the session will usually terminate. An alert frame is also used to end a TLS session.
We can create both a server and client with the mbedTLS library. To create a secure server, we first need to declare and initialize contexts for the SSL configuration and the SSL operating layer.
We also need to declare and initialize contexts for entropy, random, public key, X509 certificate, and network components in a similar fashion.
At the start of the example code, the server has to load the CA certificate and the servers’ certificate. At this point, we could also load a certificate revocation list.
Next we can load the server private key.
mbedtls_pk_parse_key( &pkey,(const unsigned char *) mbedtls_test_srv_key,mbedtls_test_srv_key_len,NULL,0 );
Now we can bind an IP port to the TCP protocol.
Our next step is to setup the ssl-config context with its default settings and to act as a server.
mbedtls_ssl_config_defaults( &conf,MBEDTLS_SSL_IS_SERVER,MBEDTLS_SSL_TRANSPORT_STREAM,MBEDTLS_SSL_PRESET_DEFAULT )
Then we can configure and add a random number generator.
mbedtls_ctr_drbg_seed( &ctr_drbg,mbedtls_entropy_func,&entropy,(const unsigned char *) pers,strlen( pers ) ) ) != 0 ) mbedtls_ssl_conf_rng( &conf,mbedtls_ctr_drbg_random,&ctr_drbg );
The TLS stack is instrumented to produce debug messages as it executes. The SSL Config context can be passed a user callback function to manage these messages. Normally, it can be written to a debug UART or the ITM, but it is also possible to log the debug data to a file.
mbedtls_ssl_conf_dbg( &conf, my_debug, stdout );
During runtime, the callback function is passed the debug message, which consists of a debug code, the file, and line of origin and an informational text string.
There are five levels of debug message and we can dynamically control the message threshold level (Table 5.9).
mbedtls_debug_set_threshold( DEBUG_LEVEL );
Next we can add the certificates to the ssl configuration context.
mbedtls_ssl_conf_ca_chain( &conf, srvcert.next, NULL );
Once the configuration structure is fully populated we can configure the working ssl context.
mbedtls_ssl_setup( &ssl, &conf );
The mbedTLS library can be connected to the underlying transport by defining the send and receive callback functions.
Both the send and receive functions are designed to map onto a typical BSD sockets interface.
int mbedtls_net_send( void *ctx,const unsignedchar *buf,size_t len ){ send (SOCK(ctx), (const char *)buf, len, 0);} int mbedtls_net_recv (void *ctx,unsigned char *buf,size_t len){ recv (SOCK(ctx), (char *)buf, len, 0);}
Once all this is configured we can accept connections from clients.
mbedtls_net_accept( &listen_fd, &client_fd, NULL, 0, NULL );
When a client accepts, we can process the TLS handshake to establish a secure connection.
If the handshake is successful, we can now read and write plaintext application packets to and from the TLS library.
If an error or other condition causes the server to shutdown, it must release each of the contexts.
The Client code follows a very similar process, but there are a few key differences. The SSL_configuration context is set to be a client:
mbedtls_ssl_config_defaults( &conf,MBEDTLS_SSL_IS_CLIENT,MBEDTLS_SSL_TRANSPORT_STREAM,MBEDTLS_SSL_PRESET_DEFAULT ) ;
During the SSL configuration, we need to set the authentication mode. This will tell the stack how to proceed if verification of the certificate fails. In the example code, VERIFY_OPTIONAL is used so that we will always establish a connection when using test certificates (Table 5.10).
Authentication mode | Description |
---|---|
MBEDTLS_SSL_VERIFY_NONE | No certificate verification is required |
MBEDTLS_SSL_VERIFY_OPTIONAL | Verify the certificate but continue if verification fails |
MBEDTLS_SSL_VERIFY_REQUIRED | Verify the certificate and abort if verification fails |
We must also set the hostname of the server. This must match the Common Name on the servers X.509 certificate. While the certificate is public information, only the server has the matching private key.
mbedtls_ssl_set_hostname( &ssl, "mbed Test Server 1" );
Once the SSL context is configured, we can connect to the server and perform the handshake.
mbedtls_net_connect( &server_fd,SERVER_NAME,SERVER_PORT,MBEDTLS_NET_PROTO_TCP ); mbedtls_ssl_handshake( &ssl );
Once we have finished the handshake, we can verify the certificate.
result = mbedtls_ssl_get_verify_result( &ssl );
If everything is OK, we can start to communicate to the server using the same ssl_read and write functions.
When the client has finished its session, we must close the TLS session and then free the contexts.
This exercise will configure the evaluation board as a client and use a PC server application based on mbedTLS as the server. This will give us a simple local network that can be used to test and experiment with x.509 certificates and the mbedTLS networking API.
The exercise directory contains an mbedTLS client project for the LPC55S69 evaluation board. This example uses the mbedTLS test certificates, which are provided as header files. The certificates will fail verification in a couple of ways which is useful to see.
When the device connects to the WiFi Network, it will print out its IP address to the console window.
The debugger console window will show the TLS connection initializing and then waiting for a connection (Fig. 5.46).
The HTTPS is important as it tells the browser to establish a secure connection. The browser will report a bad certificate, but you can force it to proceed. It will connect to the evaluation board, and a HTML message will be sent to the browser (Fig. 5.47). The CA certificate has not been installed within the browser, we have just used the browser to show that the certificate is faulty.
This will connect to the same port and display more detailed diagnostic messages.
Again we can see that the certificate has failed. Two reasons are given: first, the Common Name is wrong, and second, the certificate has been signed an unacceptable hash, in this case, a deprecated SHA-1 algorithm was used (Fig. 5.48).
If the authentication mode is changed to “verification required” then the communication session would be terminated when the handshake fails.
So far, we have looked at a standard TLS handshake which is typically used when a web browser establishes a secure connection to a website. This system has the attraction that the client only needs to hold the CA certificate, which is public information and can have a long lifetime. However, for an IoT system, we really need to go a bit further and use a client certificate that will allow the server to authenticate the client. This mutual authentication provides a much higher level of security and can remove the need for a further sign-on password. However, it does mean that the client has to be provisioned with its own individual certificate and private key.
In such a system, the server will demand a client certificate once it has sent its certificate. Once the client has validated the server certificate, it will send its certificate to the server. The server will validate the client certificate and reply with a hash of all the existing handshake messages signed with the client's public key (Fig. 5.49).
In our existing server code, we can force the server to authenticate the client by setting an authentication mode:
The server will now request and validate the client certificate as part of the TLS handshake.
On the client side, in addition to the CA certificate, we need to load the client certificate:
ret = mbedtls_x509_crt_parse( &clicert,(const unsigned char *) mbedtls_test_cli_crt,mbedtls_test_cli_crt_len );
Along with the client private key which must also be added to the TLS configuration context.
In this exercise, we will configure our network client and server to perform mutual authentication. The code is basically the same as the last example but has been extended to provision the client with a device certificate and private key. The server is configured to request the device certificate, which will be validated by the servers CA certificate.
Copy the CA and broker certificates created in project 5.9 plus the broker private key to the PC_Server directory or use the existing files.
This will establish a secure connection which mutually authenticates both the Server and the Client.
In the last two chapters, we have covered the main encryption algorithms that you are likely to need when developing an IoT device. While the focus here has been on secure communication, the same algorithms are used to provide protection for data at rest and also to sign and verify device images to establish trust in the integrity of a device's firmware. To finish the communication section, the next chapter will introduce protocols and data formats commonly used with IoT devices. We will also look at connecting our device to both a local server and a commercial cloud server.
3.239.76.211