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:
- Creation of GBL files from raw application images
- Zero-copy parsing of existing GBL files
- Encrypting and decrypting GBL files using an AES-128 key
- Signing GBL containers and verifying the signature using an ECDSA key pair
- Signing application images for secure boot
- A simple command-line tool wrapping the library
- You can try it out using
cargo install gbl
- You can try it out using
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:
- Ignore the problem and encrypt the data anyway, producing an invalid signature
- Implicitly remove the signature before encrypting
- Fail at runtime, returning a
Result
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.