Skip to content

der: *Document type improvements #421

@tarcieri

Description

@tarcieri

Several crates that implement ASN.1 DER-based formats in this repo define "document" types such as:

  • sec1::EcPrivateKeyDocument
  • spki::PublicKeyDocument
  • pkcs1::{RsaPrivateKeyDocument, RsaPublicKeyDocument}
  • pkcs10::CertReqDocument
  • pkcs8::{PrivateKeyDocument, EncryptedPrivateKeyDocument}

These types are all newtype wrappers around a Vec<u8> containing a DER-encoded document which is guaranteed to infallibly parse to a particular message type which impls the der crate's Decodable and Encodable traits.

Where the der crate is designed around zero-copy borrowed types in order to accommodate heapless no_std targets, the "document" types provide an owned analogue of each message type without the need to duplicate each struct definition so as to have an owned and borrowed equivalent of each type.

This is codified in the der::Document trait, which has an associated Message type which incorporates the aforementioned bounds. The der::Document::decode method infallibly parses the contents of the Vec<u8> into the associated Message type, having already checked in advance that it parses correctly at the time it's instantiated.

The der::Document trait also wraps up common functionality around decoding PEM to DER as well as encoding DER to PEM, and also provides utility functions for reading/writing documents from/to the filesystem, also incorporating awareness as to whether or not the document is a secret and doing things like setting file permissions appropriately.

Generic document type

It would be nice to not have to define separate document types for every message type, instead changing der::Document from a trait into a struct which is generic around the inner message type.

This has been blocked on the problem of specifying the lifetime to associate with Decodable<'a>. However, that might be resolvable by avoiding any bounds on the generic type in the struct definition, and hanging the lifetime off of the der::Document::decode method instead. Something like:

use core::marker::PhantomData;

pub struct Document<M> {
    der: Vec<u8>,
    message: PhantomData<M>
}

impl<M> Document<M> {
    pub fn decode<'a>(&'a self) -> M
    where
        M: Decode<'a> + Encode + Sized
    {
        [...]
    }
}

The bounds will need to be repeated for every method on the struct.

Avoid reparsing messages

Right now each call to der::Document::decode parses the message. This means the message must be parsed twice at a minimum: once to instantiate the document type (which validates the DER parses successfully as a given type, then throws the result away), and once to actually access the parsed results.

It would be nice to store the M: Decode<'a> type along with the Vec<u8>, but this gets into the classical Rust problem of borrowing from self. The situation isn't that bad though, as the unparsed DER document is stored on the heap as a Vec<u8>, which means if the type acts as an immutable document accessor which never mutates that Vec<u8> we don't need to worry about the address of its contents changing.

A number of crates have emerged which try to solve the self-borrowing problem. A popular one is ouroboros, which would allow us to do something like this:

use core::marker::PhantomData;
use ouroboros::self_referencing;

#[self_referencing]
pub struct Document<M> {
    der: Vec<u8>,
    #[borrows(der)]
    message_data: M,
    message_type: PhantomData<M>
}

This would allow &M to be accessed as a simple reference conversion: in fact, we could easily implement traits like AsRef and Borrow this way. We could also potentially implement ToOwned in such cases, which would serialize a given message type to its corresponding document type, which would be very nice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions