>

TLV Helpers

As seen in the Project 2 spec, instead of using packet headers with predefined byte boundaries, we’ll be using Type-Length-Value (TLV) encoding for the security messages. TLV can be a bit tricky to work with, so we’re providing some TLV helpers in the consts.h file found in the starter code.

#Using security.c

Before going into how to use the TLV helpers, it’s important to know how the programming model in security.c works.

In Project 1, we implemented a reliable data transfer protocol. Our “upper layer” was standard input and standard output. We interfaced with stdin and stdout using the functions provided in io.h.

In Project 2, our “upper layer” is our security layer. You’ll see that the functions in security.h are similar to the functions in io.h. This allows us to “hot swap” our layers very easily when transitioning from Project 1 to Project 2. (I encourage you to take a look at the difference between server.c/client.c in the Project 1 and Project 2 starter code. There’s barely any difference!)

The starter code as-is functions as a Project 1 solution since we pass our data directly to the io functions.

#init_sec

Similar to init_io, this is where we make preparations before we start interfacing with the upper layer. init_sec has two parameters:

Take these two values and somehow save them for future use.

#input_sec

This function allows you to provide input to the transport layer. The transport layer will call this method when creating new packets. As such, it’s guaranteed that any data you write to the buf argument will all be in one packet (just make sure to not write more than max_length, or else you’ll get a buffer overflow).

For example,

ssize_t input_sec(uint8_t* buf, size_t max_length) {
    strcpy((char*) buf, "Hello!");

    return strlen("Hello!");
}

will always send out packets with the payload Hello!.

#output_sec

Similar to input_sec, the data in buf corresponds to data from one packet. Each time the transport layer receives a new, in-order packet, it will call this function.

#TLV Examples

Let’s go over sending and receiving a (modified) Client-Hello message using the TLV functions provided in consts.h.

#Sending

In the input_sec function, let’s create a TLV packet.

ssize_t input_sec(uint8_t* buf, size_t max_length) {
    tlv* ch = create_tlv(CLIENT_HELLO);

create_tlv will dynamically allocate memory for this TLV object.

Now, let’s place a nonce inside this Client Hello. Firstly, let’s create the Nonce TLV object.

    tlv* nn = create_tlv(NONCE);

Then, generate the nonce using functions from libsecurity.

    uint8_t nonce[NONCE_SIZE];
    generate_nonce(nonce, NONCE_SIZE);

We can now place this data inside the Nonce object. This will dynamically allocate space inside the TLV object and copy it over.

    add_val(nn, nonce, NONCE_SIZE);

Let’s add this Nonce TLV object as part of the Client Hello.

    add_tlv(ch, nn);

We’re now ready to send this TLV packet to the transport layer. Let’s serialize the TLV into bytes by writing it directly to the transport layer’s buffer (the buf argument in input_sec).

    uint16_t len = serialize_tlv(buf, ch);

Since all these TLV objects are dynamically allocated, we need to free them to conserve memory (this is a toy project, sure, but these are best practices…)

    free_tlv(ch);

This will recursively go through each TLV in the TLV object tree and free the object and its value. It’s not recommended for TLV objects to have multiple parents or to be modified after adding a parent. This has undefined behavior.

To let the transport layer know about the data we’ve sent, let’s return the length of the data.

    return len;
}

#Receiving

In the output_sec function, let’s try to deserialize a TLV packet.

void output_sec(uint8_t* buf, size_t length) {
    tlv* ch = deserialize_tlv(buf, length);

Make sure that the resulting value is not NULL. If it is, that means that the data inside buf does not represent a valid TLV packet. Feel free to use the print_tlv_bytes function to print as much of the packet as possible.

deserialize_tlv will recursively parse TLV packets. As such, our Nonce TLV object has been parsed already. We can now retrieve it and inspect its contents. (Before this, also make sure that the result from get_tlv is not NULL.)

    tlv* nn = get_tlv(ch, NONCE);
    nn->length; // NONCE_SIZE
    nn->val; // Contains our random nonce
}

Please note that get_tlv performs a breadth-first search in the TLV object for the requested type. To get the other type within a nested topology, get the intermediate type first. (e.g. Server-Hello has a top-level signature and another one inside its certificate.)

#Debugging

The print_tlv_bytes function is very useful to see how TLV packets may be malformed and/or missing fields.