Roughenough

repository·master·Indexed 40 days ago

https://github.com/int08h/roughenough

A full implementation of the IETF Roughtime secure time synchronization protocol, featuring high-performance UDP servers and command-line clients for cryptographically verifiable time. Built in Rust, it includes support for various key protection backends such as Linux Kernel Keyring, SSH agent, PKCS#11, and cloud KMS/Secret Manager services from AWS and GCP.

Tokens
43K
Snippets
139
Records
192
Agent score
100%

What's inside Roughenough

  1. Roughtime Message Format (TLV)

    master

    Messages are encoded using a Tag-Length-Value (TLV) structure. All integers are little-endian.

    Header Structure

    • Count: A uint32 representing the number of pairs ($N$).
    • Offsets: An array of $N-1$ uint32 values.
    • Tags: An array of $N$ uint32 values.

    Encoding Rules

    • Tags: Must be sorted numerically.
    • Offsets: Must be multiples of 4 and must be increasing.
    • Values: The values section follows the header at the specified offsets.
  2. How the Roughtime Merkle Tree works

    master

    Roughtime uses a Merkle tree to provide proofs of server malfeasance. The tree allows the client to verify that a specific response was part of a specific batch of requests.

    Tree Construction

    • Leaf nodes: H(0x00 || full_request_packet)
    • Internal nodes: H(0x01 || left_child || right_child)
    • Hash Function (H): SHA-512 truncated to the first 32 bytes (SHA-512[0:32]).

    Verification

    • The PATH tag in the response contains the sibling hashes from the leaf up to the root.
    • The ROOT tag in the SREP contains the 32-byte Merkle tree root.
    • The maximum tree height should ensure that the PATH contains $\le 32$ hashes.
  3. Verify the Roughtime Trust Chain

    master

    The security of a Roughtime exchange relies on a hierarchical trust chain that the client verifies to ensure the time measurement is authentic and hasn't been tampered with:

    1. Long-term key signs the DELE (which contains the online key and the MINT/MAXT time bounds).
    2. DELE.PUBK (the online key) signs the SREP (which contains the ROOT, MIDP, and RADI).
    3. The ROOT commits to a Merkle tree that contains the client's specific request (identified by its nonce).
  4. Roughtime Wire Format and Transport

    master

    Roughtime uses a packet-based format over UDP. The protocol is designed to provide authenticated timestamps with proof of server malfeasance using Ed25519 signatures and SHA-512 (first 32 bytes) for hashing.

    Packet Structure

    • Magic: 8-byte constant 0x524f55474854494d ("ROUGHTIM" in ASCII).
    • Length: 4-byte little-endian integer representing the message body length.
    • Body: The TLV-encoded message.

    Transport Constraints

    • Transport: This implementation is UDP-only. It does not support TCP.
    • Request Size: Requests must be at least 1024 bytes. This implementation accepts up to 1472 bytes (full Ethernet-MTU UDP payload). If a request is shorter than 1024 bytes, it must be padded using the ZZZZ tag containing zeros.
    • Response Size: To prevent amplification attacks, responses must not exceed the size of the request.
  5. Protect the long-term seed during runtime

    master

    Because the long-term identity is required during runtime to sign new OnlineKeys, the seed must be accessible without introducing a remote service dependency into the request-serving path. Roughenough solves this by keeping the seed outside the server's address space using one of two backends:

    • Mechanism: Retrieves the seed from kernel memory, uses it to sign the new OnlineKey, and immediately zeroizes the memory.
    • Security Profile: The seed is only present in the server's address space for a very brief window (milliseconds) before being overwritten.
    • Best Use Case: Recommended for Linux environments where simplicity is preferred ("it just works").

    ssh-agent Backend

    • Mechanism: Performs an IPC call over a Unix domain socket to a local ssh-agent process. The ssh-agent performs the signing operation.
    • Security Profile: The seed contents are never present in the server's address space after the initial startup (when the seed is loaded into the agent).
    • Best Use Case: Provides higher security by ensuring the seed never enters the server process memory, but requires managing an ssh-agent process.
  6. Understand the Roughtime Request/Response Flow

    master

    A single Roughtime exchange follows a specific sequence of events to ensure time authenticity and integrity.

    Server-Side Processing

    1. Batching: The server uses worker threads with SO_REUSEPORT to load-balance UDP datagrams. To optimize performance, the server processes requests in batches. It drains up to batch_size requests, commits them to a single Merkle tree, and then generates responses.
    2. Response Generation: For each batch, the server computes a Merkle root. It lazily creates a response template (SREP) for each distinct protocol version found in the batch. The SREP includes {VER, RADI, MIDP, VERS, ROOT} and is signed.
    3. Individual Responses: Each response in a batch is unique only in its PATH, NONC (nonce), and INDX (index). The server provides the Merkle path (siblings) for each request so the client can verify the root.

    Client-Side Verification

    The client performs several checks to validate the response:

    1. DELE Signature: Verifies the long-term key signed the DELE (which contains the online key and the MINT/MAXT time bounds).
    2. Midpoint Check: Ensures the received midpoint (MIDP) is within the bounds MINT <= MIDP <= MAXT.
    3. Merkle Proof: Reconstructs the root using the provided PATH, the client's original request, and the INDX to ensure root_from_paths(INDX, my_request, PATH) == ROOT.
    4. SREP Signature: Verifies that the DELE.PUBK (online key) signed the SREP.

    Upon successful validation, the client obtains an authenticated MIDP paired with RADI (the measurement).

  7. Run the Roughenough Server

    master

    The server provides cryptographically verifiable time synchronization via UDP (default port 2003).

    Seed File Requirements:

    • The server requires a long-term identity seed file.
    • On Unix-like systems, the file must be a regular file (no symlinks) with 0400 or 0600 permissions.
    • The seed file can be:
      • 32 bytes of binary random data
      • A base64 or hex encoded value (prefixed with seed://)
      • An AWS or GCP KMS envelope (aws-kms:// or gcp-kms://)
      • An AWS or GCP Secret Manager reference (aws-secret:// or gcp-secret://)

    Configuration: Pass the seed path using the --seed-file flag or the ROUGHENOUGH_SEED_FILE environment variable.

    # Generate a new mode-0600 raw seed file
    (umask 077 && openssl rand -hex 32 > roughenough.seed)
    
    # Release build with a real seed file
    cargo run --release --bin roughenough_server -- --seed-file roughenough.seed
    
    # Run the server binary directly
    target/release/roughenough_server --seed-file /run/secrets/roughenough.seed
    
    # Debug build (testing only, uses an all-zero seed)
    cargo run --bin roughenough_server -- --insecure-zero-seed
  8. Use the Roughenough Client

    master

    The command-line client allows you to query Roughtime servers and verify time synchronization. You can query a single server, multiple servers from a JSON list, or specify different output formats.

    # Query a Roughtime server
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003
    
    # Verify server public key
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003 -k <base64-or-hex-key>
    
    # Perform multiple requests
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003 -n 10
    
    # Use verbose output
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003 -v
    
    # Output time in Unix timestamp (epoch) format
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003 --epoch
    
    # Output time in ISO 8601 UTC (zulu) format
    cargo run --bin roughenough_client -- roughtime.int08h.com 2003 --zulu
    
    # Query multiple servers from an RFC compliant JSON list
    cargo run --bin roughenough_client -- -l servers.json
  9. Implement the Roughtime Client Protocol

    master

    To implement a Roughtime client, follow these steps:

    1. Send Request: Generate a random 32-byte nonce and send a request packet.
    2. Verify Response:
      • Verify the Ed25519 signatures (using the context "RoughTime v1 response signature\0").
      • Verify the Merkle path against the provided ROOT.
    3. Chaining (Optional): To ensure causality between requests, calculate the next nonce as: next_nonce = H(response_packet || 32_random_bytes).
    4. Check Causality: If performing multiple requests, ensure that for $i < j$: MIDP[i] - RADI[i] <= MIDP[j] + RADI[j] (where MIDP is the timestamp and RADI is the accuracy radius).
  10. Install Roughenough

    master

    Build the Roughenough components using Cargo. You can build the standard release or include all optional features (such as advanced key protection backends and client reporting).

    System Requirements:

    • MSRV 1.88, Rust 2024 edition
    • Linux, MacOS, or other Unix-like operating system
    • (Optional) Cloud provider credentials if using KMS/Secret Manager backends.
    # Build all components
    cargo build --release
    
    # Build with all optional features
    cargo build --release --all-features
  11. Set up the fuzzing environment

    master

    The fuzzing infrastructure for the Roughtime protocol requires a nightly Rust toolchain and cargo-fuzz because it utilizes libFuzzer.

    To prepare your environment, ensure you have nightly Rust installed and then run the following command to install cargo-fuzz.

    cargo +nightly install cargo-fuzz
  12. Run Roughtime fuzz tests

    master

    Fuzz tests should be executed from the root of the project. The following targets test different parts of the protocol implementation.

    Note that -max_len flags are used to constrain input sizes to realistic Roughtime protocol bounds (e.g., 1280 for requests, 800 for responses) to prevent the fuzzer from wasting time on inputs that would be rejected in real-world usage.

    # Fuzz request parsing
    cargo +nightly fuzz run fuzz_request_parse -- -max_len=1280
    
    # Fuzz response parsing
    cargo +nightly fuzz run fuzz_response_parse -- -max_len=800
    
    # Structure-aware fuzzing of complex message types
    cargo +nightly fuzz run fuzz_structured -- -max_len=1024