From 89840f9f151369613d2b6ad59f812b6143968e96 Mon Sep 17 00:00:00 2001 From: arckoor <33837362+arckoor@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:09:56 +0200 Subject: [PATCH] Add support for certificate creation --- botan-sys/src/lib.rs | 2 + botan-sys/src/x509.rs | 351 ++++++++++++++++++++++- botan-sys/src/x509_ext.rs | 109 ++++++++ botan/src/ec_group.rs | 4 + botan/src/lib.rs | 2 + botan/src/pubkey.rs | 19 ++ botan/src/utils.rs | 4 + botan/src/x509_cert.rs | 574 +++++++++++++++++++++++++++++++++++++- botan/src/x509_crl.rs | 201 ++++++++++++- botan/src/x509_ext.rs | 321 +++++++++++++++++++++ botan/tests/tests.rs | 308 ++++++++++++++++++++ 11 files changed, 1878 insertions(+), 17 deletions(-) create mode 100644 botan-sys/src/x509_ext.rs create mode 100644 botan/src/x509_ext.rs diff --git a/botan-sys/src/lib.rs b/botan-sys/src/lib.rs index 022a2dc..e6b8008 100644 --- a/botan-sys/src/lib.rs +++ b/botan-sys/src/lib.rs @@ -21,6 +21,7 @@ mod rng; mod utils; mod version; mod x509; +mod x509_ext; mod zfec; pub mod ffi_types { @@ -57,4 +58,5 @@ pub use rng::*; pub use utils::*; pub use version::*; pub use x509::*; +pub use x509_ext::*; pub use zfec::*; diff --git a/botan-sys/src/x509.rs b/botan-sys/src/x509.rs index dff0328..34547dd 100644 --- a/botan-sys/src/x509.rs +++ b/botan-sys/src/x509.rs @@ -1,7 +1,13 @@ +#[cfg(botan_ffi_20251104)] +use crate::botan_mp_t; use crate::ffi_types::*; +#[cfg(botan_ffi_20251104)] +use crate::oid::botan_asn1_oid_t; use crate::pubkey::{botan_privkey_t, botan_pubkey_t}; use crate::rng::botan_rng_t; +#[cfg(botan_ffi_20251104)] +use crate::x509_ext::{botan_x509_ext_as_blocks_t, botan_x509_ext_ip_addr_blocks_t}; pub enum botan_x509_cert_struct {} pub type botan_x509_cert_t = *mut botan_x509_cert_struct; @@ -9,8 +15,19 @@ pub type botan_x509_cert_t = *mut botan_x509_cert_struct; pub enum botan_x509_crl_struct {} pub type botan_x509_crl_t = *mut botan_x509_crl_struct; +#[cfg(botan_ffi_20251104)] +pub enum botan_x509_cert_params_builder_struct {} +#[cfg(botan_ffi_20251104)] +pub type botan_x509_cert_params_builder_t = *mut botan_x509_cert_params_builder_struct; + +#[cfg(botan_ffi_20251104)] +pub enum botan_x509_pkcs10_req_struct {} +#[cfg(botan_ffi_20251104)] +pub type botan_x509_pkcs10_req_t = *mut botan_x509_pkcs10_req_struct; + #[repr(u32)] #[allow(clippy::upper_case_acronyms)] +#[cfg_attr(botan_ffi_20251104, derive(Copy, Clone))] pub enum X509KeyConstraints { NO_CONSTRAINTS = 0, DIGITAL_SIGNATURE = 32768, @@ -24,6 +41,42 @@ pub enum X509KeyConstraints { DECIPHER_ONLY = 128, } +#[repr(u8)] +#[allow(clippy::upper_case_acronyms)] +#[cfg_attr(botan_ffi_20251104, derive(Copy, Clone))] +pub enum X509CrlCode { + UNSPECIFIED = 0, + KEY_COMPROMISE = 1, + CA_COMPROMISE = 2, + AFFILIATION_CHANGED = 3, + SUPERSEDED = 4, + CESSATION_OF_OPERATION = 5, + CERTIFICATE_HOLD = 6, + REMOVE_FROM_CRL = 8, + PRIVILIGE_WITHDRAWN = 9, + AA_COMPROMISE = 10, +} + +impl TryFrom for X509CrlCode { + type Error = (); + + fn try_from(value: u8) -> core::result::Result { + match value { + 0 => Ok(X509CrlCode::UNSPECIFIED), + 1 => Ok(X509CrlCode::KEY_COMPROMISE), + 2 => Ok(X509CrlCode::CA_COMPROMISE), + 3 => Ok(X509CrlCode::AFFILIATION_CHANGED), + 4 => Ok(X509CrlCode::SUPERSEDED), + 5 => Ok(X509CrlCode::CESSATION_OF_OPERATION), + 6 => Ok(X509CrlCode::CERTIFICATE_HOLD), + 8 => Ok(X509CrlCode::REMOVE_FROM_CRL), + 9 => Ok(X509CrlCode::PRIVILIGE_WITHDRAWN), + 10 => Ok(X509CrlCode::AA_COMPROMISE), + _ => Err(()), + } + } +} + extern "C" { pub fn botan_x509_cert_load( cert_obj: *mut botan_x509_cert_t, @@ -76,6 +129,23 @@ extern "C" { out: *mut u8, out_len: *mut usize, ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_is_ca( + cert: botan_x509_cert_t, + is_ca: *mut c_int, + limit: *mut usize, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_get_allowed_usage(cert: botan_x509_cert_t, usage: *mut u32) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_get_ocsp_responder( + cert: botan_x509_cert_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_is_self_signed(cert: botan_x509_cert_t, out: *mut c_int) -> c_int; + #[cfg(botan_ffi_20251104)] pub fn botan_x509_cert_get_public_key_bits( cert: botan_x509_cert_t, out: *mut u8, @@ -85,6 +155,11 @@ extern "C" { cert: botan_x509_cert_t, key: *mut botan_pubkey_t, ) -> c_int; + pub fn botan_x509_cert_get_issuer_dn_count( + cert: botan_x509_cert_t, + key: *const c_char, + len: *mut usize, + ) -> c_int; pub fn botan_x509_cert_get_issuer_dn( cert: botan_x509_cert_t, key: *const c_char, @@ -92,6 +167,11 @@ extern "C" { out: *mut u8, out_len: *mut usize, ) -> c_int; + pub fn botan_x509_cert_get_subject_dn_count( + cert: botan_x509_cert_t, + key: *const c_char, + len: *mut usize, + ) -> c_int; pub fn botan_x509_cert_get_subject_dn( cert: botan_x509_cert_t, key: *const c_char, @@ -99,6 +179,16 @@ extern "C" { out: *mut u8, out_len: *mut usize, ) -> c_int; + pub fn botan_x509_cert_get_subject_name( + cert: botan_x509_cert_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; + pub fn botan_x509_cert_get_issuer_name( + cert: botan_x509_cert_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; pub fn botan_x509_cert_to_string( cert: botan_x509_cert_t, out: *mut c_char, @@ -140,6 +230,190 @@ extern "C" { view_fn: botan_view_str_fn, ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_view_pem( + cert: botan_x509_cert_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_destroy(opts: botan_x509_cert_params_builder_t) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_create( + builder: *mut botan_x509_cert_params_builder_t, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_common_name( + builder: botan_x509_cert_params_builder_t, + name: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_country( + builder: botan_x509_cert_params_builder_t, + country: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_state( + builder: botan_x509_cert_params_builder_t, + state: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_locality( + builder: botan_x509_cert_params_builder_t, + locality: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_serial_number( + builder: botan_x509_cert_params_builder_t, + serial_number: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_organization( + builder: botan_x509_cert_params_builder_t, + organization: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_organizational_unit( + builder: botan_x509_cert_params_builder_t, + org_unit: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_email( + builder: botan_x509_cert_params_builder_t, + email: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_dns( + builder: botan_x509_cert_params_builder_t, + dns: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_uri( + builder: botan_x509_cert_params_builder_t, + uri: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_xmpp( + builder: botan_x509_cert_params_builder_t, + xmpp: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_ipv4( + builder: botan_x509_cert_params_builder_t, + ip: u32, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_allowed_usage( + builder: botan_x509_cert_params_builder_t, + usage: u32, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_allowed_extended_usage( + builder: botan_x509_cert_params_builder_t, + oid: botan_asn1_oid_t, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_set_as_ca_certificate( + builder: botan_x509_cert_params_builder_t, + limit: *const usize, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_ext_ip_addr_blocks( + builder: botan_x509_cert_params_builder_t, + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + is_critical: c_int, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_add_ext_as_blocks( + builder: botan_x509_cert_params_builder_t, + as_blocks: botan_x509_ext_as_blocks_t, + is_critical: c_int, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_into_self_signed( + cert_obj: *mut botan_x509_cert_t, + key: botan_privkey_t, + builder: botan_x509_cert_params_builder_t, + rng: botan_rng_t, + not_before: u64, + not_after: u64, + serial_number: *const botan_mp_t, + hash_fn: *const c_char, + padding: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_cert_params_builder_into_pkcs10_req( + req_obj: *mut botan_x509_pkcs10_req_t, + key: botan_privkey_t, + builder: botan_x509_cert_params_builder_t, + rng: botan_rng_t, + hash_fn: *const c_char, + padding: *const c_char, + challenge_password: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_destroy(req: botan_x509_pkcs10_req_t) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_load_file( + req_obj: *mut botan_x509_pkcs10_req_t, + req_path: *const c_char, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_load( + req_obj: *mut botan_x509_pkcs10_req_t, + req_bits: *const u8, + req_bits_len: usize, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_get_public_key( + req: botan_x509_pkcs10_req_t, + key: *mut botan_pubkey_t, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_get_allowed_usage( + req: botan_x509_pkcs10_req_t, + usage: *mut u32, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_is_ca( + req: botan_x509_pkcs10_req_t, + is_ca: *mut c_int, + limit: *mut usize, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_verify_signature( + req: botan_x509_pkcs10_req_t, + key: botan_pubkey_t, + result: *mut c_int, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_view_pem( + req: botan_x509_pkcs10_req_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_view_der( + req: botan_x509_pkcs10_req_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_bin_fn, + ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_pkcs10_req_sign( + subject_cert: *mut botan_x509_cert_t, + subject_req: botan_x509_pkcs10_req_t, + issuing_cert: botan_x509_cert_t, + issuing_key: botan_privkey_t, + rng: botan_rng_t, + not_before: u64, + not_after: u64, + serial_number: *const botan_mp_t, + hash_fn: *const c_char, + padding: *const c_char, + ) -> c_int; + pub fn botan_x509_crl_load_file(crl: *mut botan_x509_crl_t, file_path: *const c_char) -> c_int; pub fn botan_x509_crl_load( @@ -148,10 +422,83 @@ extern "C" { data_len: usize, ) -> c_int; + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_create( + crl_obj: *mut botan_x509_crl_t, + rng: botan_rng_t, + ca_cert: botan_x509_cert_t, + ca_key: botan_privkey_t, + issue_time: u64, + next_update: u32, + hash_fn: *const c_char, + padding: *const c_char, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_update( + crl_obj: *mut botan_x509_crl_t, + last_crl: botan_x509_crl_t, + rng: botan_rng_t, + ca_cert: botan_x509_cert_t, + ca_key: botan_privkey_t, + issue_time: u64, + next_update: u32, + revoked: *const botan_x509_cert_t, + revoked_len: usize, + reason: u8, + hash_fn: *const c_char, + padding: *const c_char, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_get_count(crl: botan_x509_crl_t, count: *mut usize) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_get_entry( + crl: botan_x509_crl_t, + i: usize, + serial: botan_mp_t, + expire_time: *mut u64, + reason: *mut u8, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_verify_signature( + crl: botan_x509_crl_t, + key: botan_pubkey_t, + result: *mut c_int, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_view_pem( + crl: botan_x509_crl_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_str_fn, + ) -> c_int; + + #[cfg(botan_ffi_20251104)] + pub fn botan_x509_crl_view_der( + crl: botan_x509_crl_t, + view_ctx: botan_view_ctx, + view_fn: botan_view_bin_fn, + ) -> c_int; + pub fn botan_x509_crl_destroy(crl: botan_x509_crl_t) -> c_int; pub fn botan_x509_is_revoked(crl: botan_x509_crl_t, cert: botan_x509_cert_t) -> c_int; - // TODO: botan_x509_cert_verify_with_crl - + pub fn botan_x509_cert_verify_with_crl( + validation_result: *mut c_int, + cert: botan_x509_cert_t, + intermediates: *const botan_x509_cert_t, + intermediates_len: usize, + trusted: *const botan_x509_cert_t, + trusted_len: usize, + crls: *const botan_x509_crl_t, + crls_len: usize, + trusted_path: *const c_char, + required_strength: usize, + hostname: *const c_char, + reference_time: u64, + ) -> c_int; } diff --git a/botan-sys/src/x509_ext.rs b/botan-sys/src/x509_ext.rs new file mode 100644 index 0000000..e9a408b --- /dev/null +++ b/botan-sys/src/x509_ext.rs @@ -0,0 +1,109 @@ +#[cfg(botan_ffi_20251104)] +use crate::{botan_x509_cert_t, ffi_types::*}; + +#[cfg(botan_ffi_20251104)] +pub enum botan_x509_ext_as_blocks_struct {} +#[cfg(botan_ffi_20251104)] +pub type botan_x509_ext_as_blocks_t = *mut botan_x509_ext_as_blocks_struct; + +#[cfg(botan_ffi_20251104)] +pub enum botan_x509_ext_ip_addr_blocks_struct {} +#[cfg(botan_ffi_20251104)] +pub type botan_x509_ext_ip_addr_blocks_t = *mut botan_x509_ext_ip_addr_blocks_struct; + +#[cfg(botan_ffi_20251104)] +extern "C" { + pub fn botan_x509_ext_ip_addr_blocks_destroy( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_destroy(as_blocks: botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_create( + ip_addr_blocks: *mut botan_x509_ext_ip_addr_blocks_t, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_create_from_cert( + ip_addr_blocks: *mut botan_x509_ext_ip_addr_blocks_t, + cert: botan_x509_cert_t, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_add_ip_addr( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + min: *const u8, + max: *const u8, + ipv6: c_int, + safi: *const u8, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_restrict( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + ipv6: c_int, + safi: *const u8, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_inherit( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + ipv6: c_int, + safi: *const u8, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_get_counts( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + v4_count: *mut usize, + v6_count: *mut usize, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_get_family( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + ipv6: c_int, + i: usize, + has_safi: *mut c_int, + safi: *mut u8, + present: *mut c_int, + count: *mut usize, + ) -> c_int; + pub fn botan_x509_ext_ip_addr_blocks_get_address( + ip_addr_blocks: botan_x509_ext_ip_addr_blocks_t, + ipv6: c_int, + i: usize, + entry: usize, + min_out: *mut u8, + max_out: *mut u8, + out_len: *mut usize, + ) -> c_int; + + pub fn botan_x509_ext_as_blocks_create(as_blocks: *mut botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_as_blocks_create_from_cert( + as_blocks: *mut botan_x509_ext_as_blocks_t, + cert: botan_x509_cert_t, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_add_asnum( + as_blocks: botan_x509_ext_as_blocks_t, + min: u32, + max: u32, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_restrict_asnum(as_blocks: botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_as_blocks_inherit_asnum(as_blocks: botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_as_blocks_add_rdi( + as_blocks: botan_x509_ext_as_blocks_t, + min: u32, + max: u32, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_restrict_rdi(as_blocks: botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_as_blocks_inherit_rdi(as_blocks: botan_x509_ext_as_blocks_t) -> c_int; + pub fn botan_x509_ext_as_blocks_get_asnum( + as_blocks: botan_x509_ext_as_blocks_t, + present: *mut c_int, + count: *mut usize, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_get_asnum_at( + as_blocks: botan_x509_ext_as_blocks_t, + i: usize, + min: *mut u32, + max: *mut u32, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_get_rdi( + as_blocks: botan_x509_ext_as_blocks_t, + present: *mut c_int, + count: *mut usize, + ) -> c_int; + pub fn botan_x509_ext_as_blocks_get_rdi_at( + as_blocks: botan_x509_ext_as_blocks_t, + i: usize, + min: *mut u32, + max: *mut u32, + ) -> c_int; +} diff --git a/botan/src/ec_group.rs b/botan/src/ec_group.rs index 3d9bc15..e64b2e6 100644 --- a/botan/src/ec_group.rs +++ b/botan/src/ec_group.rs @@ -21,6 +21,10 @@ botan_impl_drop!(EcGroup, botan_ec_group_destroy); #[cfg(botan_ffi_20250506)] impl EcGroup { + pub(crate) fn handle(&self) -> botan_ec_group_t { + self.obj + } + /// Does this build configuration support application specific groups pub fn supports_application_specific_groups() -> Result { let mut result = 0; diff --git a/botan/src/lib.rs b/botan/src/lib.rs index 5024f9a..5d42473 100644 --- a/botan/src/lib.rs +++ b/botan/src/lib.rs @@ -118,6 +118,7 @@ mod utils; mod version; mod x509_cert; mod x509_crl; +mod x509_ext; mod zfec; pub use asn1::*; @@ -141,6 +142,7 @@ pub use utils::*; pub use version::*; pub use x509_cert::*; pub use x509_crl::*; +pub use x509_ext::*; pub use zfec::*; #[cfg(botan_ffi_20230403)] diff --git a/botan/src/pubkey.rs b/botan/src/pubkey.rs index 61cb2b2..37162a6 100644 --- a/botan/src/pubkey.rs +++ b/botan/src/pubkey.rs @@ -1,4 +1,6 @@ use crate::utils::*; +#[cfg(botan_ffi_20250506)] +use crate::EcGroup; use botan_sys::*; use crate::mp::MPI; @@ -67,6 +69,23 @@ impl Privkey { Ok(Self { obj }) } + #[cfg(botan_ffi_20250506)] + pub fn create_ec( + algo: &str, + ec_group: &EcGroup, + rng: &mut RandomNumberGenerator, + ) -> Result { + let algo = make_cstr(algo)?; + let obj = botan_init!( + botan_ec_privkey_create, + algo.as_ptr(), + ec_group.handle(), + rng.handle() + )?; + + Ok(Self { obj }) + } + /// Load an RSA private key (p,q,e) /// /// # Examples diff --git a/botan/src/utils.rs b/botan/src/utils.rs index bfd9531..3aea584 100644 --- a/botan/src/utils.rs +++ b/botan/src/utils.rs @@ -25,6 +25,10 @@ pub(crate) fn make_cstr(input: &str) -> Result { Ok(cstr) } +pub(crate) fn make_optional_cstr(input: Option<&str>) -> Result> { + input.map(|s| make_cstr(s)).transpose() +} + pub(crate) fn call_botan_ffi_returning_vec_u8( initial_size: usize, cb: &dyn Fn(*mut u8, *mut usize) -> c_int, diff --git a/botan/src/x509_cert.rs b/botan/src/x509_cert.rs index de434bc..1226cdf 100644 --- a/botan/src/x509_cert.rs +++ b/botan/src/x509_cert.rs @@ -1,4 +1,13 @@ +#[cfg(botan_ffi_20251104)] +use core::net::Ipv4Addr; + use crate::utils::*; +#[cfg(botan_ffi_20251104)] +use crate::{ + pubkey::Privkey, + x509_ext::{ASBlocks, IpAddrBlocks}, + RandomNumberGenerator, CRL, MPI, OID, +}; use botan_sys::*; use crate::pubkey::Pubkey; @@ -22,7 +31,7 @@ impl Clone for Certificate { } /// Indicates if the certificate key is allowed for a particular usage -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum CertUsage { /// No particular usage restrictions NoRestrictions, @@ -46,6 +55,40 @@ pub enum CertUsage { DecipherOnly, } +#[cfg(botan_ffi_20251104)] +impl CertUsage { + pub(crate) fn to_bits(constraints: &[Self]) -> u32 { + constraints + .iter() + .map(|usage| X509KeyConstraints::from(*usage)) + .fold(0, |acc, constraint| acc | (constraint as u32)) + } + + pub(crate) fn from_bits(bits: u32) -> Vec { + if bits == 0 { + return vec![Self::NoRestrictions]; + } + + let all_constraints = [ + X509KeyConstraints::DIGITAL_SIGNATURE, + X509KeyConstraints::NON_REPUDIATION, + X509KeyConstraints::KEY_ENCIPHERMENT, + X509KeyConstraints::DATA_ENCIPHERMENT, + X509KeyConstraints::KEY_AGREEMENT, + X509KeyConstraints::KEY_CERT_SIGN, + X509KeyConstraints::CRL_SIGN, + X509KeyConstraints::ENCIPHER_ONLY, + X509KeyConstraints::DECIPHER_ONLY, + ]; + + all_constraints + .into_iter() + .filter(|&constraint| (bits & constraint as u32) != 0) + .map(CertUsage::from) + .collect() + } +} + impl From for CertUsage { fn from(err: X509KeyConstraints) -> CertUsage { match err { @@ -237,6 +280,67 @@ impl Certificate { Ok(Pubkey::from_handle(key)) } + #[cfg(botan_ffi_20251104)] + pub fn ocsp_responder(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_cert_get_ocsp_responder(self.obj, ctx, cb) + }) + } + + #[cfg(botan_ffi_20251104)] + pub fn issuer_dn(&self, key: &str) -> Result> { + let mut count = 0; + let key = make_cstr(key)?; + botan_call!( + botan_x509_cert_get_issuer_dn_count, + self.obj, + key.as_ptr(), + &mut count + )?; + let mut entries = Vec::new(); + for i in 0..count { + let item = call_botan_ffi_returning_string(0, &|out_buf, out_len| unsafe { + botan_x509_cert_get_issuer_dn(self.obj, key.as_ptr(), i, out_buf, out_len) + })?; + entries.push(item); + } + Ok(entries) + } + + #[cfg(botan_ffi_20251104)] + pub fn subject_dn(&self, key: &str) -> Result> { + let mut count = 0; + let key = make_cstr(key)?; + botan_call!( + botan_x509_cert_get_subject_dn_count, + self.obj, + key.as_ptr(), + &mut count + )?; + let mut entries = Vec::new(); + for i in 0..count { + let item = call_botan_ffi_returning_string(0, &|out_buf, out_len| unsafe { + botan_x509_cert_get_subject_dn(self.obj, key.as_ptr(), i, out_buf, out_len) + })?; + entries.push(item); + } + Ok(entries) + } + + #[cfg(botan_ffi_20251104)] + pub fn subject_name(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_cert_get_subject_name(self.obj, ctx, cb) + }) + } + + #[cfg(botan_ffi_20251104)] + pub fn issuer_name(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_cert_get_issuer_name(self.obj, ctx, cb) + }) + } + /// Return a free-form string representation of this certificate pub fn to_string(&self) -> Result { #[cfg(not(botan_ffi_20230403))] @@ -255,6 +359,34 @@ impl Certificate { } } + /// Return the certificate in PEM form + #[cfg(botan_ffi_20251104)] + pub fn to_pem(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_cert_view_pem(self.obj, ctx, cb) + }) + } + + #[cfg(botan_ffi_20251104)] + pub fn is_ca(&self) -> Result<(bool, Option)> { + let mut is_ca = 0; + let mut limit = 0; + botan_call!(botan_x509_cert_is_ca, self.obj, &mut is_ca, &mut limit)?; + let is_ca = interp_as_bool(is_ca, "botan_x509_cert_is_ca")?; + if is_ca { + Ok((true, Some(limit))) + } else { + Ok((false, None)) + } + } + + #[cfg(botan_ffi_20251104)] + pub fn is_self_signed(&self) -> Result { + let mut out = 0; + botan_call!(botan_x509_cert_is_self_signed, self.obj, &mut out)?; + interp_as_bool(out, "botan_x509_cert_is_self_signed") + } + /// Test if the certificate is allowed for a particular usage pub fn allows_usage(&self, usage: CertUsage) -> Result { let usage_bit: X509KeyConstraints = X509KeyConstraints::from(usage); @@ -264,6 +396,14 @@ impl Certificate { Ok(!r) } + /// + #[cfg(botan_ffi_20251104)] + pub fn allowed_usage(&self) -> Result> { + let mut usage = 0; + botan_call!(botan_x509_cert_get_allowed_usage, self.obj, &mut usage)?; + Ok(CertUsage::from_bits(usage)) + } + /// Attempt to verify this certificate pub fn verify( &self, @@ -278,28 +418,66 @@ impl Certificate { let trusted_path = make_cstr(trusted_path.unwrap_or(""))?; let hostname = make_cstr(hostname.unwrap_or(""))?; - // TODO: more idiomatic way to do this? - let mut trusted_h = Vec::new(); - for t in trusted { - trusted_h.push(t.handle()); - } + let trusted_h = trusted.iter().map(|t| t.handle()).collect::>(); + let intermediates_h = intermediates.iter().map(|i| i.handle()).collect::>(); + + let mut result = 0; + + let rc = unsafe { + botan_x509_cert_verify( + &mut result, + self.obj, + intermediates_h.as_ptr(), + intermediates_h.len(), + trusted_h.as_ptr(), + trusted_h.len(), + trusted_path.as_ptr(), + required_key_strength, + hostname.as_ptr(), + reference_time.unwrap_or(0), + ) + }; - let mut intermediates_h = Vec::new(); - for t in intermediates { - intermediates_h.push(t.handle()); + if rc == 0 { + Ok(CertValidationStatus::Success(result)) + } else if rc == 1 { + Ok(CertValidationStatus::Failed(result)) + } else { + Err(Error::from_rc(rc)) } + } + + /// Attempt to verify this certificate + pub fn verify_with_crl( + &self, + intermediates: &[&Certificate], + trusted: &[&Certificate], + trusted_path: Option<&str>, + hostname: Option<&str>, + reference_time: Option, + crls: &[&CRL], + ) -> Result { + let required_key_strength = 110; + + let trusted_path = make_cstr(trusted_path.unwrap_or(""))?; + let hostname = make_cstr(hostname.unwrap_or(""))?; + + let trusted_h = trusted.iter().map(|t| t.handle()).collect::>(); + let intermediates_h = intermediates.iter().map(|i| i.handle()).collect::>(); + let crls_h = crls.iter().map(|c| c.handle()).collect::>(); - // TODO this information is lost :( let mut result = 0; let rc = unsafe { - botan_x509_cert_verify( + botan_x509_cert_verify_with_crl( &mut result, self.obj, intermediates_h.as_ptr(), intermediates_h.len(), trusted_h.as_ptr(), trusted_h.len(), + crls_h.as_ptr(), + crls_h.len(), trusted_path.as_ptr(), required_key_strength, hostname.as_ptr(), @@ -329,4 +507,378 @@ impl Certificate { Err(Error::from_rc(rc)) } } + + #[cfg(botan_ffi_20251104)] + pub fn ext_ip_addr_blocks(&self) -> Result { + IpAddrBlocks::from_cert(self) + } + + #[cfg(botan_ffi_20251104)] + pub fn ext_as_blocks(&self) -> Result { + ASBlocks::from_cert(self) + } +} + +#[cfg(botan_ffi_20251104)] +#[derive(Debug)] +/// X.509 certificate Builder +pub struct CertificateBuilder { + obj: botan_x509_cert_params_builder_t, +} +#[cfg(botan_ffi_20251104)] +unsafe impl Sync for CertificateBuilder {} +#[cfg(botan_ffi_20251104)] +unsafe impl Send for CertificateBuilder {} +#[cfg(botan_ffi_20251104)] +botan_impl_drop!(CertificateBuilder, botan_x509_cert_params_builder_destroy); + +#[cfg(botan_ffi_20251104)] +impl CertificateBuilder { + pub fn new() -> Result { + let obj = botan_init!(botan_x509_cert_params_builder_create,)?; + Ok(CertificateBuilder { obj }) + } + + pub fn add_common_name(&mut self, name: &str) -> Result<()> { + let name = make_cstr(name)?; + botan_call!( + botan_x509_cert_params_builder_add_common_name, + self.obj, + name.as_ptr() + ) + } + + pub fn add_country(&mut self, country: &str) -> Result<()> { + let country = make_cstr(country)?; + botan_call!( + botan_x509_cert_params_builder_add_country, + self.obj, + country.as_ptr() + ) + } + + pub fn add_organization(&mut self, organization: &str) -> Result<()> { + let organization = make_cstr(organization)?; + botan_call!( + botan_x509_cert_params_builder_add_organization, + self.obj, + organization.as_ptr() + ) + } + + pub fn add_organizational_unit(&mut self, org_unit: &str) -> Result<()> { + let org_unit = make_cstr(org_unit)?; + botan_call!( + botan_x509_cert_params_builder_add_organizational_unit, + self.obj, + org_unit.as_ptr() + ) + } + + pub fn add_locality(&mut self, locality: &str) -> Result<()> { + let locality = make_cstr(locality)?; + botan_call!( + botan_x509_cert_params_builder_add_locality, + self.obj, + locality.as_ptr() + ) + } + + pub fn add_state(&mut self, state: &str) -> Result<()> { + let state = make_cstr(state)?; + botan_call!( + botan_x509_cert_params_builder_add_state, + self.obj, + state.as_ptr() + ) + } + + pub fn add_serial_number(&mut self, serial_number: &str) -> Result<()> { + let serial_number = make_cstr(serial_number)?; + botan_call!( + botan_x509_cert_params_builder_add_serial_number, + self.obj, + serial_number.as_ptr() + ) + } + + pub fn add_email(&mut self, email: &str) -> Result<()> { + let email = make_cstr(email)?; + botan_call!( + botan_x509_cert_params_builder_add_email, + self.obj, + email.as_ptr() + ) + } + + pub fn add_uri(&mut self, uri: &str) -> Result<()> { + let uri = make_cstr(uri)?; + botan_call!( + botan_x509_cert_params_builder_add_uri, + self.obj, + uri.as_ptr() + ) + } + + pub fn add_ipv4(&mut self, ip: Ipv4Addr) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_add_ipv4, + self.obj, + ip.to_bits() + ) + } + + pub fn add_dns(&mut self, dns: &str) -> Result<()> { + let dns = make_cstr(dns)?; + botan_call!( + botan_x509_cert_params_builder_add_dns, + self.obj, + dns.as_ptr() + ) + } + + pub fn add_xmpp(&mut self, xmpp: &str) -> Result<()> { + let xmpp = make_cstr(xmpp)?; + botan_call!( + botan_x509_cert_params_builder_add_xmpp, + self.obj, + xmpp.as_ptr() + ) + } + pub fn set_as_ca_certificate(&mut self, limit: Option) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_set_as_ca_certificate, + self.obj, + limit + .as_ref() + .map_or(std::ptr::null(), |exp| exp as *const _) + ) + } + + pub fn add_constraints(&mut self, usage: &[CertUsage]) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_add_allowed_usage, + self.obj, + CertUsage::to_bits(usage) + ) + } + + pub fn add_ex_constraint(&mut self, oid: &OID) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_add_allowed_extended_usage, + self.obj, + oid.handle() + ) + } + + pub fn add_ext_ip_addr_blocks( + &mut self, + ip_addr_blocks: &IpAddrBlocks, + is_critical: bool, + ) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_add_ext_ip_addr_blocks, + self.obj, + ip_addr_blocks.handle(), + is_critical as c_int + ) + } + + pub fn add_ext_as_blocks(&mut self, as_blocks: &ASBlocks, is_critical: bool) -> Result<()> { + botan_call!( + botan_x509_cert_params_builder_add_ext_as_blocks, + self.obj, + as_blocks.handle(), + is_critical as c_int + ) + } + + #[cfg(botan_ffi_20251104)] + pub fn into_self_signed( + &self, + key: &Privkey, + rng: &mut RandomNumberGenerator, + not_before: u64, + not_after: u64, + serial_number: Option<&MPI>, + hash_fn: Option<&str>, + padding: Option<&str>, + ) -> Result { + let hash_fn = make_optional_cstr(hash_fn)?; + let padding = make_optional_cstr(padding)?; + let serial_handle = serial_number.map(|sn| sn.handle()); + let serial_ptr = serial_handle + .as_ref() + .map_or(std::ptr::null(), |handle| handle as *const _); + + let obj = botan_init!( + botan_x509_cert_params_builder_into_self_signed, + key.handle(), + self.obj, + rng.handle(), + not_before, + not_after, + serial_ptr, + hash_fn + .as_ref() + .map_or(std::ptr::null(), |hash_fn| hash_fn.as_ptr()), + padding + .as_ref() + .map_or(std::ptr::null(), |padding| padding.as_ptr()) + )?; + Ok(Certificate { obj }) + } + + #[cfg(botan_ffi_20251104)] + pub fn into_request( + &self, + key: &Privkey, + rng: &mut RandomNumberGenerator, + hash_fn: Option<&str>, + padding: Option<&str>, + challenge_password: Option<&str>, + ) -> Result { + let hash_fn = make_optional_cstr(hash_fn)?; + let padding = make_optional_cstr(padding)?; + let challenge = make_optional_cstr(challenge_password)?; + + let obj = botan_init!( + botan_x509_cert_params_builder_into_pkcs10_req, + key.handle(), + self.obj, + rng.handle(), + hash_fn + .as_ref() + .map_or(std::ptr::null(), |hash_fn| hash_fn.as_ptr()), + padding + .as_ref() + .map_or(std::ptr::null(), |padding| padding.as_ptr()), + challenge + .as_ref() + .map_or(std::ptr::null(), |challenge| challenge.as_ptr()) + )?; + + Ok(CertificateRequest { obj }) + } +} + +#[cfg(botan_ffi_20251104)] +#[derive(Debug)] +pub struct CertificateRequest { + obj: botan_x509_pkcs10_req_t, +} + +#[cfg(botan_ffi_20251104)] +unsafe impl Sync for CertificateRequest {} +#[cfg(botan_ffi_20251104)] +unsafe impl Send for CertificateRequest {} + +#[cfg(botan_ffi_20251104)] +botan_impl_drop!(CertificateRequest, botan_x509_pkcs10_req_destroy); + +#[cfg(botan_ffi_20251104)] +impl CertificateRequest { + pub fn load(data: &[u8]) -> Result { + let obj = botan_init!(botan_x509_pkcs10_req_load, data.as_ptr(), data.len())?; + Ok(CertificateRequest { obj }) + } + + pub fn from_file(fsname: &str) -> Result { + let fsname = make_cstr(fsname)?; + let obj = botan_init!(botan_x509_pkcs10_req_load_file, fsname.as_ptr())?; + Ok(CertificateRequest { obj }) + } + + pub fn verify(&self, key: &Pubkey) -> Result { + let mut result = 0; + botan_call!( + botan_x509_pkcs10_req_verify_signature, + self.obj, + key.handle(), + &mut result + )?; + interp_as_bool(result, "botan_x509_pkcs10_req_verify_signature") + } + + pub fn public_key(&self) -> Result { + let mut key = ptr::null_mut(); + botan_call!(botan_x509_pkcs10_req_get_public_key, self.obj, &mut key)?; + Ok(Pubkey::from_handle(key)) + } + + pub fn allowed_usage(&self) -> Result> { + let mut usage = 0; + botan_call!( + botan_x509_pkcs10_req_get_allowed_usage, + self.obj, + &mut usage + )?; + Ok(CertUsage::from_bits(usage)) + } + + pub fn is_ca(&self) -> Result<(bool, Option)> { + let mut is_ca = 0; + let mut limit = 0; + botan_call!( + botan_x509_pkcs10_req_is_ca, + self.obj, + &mut is_ca, + &mut limit + )?; + let is_ca = interp_as_bool(is_ca, "botan_x509_pkcs10_req_is_ca")?; + if is_ca { + Ok((true, Some(limit))) + } else { + Ok((false, None)) + } + } + + pub fn sign( + &self, + issuing_cert: &Certificate, + issuing_key: &Privkey, + rng: &mut RandomNumberGenerator, + not_before: u64, + not_after: u64, + serial_number: Option<&MPI>, + hash_fn: Option<&str>, + padding: Option<&str>, + ) -> Result { + let hash_fn = make_optional_cstr(hash_fn)?; + let padding = make_optional_cstr(padding)?; + let serial_handle = serial_number.map(|sn| sn.handle()); + let serial_ptr = serial_handle + .as_ref() + .map_or(std::ptr::null(), |handle| handle as *const _); + + let obj = botan_init!( + botan_x509_pkcs10_req_sign, + self.obj, + issuing_cert.handle(), + issuing_key.handle(), + rng.handle(), + not_before, + not_after, + serial_ptr, + hash_fn + .as_ref() + .map_or(std::ptr::null(), |hash_fn| hash_fn.as_ptr()), + padding + .as_ref() + .map_or(std::ptr::null(), |padding| padding.as_ptr()) + )?; + Ok(Certificate { obj }) + } + + pub fn to_pem(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_pkcs10_req_view_pem(self.obj, ctx, cb) + }) + } + + pub fn to_der(&self) -> Result> { + call_botan_ffi_viewing_vec_u8(&|ctx, cb| unsafe { + botan_x509_pkcs10_req_view_der(self.obj, ctx, cb) + }) + } } diff --git a/botan/src/x509_crl.rs b/botan/src/x509_crl.rs index 356ec70..4e29beb 100644 --- a/botan/src/x509_crl.rs +++ b/botan/src/x509_crl.rs @@ -1,12 +1,10 @@ use crate::utils::*; +#[cfg(botan_ffi_20251104)] +use crate::{Certificate, Privkey, Pubkey, RandomNumberGenerator, MPI}; use botan_sys::*; #[derive(Debug)] /// X.509 certificate revocation list -/// -/// Warning: as of the current version you cannot do much useful -/// operations with CRLs, due to limitations of the API currently -/// exported by the C++ library pub struct CRL { obj: botan_x509_crl_t, } @@ -16,7 +14,70 @@ unsafe impl Send for CRL {} botan_impl_drop!(CRL, botan_x509_crl_destroy); +#[cfg(botan_ffi_20251104)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CrlReason { + Unspecified, + KeyCompromise, + CaCompromise, + AffiliationChanged, + Superseded, + CessationOfOperation, + CertificateHold, + RemoveFromCrl, + PrivilegeWithdrawn, + AaCompromise, +} + +#[cfg(botan_ffi_20251104)] +impl From for CrlReason { + fn from(value: X509CrlCode) -> Self { + match value { + X509CrlCode::UNSPECIFIED => CrlReason::Unspecified, + X509CrlCode::KEY_COMPROMISE => CrlReason::KeyCompromise, + X509CrlCode::CA_COMPROMISE => CrlReason::CaCompromise, + X509CrlCode::AFFILIATION_CHANGED => CrlReason::AffiliationChanged, + X509CrlCode::SUPERSEDED => CrlReason::Superseded, + X509CrlCode::CESSATION_OF_OPERATION => CrlReason::CessationOfOperation, + X509CrlCode::CERTIFICATE_HOLD => CrlReason::CertificateHold, + X509CrlCode::REMOVE_FROM_CRL => CrlReason::RemoveFromCrl, + X509CrlCode::PRIVILIGE_WITHDRAWN => CrlReason::PrivilegeWithdrawn, + X509CrlCode::AA_COMPROMISE => CrlReason::AaCompromise, + } + } +} + +#[cfg(botan_ffi_20251104)] +impl From for X509CrlCode { + fn from(value: CrlReason) -> Self { + match value { + CrlReason::Unspecified => X509CrlCode::UNSPECIFIED, + CrlReason::KeyCompromise => X509CrlCode::KEY_COMPROMISE, + CrlReason::CaCompromise => X509CrlCode::CA_COMPROMISE, + CrlReason::AffiliationChanged => X509CrlCode::AFFILIATION_CHANGED, + CrlReason::Superseded => X509CrlCode::SUPERSEDED, + CrlReason::CessationOfOperation => X509CrlCode::CESSATION_OF_OPERATION, + CrlReason::CertificateHold => X509CrlCode::CERTIFICATE_HOLD, + CrlReason::RemoveFromCrl => X509CrlCode::REMOVE_FROM_CRL, + CrlReason::PrivilegeWithdrawn => X509CrlCode::PRIVILIGE_WITHDRAWN, + CrlReason::AaCompromise => X509CrlCode::AA_COMPROMISE, + } + } +} + +#[cfg(botan_ffi_20251104)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CrlEntry { + pub serial: MPI, + pub expire_time: u64, + pub reason: CrlReason, +} + impl CRL { + pub(crate) fn handle(&self) -> botan_x509_crl_t { + self.obj + } + /// Load a X.509 CRL from DER or PEM representation pub fn load(data: &[u8]) -> Result { let obj = botan_init!(botan_x509_crl_load, data.as_ptr(), data.len())?; @@ -30,6 +91,80 @@ impl CRL { Ok(Self { obj }) } + #[cfg(botan_ffi_20251104)] + pub fn new( + rng: &mut RandomNumberGenerator, + ca_cert: &Certificate, + ca_key: &Privkey, + issue_time: u64, + next_update: u32, + hash_fn: Option<&str>, + padding: Option<&str>, + ) -> Result { + let hash_fn = make_optional_cstr(hash_fn)?; + let padding = make_optional_cstr(padding)?; + + let obj = botan_init!( + botan_x509_crl_create, + rng.handle(), + ca_cert.handle(), + ca_key.handle(), + issue_time, + next_update, + hash_fn + .as_ref() + .map_or(std::ptr::null(), |hash_fn| hash_fn.as_ptr()), + padding + .as_ref() + .map_or(std::ptr::null(), |padding| padding.as_ptr()) + )?; + + Ok(Self { obj }) + } + + #[cfg(botan_ffi_20251104)] + pub fn revoke( + &self, + rng: &mut RandomNumberGenerator, + ca_cert: &Certificate, + ca_key: &Privkey, + issue_time: u64, + next_update: u32, + revoked: &[&Certificate], + reason: CrlReason, + hash_fn: Option<&str>, + padding: Option<&str>, + ) -> Result { + let hash_fn = make_optional_cstr(hash_fn)?; + let padding = make_optional_cstr(padding)?; + + let mut revoked_h = Vec::new(); + for c in revoked { + revoked_h.push(c.handle()); + } + + let obj = botan_init!( + botan_x509_crl_update, + self.obj, + rng.handle(), + ca_cert.handle(), + ca_key.handle(), + issue_time, + next_update, + revoked_h.as_ptr(), + revoked_h.len(), + X509CrlCode::from(reason) as u8, + hash_fn + .as_ref() + .map_or(std::ptr::null(), |hash_fn| hash_fn.as_ptr()), + padding + .as_ref() + .map_or(std::ptr::null(), |padding| padding.as_ptr()) + )?; + + Ok(Self { obj }) + } + /// Return true if the provided CRL is listed as revoked in the CRL pub fn is_revoked(&self, cert: &crate::Certificate) -> Result { let rc = unsafe { botan_x509_is_revoked(self.obj, cert.handle()) }; @@ -41,4 +176,62 @@ impl CRL { _ => Err(Error::from_rc(rc)), } } + + #[cfg(botan_ffi_20251104)] + pub fn revoked(&self) -> Result> { + let mut entries = Vec::new(); + let mut count = 0; + botan_call!(botan_x509_crl_get_count, self.obj, &mut count)?; + for i in 0..count { + let mut expire_time = 0; + let mut reason = 0; + let serial = MPI::new()?; + botan_call!( + botan_x509_crl_get_entry, + self.obj, + i, + serial.handle(), + &mut expire_time, + &mut reason + )?; + entries.push(CrlEntry { + serial, + expire_time, + reason: CrlReason::from(X509CrlCode::try_from(reason).map_err(|_| { + Error::with_message( + ErrorType::InternalError, + "Unexpected CRL reason code".to_string(), + ) + })?), + }) + } + + Ok(entries) + } + + #[cfg(botan_ffi_20251104)] + pub fn verify(&self, key: &Pubkey) -> Result { + let mut result = 0; + botan_call!( + botan_x509_crl_verify_signature, + self.obj, + key.handle(), + &mut result + )?; + interp_as_bool(result, "botan_x509_crl_verify_signature") + } + + #[cfg(botan_ffi_20251104)] + pub fn to_pem(&self) -> Result { + call_botan_ffi_viewing_str_fn(&|ctx, cb| unsafe { + botan_x509_crl_view_pem(self.obj, ctx, cb) + }) + } + + #[cfg(botan_ffi_20251104)] + pub fn to_der(&self) -> Result> { + call_botan_ffi_viewing_vec_u8(&|ctx, cb| unsafe { + botan_x509_crl_view_der(self.obj, ctx, cb) + }) + } } diff --git a/botan/src/x509_ext.rs b/botan/src/x509_ext.rs new file mode 100644 index 0000000..ce10f71 --- /dev/null +++ b/botan/src/x509_ext.rs @@ -0,0 +1,321 @@ +#[cfg(botan_ffi_20251104)] +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[cfg(botan_ffi_20251104)] +use crate::{utils::*, Certificate}; +#[cfg(botan_ffi_20251104)] +use botan_sys::*; + +#[cfg(botan_ffi_20251104)] +#[derive(Debug)] +pub struct IpAddrBlocks { + obj: botan_x509_ext_ip_addr_blocks_t, +} + +#[cfg(botan_ffi_20251104)] +unsafe impl Sync for IpAddrBlocks {} +#[cfg(botan_ffi_20251104)] +unsafe impl Send for IpAddrBlocks {} + +#[cfg(botan_ffi_20251104)] +botan_impl_drop!(IpAddrBlocks, botan_x509_ext_ip_addr_blocks_destroy); + +type IpVec = Vec<(Option, Option>)>; + +#[cfg(botan_ffi_20251104)] +impl IpAddrBlocks { + pub(crate) fn handle(&self) -> botan_x509_ext_ip_addr_blocks_t { + self.obj + } + + pub(crate) fn from_cert(cert: &Certificate) -> Result { + let obj = botan_init!( + botan_x509_ext_ip_addr_blocks_create_from_cert, + cert.handle() + )?; + Ok(IpAddrBlocks { obj }) + } + + pub fn new() -> Result { + let obj = botan_init!(botan_x509_ext_ip_addr_blocks_create)?; + Ok(IpAddrBlocks { obj }) + } + + pub fn add_addr(&mut self, addr: IpAddr, safi: Option) -> Result<()> { + self.add_range(addr, addr, safi) + } + + pub fn add_range(&mut self, min: IpAddr, max: IpAddr, safi: Option) -> Result<()> { + let (min, max, ipv6) = match (min, max) { + (IpAddr::V4(min), IpAddr::V4(max)) => (min.octets().to_vec(), max.octets().to_vec(), 0), + (IpAddr::V6(min), IpAddr::V6(max)) => (min.octets().to_vec(), max.octets().to_vec(), 1), + _ => { + return Err(Error::bad_parameter( + "Both addresses must use the same IP version", + )) + } + }; + + botan_call!( + botan_x509_ext_ip_addr_blocks_add_ip_addr, + self.obj, + min.as_ptr(), + max.as_ptr(), + ipv6, + safi.as_ref() + .map_or(std::ptr::null(), |safi| safi as *const u8) + ) + } + + pub fn restrict(&mut self, ipv6: bool, safi: Option) -> Result<()> { + botan_call!( + botan_x509_ext_ip_addr_blocks_restrict, + self.obj, + ipv6 as i32, + safi.as_ref() + .map_or(std::ptr::null(), |safi| safi as *const u8) + ) + } + + pub fn inherit(&mut self, ipv6: bool, safi: Option) -> Result<()> { + botan_call!( + botan_x509_ext_ip_addr_blocks_inherit, + self.obj, + ipv6 as i32, + safi.as_ref() + .map_or(std::ptr::null(), |safi| safi as *const u8) + ) + } + + pub fn addresses(&self) -> Result<(IpVec, IpVec)> { + let mut v4 = Vec::new(); + let mut v6 = Vec::new(); + + let mut v4_count = 0; + let mut v6_count = 0; + + botan_call!( + botan_x509_ext_ip_addr_blocks_get_counts, + self.obj, + &mut v4_count, + &mut v6_count + )?; + + for (ipv6, start, stop) in [(false, 0, v4_count), (true, v4_count, v4_count + v6_count)] { + for i in start..stop { + let mut has_safi = 0; + let mut safi = 0; + let mut present = 0; + let mut count = 0; + botan_call!( + botan_x509_ext_ip_addr_blocks_get_family, + self.obj, + ipv6 as i32, + i, + &mut has_safi, + &mut safi, + &mut present, + &mut count + )?; + let has_safi = + interp_as_bool(has_safi, "botan_x509_ext_ip_addr_blocks_get_family")?; + let present = interp_as_bool(present, "botan_x509_ext_ip_addr_blocks_get_family")?; + let mut v4_ranges = None; + let mut v6_ranges = None; + if present { + let mut v4_ranges_ = Vec::new(); + let mut v6_ranges_ = Vec::new(); + for entry in 0..count { + let buffer_len = if ipv6 { 16 } else { 4 }; + let mut min = vec![0; buffer_len]; + let mut max = vec![0; buffer_len]; + let mut out_len = buffer_len; + botan_call!( + botan_x509_ext_ip_addr_blocks_get_address, + self.obj, + ipv6 as i32, + i, + entry, + min.as_mut_ptr(), + max.as_mut_ptr(), + &mut out_len + )?; + if out_len != buffer_len { + return Err(Error::with_message( + ErrorType::InternalError, + "Unexpected result from botan_x509_ext_ip_addr_blocks_get_address" + .to_string(), + )); + } + if ipv6 { + v6_ranges_.push(( + Ipv6Addr::new( + (min[0] as u16) << 8 | min[1] as u16, + (min[2] as u16) << 8 | min[3] as u16, + (min[4] as u16) << 8 | min[5] as u16, + (min[6] as u16) << 8 | min[7] as u16, + (min[8] as u16) << 8 | min[9] as u16, + (min[10] as u16) << 8 | min[11] as u16, + (min[12] as u16) << 8 | min[13] as u16, + (min[14] as u16) << 8 | min[15] as u16, + ), + Ipv6Addr::new( + (max[0] as u16) << 8 | max[1] as u16, + (max[2] as u16) << 8 | max[3] as u16, + (max[4] as u16) << 8 | max[5] as u16, + (max[6] as u16) << 8 | max[7] as u16, + (max[8] as u16) << 8 | max[9] as u16, + (max[10] as u16) << 8 | max[11] as u16, + (max[12] as u16) << 8 | max[13] as u16, + (max[14] as u16) << 8 | max[15] as u16, + ), + )); + } else { + v4_ranges_.push(( + Ipv4Addr::new(min[0], min[1], min[2], min[3]), + Ipv4Addr::new(max[0], max[1], max[2], max[3]), + )); + } + } + v4_ranges = Some(v4_ranges_); + v6_ranges = Some(v6_ranges_); + } + let safi = if has_safi { Some(safi) } else { None }; + if ipv6 { + v6.push((safi, v6_ranges)) + } else { + v4.push((safi, v4_ranges)) + } + } + } + + Ok((v4, v6)) + } +} + +#[cfg(botan_ffi_20251104)] +#[derive(Debug)] +pub struct ASBlocks { + obj: botan_x509_ext_as_blocks_t, +} + +#[cfg(botan_ffi_20251104)] +unsafe impl Sync for ASBlocks {} +#[cfg(botan_ffi_20251104)] +unsafe impl Send for ASBlocks {} + +#[cfg(botan_ffi_20251104)] +botan_impl_drop!(ASBlocks, botan_x509_ext_as_blocks_destroy); + +#[cfg(botan_ffi_20251104)] +impl ASBlocks { + pub(crate) fn handle(&self) -> botan_x509_ext_as_blocks_t { + self.obj + } + + pub(crate) fn from_cert(cert: &Certificate) -> Result { + let obj = botan_init!(botan_x509_ext_as_blocks_create_from_cert, cert.handle())?; + Ok(ASBlocks { obj }) + } + + pub fn new() -> Result { + let obj = botan_init!(botan_x509_ext_as_blocks_create)?; + Ok(ASBlocks { obj }) + } + + pub fn add_asnum(&mut self, asnum: u32) -> Result<()> { + self.add_asnum_range(asnum, asnum) + } + + pub fn add_asnum_range(&mut self, min: u32, max: u32) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_add_asnum, self.obj, min, max) + } + + pub fn restrict_asnum(&mut self) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_restrict_asnum, self.obj) + } + + pub fn inherit_asnum(&mut self) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_inherit_asnum, self.obj) + } + + pub fn add_rdi(&mut self, asnum: u32) -> Result<()> { + self.add_rdi_range(asnum, asnum) + } + + pub fn add_rdi_range(&mut self, min: u32, max: u32) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_add_rdi, self.obj, min, max) + } + + pub fn restrict_rdi(&mut self) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_restrict_rdi, self.obj) + } + + pub fn inherit_rdi(&mut self) -> Result<()> { + botan_call!(botan_x509_ext_as_blocks_inherit_rdi, self.obj) + } + + pub fn asnum(&self) -> Result>> { + let mut present = 0; + let mut count = 0; + botan_call!( + botan_x509_ext_as_blocks_get_asnum, + self.obj, + &mut present, + &mut count + )?; + let present = interp_as_bool(present, "botan_x509_ext_as_blocks_get_asnum")?; + + if !present { + return Ok(None); + } + + let mut asnums = Vec::new(); + for i in 0..count { + let mut min = 0; + let mut max = 0; + botan_call!( + botan_x509_ext_as_blocks_get_asnum_at, + self.obj, + i, + &mut min, + &mut max + )?; + asnums.push((min, max)) + } + + Ok(Some(asnums)) + } + + pub fn rdi(&self) -> Result>> { + let mut present = 0; + let mut count = 0; + botan_call!( + botan_x509_ext_as_blocks_get_rdi, + self.obj, + &mut present, + &mut count + )?; + let present = interp_as_bool(present, "botan_x509_ext_as_blocks_get_asnum")?; + + if !present { + return Ok(None); + } + + let mut rdis = Vec::new(); + for i in 0..count { + let mut min = 0; + let mut max = 0; + botan_call!( + botan_x509_ext_as_blocks_get_rdi_at, + self.obj, + i, + &mut min, + &mut max + )?; + rdis.push((min, max)) + } + + Ok(Some(rdis)) + } +} diff --git a/botan/tests/tests.rs b/botan/tests/tests.rs index 36e4676..fb36ff2 100644 --- a/botan/tests/tests.rs +++ b/botan/tests/tests.rs @@ -1345,5 +1345,313 @@ fn test_ec_group() -> Result<(), botan::Error> { assert_eq!(mycustomp256, secp256r1); } + if supports_app_groups { + let secp256r1 = botan::EcGroup::from_name("secp256r1")?; + let mut rng = botan::RandomNumberGenerator::new()?; + + let pkey = botan::Privkey::create_ec("ECDSA", &secp256r1, &mut rng)?; + assert_eq!(pkey.algo_name()?, "ECDSA"); + } + + Ok(()) +} + +#[cfg(botan_ffi_20251104)] +#[cfg(feature = "std")] +#[test] +fn test_cert_creation() { + let hash_fn = "SHA-256"; + let group = "secp256r1"; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let not_before = now - 180; + let not_after = now + 86400; + + let mut rng = botan::RandomNumberGenerator::new().unwrap(); + let ca_key = botan::Privkey::create("ECDSA", group, &mut rng).unwrap(); + let mut ca_builder = botan::CertificateBuilder::new().unwrap(); + ca_builder.add_common_name("Test CA").unwrap(); + ca_builder.add_country("US").unwrap(); + ca_builder.add_organization("Botan Project").unwrap(); + ca_builder.add_organizational_unit("Testing").unwrap(); + ca_builder.set_as_ca_certificate(Some(1)).unwrap(); + ca_builder + .add_constraints(&[botan::CertUsage::DigitalSignature]) + .unwrap(); + let ca_cert = ca_builder + .into_self_signed( + &ca_key, + &mut rng, + not_before, + not_after, + None, + Some(hash_fn), + None, + ) + .unwrap(); + + assert_eq!( + ca_cert.subject_name().unwrap(), + r#"CN="Test CA",C="US",O="Botan Project",OU="Testing""# + ); + + let constraints = ca_cert.allowed_usage().unwrap(); + assert_eq!(constraints.len(), 3); + for item in [ + botan::CertUsage::DigitalSignature, + botan::CertUsage::CertificateSign, + botan::CertUsage::CrlSign, + ] { + assert!(constraints.contains(&item)); + } + + assert!(ca_cert.is_ca().unwrap().0); + assert!(ca_cert.is_self_signed().unwrap()); + + let cert_key = botan::Privkey::create("ECDSA", group, &mut rng).unwrap(); + let mut req_builder = botan::CertificateBuilder::new().unwrap(); + req_builder.add_uri("https://botan.randombit.net").unwrap(); + + let dns_names = [ + "imaginary.botan.randombit.net", + "botan.randombit.net", + "randombit.net", + ]; + + for item in dns_names { + req_builder.add_dns(item).unwrap(); + } + + let req = req_builder + .into_request(&cert_key, &mut rng, Some(hash_fn), None, None) + .unwrap(); + + assert!(!req.is_ca().unwrap().0); + assert_eq!( + req.public_key().unwrap().fingerprint("SHA-256").unwrap(), + cert_key.pubkey().unwrap().fingerprint("SHA-256").unwrap() + ); + + let serial = botan::MPI::from_str("12345").unwrap(); + let cert = req + .sign( + &ca_cert, + &ca_key, + &mut rng, + not_before, + not_after, + Some(&serial), + Some(hash_fn), + None, + ) + .unwrap(); + assert!(!cert.is_self_signed().unwrap()); + assert_eq!( + cert.allowed_usage().unwrap(), + [botan::CertUsage::NoRestrictions] + ); + let subject_dn_dns = cert.subject_dn("DNS").unwrap(); + assert_eq!(subject_dn_dns.len(), 3); + for item in dns_names { + assert!(subject_dn_dns.contains(&item.to_string())); + } + + let serial_from_cert = botan::MPI::new_from_bytes(&cert.serial_number().unwrap()).unwrap(); + assert_eq!(serial, serial_from_cert); + + let crl = + botan::CRL::new(&mut rng, &ca_cert, &ca_key, now, 86400, Some(hash_fn), None).unwrap(); + assert!(crl.verify(&ca_cert.public_key().unwrap()).unwrap()); + assert!(crl.revoked().unwrap().is_empty()); + assert!(!crl.is_revoked(&cert).unwrap()); + + assert!(cert + .ext_as_blocks() + .is_err_and(|e| e.error_type() == botan::ErrorType::NoValueAvailable)); + assert!(cert + .ext_ip_addr_blocks() + .is_err_and(|e| e.error_type() == botan::ErrorType::NoValueAvailable)); + + let result = cert + .verify_with_crl(&[], &[&ca_cert], None, None, None, &[&crl]) + .unwrap(); + assert!(result.success()); + assert_eq!(result.to_string(), "Verified"); + + let crl = crl + .revoke( + &mut rng, + &ca_cert, + &ca_key, + now, + 86400, + &[&cert], + botan::CrlReason::KeyCompromise, + Some(hash_fn), + None, + ) + .unwrap(); + assert!(crl.verify(&ca_cert.public_key().unwrap()).unwrap()); + assert!(crl.is_revoked(&cert).unwrap()); + assert_eq!(crl.revoked().unwrap().len(), 1); + let crl_entry = crl.revoked().unwrap().swap_remove(0); + assert_eq!(crl_entry.serial, serial); + assert_eq!(crl_entry.reason, botan::CrlReason::KeyCompromise); + assert!(now - 5 < crl_entry.expire_time && crl_entry.expire_time < now + 5); +} + +#[cfg(botan_ffi_20251104)] +#[cfg(feature = "std")] +#[test] +fn test_x509_rpki() -> Result<(), botan::Error> { + use std::net::{Ipv4Addr, Ipv6Addr}; + + let group = "secp256r1"; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let not_before = now - 180; + let not_after = now + 86400; + + let mut rng = botan::RandomNumberGenerator::new()?; + let ca_key = botan::Privkey::create("ECDSA", group, &mut rng)?; + let mut ca_builder = botan::CertificateBuilder::new()?; + ca_builder.set_as_ca_certificate(None)?; + ca_builder.add_constraints(&[botan::CertUsage::DigitalSignature])?; + + let mut ca_ip_addr_blocks = botan::IpAddrBlocks::new()?; + ca_ip_addr_blocks.add_addr(std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1)), None)?; + ca_ip_addr_blocks.add_range( + std::net::IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), + std::net::IpAddr::V4(Ipv4Addr::new(10, 0, 255, 255)), + None, + )?; + ca_ip_addr_blocks.restrict(false, Some(42))?; + + ca_ip_addr_blocks.add_addr( + std::net::IpAddr::V6(Ipv6Addr::new( + 0xab01, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000, 0x001, + )), + None, + )?; + ca_ip_addr_blocks.restrict(true, Some(234))?; + + let mut ca_as_blocks = botan::ASBlocks::new()?; + ca_as_blocks.add_asnum(30)?; + ca_as_blocks.add_asnum_range(3000, 4999)?; + ca_as_blocks.restrict_rdi()?; + + ca_builder.add_ext_ip_addr_blocks(&ca_ip_addr_blocks, true)?; + ca_builder.add_ext_as_blocks(&ca_as_blocks, true)?; + + assert!(ca_builder + .add_ext_ip_addr_blocks(&ca_ip_addr_blocks, true) + .is_err_and(|e| e.error_type() == botan::ErrorType::InvalidObjectState)); + assert!(ca_builder + .add_ext_as_blocks(&ca_as_blocks, true) + .is_err_and(|e| e.error_type() == botan::ErrorType::InvalidObjectState)); + + let ca_cert = ca_builder + .into_self_signed(&ca_key, &mut rng, not_before, not_after, None, None, None) + .unwrap(); + + let mut ca_ip_addr_blocks = ca_cert.ext_ip_addr_blocks()?; + let blocks: ( + Vec<(Option, Option>)>, + Vec<(Option, Option>)>, + ) = ca_ip_addr_blocks.addresses()?; + assert_eq!( + blocks.0, + vec![ + ( + None, + Some(vec![ + (Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 255, 255)), + (Ipv4Addr::new(192, 168, 2, 1), Ipv4Addr::new(192, 168, 2, 1)) + ]) + ), + (Some(42), Some(vec![])) + ] + ); + assert_eq!( + blocks.1, + vec![ + ( + None, + Some(vec![( + Ipv6Addr::new(0xab01, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000, 0x001), + Ipv6Addr::new(0xab01, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000, 0x001) + )]) + ), + (Some(234), Some(vec![])) + ] + ); + + assert!(ca_ip_addr_blocks + .add_addr(std::net::IpAddr::V4(Ipv4Addr::new(123, 0, 0, 1)), None) + .is_err_and(|e| e.error_type() == botan::ErrorType::InvalidObjectState)); + + let mut ca_as_blocks = ca_cert.ext_as_blocks()?; + assert_eq!(ca_as_blocks.asnum()?, Some(vec![(30, 30), (3000, 4999)])); + assert_eq!(ca_as_blocks.rdi()?, Some(vec![])); + + assert!(ca_as_blocks + .add_asnum(999) + .is_err_and(|e| e.error_type() == botan::ErrorType::InvalidObjectState)); + + let cert_key = botan::Privkey::create("ECDSA", group, &mut rng)?; + let mut req_builder = botan::CertificateBuilder::new()?; + + let mut req_ip_addr_blocks = botan::IpAddrBlocks::new()?; + req_ip_addr_blocks.add_addr(std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1)), None)?; + req_ip_addr_blocks.add_range( + std::net::IpAddr::V4(Ipv4Addr::new(10, 0, 5, 5)), + std::net::IpAddr::V4(Ipv4Addr::new(10, 0, 7, 7)), + None, + )?; + req_ip_addr_blocks.restrict(false, Some(42))?; + req_ip_addr_blocks.inherit(true, None)?; + req_ip_addr_blocks.inherit(true, Some(234))?; + + let mut req_as_blocks = botan::ASBlocks::new()?; + req_as_blocks.add_asnum_range(3100, 4000)?; + req_as_blocks.inherit_rdi()?; + + req_builder.add_ext_ip_addr_blocks(&req_ip_addr_blocks, true)?; + req_builder.add_ext_as_blocks(&req_as_blocks, true)?; + let req = req_builder.into_request(&cert_key, &mut rng, None, None, None)?; + + let cert = req.sign( + &ca_cert, &ca_key, &mut rng, not_before, not_after, None, None, None, + )?; + + let req_ip_addr_blocks = cert.ext_ip_addr_blocks()?; + let blocks = req_ip_addr_blocks.addresses()?; + assert_eq!( + blocks.0, + vec![ + ( + None, + Some(vec![ + (Ipv4Addr::new(10, 0, 5, 5), Ipv4Addr::new(10, 0, 7, 7)), + (Ipv4Addr::new(192, 168, 2, 1), Ipv4Addr::new(192, 168, 2, 1)) + ]) + ), + (Some(42), Some(vec![])) + ] + ); + assert_eq!(blocks.1, vec![(None, None), (Some(234), None)]); + + let req_as_blocks = cert.ext_as_blocks()?; + assert_eq!(req_as_blocks.asnum()?, Some(vec![(3100, 4000)])); + assert_eq!(req_as_blocks.rdi()?, None); + + let result = cert.verify(&[], &[&ca_cert], None, None, None)?; + assert!(result.success()); + assert_eq!(result.to_string(), "Verified"); + Ok(()) }