Skip to main content

toolkit_security/
bin_codec.rs

1use crate::SecurityContext;
2use postcard::Error as PostcardError;
3use thiserror::Error;
4
5/// Format version, written as the first byte of every encoded blob.
6///
7/// [`decode_bin`] accepts this version and no other, so producer and consumer
8/// must agree exactly — there is no backward-compatible read path.
9///
10/// **Bump this whenever [`SecurityContext`]'s serialized field list changes.**
11/// postcard is positional and carries no field names, so adding, removing or
12/// reordering a field silently changes the layout: an older decoder would then
13/// read the new bytes as whatever its own field order says, rather than
14/// rejecting them. The version byte is the only thing that turns that into a
15/// clean `UnsupportedVersion` error, and nothing derives it automatically.
16pub const SECCTX_BIN_VERSION: u8 = 1;
17
18/// Why a [`SecurityContext`] could not be encoded.
19#[derive(Debug, Error)]
20pub enum SecCtxEncodeError {
21    /// postcard could not serialize the context.
22    #[error("security context serialization failed: {0:?}")]
23    Postcard(#[from] PostcardError),
24}
25
26/// Why a blob could not be decoded into a [`SecurityContext`].
27#[derive(Debug, Error)]
28pub enum SecCtxDecodeError {
29    /// The blob carried no bytes at all, so not even a version byte.
30    #[error("empty secctx blob")]
31    Empty,
32
33    /// The leading version byte is not [`SECCTX_BIN_VERSION`]; the payload is
34    /// left unread rather than guessed at.
35    #[error("unsupported secctx version: {0}")]
36    UnsupportedVersion(u8),
37
38    /// The version matched but the payload did not deserialize.
39    #[error("security context deserialization failed: {0:?}")]
40    Postcard(#[from] PostcardError),
41}
42
43/// Encode `SecurityContext` into a versioned binary blob using `postcard`.
44/// This does not do any signing or encryption, it is just a transport format.
45///
46/// # Errors
47/// Returns `SecCtxEncodeError` if postcard serialization fails.
48pub fn encode_bin(ctx: &SecurityContext) -> Result<Vec<u8>, SecCtxEncodeError> {
49    let mut buf = Vec::with_capacity(64);
50    buf.push(SECCTX_BIN_VERSION);
51
52    let payload = postcard::to_allocvec(ctx)?;
53    buf.extend_from_slice(&payload);
54
55    Ok(buf)
56}
57
58/// Decode `SecurityContext` from a versioned binary blob produced by `encode_bin()`.
59///
60/// # This does not authenticate anything
61///
62/// The blob is neither signed nor encrypted, and the only check here is the
63/// version byte. Whoever produced these bytes chose the subject, the tenant and
64/// the scopes in the context that comes back — so calling this on input a peer
65/// supplied is letting that peer pick its own identity.
66///
67/// The precondition is that the peer was **already authenticated** and the
68/// transport is trusted: in-process, or a link where the sender was validated
69/// by other means. Never call it on inbound metadata from an unauthenticated
70/// caller, and strip `x-secctx-bin` at any boundary where callers are not
71/// already authenticated — a header from outside must never reach this
72/// function. ADR `cpt-cf-adr-two-plane-auth` keeps cross-process calls off this
73/// path entirely: they carry a re-validated bearer token instead.
74///
75/// # Errors
76/// Returns `SecCtxDecodeError::Empty` if the input is empty.
77/// Returns `SecCtxDecodeError::UnsupportedVersion` if the version byte is not supported.
78/// Returns `SecCtxDecodeError::Postcard` if postcard deserialization fails.
79pub fn decode_bin(bytes: &[u8]) -> Result<SecurityContext, SecCtxDecodeError> {
80    if bytes.is_empty() {
81        return Err(SecCtxDecodeError::Empty);
82    }
83
84    let version = bytes[0];
85    if version != SECCTX_BIN_VERSION {
86        return Err(SecCtxDecodeError::UnsupportedVersion(version));
87    }
88
89    let payload = &bytes[1..];
90
91    let ctx: SecurityContext = postcard::from_bytes(payload)?;
92
93    Ok(ctx)
94}