SSL/TLS
This page or section is a work in progress and may thus be incomplete. Its content may be changed in the near future. |
SSL/TLS is a protocol used to ensure a secure connection in various standard networking protocols (HTTP, FTP, etc.). Even though people talk about SSL, this protocol has been since mostly replaced with TLS (versions 1.0, 1.1 or 1.2). SSL should not be used anymore as it is not considered secure.
In order to setup an HTTPS connection, SSL/TLS is used between TCP and HTTP. In other word, the HTTP command sent by the Web browser and the HTML returned by the server are encrypted using SSL/TLS.
WARNING: implementing your own TLS layer is no guarantee of security. It is indeed recommended to never even write your own implementation of known, secure cryptographic algorithms as multiple attacks have been known to exploit some faults in the implementation. Writing your own TLS layer is however useful if you want to understand how SSL/TLS works and/or if you want to access Websites which are only available through HTTPS.
There are a few tools that can assist you when developing your own TLS layer. First of all, Wireshark is a free tool that captures network traffic and explains in details how the different packets are composed, down to the signification of each byte (save the encrypted parts). Also, Python can be an invaluable tool to prototype and verify your cryptographic algorithms (you might want to write a prototype of a TLS connection in Python first). Python indeed natively supports operations over very large integers like 1024-bit integers, and it has several cryptography libraries such as PyCrypto or Scapy SSL that allows you to forge SSL packets. These two tools can greatly help testing how TLS works.
Cryptography recap
Here are the main types of cryptographic algorithms:
Public/private key | Secret key | No key | |
---|---|---|---|
Encryption | Asymmetric encryption | Symmetric encryption | |
Verification | Signing | Message Authentication Cipher | Cryptographic hash |
- Asymmetric encryption (e.g. RSA): one party generates a private/public key pair and makes the public key readily available. Anybody can encrypt data using that public key, but only the owner of the private key can decrypt it
- Symmetric encryption (e.g. AES): both parties need to use a shared secret key to encrypt and decrypt data
- Signing (e.g. RSA): one party generates a private/public key pair and makes the public key readily available. Only the owner of the private key can sign data, but anybody with the public key can verify that the signature matches the data
- Message Authentication Cipher aka MAC (e.g. HMAC): generates a signature using a secret key
- Cryptographic hash (e.g. SHA1, SHA256): generates a signature of some data, but it is very hard to find another data that would generate the same signature
Cipher Suites
A SSL/TLS connection is actually using a whole set of cryptographic algorithms called a cipher suite. On top of that, SSL/TLS does not support one but multiple cipher suites. An SSL/TLS connection might use a completely different cipher suite depending on what the client and server support. Fully supporting TLS would actually require to implement a whole series of cipher suites. Fortunately, implementing only a few popular cipher suites is enough for most cases.
You can get an exhaustive list of the cipher suites available here, and use SSL Labs' SSL test to check what cipher suites are supported by various Web sites.
A TLS cipher suite is denoted as TLS_[key exchange protocol]_WITH_[encryption]_[authentication]. Because TLS is using symmetric encryption, the two parties first need to determine a common secret key over a non-secure connection. This is where the key exchange protocol is used. Then the data is encrypted. Last but not least, it is authenticated using a MAC, in order to make sure that data was not tampered with.
We will study how things work with TLS version 1.2 when the TLS_DHE_RSA_WITH_AES_128_CBC_SHA cipher suite is used. This cipher suite indicates the algorithms used for the key exchange (DHE, using RSA for verification), for the actual encryption/decryption (AES 128-bit in CBC mode) and verification (HMAC+SHA1). This cipher suite thus requires to implement the following:
- The Diffie-Hellman Ephemeral (DHE) key exchange protocol. This protocols relies on modular exponentiation over very large numbers, although it is possible to get past it if security is not your primary goal
- Encryption and decryption using AES 128-bit in CBC mode
- The SHA1 and SHA256 cryptographic hashing algorithm
- HMAC, a Message Authentication Code (MAC). A MAC is similar to a cryptographic hash function except that it requires a secret key
- Optional: if you want to verify the server certificate, you will need to implement the RSA algorithm, which also relies on modular exponentiation as well as SHA1/SHA256/SHA384 (depending on the certificate chain)
Note that you can easily find on the Internet source code for AES, SHA1, SHA256 and HMAC.
This cipher suite is not the strongest available, but is still relatively popular and shows the key mechanisms of a secure TLS interaction. Another cipher suite useful to implement is TLS_RSA_WITH_AES_128_CBC_SHA. The only difference is that the key exchange is using RSA instead of Diffie-Hellman. People interested in implementing a stronger suite can look at TLS_ECDHE_RSA_WITH_AES_128_GCM which requires to implement the Elliptic Curve version of Diffie-Hellman as well as the Galois Counter Mode (GCM) instead of the easier-to-implement CBC mode.
TLS Packets
Any communication in TLS starts with a 5-byte TLS Record header:
typedef struct __attribute__((packed)) {
uint8_t content_type;
uint16_t version;
uint16_t length;
} TLSRecordHeader;
This header may be followed by another TLS header, such as a TLS Handshake header or a TLS Application Data header.
Record Types
Value (Hex) | Description |
---|---|
0x14 | Change Cipher Spec |
0x15 | Alert |
0x16 | Handshake |
0x17 | Application Data |
Handshake Records
typedef struct __attribute__((packed)) {
uint8_t handshake_type;
uint8_t[3] length;
} HandshakeRecordHeader;
Value (Hex) | Description |
---|---|
0x00 | Hello Request |
0x01 | Client Hello |
0x02 | Server Hello |
0x0B | Certificate |
0x0C | Server Key Exchange |
0x0D | Certificate Request |
0x0E | Server Hello Done |
0x0F | Certificate Verify |
0x10 | Client Key Exchange |
0x14 | Finished |
TLS Handshake
Like for a TCP connection, a TLS connection starts with a handshake between the client and the server to establish the cipher suite used. See TLS Handshake for more details.
TLS Encryption
Once the handshake has completed, all data sent both way is encrypted using the algorithms and secret keys agreed upon during the TLS Handshake. See TLS Encryption for more information.
Math operations on large integers
Most cipher suites require to perform operations on large integers (e.g. 1024-bit integers) - something that low-level languages such as C/C++ do not handle out of the box. You can either port an existing library (such as GMP) to your operating system or write your own large integer library.
The most common operation (used among others by the RSA and Diffie-Hellman Ephemeral key exchange) is modular exponentiation i.e. computing ab mod c. This operation requires to implement multiplication, addition and modulo (which in turns requires to implement comparison, multiplication and subtraction) over large integers. You can find on Wikipedia an algorithm for modular exponentiation which is quite efficient even when using large numbers.
For better performance, it is recommended to use the full power of 32 (or 64-bit) numbers instead of performing operations one bit at a time. Here is an example:
typedef struct {
uint16_t size; // number of 32-bit words
uint32_t *data;
} LargeInt;
// Adds a to b and stores the result in a
void LargeInt_add(LargeInt *a, LargeInt *b) {
uint64_t carry = 0;
uint32_t *carry32 = (uint32_t*)&carry;
int size;
if (a->size > b->size) size = b->size;
else size = a->size;
for (int i=0; i< size; i++) {
carry = (uint64_t)a->data[i] + (uint64_t)b->data[i] + carry;
a->data[i] = carry32[0];
carry >>= 32;
}
if (size+1 <= a->size)
a->data[size] = carry32[0];
}
The same principle can be applied to multiplication: you can use the traditional long multiplication algorithm, but instead of multiplying decimal digits (or even worse, bits), you can instead 32-bit "digits" which drastically reduce the number of operations to perform.
Mind the endian
Remember that the data sent across the network is in big endian, whereas x86 computers use little endian. Last but not least, the premaster_secret passed to the PRF should be in big endian. Don't forget to perform the necessary conversions.