2 unstable releases

0.2.0 Jul 24, 2021
0.1.0 Jul 21, 2021

#1114 in Cryptography

MIT license

15KB
112 lines

check_mate

crates.io docs.rs

Check yourself before you wreck yourself.

A small Rust utility library inspired by the ideas of "Parse, don't validate"1 and its follow-up, "Names are not type safety"2. Its goal is to extend the idea to checking invariants more generally. See the crate documentation for more information.


lib.rs:

Check yourself before you wreck yourself.

This is a small utility library inspired by the ideas of "Parse, don't validate"1 and its follow-up, "Names are not type safety"2. Its goal is to extend the idea to checking invariants more generally.

Motivating example

The motivating use-case for this crate was validating signed messages. Consider a Signed struct like the following:

struct Signed {
    payload: Vec<u8>,
    public_key: PublicKey,
    signature: Signature,
}

The struct contains a payload, a public key, and a signature. Let's give the struct a validate method that we could use to check for validity:

impl Signed {
    fn validate(&self) -> Result<(), Error> {
        self.public_key.verify(&self.payload, &self.signature)
    }
}

Now when we find a Signed we're able to verify it. Of course, whenever we see a Signed in our code, it may not immediately be clear whether it has been checked yet. In particular, if Signed appears in another struct, or as a signature to some method, has it already been checked? Should we check it anyway?

It's possible to manage this with disciplined use of documentation and convention, making it clear where signatures should be validated and relying on that being the case later in the call stack. However discipline is not always a reliable tool, particularly in an evolving codebase with multiple contributors. Perhaps we can do something better?

Parse, don't validate

This is where the ideas from "Parse, don't validate" come in. Specifically, rather than validating a Signed instance, we could 'parse' it into something else, such as CheckedSigned:

/// A [`Signed`] that has been checked and confirmed to be valid.
struct CheckedSigned(Signed);

impl CheckedSigned {
    fn try_from(signed: Signed) -> Result<Self, Error> {
        signed.public_key.verify(&signed.payload, &signed.signature)?;
        Ok(Self(signed))
    }
}

By having CheckedSigned in its own module, and keeping its field private, we can guarantee that the only way to construct one is via the try_from method, which performs the check. This means that structs and functions can use CheckedSigned and safely assume that the signature is valid.

fn process_message(message: CheckedSigned) {
    /* ... */
}

// Or

struct ProcessMessage {
    message: CheckedSigned,
}

It's immediately clear in both cases that message has already been checked, and is known to be valid.

So far so good, but since CheckedSigned's field is private, we've lost direct access to the inner value. Rust makes it easy to recover some functionality here by implementing Deref for CheckedSigned:

impl core::ops::Deref for CheckedSigned {
    type Target = Signed;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

This allows Signed methods with the &self receiver to be called directly on CheckedSigned instances.

So what about this library...?

Creating a Checked* newtype for every type that needs checked would be a lot of boilerplate, and there are many ways to skin this cat, so to speak. check_mate exists to offer a consistent pattern, with minimal boilerplate.

How to use

Let's start again from our original Signed struct above:

struct Signed {
    payload: Vec<u8>,
    public_key: PublicKey,
    signature: Signature,
}

We can use check_mate to achieve the same guarantees as CheckedSigned by implementing Check:

impl check_mate::Check for Signed {
    type Ok = Self;
    type Err = Error;

    fn check(self) -> Result<Self::Ok, Self::Err> {
        self.public_key.verify(&self.payload, &self.signature)?;
        Ok(self)
    }
}

Now we can obtain a Checked<Signed> using try_from:

#
let _ = check_mate::Checked::try_from(signed);

Checked<T> implements Deref<Target = T>, and can be converted back to the inner value with into_inner.

With the serde feature enabled, Checked<T> will also implement Serialize if T: Serialize, and Deserialize if T: Deserialize and there's a Check<Ok = T> impl to use for the check (unconstrained type parameter limitations prevent a blanket Deserialize impl for any U: Check<Ok = T> – it must be T itself).

When (not) to use this

It's hoped that check_mate will be useful for getting started with this 'parsing' style of maintaining invariants, and for internal APIs where churn is likely, so reducing the amount of code involved is desired.

However, Checked<T> can't be as ergonomic or featureful as a custom checked type could. For example, if it's known that some fields don't affect validity they could be made public, or methods that don't affect validity could take &mut self. Neither of these are possible with Checked<T> since the inner value is only exposed immutably.

It may also be unsuitable when you want a great deal of customisation over how validation is performed. This could be achieved either by including configuration in the type that implements Check, or otherwise by implementing Check on wrappers that can tailor the behaviour, but it would likely be a bit clunky to use.

Finally, as discussed in "Names are not type safety", it's always preferable to design types that simply cannot represent invalid states, though it may not always be possible.

What's next?

I want to try and use this to get a sense of whether or not it's actually useful, and what the pain points are. Some things I could imagine adding:

  • Implement additional common traits (AsRef<T>, Borrow<T>).
  • Implement additional common indirection methods (as_deref, cloned).

Dependencies

~170KB