From 50e5923516081fcd4dabd25f9a59e0d27627c562 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Fri, 30 Oct 2026 10:25:50 +0100 Subject: [PATCH] Add unchecked DER certificate setters `SslRef::set_certificate_der_unchecked` and `SslContextBuilder::set_certificate_der_unchecked` allow callers to preserve certificate DER that cannot be represented by a parsed `X509Ref`. The impl of `AsRef` for `Asn1TimeRef` lets callers inspect encoded time bytes without direct FFI. Documentation notes that the unchecked setters bypass full X.509 parsing, and tests cover both setters. --- boring/src/asn1.rs | 17 +++++++++++++++ boring/src/ssl/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++ boring/src/ssl/test/mod.rs | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/boring/src/asn1.rs b/boring/src/asn1.rs index 31aeccd1c..134907ac8 100644 --- a/boring/src/asn1.rs +++ b/boring/src/asn1.rs @@ -252,6 +252,14 @@ impl Asn1TimeRef { } } +impl AsRef for Asn1TimeRef { + fn as_ref(&self) -> &Asn1StringRef { + // SAFETY: `ASN1_TIME` is a typedef of `ASN1_STRING`, and the returned + // reference is bounded by the lifetime of `self`. + unsafe { Asn1StringRef::from_ptr(self.as_ptr().cast()) } + } +} + impl PartialEq for Asn1TimeRef { fn eq(&self, other: &Asn1TimeRef) -> bool { self.diff(other) @@ -830,6 +838,15 @@ mod tests { assert_eq!("Jan 1 00:00:00 1970 GMT", t.to_string()); } + #[test] + fn time_as_asn1_string() { + let time = Asn1Time::from_str("99991231235959Z").unwrap(); + let time_ref: &Asn1TimeRef = time.as_ref(); + let string_ref: &Asn1StringRef = time_ref.as_ref(); + + assert_eq!(string_ref.as_slice(), b"99991231235959Z"); + } + #[test] fn time_eq() { let a = Asn1Time::from_str("99991231235959Z").unwrap(); diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index e0f358d6e..7062fc927 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -1475,6 +1475,27 @@ impl SslContextBuilder { unsafe { cvt(ffi::SSL_CTX_use_certificate(self.as_ptr(), cert.as_ptr())) } } + /// Sets the leaf certificate from its DER encoding without fully parsing it. + /// + /// BoringSSL copies `der` and performs only the checks needed to extract a + /// supported public key. Other malformed certificate fields may be accepted. + /// + /// # Warning + /// + /// Callers should normally parse the DER with [`X509::from_der`] and use + /// [`Self::set_certificate`]. This method is intended for cases that must + /// preserve certificate DER which cannot be represented by an [`X509Ref`]. + #[corresponds(SSL_CTX_use_certificate_ASN1)] + pub fn set_certificate_der_unchecked(&mut self, der: &[u8]) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::SSL_CTX_use_certificate_ASN1( + self.as_ptr(), + der.len(), + der.as_ptr(), + )) + } + } + /// Appends a certificate to the certificate chain. /// /// This chain should contain all certificates necessary to go from the certificate specified by @@ -3876,6 +3897,27 @@ impl SslRef { Ok(()) } + /// Sets the leaf certificate from its DER encoding without fully parsing it. + /// + /// BoringSSL copies `der` and performs only the checks needed to extract a + /// supported public key. Other malformed certificate fields may be accepted. + /// + /// # Warning + /// + /// Callers should normally parse the DER with [`X509::from_der`] and use + /// [`Self::set_certificate`]. This method is intended for cases that must + /// preserve certificate DER which cannot be represented by an [`X509Ref`]. + #[corresponds(SSL_use_certificate_ASN1)] + pub fn set_certificate_der_unchecked(&mut self, der: &[u8]) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::SSL_use_certificate_ASN1( + self.as_ptr(), + der.as_ptr(), + der.len(), + )) + } + } + /// Sets the list of CA names sent to the client. /// /// The CA certificates must still be added to the trust root - they are not automatically set diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index 4ee5d0aef..a3d42bfb7 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; +use crate::asn1::Asn1StringRef; use crate::error::ErrorStack; use crate::hash::MessageDigest; use crate::pkey::PKey; @@ -40,6 +41,41 @@ static CERT: &[u8] = include_bytes!("../../../test/cert.pem"); static KEY: &[u8] = include_bytes!("../../../test/key.pem"); const TEST_CERT_DIGEST: &str = "585e3a58acfd3b4e5e2825659407244ddd93539c"; +fn malformed_certificate_der() -> Vec { + const MALFORMED_TIME: &[u8] = b"250231000000Z"; + + let cert = X509::from_pem(CERT).unwrap(); + let time: &Asn1StringRef = cert.not_before().as_ref(); + let original = time.as_slice(); + assert_eq!(original.len(), MALFORMED_TIME.len()); + + let mut der = cert.to_der().unwrap(); + let offset = der + .windows(original.len()) + .position(|window| window == original) + .unwrap(); + der[offset..offset + MALFORMED_TIME.len()].copy_from_slice(MALFORMED_TIME); + assert!(X509::from_der(&der).is_err()); + + der +} + +#[test] +fn set_context_certificate_der_unchecked() { + let mut context = SslContext::builder(SslMethod::tls()).unwrap(); + context + .set_certificate_der_unchecked(&malformed_certificate_der()) + .unwrap(); +} + +#[test] +fn set_certificate_der_unchecked() { + let context = SslContext::builder(SslMethod::tls()).unwrap().build(); + let mut ssl = Ssl::new(&context).unwrap(); + ssl.set_certificate_der_unchecked(&malformed_certificate_der()) + .unwrap(); +} + #[test] fn get_ctx_options() { let ctx = SslContext::builder(SslMethod::tls()).unwrap();