mkit-server 0.5.0

Runtime-agnostic core of the mkit server: operation model, errors, runtime and telemetry vocabulary
Documentation
#![allow(clippy::unwrap_used)]

use super::*;
use core::time::Duration;
use ed25519_dalek::SigningKey;
use mkit_core::hash::to_hex;

fn scanner() -> String {
    to_hex(SigningKey::from_bytes(&[41; 32]).verifying_key().as_bytes())
}

fn config() -> RetrievalConfig {
    RetrievalConfig::parse(&format!("active current {}", to_hex(&[42; 32])), &scanner()).unwrap()
}

fn assignment() -> Assignment {
    Assignment {
        namespace: "test".into(),
        repo_name: "room".into(),
        repository: "test/room".into(),
        ref_name: "refs/heads/main".into(),
        signer: [43; 32],
        packs: vec![
            PackGrant {
                id: [44; 32],
                length: 200,
                tickets: vec![[45; 32]],
            },
            PackGrant {
                id: [46; 32],
                length: 300,
                tickets: vec![[47; 32]],
            },
        ],
    }
}

#[test]
fn each_attempt_has_fresh_capability_with_stable_id_and_ordered_lengths() {
    let config = config();
    let assignment = assignment();
    let first = config
        .mint(
            "https://scanner.test",
            "stable",
            &assignment,
            Duration::from_secs(10),
            1_000,
        )
        .unwrap();
    let second = config
        .mint(
            "https://scanner.test",
            "stable",
            &assignment,
            Duration::from_secs(10),
            1_000,
        )
        .unwrap();
    assert_eq!(first.endpoint_path.as_deref(), Some(PATH));
    assert_eq!(first.expires_at_ms, Some(12_000));
    assert_ne!(first.capability, second.capability);
    assert_eq!(first.packs, second.packs);
    for (wire, grant) in first.packs.iter().zip(&assignment.packs) {
        assert_eq!(wire.id.as_deref(), Some(grant.id.as_slice()));
        assert_eq!(wire.length, Some(grant.length));
    }
    let a = config
        .verify(
            first.capability.as_ref().unwrap(),
            "https://scanner.test",
            1_000,
        )
        .unwrap();
    let b = config
        .verify(
            second.capability.as_ref().unwrap(),
            "https://scanner.test",
            1_000,
        )
        .unwrap();
    assert_eq!(a.inspection_id, "stable");
    assert_eq!(b.inspection_id, "stable");
    assert_ne!(a.nonce, b.nonce);
    assert_eq!(a.assignment, assignment);
}

#[test]
fn capability_binds_origin_bytes_and_exclusive_validity_window() {
    let config = config();
    let wire = config
        .mint(
            "https://scanner.test",
            "stable",
            &assignment(),
            Duration::from_millis(1),
            1_000,
        )
        .unwrap();
    let token = wire.capability.unwrap();
    assert!(config.verify(&token, "https://scanner.test", 2_000).is_ok());
    for (candidate, audience, now) in [
        (token.clone(), "https://other.test", 1_000),
        (token.clone(), "https://scanner.test", 999),
        (token.clone(), "https://scanner.test", 2_001),
        (token.to_uppercase(), "https://scanner.test", 1_000),
        (
            token.replacen("r1.", "r2.", 1),
            "https://scanner.test",
            1_000,
        ),
        (
            token.replace("current", "unknown"),
            "https://scanner.test",
            1_000,
        ),
        (format!("{token}0"), "https://scanner.test", 1_000),
        (String::new(), "https://scanner.test", 1_000),
    ] {
        let error = config.verify(&candidate, audience, now).err().unwrap();
        assert_eq!(error.code(), crate::Code::NotFound);
        assert_eq!(error.public_message(), "pack not found");
    }
    let mut fields: Vec<_> = token.split('.').map(str::to_owned).collect();
    fields[2].replace_range(0..2, "00");
    assert!(
        config
            .verify(&fields.join("."), "https://scanner.test", 1_000)
            .is_err()
    );
}

#[test]
fn capability_claim_substitutions_cannot_reuse_the_original_mac() {
    let config = config();
    let token = config
        .mint(
            "https://scanner.test",
            "stable",
            &assignment(),
            Duration::from_secs(10),
            1_000,
        )
        .unwrap()
        .capability
        .unwrap();
    let claims = config
        .verify(&token, "https://scanner.test", 1_000)
        .unwrap();
    let original = serde_json::to_vec(&claims).unwrap();
    assert_eq!(
        token.split('.').nth(2).unwrap(),
        mkit_core::hash::to_hex_bytes(&original)
    );
    for case in 0..5 {
        let mut changed: super::Claims = serde_json::from_slice(&original).unwrap();
        match case {
            0 => changed.assignment.packs[0].id = [99; 32],
            1 => changed.assignment.repository = "test/other".into(),
            2 => changed.audience = "https://other.test".into(),
            3 => changed.expires_at_ms += 1,
            _ => changed.inspection_id = "another-inspection".into(),
        }
        let mut fields: Vec<_> = token.split('.').map(str::to_owned).collect();
        fields[2] = mkit_core::hash::to_hex_bytes(&serde_json::to_vec(&changed).unwrap());
        let error = config
            .verify(&fields.join("."), "https://scanner.test", 1_000)
            .err()
            .unwrap();
        assert_eq!(error.code(), crate::Code::NotFound, "substitution {case}");
        assert_eq!(error.public_message(), "pack not found");
    }
}

