wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! What a machine identity must be: stable, private, and derived the same way
//! on both sides of a pairing.

use super::*;

/// TEST9952: an identity is created once and then kept.
///
/// A daemon that generated a new key on every start would break every peer
/// relationship it has, silently, and the operator would see only "the peer's
/// identity changed" on the other machine.
#[test]
fn test9952_an_identity_is_generated_once_and_reused() {
    let dir = tempfile::tempdir().expect("tempdir");
    let first = MachineIdentity::load_or_create(dir.path(), "desktop").expect("created");
    let second = MachineIdentity::load_or_create(dir.path(), "ignored").expect("loaded");

    assert_eq!(first.id(), second.id());
    assert_eq!(first.public_key(), second.public_key());
    assert_eq!(
        second.name(),
        "desktop",
        "the name comes from the file, not from the caller's default"
    );

    // A different machine gets a different identity.
    let other = tempfile::tempdir().expect("tempdir");
    let laptop = MachineIdentity::load_or_create(other.path(), "laptop").expect("created");
    assert_ne!(first.id(), laptop.id());
}

/// TEST9953: the secret half is private on disk, and never printed.
#[test]
fn test9953_the_secret_half_stays_private() {
    let dir = tempfile::tempdir().expect("tempdir");
    let identity = MachineIdentity::load_or_create(dir.path(), "desktop").expect("created");
    let path = MachineIdentity::path_in(dir.path());

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mode = std::fs::metadata(&path).expect("metadata").permissions().mode();
        assert_eq!(mode & 0o077, 0, "the identity file is readable by others: {mode:o}");
    }

    // Debug is what ends up in a log line by accident.
    let rendered = format!("{identity:?}");
    let stored = std::fs::read_to_string(&path).expect("read");
    let secret: serde_json::Value = serde_json::from_str(&stored).expect("json");
    let secret = secret["secret_key"].as_str().expect("a key");
    assert!(!rendered.contains(secret), "the secret key reached Debug: {rendered}");
    assert!(rendered.contains(identity.id().as_str()));
}

/// TEST9954: a corrupt identity file is refused rather than replaced.
///
/// Generating a new key over an unreadable one would look like recovery and
/// be a silent break of every peer relationship this machine has.
#[test]
fn test9954_a_damaged_identity_is_refused_not_replaced() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(MachineIdentity::path_in(dir.path()), b"not an identity").expect("write");

    let error = MachineIdentity::load_or_create(dir.path(), "desktop")
        .expect_err("a damaged identity must be refused");
    assert!(error.contains("not a machine identity"), "{error}");

    // A well-formed file with a key of the wrong size is refused too.
    std::fs::write(
        MachineIdentity::path_in(dir.path()),
        br#"{"secret_key":"c2hvcnQ=","name":"desktop"}"#,
    )
    .expect("write");
    let error = MachineIdentity::load_or_create(dir.path(), "desktop").expect_err("refused");
    assert!(error.contains("32-byte"), "{error}");
}

/// TEST9955: the id and the fingerprint are derived from the key alone.
///
/// Both machines compute the peer's id from the key it presents. If either
/// depended on a name or an address, two machines would disagree about what to
/// call the same peer — and the relationship is keyed by that name.
#[test]
fn test9955_identity_is_derived_from_the_key_alone() {
    let dir = tempfile::tempdir().expect("tempdir");
    let identity = MachineIdentity::load_or_create(dir.path(), "desktop").expect("created");
    let id = identity.id().clone();
    let fingerprint = identity.fingerprint().clone();

    identity.rename(dir.path(), "workshop").expect("renamed");
    assert_eq!(identity.name(), "workshop");
    assert_eq!(identity.id(), &id, "a rename must not change the identity");
    assert_eq!(identity.fingerprint(), &fingerprint);

    // Reloaded from disk, it is still the same machine under the new name.
    let reloaded = MachineIdentity::load_or_create(dir.path(), "ignored").expect("loaded");
    assert_eq!(reloaded.id(), &id);
    assert_eq!(reloaded.name(), "workshop");

    // The id a peer derives from the key matches the one the machine derives.
    assert_eq!(peer_id_of(&reloaded.public_key()), id);
    // And it is a legal id, which is what makes it safe in a path.
    assert!(super::super::model::PeerId::parse(id.as_str()).is_ok());
}

/// TEST9956: a pairing code survives being read off one screen and typed into
/// another, and nothing else.
#[test]
fn test9956_pairing_codes_forgive_typing_and_nothing_else() {
    let code = PairingCode::mint();
    let text = code.to_string();
    assert_eq!(text.split('-').count(), 3, "{text} is not three groups");

    // The ways a person retypes it.
    for spelling in [
        text.clone(),
        text.to_lowercase(),
        format!("  {text}  "),
        text.replace('-', " - "),
    ] {
        assert_eq!(
            PairingCode::parse(&spelling).expect("a code"),
            code,
            "{spelling:?} did not parse back to the same code"
        );
    }

    for hostile in ["", "FERN", "FERN-74", "FERN-74-MOON-EXTRA"] {
        assert!(
            PairingCode::parse(hostile).is_err(),
            "{hostile:?} was accepted as a code"
        );
    }

    // Two codes derive different secrets, and the same code derives the same
    // one on both machines.
    let same = PairingCode::parse(&text).expect("a code");
    assert_eq!(code.secret(), same.secret());
    assert_ne!(code.secret(), PairingCode::mint().secret());
}