roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! The crate's structured error type.

use core::fmt;

/// Errors produced by this crate.
///
/// Deliberately structured (rather than string-based) so error paths never need to allocate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// The message was too short to contain a valid header.
    MessageTooShort,
    /// The message declared zero tags, which is not a valid Roughtime message.
    NoTags,
    /// A tag offset was out of bounds, decreasing, or not a multiple of 4.
    InvalidOffsets,
    /// A required tag was missing from a parsed message.
    MissingTag(u32),
    /// A tag's value had the wrong size for its expected type.
    InvalidFieldSize(u32),
    /// A base64-encoded public key failed to decode.
    InvalidPublicKeyEncoding,
    /// An Ed25519 signature failed to verify.
    SignatureInvalid,
    /// The delegated key's validity interval (`MINT`..=`MAXT`) does not contain the response's
    /// reported midpoint.
    DelegationNotValidAtMidpoint,
    /// The Merkle inclusion proof's `PATH` length was not a multiple of 32 bytes.
    InvalidMerklePathLength,
    /// The computed Merkle root did not match the server's reported `ROOT`.
    MerkleRootMismatch,
    /// The Merkle index was not fully consumed by the proof (index too large for the path depth).
    MerkleIndexOutOfBounds,
    /// A verified time was earlier than the crate's anti-rollback floor.
    BeforeFloor {
        /// The rejected candidate time, in Unix seconds.
        candidate_secs: u64,
        /// The effective anti-rollback floor, in Unix seconds.
        floor_secs: u64,
    },
    /// No valid responses were received from any queried server.
    NoValidResponses,
    /// The system clock could not be read or set.
    ClockUnavailable(crate::clock::ClockError),
    /// A cryptographically secure random-number source was unavailable.
    RandomUnavailable,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MessageTooShort => f.write_str("Roughtime message too short"),
            Self::NoTags => f.write_str("Roughtime message declares zero tags"),
            Self::InvalidOffsets => f.write_str("Roughtime message has invalid tag offsets"),
            Self::MissingTag(tag) => write!(f, "missing required tag {tag:#010x}"),
            Self::InvalidFieldSize(tag) => write!(f, "invalid field size for tag {tag:#010x}"),
            Self::InvalidPublicKeyEncoding => f.write_str("invalid base64 public key encoding"),
            Self::SignatureInvalid => f.write_str("Ed25519 signature verification failed"),
            Self::DelegationNotValidAtMidpoint => {
                f.write_str("delegated key is not valid at the reported midpoint")
            }
            Self::InvalidMerklePathLength => {
                f.write_str("Merkle path length is not a multiple of 32 bytes")
            }
            Self::MerkleRootMismatch => f.write_str("computed Merkle root does not match ROOT"),
            Self::MerkleIndexOutOfBounds => {
                f.write_str("Merkle index out of bounds for the path depth")
            }
            Self::BeforeFloor {
                candidate_secs,
                floor_secs,
            } => write!(
                f,
                "candidate time {candidate_secs}s is before the anti-rollback floor {floor_secs}s"
            ),
            Self::NoValidResponses => {
                f.write_str("no valid Roughtime responses received from any server")
            }
            Self::ClockUnavailable(clock_err) => {
                write!(f, "system clock unavailable: {clock_err:?}")
            }
            Self::RandomUnavailable => {
                f.write_str("cryptographically secure random source unavailable")
            }
        }
    }
}

impl core::error::Error for Error {}

#[cfg(test)]
mod tests {
    use super::Error;
    use crate::clock::ClockError;
    use alloc::string::ToString as _;

    #[test]
    fn display_covers_every_variant() {
        let cases: &[(Error, &str)] = &[
            (Error::MessageTooShort, "Roughtime message too short"),
            (Error::NoTags, "Roughtime message declares zero tags"),
            (
                Error::InvalidOffsets,
                "Roughtime message has invalid tag offsets",
            ),
            (
                Error::MissingTag(0x1234_5678),
                "missing required tag 0x12345678",
            ),
            (
                Error::InvalidFieldSize(0x1234_5678),
                "invalid field size for tag 0x12345678",
            ),
            (
                Error::InvalidPublicKeyEncoding,
                "invalid base64 public key encoding",
            ),
            (
                Error::SignatureInvalid,
                "Ed25519 signature verification failed",
            ),
            (
                Error::DelegationNotValidAtMidpoint,
                "delegated key is not valid at the reported midpoint",
            ),
            (
                Error::InvalidMerklePathLength,
                "Merkle path length is not a multiple of 32 bytes",
            ),
            (
                Error::MerkleRootMismatch,
                "computed Merkle root does not match ROOT",
            ),
            (
                Error::MerkleIndexOutOfBounds,
                "Merkle index out of bounds for the path depth",
            ),
            (
                Error::BeforeFloor {
                    candidate_secs: 1,
                    floor_secs: 2,
                },
                "candidate time 1s is before the anti-rollback floor 2s",
            ),
            (
                Error::NoValidResponses,
                "no valid Roughtime responses received from any server",
            ),
            (
                Error::RandomUnavailable,
                "cryptographically secure random source unavailable",
            ),
        ];

        for (err, expected) in cases {
            assert_eq!(&err.to_string(), expected);
        }

        let clock_err = Error::ClockUnavailable(ClockError::Unsupported);
        assert!(clock_err.to_string().contains("system clock unavailable"));
    }

    #[test]
    fn errors_are_comparable_and_cloneable() {
        let a = Error::MessageTooShort;
        let b = a;
        assert_eq!(a, b);
    }
}