2 unstable releases
0.2.0 | Jul 24, 2021 |
---|---|
0.1.0 | Jul 21, 2021 |
#1105 in Cryptography
15KB
112 lines
check_mate
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
~160KB