roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! Building Roughtime request messages.

use alloc::vec::Vec;

use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::tags::{TAG_NONC, TAG_PAD};

/// The minimum size (in bytes) of a Roughtime request, per the protocol spec.
const MIN_REQUEST_LEN: usize = 1024;

/// A client-generated 64-byte nonce.
///
/// Zeroized on drop: the nonce is the one value this client generates itself that carries any
/// unpredictability, so it is scrubbed as defense-in-depth even though the protocol does not
/// treat it as a secret in the classic sense (see the crate-level security notes).
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Nonce([u8; 64]);

impl core::fmt::Debug for Nonce {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Nonce").finish_non_exhaustive()
    }
}

impl Nonce {
    /// Wraps caller-supplied nonce bytes.
    ///
    /// This is the `no_std`-safe constructor: a bare-metal caller supplies its own entropy
    /// (e.g. from a hardware RNG) rather than relying on an OS random source.
    #[must_use]
    pub const fn new(bytes: [u8; 64]) -> Self {
        Self(bytes)
    }

    /// Returns the nonce's raw bytes.
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8; 64] {
        &self.0
    }

    /// Generates a nonce using the active crypto backend's random source.
    ///
    /// # Errors
    ///
    /// Returns [`crate::Error::RandomUnavailable`] if the OS random source is unavailable.
    #[cfg(feature = "std")]
    pub fn random() -> Result<Self, crate::Error> {
        let mut bytes = [0u8; 64];
        crate::crypto::fill_random(&mut bytes).map_err(|_err| crate::Error::RandomUnavailable)?;
        Ok(Self(bytes))
    }
}

/// Builds a Roughtime request message containing `nonce`, padded to the protocol minimum size.
#[must_use]
pub fn build_request(nonce: &Nonce) -> Vec<u8> {
    let mut req = Vec::with_capacity(MIN_REQUEST_LEN);
    req.extend_from_slice(&2u32.to_le_bytes()); // num_tags
    req.extend_from_slice(&64u32.to_le_bytes()); // offset of tag 1 (PAD)
    req.extend_from_slice(&TAG_NONC.to_le_bytes());
    req.extend_from_slice(&TAG_PAD.to_le_bytes());
    req.extend_from_slice(nonce.as_bytes());
    req.resize(MIN_REQUEST_LEN, 0);
    req
}

/// Generates a random nonce and builds a request containing it, returning both.
///
/// # Errors
///
/// Returns [`crate::Error::RandomUnavailable`] if the OS random source is unavailable.
#[cfg(feature = "std")]
pub fn build_request_random() -> Result<(Nonce, Vec<u8>), crate::Error> {
    let nonce = Nonce::random()?;
    let req = build_request(&nonce);
    Ok((nonce, req))
}

#[cfg(test)]
mod tests {
    use super::{Nonce, build_request};

    #[test]
    fn builds_padded_request() {
        let nonce = Nonce::new([0x42; 64]);
        let req = build_request(&nonce);
        assert_eq!(req.len(), 1024);
        assert_eq!(&req[16..80], nonce.as_bytes());
    }
}