use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::common::{PcStatus, guard, out_write, slice, wipe_array, wipe_vec};
use crate::ec::{BoxedEcdsaPrivateKey, Ed448PrivateKey, Ed25519PrivateKey};
use crate::rsa::BoxedRsaPrivateKey;
use crate::tls::{
ClientAuth, Config, ConfigBuilder, Connection, CrlStore, HandshakeStatus, ProtocolVersion,
RootCertStore, SigningKey,
};
#[repr(i32)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Role {
Client = 0,
Server = 1,
}
impl Role {
fn from_i32(v: i32) -> Option<Self> {
Some(match v {
0 => Role::Client,
1 => Role::Server,
_ => return None,
})
}
}
#[repr(i32)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Version {
Tls12 = 0x0303,
Tls13 = 0x0304,
Dtls12 = 0xFEFD_u16 as i32,
Dtls13 = 0xFEFC_u16 as i32,
}
impl Version {
fn from_i32(v: i32) -> Option<Self> {
Some(match v as u16 {
0x0303 => Version::Tls12,
0x0304 => Version::Tls13,
0xFEFD => Version::Dtls12,
0xFEFC => Version::Dtls13,
_ => return None,
})
}
fn to_protocol_version(self) -> ProtocolVersion {
match self {
Version::Tls12 => ProtocolVersion::TLSv1_2,
Version::Tls13 => ProtocolVersion::TLSv1_3,
Version::Dtls12 => ProtocolVersion::DTLSv1_2,
Version::Dtls13 => ProtocolVersion::DTLSv1_3,
}
}
}
pub struct PcTlsCfg {
role: Role,
version: Version,
roots_pem: Vec<String>,
crls_pem: Vec<String>,
client_auth_roots_pem: Vec<String>,
client_auth_required: bool,
server_name: Option<String>,
cert: Option<CertAndKey>,
alpn: Vec<Vec<u8>>,
verify_certs: bool,
cookie_secret: Option<[u8; 32]>,
no_cookie: bool,
}
impl Drop for PcTlsCfg {
fn drop(&mut self) {
if let Some(secret) = self.cookie_secret.as_mut() {
wipe_array(secret);
}
}
}
struct CertAndKey {
chain_der: Vec<Vec<u8>>,
key: PcKey,
}
#[allow(clippy::large_enum_variant)]
enum PcKey {
Rsa(BoxedRsaPrivateKey),
Ecdsa(BoxedEcdsaPrivateKey),
Ed25519(Ed25519PrivateKey),
Ed448(Ed448PrivateKey),
}
impl PcKey {
fn to_signing_key(&self) -> SigningKey {
match self {
PcKey::Rsa(k) => SigningKey::Rsa(k.clone()),
PcKey::Ecdsa(k) => SigningKey::Ecdsa(k.clone()),
PcKey::Ed25519(k) => SigningKey::Ed25519(k.clone()),
PcKey::Ed448(k) => SigningKey::Ed448(k.clone()),
}
}
}
impl PcTlsCfg {
fn new(role: Role, version: Version) -> Self {
PcTlsCfg {
role,
version,
roots_pem: Vec::new(),
crls_pem: Vec::new(),
client_auth_roots_pem: Vec::new(),
client_auth_required: false,
server_name: None,
cert: None,
alpn: Vec::new(),
verify_certs: true,
cookie_secret: None,
no_cookie: false,
}
}
fn build_roots(&self) -> RootCertStore {
let mut store = RootCertStore::new();
for pem in &self.roots_pem {
store
.add_pem(pem)
.expect("build_roots: pre-validated PEM failed to re-parse");
}
store
}
fn build_crls(&self) -> CrlStore {
let mut store = CrlStore::new();
for pem in &self.crls_pem {
store
.add_pem(pem)
.expect("build_crls: pre-validated PEM failed to re-parse");
}
store
}
fn build_client_auth_roots(&self) -> RootCertStore {
let mut store = RootCertStore::new();
for pem in &self.client_auth_roots_pem {
store
.add_pem(pem)
.expect("build_client_auth_roots: pre-validated PEM failed to re-parse");
}
store
}
fn build_config(&self) -> Option<Config> {
let mut b: ConfigBuilder = Config::builder()
.versions(
self.version.to_protocol_version(),
self.version.to_protocol_version(),
)
.verify_certificates(self.verify_certs)
.roots(self.build_roots());
if !self.alpn.is_empty() {
b = b.alpn(self.alpn.clone());
}
if !self.crls_pem.is_empty() {
b = b.crls(self.build_crls());
}
if let Some(sni) = &self.server_name {
b = b.server_name(sni.clone());
}
if let Some(secret) = self.cookie_secret {
b = b.cookie_secret(secret);
}
if self.no_cookie {
b = b.no_cookie();
}
if let Some(ck) = &self.cert {
b = b.identity(ck.chain_der.clone(), ck.key.to_signing_key());
}
if !self.client_auth_roots_pem.is_empty() {
b = b.client_auth(ClientAuth {
roots: self.build_client_auth_roots(),
required: self.client_auth_required,
});
}
Some(b.build())
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pc_tls_cfg_new(role: i32, version: i32) -> *mut PcTlsCfg {
crate::ffi::common::guard_ptr(|| {
let Some(r) = Role::from_i32(role) else {
return core::ptr::null_mut();
};
let Some(v) = Version::from_i32(version) else {
return core::ptr::null_mut();
};
Box::into_raw(Box::new(PcTlsCfg::new(r, v)))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_free(cfg: *mut PcTlsCfg) {
if !cfg.is_null() {
drop(unsafe { Box::from_raw(cfg) });
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_add_root_pem(
cfg: *mut PcTlsCfg,
pem: *const u8,
len: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
let Some(b) = (unsafe { slice(pem, len) }) else {
return PcStatus::NullPointer;
};
let Ok(s) = core::str::from_utf8(b) else {
return PcStatus::BadEncoding;
};
let mut tmp = RootCertStore::new();
if tmp.add_pem(s).is_err() {
return PcStatus::BadEncoding;
}
unsafe { &mut *cfg }.roots_pem.push(s.to_string());
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_set_server_name(
cfg: *mut PcTlsCfg,
sni: *const core::ffi::c_char,
) -> PcStatus {
guard(|| {
if cfg.is_null() || sni.is_null() {
return PcStatus::NullPointer;
}
let cs = unsafe { core::ffi::CStr::from_ptr(sni) };
let Ok(s) = cs.to_str() else {
return PcStatus::BadEncoding;
};
unsafe { &mut *cfg }.server_name = Some(s.to_string());
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_set_certificate(
cfg: *mut PcTlsCfg,
chain_pem: *const u8,
chain_len: usize,
key_pem: *const u8,
key_pem_len: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
let (Some(chain), Some(kp)) = (unsafe { slice(chain_pem, chain_len) }, unsafe {
slice(key_pem, key_pem_len)
}) else {
return PcStatus::NullPointer;
};
let chain_str = match core::str::from_utf8(chain) {
Ok(s) => s,
Err(_) => return PcStatus::BadEncoding,
};
let chain_der = match pem_split_cert_chain(chain_str) {
Ok(v) => v,
Err(e) => return e.to_status(),
};
let key_str = match core::str::from_utf8(kp) {
Ok(s) => s,
Err(_) => return PcStatus::BadEncoding,
};
let key = if let Ok(k) = BoxedRsaPrivateKey::from_pkcs1_pem(key_str) {
PcKey::Rsa(k)
} else if let Ok(k) = BoxedRsaPrivateKey::from_pkcs8_pem(key_str) {
PcKey::Rsa(k)
} else if let Ok(k) = BoxedEcdsaPrivateKey::from_sec1_pem(key_str) {
PcKey::Ecdsa(k)
} else if let Ok(k) = Ed25519PrivateKey::from_pkcs8_pem(key_str) {
PcKey::Ed25519(k)
} else if let Ok(k) = Ed448PrivateKey::from_pkcs8_pem(key_str) {
PcKey::Ed448(k)
} else {
return PcStatus::BadEncoding;
};
unsafe { &mut *cfg }.cert = Some(CertAndKey { chain_der, key });
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_set_alpn(
cfg: *mut PcTlsCfg,
protocols: *const *const core::ffi::c_char,
n: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
const PC_ALPN_MAX: usize = 256;
if n > PC_ALPN_MAX {
return PcStatus::Unsupported;
}
if n > 0 && protocols.is_null() {
return PcStatus::NullPointer;
}
let mut out: Vec<Vec<u8>> = Vec::with_capacity(n);
for i in 0..n {
let p = unsafe { *protocols.add(i) };
if p.is_null() {
return PcStatus::NullPointer;
}
let cs = unsafe { core::ffi::CStr::from_ptr(p) };
let bytes = cs.to_bytes();
if bytes.is_empty() || bytes.len() > 255 {
return PcStatus::Unsupported;
}
out.push(bytes.to_vec());
}
unsafe { &mut *cfg }.alpn = out;
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_set_verify_certificates(
cfg: *mut PcTlsCfg,
verify: i32,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
unsafe { &mut *cfg }.verify_certs = verify != 0;
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_set_client_auth(
cfg: *mut PcTlsCfg,
required: i32,
roots_pem: *const u8,
roots_pem_len: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
let Some(rp) = (unsafe { slice(roots_pem, roots_pem_len) }) else {
return PcStatus::NullPointer;
};
let Ok(s) = core::str::from_utf8(rp) else {
return PcStatus::BadEncoding;
};
let blocks = match pem_split(s, "CERTIFICATE") {
Ok(v) => v,
Err(e) => return e.to_status(),
};
let mut tmp = RootCertStore::new();
for cert in &blocks {
if tmp.add_pem(cert).is_err() {
return PcStatus::BadEncoding;
}
}
let cfg_ref = unsafe { &mut *cfg };
cfg_ref.client_auth_roots_pem = blocks;
cfg_ref.client_auth_required = required != 0;
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_cfg_add_crl_pem(
cfg: *mut PcTlsCfg,
pem: *const u8,
len: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
let Some(b) = (unsafe { slice(pem, len) }) else {
return PcStatus::NullPointer;
};
let Ok(s) = core::str::from_utf8(b) else {
return PcStatus::BadEncoding;
};
let mut tmp = CrlStore::new();
if tmp.add_pem(s).is_err() {
return PcStatus::BadEncoding;
}
unsafe { &mut *cfg }.crls_pem.push(s.to_string());
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_dtls_cfg_set_no_cookie(cfg: *mut PcTlsCfg) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
unsafe { &mut *cfg }.no_cookie = true;
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_dtls_cfg_set_cookie_secret(
cfg: *mut PcTlsCfg,
secret: *const u8,
secret_len: usize,
) -> PcStatus {
guard(|| {
if cfg.is_null() {
return PcStatus::NullPointer;
}
let Some(bytes) = (unsafe { slice(secret, secret_len) }) else {
return PcStatus::NullPointer;
};
let buf: [u8; 32] = match bytes.try_into() {
Ok(a) => a,
Err(_) => return PcStatus::Unsupported,
};
unsafe { &mut *cfg }.cookie_secret = Some(buf);
PcStatus::Ok
})
}
pub struct PcTls {
inner: Connection,
pending_pop: Option<Vec<u8>>,
pending_recv: Option<Vec<u8>>,
}
impl Drop for PcTls {
fn drop(&mut self) {
if let Some(buf) = self.pending_recv.as_mut() {
wipe_vec(buf);
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_new(cfg: *const PcTlsCfg) -> *mut PcTls {
crate::ffi::common::guard_ptr(|| {
if cfg.is_null() {
return core::ptr::null_mut();
}
let c = unsafe { &*cfg };
let config = match c.build_config() {
Some(cfg) => cfg,
None => return core::ptr::null_mut(),
};
let conn = match c.role {
Role::Client => Connection::client(&config),
Role::Server => Connection::server(&config),
};
let Ok(inner) = conn else {
return core::ptr::null_mut();
};
Box::into_raw(Box::new(PcTls {
inner,
pending_pop: None,
pending_recv: None,
}))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_free(tls: *mut PcTls) {
if !tls.is_null() {
drop(unsafe { Box::from_raw(tls) });
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_feed(
tls: *mut PcTls,
wire_in: *const u8,
in_len: usize,
consumed: *mut usize,
) -> PcStatus {
guard(|| {
let write_consumed = |n: usize| {
if !consumed.is_null() {
unsafe { *consumed = n };
}
};
write_consumed(0);
if tls.is_null() {
return PcStatus::NullPointer;
}
let Some(b) = (unsafe { slice(wire_in, in_len) }) else {
return PcStatus::NullPointer;
};
let conn = &mut unsafe { &mut *tls }.inner;
match conn.feed(b) {
Ok(n) => {
write_consumed(n);
PcStatus::Ok
}
Err(_) => {
write_consumed(in_len);
PcStatus::Internal
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_pop(
tls: *mut PcTls,
wire_out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let handle = unsafe { &mut *tls };
let bytes = match handle.pending_pop.take() {
Some(b) => b,
None => handle.inner.pop().unwrap_or_default(),
};
let st = unsafe { out_write(&bytes, wire_out, out_len) };
if st != PcStatus::Ok {
handle.pending_pop = Some(bytes);
}
st
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_send(
tls: *mut PcTls,
app_in: *const u8,
in_len: usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let Some(b) = (unsafe { slice(app_in, in_len) }) else {
return PcStatus::NullPointer;
};
let conn = &mut unsafe { &mut *tls }.inner;
if !conn.is_handshake_complete() {
return PcStatus::WantHandshake;
}
match conn.send(b) {
Ok(()) => PcStatus::Ok,
Err(_) => PcStatus::Internal,
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_recv(
tls: *mut PcTls,
app_out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let handle = unsafe { &mut *tls };
let mut bytes = match handle.pending_recv.take() {
Some(b) => b,
None => handle.inner.recv().unwrap_or_default(),
};
let st = unsafe { out_write(&bytes, app_out, out_len) };
if st == PcStatus::Ok {
wipe_vec(&mut bytes);
} else {
handle.pending_recv = Some(bytes);
}
st
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_handshake(tls: *mut PcTls) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let conn = &mut unsafe { &mut *tls }.inner;
match conn.handshake() {
Ok(HandshakeStatus::Complete) => PcStatus::Ok,
Ok(HandshakeStatus::WantWrite) => PcStatus::WantWrite,
Ok(HandshakeStatus::WantRead) => PcStatus::WantRead,
Err(_) => PcStatus::Internal,
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_is_handshake_complete(tls: *const PcTls) -> i32 {
crate::ffi::common::guard_i32(0, || {
if tls.is_null() {
return 0;
}
if unsafe { &*tls }.inner.is_handshake_complete() {
1
} else {
0
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_negotiated_version(tls: *const PcTls, out: *mut u16) -> PcStatus {
guard(|| {
if tls.is_null() || out.is_null() {
return PcStatus::NullPointer;
}
let v = unsafe { &*tls }
.inner
.negotiated_version()
.map(|p| p.as_u16())
.unwrap_or(0);
unsafe { *out = v };
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_negotiated_cipher_suite(
tls: *const PcTls,
out: *mut u16,
) -> PcStatus {
guard(|| {
if tls.is_null() || out.is_null() {
return PcStatus::NullPointer;
}
let v = unsafe { &*tls }
.inner
.negotiated_cipher_suite()
.unwrap_or(0);
unsafe { *out = v };
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_negotiated_cipher_suite_name(
tls: *const PcTls,
out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let name: &[u8] = unsafe { &*tls }
.inner
.negotiated_cipher_suite_name()
.map(str::as_bytes)
.unwrap_or(&[]);
unsafe { out_write(name, out, out_len) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_peer_server_name(
tls: *const PcTls,
out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let name: &[u8] = unsafe { &*tls }
.inner
.peer_server_name()
.map(str::as_bytes)
.unwrap_or(&[]);
unsafe { out_write(name, out, out_len) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_alpn_selected(
tls: *const PcTls,
out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let alpn: &[u8] = unsafe { &*tls }.inner.alpn_selected().unwrap_or(&[]);
unsafe { out_write(alpn, out, out_len) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_peer_certificate(
tls: *const PcTls,
out: *mut u8,
out_len: *mut usize,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let chain: &[Vec<u8>] = unsafe { &*tls }.inner.peer_certificates();
let Some(leaf) = chain.first() else {
return PcStatus::BadEncoding;
};
unsafe { out_write(leaf, out, out_len) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_tls_close(tls: *mut PcTls) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let conn = &mut unsafe { &mut *tls }.inner;
let _ = conn.close();
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_dtls_next_timeout(
tls: *const PcTls,
seconds_out: *mut u64,
nanos_out: *mut u32,
has_timeout: *mut i32,
) -> PcStatus {
guard(|| {
if tls.is_null() || seconds_out.is_null() || nanos_out.is_null() || has_timeout.is_null() {
return PcStatus::NullPointer;
}
let conn = unsafe { &*tls };
let v = conn.inner.negotiated_version();
if !matches!(
v,
Some(ProtocolVersion::DTLSv1_2) | Some(ProtocolVersion::DTLSv1_3)
) {
return PcStatus::Unsupported;
}
let dur = conn.inner.next_timeout();
match dur {
Some(d) => unsafe {
*seconds_out = d.as_secs();
*nanos_out = d.subsec_nanos();
*has_timeout = 1;
},
None => unsafe {
*seconds_out = 0;
*nanos_out = 0;
*has_timeout = 0;
},
}
PcStatus::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_dtls_on_timeout(
tls: *mut PcTls,
now_seconds: u64,
now_nanos: u32,
) -> PcStatus {
guard(|| {
if tls.is_null() {
return PcStatus::NullPointer;
}
let conn = unsafe { &mut *tls };
let v = conn.inner.negotiated_version();
if !matches!(
v,
Some(ProtocolVersion::DTLSv1_2) | Some(ProtocolVersion::DTLSv1_3)
) {
return PcStatus::Unsupported;
}
let dur = core::time::Duration::new(now_seconds, now_nanos);
conn.inner.on_timeout(dur);
PcStatus::Ok
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PemSplitError {
NoBlocks,
Malformed,
}
impl PemSplitError {
pub(crate) fn to_status(self) -> PcStatus {
match self {
PemSplitError::NoBlocks | PemSplitError::Malformed => PcStatus::BadEncoding,
}
}
}
fn pem_split(pem: &str, label: &str) -> Result<Vec<String>, PemSplitError> {
let begin = alloc::format!("-----BEGIN {label}-----");
let end = alloc::format!("-----END {label}-----");
let mut out = Vec::new();
let mut rest = pem;
let mut saw_begin = false;
while let Some(b_off) = rest.find(&begin) {
saw_begin = true;
let after_b = b_off;
if let Some(e_off) = rest[after_b..].find(&end) {
let abs_end = after_b + e_off + end.len();
out.push(rest[after_b..abs_end].to_string());
rest = &rest[abs_end..];
} else {
return Err(PemSplitError::Malformed);
}
}
if out.is_empty() {
if saw_begin {
return Err(PemSplitError::Malformed);
}
return Err(PemSplitError::NoBlocks);
}
Ok(out)
}
fn pem_split_cert_chain(pem: &str) -> Result<Vec<Vec<u8>>, PemSplitError> {
let blocks = pem_split(pem, "CERTIFICATE")?;
let mut chain = Vec::with_capacity(blocks.len());
for block in blocks {
let der =
crate::der::pem_decode(&block, "CERTIFICATE").map_err(|_| PemSplitError::Malformed)?;
chain.push(der);
}
if chain.is_empty() {
return Err(PemSplitError::NoBlocks);
}
Ok(chain)
}
#[cfg(test)]
mod tls_ffi_tests {
use super::*;
use crate::ffi::common::PcStatus;
#[test]
fn pem_split_no_blocks_returns_no_blocks() {
let err = pem_split("no PEM here, just chatter", "CERTIFICATE").unwrap_err();
assert_eq!(err, PemSplitError::NoBlocks);
assert_eq!(err.to_status(), PcStatus::BadEncoding);
}
#[test]
fn pem_split_empty_input_returns_no_blocks() {
let err = pem_split("", "CERTIFICATE").unwrap_err();
assert_eq!(err, PemSplitError::NoBlocks);
}
#[test]
fn pem_split_begin_without_end_returns_malformed() {
let bad = "-----BEGIN CERTIFICATE-----\nAAAA\n(missing END)\n";
let err = pem_split(bad, "CERTIFICATE").unwrap_err();
assert_eq!(err, PemSplitError::Malformed);
assert_eq!(err.to_status(), PcStatus::BadEncoding);
}
#[test]
fn pem_split_label_mismatch_treated_as_no_blocks() {
let p = "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----\n";
let err = pem_split(p, "CERTIFICATE").unwrap_err();
assert_eq!(err, PemSplitError::NoBlocks);
}
#[test]
fn pem_split_two_valid_blocks_returns_both() {
let two = "\
-----BEGIN CERTIFICATE-----
AAAA
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
BBBB
-----END CERTIFICATE-----
";
let blocks = pem_split(two, "CERTIFICATE").unwrap();
assert_eq!(blocks.len(), 2);
assert!(blocks[0].contains("AAAA"));
assert!(blocks[1].contains("BBBB"));
}
#[test]
fn pem_split_cert_chain_undecodable_block_is_malformed() {
let bad = "\
-----BEGIN CERTIFICATE-----
!!! not base64 !!!
-----END CERTIFICATE-----
";
let err = pem_split_cert_chain(bad).unwrap_err();
assert_eq!(err, PemSplitError::Malformed);
}
#[test]
fn pem_split_cert_chain_no_blocks_is_no_blocks() {
let err = pem_split_cert_chain("not pem at all").unwrap_err();
assert_eq!(err, PemSplitError::NoBlocks);
}
fn client_tls() -> *mut PcTls {
let cfg = pc_tls_cfg_new(Role::Client as i32, Version::Tls13 as i32);
assert!(!cfg.is_null());
let st = unsafe { pc_tls_cfg_set_verify_certificates(cfg, 0) };
assert_eq!(st, PcStatus::Ok);
let sni = b"loopback.example\0";
let st =
unsafe { pc_tls_cfg_set_server_name(cfg, sni.as_ptr() as *const core::ffi::c_char) };
assert_eq!(st, PcStatus::Ok);
let tls = unsafe { pc_tls_new(cfg) };
unsafe { pc_tls_cfg_free(cfg) };
assert!(!tls.is_null());
tls
}
#[test]
fn feed_empty_reports_zero_consumed() {
let tls = client_tls();
let mut consumed: usize = 0xdead_beef;
let st = unsafe { pc_tls_feed(tls, core::ptr::null(), 0, &mut consumed) };
assert_eq!(st, PcStatus::Ok);
assert_eq!(consumed, 0);
unsafe { pc_tls_free(tls) };
}
#[test]
fn feed_writes_consumed_on_error_path() {
let tls = client_tls();
let bad: [u8; 12] = [0x17, 0x03, 0x03, 0x00, 0xff, 0xee, 0xdd, 0, 0, 0, 0, 0];
let mut consumed: usize = 0xdead_beef;
let st = unsafe { pc_tls_feed(tls, bad.as_ptr(), bad.len(), &mut consumed) };
assert_ne!(
consumed, 0xdead_beef,
"pc_tls_feed must write *consumed before returning",
);
if st != PcStatus::Ok {
assert_eq!(
consumed,
bad.len(),
"engine buffered all bytes before erroring",
);
}
unsafe { pc_tls_free(tls) };
}
#[test]
fn feed_with_null_consumed_does_not_crash() {
let tls = client_tls();
let bytes = [0u8; 4];
let st = unsafe { pc_tls_feed(tls, bytes.as_ptr(), bytes.len(), core::ptr::null_mut()) };
let _ = st;
unsafe { pc_tls_free(tls) };
}
#[test]
fn feed_null_handle_reports_zero_consumed() {
let mut consumed: usize = 99;
let st = unsafe { pc_tls_feed(core::ptr::null_mut(), core::ptr::null(), 0, &mut consumed) };
assert_eq!(st, PcStatus::NullPointer);
assert_eq!(consumed, 0);
}
}