#x509 #client-certificate #pem #der #http-transport #pkcs7 #http

x509-client

Reqwest-based async X509 certificate transport and deserializer client. Transports: HTTP/S, File. Formats: DER, PEM, PKCS7.

2 stable releases

2.0.1 Aug 24, 2023
1.1.0 Aug 19, 2023
1.0.3 Aug 19, 2023

#1063 in Parser implementations

23 downloads per month
Used in x509-path-finder

Apache-2.0

45KB
898 lines

X509 Client

X509 Client is an async X509 certificate transport and deserializer for Rust.

CI Status

Synopsis

Supported transports:

  • HTTP/S
  • File

Supported encoding formats:

  • CER - single DER-encoded certificate
  • PEM - stack of one or more PEM-encoded certificates
  • PKCS7 - DER-encoded PKCS7 certificate bundle

Usage

The RustCrypto-based DefaultX509Iterator implementation is available by default.

[dependencies]
x509_client = { version = "1" }

Enable the openssl feature for access to the provided OpenSSL-based OpenSSLX509Iterator deserializer.

[dependencies]
x509_client = { version = "1", features = ["openssl"] }

The X509 Client is data-model agnostic. When constructing the client, use the turbofish expression to choose the deserializer implementation.

use x509_client::{X509Client, X509ClientConfiguration};
use x509_client::provided::default::DefaultX509Iterator;

#[tokio::test]
async fn test() {    
    // default X509 Client with the default DefaultX509Iterator 
    let client = X509Client::<DefaultX509Iterator>::default();    
    assert!(client.get(&url::Url::parse("http://localhost")?).await.is_ok());
    
    // Configured X509 Client with the default DefaultX509Iterator
    let client = X509Client::<DefaultX509Iterator>::new(X509ClientConfiguration::default());
    assert!(client.get_all(&url::Url::parse("http://localhost")?)?.into_inter().len() >= 0);
}

Example

Transfer and parse a single certificate and multiple certificates, using the default DefaultX509Iterator implementation.

use cms::cert::x509::Certificate;
use x509_client::{X509Client, X509ClientConfiguration, X509ClientResult};
use x509_client::provided::default::DefaultX509Iterator;
use x509_client::reqwest::ClientBuilder;

async fn get_first_certificate(url: &url::Url) -> X509ClientResult<Certificate> {
    // default X509 Client with the default DefaultX509Iterator 
    let client = X509Client::<DefaultX509Iterator>::default();    
    Ok(client.get(&url).await?)    
}

async fn get_all_certificates(url: &url::Url) -> X509ClientResult<Vec<Certificate>> {
    // configure reqwest
    let config = X509ClientConfiguration {
        strict: true,
        files: false,
        limit: None,
        http_client: Some(
            ClientBuilder::new()
            .redirect(reqwest::redirect::Policy::limited(2))
            .build()?,
        ),
    };
        
    // Configured X509 Client with the default DefaultX509Iterator
    let client = X509Client::<DefaultX509Iterator>::new(config);
            
    // HTTP GET and parse all certificates, returning all
    Ok(client.get_all(&url).await?.into_iter().collect())    
}

Instantiation and Configuration

A default X509 Client can be instantiated with the crate::X509Client::default trait implementation.

let client = X509Client::<DefaultX509Iterator>::default();

The X509 Client can be configured by passing the X509ClientConfiguration to the client crate::X509Client::new constructor:

let client = X509Client::<DefaultX509Iterator>::new(config);

The X509ClientConfiguration struct is defined as:

// Default configuration
X509ClientConfiguration {
    strict: false,
    files: false,
    limit: None,
    http_client: None
};

pub struct X509ClientConfiguration {
    /// If true, only attempt parse once.
    /// Use either filename extension or http header to determine type.
    /// If false, attempt to parse from all known formats before returning error.
    pub strict: bool,

    /// If true, allow `File` transport scheme.
    /// If false, transport attempts will fail for `File` scheme.
    pub files: bool,

    /// Limits max transfer size in bytes. If None, apply no limit.
    pub limit: Option<usize>,

    /// Optional Reqwest client.
    /// If None, a default Reqwest client will be instantiated.
    pub http_client: Option<x509_client::reqwest::Client>,
}

Transfer and Deserialize

The X509Client::get method transfers and parses the first certificate, returning an error on empty.

The X509Client::get_all method transfers and parses all certificates.

Deserialization

The client will attempt to determine the encoding of the remote certificate before parsing.

If strict configuration is enabled, the client will only attempt to parse once. The client will return an error immediately if the encoding type cannot be determined.

If strict configuration is disabled (default), the client will attempt to parse all known formats (starting with its best guess) before returning an error.

Some deserialization implementations may return an empty iterator. The text encoding specification for PKIX (PEM) RFC 7468 states that:

Parsers MUST handle non-conforming data gracefully.

And:

Files MAY contain multiple textual encoding instances. This is used, for example, when a file contains several certificates.

Implying an "empty" PEM file is valid. For this reason, the X509 Client always attempts to parse PEM last when strict is disabled.

For HTTP transport, certificate type is determined by the Content-Type http header:

  • application/pkix-cert : CER
  • application/pem-certificate-chain : PEM
  • application/pkcs7-mime : PKCS7

For File scheme, certificate type is determined by the filename extension (.ext):

  • .cer : CER
  • .pem : PEM
  • .p7c : PKCS7

API

The X509 Client is data-model agnostic - the X509Iterator trait is used to define the deserializer interface.

use std::fmt::{Debug, Display};

/// X509 Deserializer API
pub trait X509Iterator: IntoIterator
where
    Self: Sized,
{
    /// Error type
    type X509IteratorError: X509IteratorError;

    /// Attempt to deserialize, assume input is a single DER-encoded certificate
    fn from_cer<T: AsRef<[u8]>>(src: T) -> Result<Self, Self::X509IteratorError>;
    /// Attempt to deserialize, assume input is a stack of zero or more PEM-encoded certificates
    fn from_pem<T: AsRef<[u8]>>(src: T) -> Result<Self, Self::X509IteratorError>;
    /// Attempt to deserialize, assume input is a DER-encoded PKCS7 certificate bundle
    fn from_pkcs7<T: AsRef<[u8]>>(src: T) -> Result<Self, Self::X509IteratorError>;
}

/// Error type bounds
pub trait X509IteratorError: Display + Debug {}

Error Handling

An X509Iterator implementation can return any error type defined by the X509Iterator::X509IteratorError associated type, bound by the X509IteratorError trait. The X509IteratorError trait itself is bound only by Display + Debug.

Iterator errors will be surfaced to the caller in the X509ClientError::X509IteratorError variant.

Error conversion is implemented as:

use std::fmt::{Debug, Display, Formatter};
use x509_client::X509ClientError;
use x509_client::api::X509IteratorError;

#[derive(Debug)]
struct MyX509IteratorError;

impl Display for MyX509IteratorError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "")
    }
}

impl X509IteratorError for MyX509IteratorError {}

impl From<MyX509IteratorError> for X509ClientError {
    fn from(e: MyX509IteratorError) -> Self {
        Self::X509IteratorError(Box::new(e))
    }
}

Implementations

Default

The RustCrypto-based DefaultX509Iterator implementation is available if default features are enabled.

OpenSSL

The OpenSSL-based implementation OpenSSLX509Iterator is available if the openssl feature is enabled.

Debug

The debug implementation DebugX509Iterator is always available. It copies the bytes returned by server into a Once<bytes::Bytes> iterator.

Dependencies

~7–20MB
~285K SLoC