Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210//! `key create` end to end: a key made, published, and found again.//!//! `plan/testing.md` counts the `key` family among the commands with no flow//! test, and the half worth having first is this one: `key create` exists//! because an agent identity has no `~/.ssh`, and every part of that claim —//! where the pair lands, what mode it lands at, that the published record//! carries the public half, and that the next lookup finds it — is invisible//! to a unit test of any one function.
mod support;
use support::{ALICE, Scenario};
/// The whole of what the command promises, in one run.#[test]fn key_create_makes_a_pair_and_publishes_the_public_half() { let world = Scenario::new("key-create"); world.grant_agent_plane(ALICE, "alice.test");
let run = world .command(&["key", "create", "--json"]) .env("ATGC_ACCOUNT", ALICE) .env("CLAUDECODE", "1") .finish() .success(); let published = run.json();
// Published, and as a key rather than as whatever was lying around. assert_eq!(published["added"], true, "{published:#}"); assert_eq!(published["kind"], "ssh-ed25519", "{published:#}"); let fingerprint = published["fingerprint"] .as_str() .expect("a fingerprint") .to_string(); assert!(fingerprint.starts_with("SHA256:"), "{fingerprint}");
// The pair is where the command says it is, under the account's DID. let dir = world .config_path("dids") .join("plc") .join(ALICE.trim_start_matches("did:plc:")); let private = dir.join("id_ed25519"); let public = dir.join("id_ed25519.pub"); assert!( private.is_file(), "no private half at {}", private.display() ); assert!(public.is_file(), "no public half at {}", public.display());
#[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // Both halves: ssh-keygen writes the public one 0644, and `doctor` // treats any file in this directory with group or other bits as a // problem. The public half is not secret, but the exception is how a // mode gets copied somewhere it matters. for (half, path) in [("private", &private), ("public", &public)] { let mode = std::fs::metadata(path).unwrap().permissions().mode() & 0o777; assert_eq!(mode, 0o600, "the {half} half must be owner-only"); } let dir_mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777; assert_eq!(dir_mode, 0o700, "nor its directory"); }
// The record the PDS received carries *this* key, not some other one. let line = std::fs::read_to_string(&public).expect("the public half"); let blob = line.split_whitespace().nth(1).expect("a key blob"); let records = world.with(|w| w.collection(ALICE, "sh.tangled.publicKey")); assert_eq!(records.len(), 1, "exactly one key record was written"); let written = records[0].1.value["key"].as_str().unwrap_or_default(); assert!( written.contains(blob), "the published record is not the generated key:\n wrote {written}\n made {line}" );
// The key is on this machine, and the listing says so. `key create` puts // it in the identity directory rather than `~/.ssh`, which is the whole // point of the command — so a scan of the person's drawer said "none of // these keys is on this machine" about the key it had just made. let listed = world .command(&["key", "list", "--json"]) .env("ATGC_ACCOUNT", ALICE) .env("CLAUDECODE", "1") .finish() .success(); let rows = listed.json(); let rows = rows.as_array().expect("an array of keys"); assert_eq!(rows.len(), 1, "{rows:#?}"); assert_eq!( rows[0]["on_this_machine"].as_str(), Some(public.to_string_lossy().as_ref()), "the listing did not find the key it just made: {rows:#?}" ); assert!( !listed.stderr.contains("none of these keys"), "the listing denied a key that is here:\n{}", listed.stderr );
// And a second run is a no-op rather than a second key: a bootstrap // script that runs twice must not orphan the first pair or publish a // duplicate record. let again = world .command(&["key", "create"]) .env("ATGC_ACCOUNT", ALICE) .env("CLAUDECODE", "1") .finish() .success(); assert!( again.stderr.contains("already made") || again.stdout.contains("already registered"), "the second run should have found the first:\n{}\n{}", again.stdout, again.stderr ); let after = world.with(|w| w.collection(ALICE, "sh.tangled.publicKey")); assert_eq!(after.len(), 1, "a second key record was written");}
/// An account with no agent login is refused, and told where a person's key goes.////// The gate matters more than the filing: `push_key` prefers a key in the/// agent directory over one in `~/.ssh`, so a person who got through here/// once would have every later push for this account signed by a key in a/// directory they would never look in, while the one they know about sat/// unused. A refusal that names `ssh-keygen` and `key add` costs them a/// sentence; the alternative costs them an afternoon.#[test]fn key_create_refuses_an_account_that_is_not_an_agent() { let world = Scenario::new("key-create-human");
let run = world .command(&["key", "create"]) .env("ATGC_ACCOUNT", ALICE) .finish() .refused_with(2, "has no agent login"); assert!( run.stderr.contains("ssh-keygen") && run.stderr.contains("atgc key add"), "the refusal has to say where a person's key goes:\n{}", run.stderr );
// And no key was made on the way to refusing. The *directory* is no // longer the probe for that: it is the identity's directory now, holding // the session too, so it exists for any account that has ever logged in. // What must not exist is a key in it. let dir = world .config_path("dids") .join("plc") .join(ALICE.trim_start_matches("did:plc:")); for half in ["id_ed25519", "id_ed25519.pub"] { assert!(!dir.join(half).exists(), "a key was created anyway: {half}"); }}
/// The gate is a fact about the account, not a claim by the process.#[test]fn the_gate_asks_the_account_not_the_environment() { let world = Scenario::new("key-create-env-is-not-enough");
// Declaring the process an agent is not what opens this command: the // account has to actually hold an agent login. That is the difference // between a fact somebody authorized and a guess about who is running. world .command(&["key", "create"]) .env("ATGC_ACCOUNT", ALICE) .env("CLAUDECODE", "1") .finish() .refused_with(2, "has no agent login");}
/// An account with only an agent login: what a *person* actually gets.////// The gate asks whether the account holds an agent grant, which lets a/// person past it — and publishing the key then needs a session on the/// person's plane, which such an account has not got. So the claim that a/// person can provision an agent's key from their own terminal has to be/// true all the way through or not made at all. This pins which it is.#[test]fn a_person_provisioning_an_agent_only_account() { let world = Scenario::new("key-create-provision"); world.grant_agent_plane(ALICE, "alice.test");
// Strip the person's grant, leaving an agent-only account. let path = world.account_file(ALICE); let mut entry: serde_json::Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).expect("an entry"); entry.as_object_mut().expect("an entry").remove("human"); std::fs::write(&path, serde_json::to_vec_pretty(&entry).unwrap()).unwrap();
// And the person's *session*, which is the other half of what makes an // account agent-only. Leaving it would be a grant recorded nowhere but // still usable, which the fallback is right to read as the person's: // that is exactly what every login made before planes looks like. let store_path = world.session_shard(ALICE); let mut store: serde_json::Value = serde_json::from_slice(&std::fs::read(&store_path).unwrap()).expect("a store"); store .as_object_mut() .unwrap() .remove(&format!("oauth:{ALICE}/test-session-alice.test")); std::fs::write(&store_path, serde_json::to_vec_pretty(&store).unwrap()).unwrap();
world .command(&["key", "create"]) .env("ATGC_ACCOUNT", ALICE) .finish() .refused("has an agent login here, but no login of your own");}