What is the purpose of reqwest::tls::Certificate for custom certificate validation and pinning?
reqwest::tls::Certificate represents a TLS certificate that can be added to a client's trust store for custom certificate validation, enabling certificate pinning—the practice of validating that a server presents a specific certificate or certificate chain rather than relying solely on the system's default certificate authorities. This allows applications to establish trust with servers using self-signed certificates, internal certificate authorities, or to implement certificate pinning as a defense against man-in-the-middle attacks.
The TLS Certificate Trust Model
// Standard TLS validation uses a hierarchy of trust:
// 1. Server presents certificate chain
// 2. Client validates chain leads to trusted root CA
// 3. System root CA store provides default trust anchors
// Default reqwest behavior:
// - Uses system's certificate store
// - Validates server certificate against public CAs
// - Rejects self-signed or unknown certificates
use reqwest::Client;
fn default_tls_validation() {
// This client uses system CA store
let client = Client::new();
// Works for publicly-signed certificates
let response = client
.get("https://www.example.com")
.send()
.unwrap();
// Fails for self-signed certificates:
// let response = client
// .get("https://self-signed.local")
// .send()
// .unwrap();
// Error: certificate verify failed
}By default, reqwest validates certificates against the system's root CA store, rejecting unknown certificates.
Certificate Type and Creation
use reqwest::Certificate;
fn create_certificate() {
// Certificate is created from PEM or DER encoded data
// PEM format: Base64 with header/footer
let pem_data = br#"-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHBfpLrAAAAADANBgkqhkiG9w0BAQsFADAnMQswCQYDVQQGEwJV
...
-----END CERTIFICATE-----
"#;
// Create from PEM
let cert_pem = Certificate::from_pem(pem_data).unwrap();
// DER format: Binary encoded (no headers)
let der_data: &[u8] = &[0x30, 0x82, /* ... */ ];
let cert_der = Certificate::from_der(der_data).unwrap();
// The Certificate type wraps native_tls::Certificate
// It represents a parsed X.509 certificate
// Can be added to a Client's trust store
}Certificate can be created from PEM or DER encoded certificate data.
Adding Certificates to Client Trust Store
use reqwest::{Client, Certificate};
fn add_certificate_to_client() {
// Load certificate (e.g., from file)
let pem_data = include_bytes!("ca-cert.pem");
let cert = Certificate::from_pem(pem_data).unwrap();
// Build client with custom certificate
let client = Client::builder()
.add_root_certificate(cert)
.build()
.unwrap();
// This client now trusts certificates signed by our custom CA
// Useful for:
// - Internal PKI systems
// - Development environments
// - Self-hosted services
}add_root_certificate adds a certificate to the client's trust store.
Self-Signed Certificate Handling
use reqwest::{Client, Certificate};
use std::fs;
fn self_signed_certificate() {
// Scenario: Server uses self-signed certificate
// Load the self-signed certificate
let cert_pem = fs::read("server-cert.pem").unwrap();
let cert = Certificate::from_pem(&cert_pem).unwrap();
// Create client that trusts this certificate
let client = Client::builder()
.add_root_certificate(cert)
.build()
.unwrap();
// Now requests to the self-signed server succeed
let response = client
.get("https://internal.example.com")
.send()
.unwrap();
// The certificate is treated as a trust anchor
// Server certificate must match or chain to this certificate
}Self-signed certificates can be added to trust the server directly.
Certificate Pinning Concept
// Certificate pinning validates that the server presents
// a specific certificate (or one from a specific set)
// Two main pinning strategies:
// 1. Pin to specific leaf certificate
// 2. Pin to specific CA certificate
// Pinning use cases:
// - Prevent MITM attacks (including compromised CAs)
// - Ensure connection to specific server
// - Internal services with known certificates
// Pinning risks:
// - Certificate expiration requires client update
// - Key rotation requires client update
// - Backup certificates must be pre-pinnedCertificate pinning restricts which certificates are accepted, reducing the attack surface.
Implementing Certificate Pinning
use reqwest::{Client, Certificate};
use std::fs;
fn certificate_pinning() {
// Pin to a specific server certificate
let server_cert_pem = fs::read("pinned-server-cert.pem").unwrap();
let pinned_cert = Certificate::from_pem(&server_cert_pem).unwrap();
let client = Client::builder()
.add_root_certificate(pinned_cert)
// Disable default CA validation (only trust pinned cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// Now only this specific certificate (or its issuer chain) is trusted
// Any other certificate, including valid public CAs, will be rejected
// This is strong pinning:
// - Only the pinned certificate is accepted
// - System CA store is not used
// - Protects against compromised CAs
}Strong pinning disables default CA validation, trusting only the pinned certificate.
Pinning to Certificate Authority
use reqwest::{Client, Certificate};
fn ca_pinning() {
// Pin to a specific CA instead of leaf certificate
// More flexible for key rotation
// Your internal CA certificate
let ca_cert_pem = include_bytes!("internal-ca.pem");
let ca_cert = Certificate::from_pem(ca_cert_pem).unwrap();
let client = Client::builder()
.add_root_certificate(ca_cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// Now any certificate signed by this CA is trusted
// - Server can rotate its certificate
// - Multiple servers with different certs from same CA work
// - CA itself still must be protected
// This is CA pinning:
// - More flexible than leaf pinning
// - Still protects against compromised public CAs
}Pinning to a CA allows certificate rotation while maintaining security.
Multiple Pinned Certificates
use reqwest::{Client, Certificate};
use std::fs;
fn multiple_pinned_certificates() {
// Pin multiple certificates for backup/failover
// Primary certificate
let primary_cert_pem = fs::read("primary-cert.pem").unwrap();
let primary_cert = Certificate::from_pem(&primary_cert_pem).unwrap();
// Backup certificate (for key rotation)
let backup_cert_pem = fs::read("backup-cert.pem").unwrap();
let backup_cert = Certificate::from_pem(&backup_cert_pem).unwrap();
// Internal CA (for flexibility)
let ca_pem = fs::read("internal-ca.pem").unwrap();
let ca_cert = Certificate::from_pem(&ca_pem).unwrap();
let client = Client::builder()
.add_root_certificate(primary_cert)
.add_root_certificate(backup_cert)
.add_root_certificate(ca_cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// Any of these certificates (or certs they sign) are trusted
// Enables:
// - Key rotation with overlap period
// - Multiple server certificates
// - CA hierarchy
}Multiple certificates can be pinned for backup and rotation scenarios.
Certificate Validation Workflow
use reqwest::{Client, Certificate};
fn validation_workflow() {
// When a TLS connection is established:
// 1. Server presents certificate chain
// 2. Client validates:
// a. Certificate signature (cryptographic verification)
// b. Certificate validity period (not expired)
// c. Certificate chain to trust anchor
// d. Hostname matches certificate (CN/SAN)
// With add_root_certificate:
// - Certificate is added to trust anchors
// - Chain validation includes this certificate
// - Standard validation rules still apply
// Let cert = Certificate::from_pem(pem_data).unwrap();
let client = Client::builder()
.add_root_certificate(cert)
.build()
.unwrap();
// Request triggers validation:
// - Server cert must chain to system CAs OR added cert
// - With tls_built_in_root_certs(false):
// - Only added certificates are trust anchors
}The validation process checks cryptographic signatures, validity periods, and chain of trust.
Practical Example: Internal API Client
use reqwest::{Client, Certificate};
use std::fs;
struct InternalApiClient {
client: Client,
}
impl InternalApiClient {
fn new(ca_cert_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
// Load internal CA certificate
let ca_pem = fs::read(ca_cert_path)?;
let ca_cert = Certificate::from_pem(&ca_pem)?;
let client = Client::builder()
.add_root_certificate(ca_cert)
.tls_built_in_root_certs(false) // Only trust our CA
.danger_accept_invalid_certs(false) // Still validate certs
.build()?;
Ok(Self { client })
}
async fn call_internal_service(&self, path: &str) -> Result<String, reqwest::Error> {
self.client
.get(&format!("https://api.internal.example.com{}", path))
.send()
.await?
.text()
.await
}
}
fn use_internal_api() {
let api_client = InternalApiClient::new("/etc/ssl/internal-ca.pem").unwrap();
// This only works with certificates signed by internal CA
// Public certificates are rejected
// Self-signed certs (not our CA) are rejected
}Internal APIs can use private CAs with pinning for security and isolation.
Development Environment Setup
use reqwest::{Client, Certificate};
fn development_environment() {
// Development often uses self-signed certificates
// Option 1: Add development CA
let dev_ca = include_bytes!("dev-ca.pem");
let dev_cert = Certificate::from_pem(dev_ca).unwrap();
let dev_client = Client::builder()
.add_root_certificate(dev_cert)
.build()
.unwrap();
// Option 2: Accept invalid certs (dangerous!)
// Only for development, never production
#[cfg(debug_assertions)]
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
// Option 3: Use environment-based configuration
fn create_client() -> Result<Client, reqwest::Error> {
let mut builder = Client::builder();
// Add dev certificate in development
#[cfg(debug_assertions)]
{
let dev_cert = include_bytes!("dev-cert.pem");
builder = builder.add_root_certificate(
Certificate::from_pem(dev_cert).unwrap()
);
}
builder.build()
}
}Development environments can use custom certificates or disable validation for local testing.
Certificate Verification Details
use reqwest::{Client, Certificate};
fn verification_details() {
// add_root_certificate affects certificate chain validation
// Scenario 1: Leaf certificate is added
let leaf_cert_pem = include_bytes!("server-cert.pem");
let leaf_cert = Certificate::from_pem(leaf_cert_pem).unwrap();
let client1 = Client::builder()
.add_root_certificate(leaf_cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// If server presents exactly this certificate:
// - Validation succeeds (leaf is trust anchor)
// If server presents different certificate:
// - Validation fails (no chain to trust anchor)
// Scenario 2: CA certificate is added
let ca_cert_pem = include_bytes!("ca-cert.pem");
let ca_cert = Certificate::from_pem(ca_cert_pem).unwrap();
let client2 = Client::builder()
.add_root_certificate(ca_cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// If server presents certificate signed by this CA:
// - Chain validation succeeds
// Any certificate signed by this CA is trusted
// Note: Hostname verification still applies
// Certificate must be valid for the hostname you're connecting to
}Adding a certificate affects which certificate chains are trusted, but hostname verification still applies.
Handling Certificate Errors
use reqwest::{Client, Certificate, Error};
fn handle_certificate_errors() {
let client = Client::new();
let result = client.get("https://self-signed.example.com").send();
match result {
Ok(response) => {
println!("Request succeeded: {:?}", response.status());
}
Err(e) => {
// Certificate errors appear as reqwest::Error
if e.is_connect() {
// Connection failed (could be cert error)
if let Some(source) = e.source() {
if let Some(tls_error) = source.downcast_ref::<native_tls::Error>() {
println!("TLS error: {:?}", tls_error);
}
}
}
// Common certificate errors:
// - CertificateUnknown: Not in trust store
// - BadCertificate: Signature invalid
// - CertificateExpired: Past validity period
// - CertificateRevoked: In CRL or OCSP
// - HostnameMismatch: CN/SAN doesn't match
}
}
}Certificate errors appear as connection errors with TLS-specific details.
Certificate Loading Patterns
use reqwest::{Client, Certificate};
use std::fs;
use std::path::Path;
fn certificate_loading_patterns() {
// Pattern 1: Load from file at runtime
fn load_from_file(path: &Path) -> Result<Certificate, Box<dyn std::error::Error>> {
let data = fs::read(path)?;
Certificate::from_pem(&data).map_err(Into::into)
}
// Pattern 2: Embed at compile time
fn embedded_certificate() -> Certificate {
static CERT_PEM: &[u8] = include_bytes!("cert.pem");
Certificate::from_pem(CERT_PEM).unwrap()
}
// Pattern 3: Load from environment variable
fn from_env() -> Result<Certificate, Box<dyn std::error::Error>> {
let cert_b64 = std::env::var("TLS_CERT_BASE64")?;
let cert_der = base64::decode(&cert_b64)?;
Certificate::from_der(&cert_der).map_err(Into::into)
}
// Pattern 4: Load multiple from directory
fn load_all_from_dir(dir: &Path) -> Result<Vec<Certificate>, Box<dyn std::error::Error>> {
let mut certs = Vec::new();
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().map(|e| e == "pem").unwrap_or(false) {
let data = fs::read(&path)?;
if let Ok(cert) = Certificate::from_pem(&data) {
certs.push(cert);
}
}
}
Ok(certs)
}
}Certificates can be loaded from files, embedded, or loaded from configuration.
Certificate Pinning with Multiple Endpoints
use reqwest::{Client, Certificate};
use std::collections::HashMap;
struct PinnedClient {
clients: HashMap<String, Client>,
}
impl PinnedClient {
fn new() -> Self {
Self {
clients: HashMap::new(),
}
}
fn add_endpoint(
&mut self,
host: &str,
cert_pem: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
let cert = Certificate::from_pem(cert_pem)?;
let client = Client::builder()
.add_root_certificate(cert)
.tls_built_in_root_certs(false)
.build()?;
self.clients.insert(host.to_string(), client);
Ok(())
}
async fn request(
&self,
host: &str,
path: &str,
) -> Result<reqwest::Response, reqwest::Error> {
let client = self.clients.get(host)
.expect("Unknown host");
let url = format!("https://{}{}", host, path);
client.get(&url).send().await
}
}
// Each endpoint has its own pinned certificate
fn use_pinned_clients() {
let mut pinned = PinnedClient::new();
pinned.add_endpoint(
"api.service1.internal",
include_bytes!("service1-cert.pem"),
).unwrap();
pinned.add_endpoint(
"api.service2.internal",
include_bytes!("service2-cert.pem"),
).unwrap();
// Each request uses the appropriate pinned certificate
}Multiple endpoints can each have their own pinned certificates.
Security Considerations
use reqwest::{Client, Certificate};
fn security_considerations() {
// Certificate pinning security trade-offs:
// Benefits:
// - Protects against compromised certificate authorities
// - Prevents MITM with fraudulently issued certificates
// - Ensures connection to specific server
// - Reduces attack surface for TLS
// Risks:
// - Certificate expiration breaks clients without update
// - Key rotation requires client update
// - Backup certificates must be pre-configured
// - Lost private key means service denial
// Best practices:
// 1. Pin to CA, not leaf certificate (more flexibility)
let ca_cert = include_bytes!("ca.pem");
let client = Client::builder()
.add_root_certificate(Certificate::from_pem(ca_cert).unwrap())
.tls_built_in_root_certs(false)
.build()
.unwrap();
// 2. Have backup certificates
let backup_ca = include_bytes!("backup-ca.pem");
// 3. Monitor certificate expiration
// 4. Plan key rotation strategy
// 5. Test certificate rotation before expiration
// Anti-patterns:
// - Pinning without backup
// - Pinning to expired certificates
// - Accepting invalid certs in production
// - Hardcoded certificates without rotation plan
}Certificate pinning provides security but requires careful planning for rotation and expiration.
Integration with Certificate Rotation
use reqwest::{Client, Certificate};
use std::time::{Duration, Instant};
struct RotatingCertClient {
clients: Vec<Client>,
current_index: usize,
last_rotation: Instant,
rotation_interval: Duration,
}
impl RotatingCertClient {
fn new(cert_pems: &[&[u8]], rotation_interval: Duration) -> Result<Self, reqwest::Error> {
let clients = cert_pems.iter()
.map(|pem| {
let cert = Certificate::from_pem(pem).unwrap();
Client::builder()
.add_root_certificate(cert)
.build()
.unwrap()
})
.collect();
Ok(Self {
clients,
current_index: 0,
last_rotation: Instant::now(),
rotation_interval,
})
}
fn current_client(&mut self) -> &Client {
// Check if rotation needed
if self.last_rotation.elapsed() > self.rotation_interval {
self.rotate();
}
&self.clients[self.current_index]
}
fn rotate(&mut self) {
self.current_index = (self.current_index + 1) % self.clients.len();
self.last_rotation = Instant::now();
}
async fn request(&mut self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
self.current_client().get(url).send().await
}
}
fn rotation_example() {
// Pre-load current and future certificates
let client = RotatingCertClient::new(
&[
include_bytes!("current-ca.pem") as &[u8],
include_bytes!("next-ca.pem") as &[u8],
],
Duration::from_secs(60),
).unwrap();
// Client can rotate to new certificate when old one expires
// Both certificates are trusted, enabling smooth transition
}Certificate rotation requires pre-loading backup certificates for seamless transitions.
Comparison: Certificate vs Danger Accept Invalid
use reqwest::{Client, Certificate};
fn comparison() {
// Option 1: Accept invalid certs (dangerous)
let dangerous_client = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
// Accepts ANY certificate:
// - No verification
// - Vulnerable to MITM
// - Only for development/testing
// - NEVER use in production
// Option 2: Add specific certificate
let cert = Certificate::from_pem(include_bytes!("server.pem")).unwrap();
let secure_client = Client::builder()
.add_root_certificate(cert)
.tls_built_in_root_certs(false)
.build()
.unwrap();
// Accepts ONLY this certificate (or its chain):
// - Proper verification
// - Protected against MITM
// - Suitable for production
// - Requires certificate management
// Option 3: System CAs + additional certificate
let cert = Certificate::from_pem(include_bytes!("internal-ca.pem")).unwrap();
let hybrid_client = Client::builder()
.add_root_certificate(cert)
// tls_built_in_root_certs is true by default
.build()
.unwrap();
// Accepts system CAs OR added certificate:
// - Public certificates work
// - Internal certificates work
// - More flexible
// - Less secure than pure pinning
}danger_accept_invalid_certs disables verification entirely; add_root_certificate adds specific trust anchors.
Summary Table
fn summary() {
// | Configuration | Trust Anchors | Use Case |
// |---------------|---------------|----------|
// | Default | System CAs | Public web services |
// | add_root_certificate | System CAs + added | Internal + public |
// | add_root + tls_built_in_root_certs(false) | Only added | Pure pinning |
// | danger_accept_invalid_certs(true) | None (dangerous) | Dev only |
// | Method | Purpose |
// |--------|---------|
// | Certificate::from_pem | Load PEM-encoded certificate |
// | Certificate::from_der | Load DER-encoded certificate |
// | ClientBuilder::add_root_certificate | Add to trust store |
// | ClientBuilder::tls_built_in_root_certs | Control system CA usage |
// | Pinning Strategy | Flexibility | Security |
// |-------------------|-------------|----------|
// | Leaf certificate | Low | High |
// | CA certificate | Medium | High |
// | Multiple CAs | High | High |
// | System CAs | Highest | Lower |
}Synthesis
Quick reference:
use reqwest::{Client, Certificate};
// Load certificate
let cert = Certificate::from_pem(include_bytes!("ca.pem")).unwrap();
// Create client with pinned certificate
let client = Client::builder()
.add_root_certificate(cert) // Add custom trust anchor
.tls_built_in_root_certs(false) // Disable system CAs for pure pinning
.build()
.unwrap();
// Use client
let response = client.get("https://internal.example.com").send().await.unwrap();Key insight: reqwest::tls::Certificate enables applications to define custom trust anchors for TLS connections, moving beyond reliance on the system's certificate authority store. This serves two primary purposes: enabling connections to services with self-signed or private-CA certificates, and implementing certificate pinning as a security measure. The add_root_certificate method adds certificates to the trust store—when combined with tls_built_in_root_certs(false), it creates a pinned configuration where only the specified certificates are trusted. This protects against compromised certificate authorities and man-in-the-middle attacks, but requires careful certificate lifecycle management: certificates expire, keys rotate, and backup certificates should be pre-configured to maintain service continuity. The choice between pinning to leaf certificates (maximum security, minimal flexibility) versus CA certificates (good security, better rotation support) depends on your security requirements and operational capabilities. Always use Certificate for custom trust rather than danger_accept_invalid_certs(true), which disables all verification.
