clawft-kernel 0.8.3

WeftOS kernel: boot sequence, process table, service registry, and health subsystem
Documentation
//! Steward to `weft-licence` request signing.

use super::request::*;
use super::tests_common::*;
use super::*;

const NODE: &str = "node-steward";
const AUD: &str = "seed-test";
/// A time past the clock floor, in ms.
pub(super) const NOW_MS: u64 = 1_790_000_000_000;
const NONCE: &str = "abcdefghijklmnop0123456789ABCDEF";

fn steward() -> ed25519_dalek::SigningKey {
    sk(21)
}

fn pk() -> [u8; 32] {
    steward().verifying_key().to_bytes()
}

fn signed(path: &str, body: &[u8], ts: u64) -> LicenceRequest {
    sign_request(&steward(), NODE, AUD, "POST", path, body.to_vec(), ts, NONCE)
}

fn verify(r: &LicenceRequest, now: u64, g: &mut ReplayGuard) -> Result<(), RequestRefused> {
    verify_request(r, &pk(), NODE, AUD, now, g)
}

/// A kernel request as `weft-licence`'s parsed HTTP request (headers as sent).
fn as_seed_request(r: &LicenceRequest) -> weft_licence::request::Request {
    let a = r.auth.as_ref().unwrap();
    weft_licence::request::Request {
        method: r.method.clone(),
        target: r.path.clone(),
        headers: [
            ("x-licence-node", a.node.clone()),
            ("x-licence-ts", a.timestamp_ms.to_string()),
            ("x-licence-nonce", a.nonce.clone()),
            ("x-licence-sig", a.signature.clone()),
        ]
        .into_iter()
        .map(|(k, v)| (k.to_string(), v))
        .collect(),
        body: r.body.clone(),
    }
}

#[test]
fn what_the_kernel_client_signs_verifies_under_weft_licences_verifier() {
    // Both sides use the one builder in weft-licence-wire; this proves the
    // pieces fit end to end, including the audience.
    let r = sign_request(&steward(), NODE, AUD, "POST", "/licence/v1/checkout?x=1", b"{\"a\":1}".to_vec(), NOW_MS, NONCE);
    let seed = as_seed_request(&r);
    let ok = weft_licence::request::verify(&seed, &pk(), NODE, AUD, NOW_MS, REQUEST_WINDOW_MS);
    assert!(ok.is_ok(), "{ok:?}");
    // Another Seed (audience), another node, a tampered body and a stale clock all fail there.
    use weft_licence::request::AuthError;
    let v = |req: &weft_licence::request::Request, aud: &str, node: &str, now| {
        weft_licence::request::verify(req, &pk(), node, aud, now, REQUEST_WINDOW_MS).map(|_| ())
    };
    assert_eq!(v(&seed, "another-seed", NODE, NOW_MS), Err(AuthError::BadSignature));
    assert_eq!(v(&seed, AUD, "another-node", NOW_MS), Err(AuthError::WrongNode));
    assert_eq!(v(&seed, AUD, NODE, NOW_MS + REQUEST_WINDOW_MS + 1), Err(AuthError::Stale));
    let mut tampered = seed.clone();
    tampered.body = b"x".to_vec();
    assert_eq!(v(&tampered, AUD, NODE, NOW_MS), Err(AuthError::BadSignature));
}

#[test]
fn what_weft_licence_signs_verifies_under_the_kernel_verifier() {
    let hdr = weft_licence::request::sign_request(&steward(), NODE, AUD, "GET", "/licence/v1/grants?since=0", b"", NOW_MS, NONCE);
    let req = LicenceRequest {
        method: "GET".into(),
        path: "/licence/v1/grants?since=0".into(),
        auth: Some(RequestAuth {
            node: hdr["x-licence-node"].clone(),
            timestamp_ms: hdr["x-licence-ts"].parse().unwrap(),
            nonce: hdr["x-licence-nonce"].clone(),
            signature: hdr["x-licence-sig"].clone(),
        }),
        body: vec![],
    };
    assert_eq!(verify(&req, NOW_MS, &mut ReplayGuard::default()), Ok(()));
}

#[test]
fn the_shared_constants_are_the_bridges() {
    assert_eq!(REQUEST_DOMAIN, "weft-licence-v1/request");
    assert_eq!(REQUEST_WINDOW_MS, 120_000);
    assert_eq!(CLOCK_FLOOR_SECS, 1_780_000_000);
    assert_eq!(weft_licence::CLOCK_FLOOR, CLOCK_FLOOR_SECS, "one definition");
    let s = signing_string("POST", "/p", "n", "seed-1", 1_791_000_000_123, "abcdef0123456789", b"x");
    let lines: Vec<&str> = s.lines().collect();
    assert_eq!(
        &lines[..7],
        ["weft-licence-v1/request", "POST", "/p", "n", "seed-1", "1791000000123", "abcdef0123456789"]
    );
    assert_eq!(lines[7], sha256_hex(b"x"));
}

#[test]
fn nonces_are_16_to_64_alphanumerics() {
    assert!(valid_nonce(&"a".repeat(16)) && valid_nonce(&"Z9".repeat(32)));
    assert!(!valid_nonce(&"a".repeat(15)) && !valid_nonce(&"a".repeat(65)));
    assert!(!valid_nonce("abcdefghijklmnop-"));
    assert!(valid_nonce(&super::client::request_nonce_for_tests()), "generated nonces are accepted");
}

