2 stable releases
2.0.1 | Aug 24, 2023 |
---|---|
1.1.0 | Aug 19, 2023 |
1.0.3 |
|
#545 in HTTP client
Used in x509-path-finder
45KB
898 lines
X509 Client
X509 Client is an async X509 certificate transport and deserializer for Rust.
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
~6–19MB
~284K SLoC