Releasing gbl 0.1.0

A typestate-powered zero-copy crate for GBL firmware update files

After a few weeks of reverse-engineering, internal dogfooding, and API design discussion, we're finally publishing our gbl crate for good.

The library implements a parser and writer for GBL firmware update containers, which are used to perform secure OTA updates for certain microcontrollers. The current feature set includes:

At 1aim, we're using this library to automate the distribution and encryption of firmware updates for our products.

Typestate

What makes this library special is its heavy use of "typestate" to prevent accidental misuse. Typestate is used to track whether a GBL contains encrypted or unencrypted program data, and whether it contains a digital signature. Consider this example, which attempts to create a signed and encrypted GBL:

let gbl = Gbl::from_app_image(...);  // unencrypted, unsigned GBL
let signed = gbl.sign(ecdsa_key)?;
let encrypted = signed.encrypt(aes_key);

This program has a non-obvious problem: It is not possible to encrypt a GBL that is signed, because the signature is computed over the contained data, which changes when encrypting the GBL. Without typestate, the signed.encrypt(...) operation can do one of these things:

Of these options, returning a Result is probably the best one. However, if we use typestate, we can do better. If you try to write the code above, the program will not compile:

error[E0599]: no method named `encrypt` found for type `gbl::Gbl<gbl::marker::NotEncrypted<'_>, gbl::marker::Signed<'_>>` in the current scope
  --> src/lib.rs:57:24
   |
17 | let encrypted = signed.encrypt(aes_key);
   |                        ^^^^^^^

As you can see, the encrypt method simply isn't available when the GBL is already signed. From the error message we see that the Gbl type we attempted to use was Gbl<NotEncrypted<'a>, Signed<'a>>, while the encrypt method is defined in an impl that looks like this:

impl<'a> Gbl<NotEncrypted<'a>, NotSigned<'a>> {
    pub fn encrypt(self, key: AesKey) -> Gbl<Encrypted<'a>, NotSigned<'a>> {
        ...
    }
}

We can see that we need a Gbl<NotEncrypted<'a>, NotSigned<'a>> to be able to call encrypt, which is the return type of Gbl::from_app_image. Let's take a look at the requirements for the sign method:

impl<'a, E> Gbl<E, NotSigned<'a>>
where
    E: EncryptionState<'a>,
{
    pub fn sign(self, pem_private_key: &str) -> Result<Gbl<E, Signed<'a>>, Error> {
        ...
    }
}

This impl is generic over all E: EncryptionState, which means that sign works with any encryption state. However, it requires that the GBL is NotSigned, preventing us from signing a GBL that already has a signature (gbl.remove_signature() will remove an existing signature, but you have to do so explicitly).

This tells us that we can call sign at any point (as long as we only call it once), but need to call encrypt before that. Let's try doing that by simply swapping the operations of the snippet above:

let gbl = Gbl::from_app_image(...);  // unencrypted, unsigned GBL
let encrypted = gbl.encrypt(aes_key);
let signed = encrypted.sign(ecdsa_key)?;

...and indeed this work, yielding an encrypted GBL with a valid signature.

For more info on this API, you can take a look at the API documentation. Particularly interesting is how it works when parsing unknown GBLs, since those can't provide compile-time information about encryption and signing state.

Of course, the downside of typestate-based APIs is that they are pretty complex, so one must decide whether the added safety is worth the complexity increase. Since we already had problems with an older iteration of this library that didn't use typestate, I think the added complexity is worth it in this case.