#[test]
fn a_good_request_verifies_once_and_a_replay_is_refused() {
    let r = signed("/licence/v1/checkout", b"{\"a\":1}", NOW_MS);
    let mut g = ReplayGuard::default();
    assert_eq!(verify(&r, NOW_MS + 5_000, &mut g), Ok(()));
    assert_eq!(verify(&r, NOW_MS + 5_000, &mut g), Err(RequestRefused::Replay));
}

#[test]
fn changing_any_signed_field_breaks_the_signature() {
    let base = signed("/licence/v1/checkout", b"body", NOW_MS);
    let mut cases = Vec::new();
    let mut r = base.clone();
    r.method = "GET".into();
    cases.push(("method", r));
    let mut r = base.clone();
    r.path = "/licence/v1/grants".into();
    cases.push(("path", r));
    let mut r = base.clone();
    r.body = b"other".to_vec();
    cases.push(("body", r));
    let mut r = base.clone();
    r.auth.as_mut().unwrap().timestamp_ms += 1;
    cases.push(("timestamp", r));
    let mut r = base.clone();
    r.auth.as_mut().unwrap().nonce = "z".repeat(32);
    cases.push(("nonce", r));
    for (what, r) in cases {
        let mut g = ReplayGuard::default();
        assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::BadSignature), "{what}");
    }
}

#[test]
fn unsigned_wrong_node_stale_malformed_and_oversized_requests_are_refused() {
    let mut g = ReplayGuard::default();
    let mut r = signed("/p", b"", NOW_MS);
    r.auth = None;
    assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::Unsigned));

    let r = sign_request(&steward(), "another-node", AUD, "POST", "/p", vec![], NOW_MS, NONCE);
    assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::WrongNode));

    let r = sign_request(&sk(99), NODE, AUD, "POST", "/p", vec![], NOW_MS, NONCE);
    assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::BadSignature), "another key");

    let r = signed("/p", b"", NOW_MS);
    assert_eq!(verify(&r, NOW_MS + REQUEST_WINDOW_MS + 1, &mut g), Err(RequestRefused::Stale));
    assert_eq!(verify(&r, NOW_MS - REQUEST_WINDOW_MS - 1, &mut g), Err(RequestRefused::Stale));
    assert_eq!(verify(&r, NOW_MS + REQUEST_WINDOW_MS, &mut g), Ok(()), "the window edge is inside");

    let r = sign_request(&steward(), NODE, AUD, "POST", "/p", vec![], NOW_MS, "short");
    assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::Malformed));

    let big = vec![0u8; MAX_REQUEST_BODY + 1];
    let r = signed("/p", &big, NOW_MS);
    assert_eq!(verify(&r, NOW_MS, &mut g), Err(RequestRefused::TooLarge));
}

#[test]
fn the_replay_memory_evicts_by_timestamp_and_refuses_when_full_never_forgetting_everything() {
    let mut g = ReplayGuard::default();
    let t0 = NOW_MS;
    for i in 0..4096 {
        assert!(g.first_use(&format!("n{i:020}"), t0, t0), "{i}");
    }
    // Full of live entries: a new nonce is refused, and the old ones are still remembered.
    assert!(!g.first_use("fresh0000000000000", t0, t0 + 1));
    assert!(!g.first_use(&format!("n{:020}", 7), t0, t0 + 1), "an old nonce is still a replay");
    // Once they age out of the window the memory frees up by itself.
    let later = t0 + REQUEST_WINDOW_MS + 1;
    assert!(g.first_use("fresh0000000000000", later, later));
    assert!(g.first_use(&format!("n{:020}", 7), later, later), "evicted by timestamp");
}

#[test]
fn a_refused_request_does_not_use_up_a_nonce() {
    let good = signed("/p", b"x", NOW_MS);
    let mut forged = good.clone();
    forged.body = b"y".to_vec();
    let mut g = ReplayGuard::default();
    assert_eq!(verify(&forged, NOW_MS, &mut g), Err(RequestRefused::BadSignature));
    assert_eq!(verify(&good, NOW_MS, &mut g), Ok(()));
}

#[test]
fn a_request_signed_for_another_seed_is_refused() {
    let r = sign_request(&steward(), NODE, "another-seed", "POST", "/p", vec![], NOW_MS, NONCE);
    assert_eq!(verify(&r, NOW_MS, &mut ReplayGuard::default()), Err(RequestRefused::BadSignature));
}

#[test]
fn a_grant_signature_is_not_a_request_signature() {
    let env = grant(1, T0, 1000, &["aarch64"]);
    let mut r = signed("/p", b"", NOW_MS);
    r.auth.as_mut().unwrap().signature = env.signature.clone();
    let mut g = ReplayGuard::default();
    assert!(verify(&r, NOW_MS, &mut g).is_err());
}

#[tokio::test]
async fn a_client_whose_clock_is_below_the_floor_signs_nothing() {
    use super::tests_stub::*;
    use std::sync::atomic::AtomicU64;
    let stub = StubLicence::new(Arc::new(AtomicU64::new(T0)), std::time::Duration::ZERO);
    let early: ClockMs = Arc::new(|| (CLOCK_FLOOR_SECS - 1) * 1000);
    let c = SignedLicenceClient::new(sk(21), STEWARD_NODE, "seed-test", StubLink(stub.clone()), early);
    let e = c.checkout(&wire("aarch64")).await.unwrap_err();
    assert_eq!(e, LicenceClientError::Refused { status: 0, code: "clock_not_set".into() });
    assert_eq!(stub.checkouts.load(std::sync::atomic::Ordering::SeqCst), 0);
}