#[test]
fn retained_key_rotation_is_bounded_and_mint_uses_active_key() {
    let old = config();
    let token = old
        .mint(
            "https://scanner.test",
            "stable",
            &assignment(),
            Duration::from_mins(5),
            1_100,
        )
        .unwrap()
        .capability
        .unwrap();
    let rotated = RetrievalConfig::parse(
        &format!(
            "active next {}\nretained current {} 1000",
            to_hex(&[48; 32]),
            to_hex(&[42; 32])
        ),
        &scanner(),
    )
    .unwrap();
    assert!(
        rotated
            .verify(&token, "https://scanner.test", 1_100)
            .is_ok()
    );
    assert!(
        rotated
            .verify(&token, "https://scanner.test", 301_999)
            .is_ok()
    );
    assert!(
        rotated
            .verify(&token, "https://scanner.test", 302_000)
            .is_err()
    );
    let fresh = rotated
        .mint(
            "https://scanner.test",
            "stable",
            &assignment(),
            Duration::from_secs(1),
            1_000,
        )
        .unwrap()
        .capability
        .unwrap();
    assert!(fresh.starts_with("r1.next."));
    assert!(old.verify(&fresh, "https://scanner.test", 1_000).is_err());
    let removed =
        RetrievalConfig::parse(&format!("active next {}", to_hex(&[48; 32])), &scanner()).unwrap();
    assert!(
        removed
            .verify(&token, "https://scanner.test", 1_100)
            .is_err()
    );
}

#[test]
fn all_retained_and_active_keys_are_checked_for_role_reuse() {
    let config = RetrievalConfig::parse(
        &format!(
            "active current {}\nretained old {} 1000",
            to_hex(&[42; 32]),
            to_hex(&[48; 32])
        ),
        &scanner(),
    )
    .unwrap();
    for secret in [[42; 32], [48; 32], [41; 32]] {
        assert!(config.check_role_keys(&[], &[secret]).is_err());
        let public = SigningKey::from_bytes(&secret).verifying_key().to_bytes();
        assert!(config.check_role_keys(&[public], &[]).is_err());
        assert!(config.check_role_keys(&[], &[public]).is_err());
    }
    assert!(config.check_role_keys(&[], &[[49; 32]]).is_ok());
    let scanner_seed = format!("active current {}", to_hex(&[41; 32]));
    assert!(RetrievalConfig::parse(&scanner_seed, &scanner()).is_err());
}

#[test]
fn configuration_and_capability_limits_fail_closed() {
    let secret = to_hex(&[42; 32]);
    for grammar in [
        String::new(),
        format!("retained old {secret} 1"),
        format!("active a {secret}\nactive b {}", to_hex(&[48; 32])),
        format!("active a {secret}\nretained b {secret} 1"),
        format!("active a {secret}\nretained a {} 1", to_hex(&[48; 32])),
        format!("active a {secret}\nretained b {} 01", to_hex(&[48; 32])),
        format!("active bad/id {secret}"),
        format!("active a {secret} extra"),
    ] {
        assert!(
            RetrievalConfig::parse(&grammar, &scanner()).is_err(),
            "{grammar}"
        );
    }
    let valid = format!("active current {secret}");
    assert!(RetrievalConfig::parse(&valid, "").is_err());
    assert!(RetrievalConfig::parse(&valid, &format!("{}\n{}", scanner(), scanner())).is_err());
    assert!(RetrievalConfig::parse(&valid, &to_hex(&[0; 32])).is_err());
    let config = config();
    for timeout in [
        Duration::ZERO,
        Duration::from_millis(300_001),
        Duration::MAX,
    ] {
        assert!(
            config
                .mint(
                    "https://scanner.test",
                    "stable",
                    &assignment(),
                    timeout,
                    1_000
                )
                .is_err()
        );
    }
    assert!(
        config
            .mint(
                "https://scanner.test",
                "stable",
                &assignment(),
                Duration::from_secs(1),
                u64::MAX
            )
            .is_err()
    );
    let mut duplicate = assignment();
    duplicate.packs.push(duplicate.packs[0].clone());
    assert!(
        config
            .mint(
                "https://scanner.test",
                "stable",
                &duplicate,
                Duration::from_secs(1),
                1_000
            )
            .is_err()
    );
    let mut unbound = assignment();
    unbound.packs[0].tickets.clear();
    assert!(
        config
            .mint(
                "https://scanner.test",
                "stable",
                &unbound,
                Duration::from_secs(1),
                1_000
            )
            .is_err()
    );
}