Hiding lifetime behind a wrapper/facade

I need to sign/verify some data in an application and I would like my API to be implementation agnostic. I'm currently using OpenSSL, but that should be an implementation detail that developers do not need to know about, and if someone wants to switch the backend implementation I don't want the application code to suffer.

I want to wrap all the signing/verifying logic into something very straight-forward, along the line of (trimming some methods and implementation for brevity):

struct PrivKey { /* ... */ }
impl PrivKey {
  pub fn new(gp: GenParams) -> Result<Self, Error> { /* ... */ }
  pub fn pubkey(&self) -> Result<PubKey, Error> { /* ... */ }
  pub fn signer(&self) -> Result<MySigner, Error> { /* ... */ }
}

pub struct MySigner { /* ... */ }
impl MySigner {
  pub fn update(&mut self, buf: &[u8]) -> Result<(), Error> { /* ... */ }
  pub fn finalize(mut self) -> Result<Vec<u8>, Error> { /* ... */ }
}

pub struct PubKey { /* ... */ }
impl PubKey {
  pub fn verifier(&self) -> Result<MyVerifier, Error> { /* ... */ }
}

pub struct MyVerifier { /* ... */ }
impl MyVerifier {
  pub fn update(&mut self, buf: &[u8]) -> Result<(), Error> { /* ... */ }
  pub fn verify(mut self, sig: &[u8]) -> Result<bool, Error> { /* ... */ }
}

As it happens, the OpenSSL wrapper is not completely dissimilar from this, but it has one (for me) major caveat: When creating a OpenSSL's Signer and Verifier objects, it takes in a &'a PKeyRef (PKey, which hold private and public keys, objects can be deref'd into PKeyRef). This ties the lifetime of the Signer and Verifier objects to the PKeyRef. I would need to do this:

pub struct MySigner<'a> {
  inner: sign::Signer<'a>
}

I would prefer not to introduce lifetimes to my simplified interface, and I would like the application to be able to pass around the MySigner/MyVerifier objects over channels. In an ideal world I would clone the public/private keys into MySigner and MyVerifier so they simply travel along with the Signer/Verifier objects, but it's obviously not that simple.

I feel that I need a fresh perspective on this. Given the boundary conditions:

  • Use OpenSSL's sign module (i.e. PKey, PKeyRef, Signer, Verifier).
  • Expose no explicit lifetimes to the application.
  • Allow MySigner/MyVerifier to be passed over channels.

Is it possible to achieve this, or do all these roads lead to self-referential structs or other nasty hairballs?

I've seen efforts to solve this problem - I'm not sure this approach is totally sound but it seems they're actively working on it.

You would use Yoke<Signer<'static>, Arc<PKey>> since you want Send

4 Likes

This is the kind of adventure I'm looking for.

"This crate has some pretty nasty protruding spikes!" vs "It's the icu4x people -- how bad can it be?". :slight_smile:

I'm gonna try it and see.

I'm helping track the known soundness concerns of yoke. TL;DR:

  • The Rust project wants to ensure that some form of the yoke pattern is possible to implement soundly.
  • There is a known lifetime soundness hole in yoke, which already has three separate proposed fixes with different tradeoffs.
    • The actual hole is obscure enough (the cart payload must contain a contravariant lifetime) that you're probably not hitting it accidentally.
    • The fix I think most likely to be adopted is to restrict safely constructable cart payloads to 'static types; e.g. Arc<[u8]> is allowed but not Arc<&[u8]>.
  • A Box<T> or &mut T cart are formally unsound (mostly) not due to API but due to language limitations.
    • Because the yoke API only exposes shared access to the cart payload, no UB is currently emitted at the LLVM level.
    • The only public API impacted is that the unsafe fn replace_cart is essentially unusable for Box or &mut.
    • The language addition needed to make this properly formally sound is in the RFC process.

If you use yoke with a cart type of Arc<T> (or Rc<T>) where T contains no lifetimes, it's currently believed that the yoke API is 100% sound.

I will also say that yoke is probably the most likely crate of the current landscape of self reference implementing crates to stay actively maintained the longest.

8 Likes

Generally in Rust you can't abstract over a reference existing. Abstracting away lifetime of objects is a Garbage Collector's job.

When generic code can accept either a self-contained owning type or a temporary reference, it will be forced to always handle the worst-case scenario of the most restrictive reference type allowed, regardless whether that's necessary or not for a particular implementation. So in this case you gain nothing in flexibility.

If the converse was true, and the generic code could act unbound by lifetime restrictions, then it'd obviously be unsafe when used with a lifetime-dependent implementation that needs to restrict how it can be used.

You can hide lifetime annotations behind just a plain T type, like Vec<T> allows Vec<i32> and Vec<&i32>, but that won't give you flexibility of using the T as a self-contained type (e.g. send it over a channel to something that may or may not outlive it), and if you make it T: 'static then it won't work with temporary references.

There's also Cow<'a, T> which has a special case of a self-contained Cow<'static, T>, but again you need lifetimes in the API, and the implementations will have to deal with both temporary and non-temporary cases somehow.

So you have to pick one. If you don't allow temporary references, then implementations can still use something like Arc to share data without copying and without lifetime bound.