//! Adds the label signing key (`#atproto_label`) and the labeler endpoint //! (`#atproto_labeler`) to the account's DID document. Needs the account's main //! password and a code emailed by the PDS. Run it once the labeler is live at //! `LABELER_URL`; it exits early if both entries are already in place. use std::collections::BTreeMap; use std::io::Write; use anyhow::{Context, Result, bail}; use cities_labeler::config::Config; use cities_labeler::login; use jacquard::AuthorizationToken; use jacquard::api::com_atproto::identity::sign_plc_operation::SignPlcOperation; use jacquard::api::com_atproto::identity::submit_plc_operation::SubmitPlcOperation; use jacquard::client::{Agent, MemoryCredentialSession}; use jacquard::common::to_data; use jacquard::common::types::crypto::multikey; use jacquard::prelude::*; use k256::ecdsa::SigningKey; use serde::{Deserialize, Serialize}; const PLC_DIRECTORY: &str = "https://plc.directory"; /// The parts of the DID's current PLC state we change; everything else /// (rotation keys, handle) is left as the PDS has it. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct PlcData { verification_methods: BTreeMap, services: BTreeMap, } #[derive(Debug, PartialEq, Deserialize, Serialize)] struct PlcService { #[serde(rename = "type")] kind: String, endpoint: String, } pub async fn run(config: &Config) -> Result<()> { let key = SigningKey::from_slice(&hex::decode(&config.signing_key_hex)?) .context("SIGNING_KEY_HEX")?; let public = key.verifying_key().to_encoded_point(true); let label_key = format!("did:key:{}", multikey(0xE7, public.as_bytes())); let labeler = PlcService { kind: "AtprotoLabeler".into(), endpoint: config.labeler_url.clone(), }; // identity changes need the main password; app passwords are refused let password = rpassword::prompt_password(format!("main password for {}: ", config.handle))?; let (agent, did) = login(&config.handle, &password).await?; let url = format!("{PLC_DIRECTORY}/{did}/data"); let mut plc: PlcData = reqwest::get(&url) .await? .error_for_status()? .json() .await .with_context(|| format!("reading {url}"))?; if plc.verification_methods.get("atproto_label") == Some(&label_key) && plc.services.get("atproto_labeler") == Some(&labeler) { println!("{did} already points at {}", config.labeler_url); return Ok(()); } plc.verification_methods .insert("atproto_label".into(), label_key.clone()); plc.services.insert("atproto_labeler".into(), labeler); println!( "will set on {did}:\n #atproto_label {label_key}\n #atproto_labeler {}", config.labeler_url ); request_signature(&agent).await?; let token = prompt("code emailed to the account: ")?; let signed = agent .send(SignPlcOperation { token: Some(token.as_str().into()), verification_methods: Some(to_data(&plc.verification_methods)?), services: Some(to_data(&plc.services)?), also_known_as: None, rotation_keys: None, extra_data: None, }) .await? .into_output()?; if prompt("submit this PLC operation? [y/N] ")? != "y" { bail!("aborted; nothing submitted"); } let response = agent .send(SubmitPlcOperation { operation: signed.operation, extra_data: None, }) .await?; // success is an empty body, which into_output() can't decode as JSON if !response.status().is_success() { response.into_output()?; bail!("submitPlcOperation failed"); } println!("submitted; check {PLC_DIRECTORY}/{did}"); Ok(()) } /// Asks the PDS to email a PLC operation code. Sent by hand because jacquard /// serializes this no-input procedure as a `null` JSON body, which the PDS /// rejects with "A request body was provided when none was expected". async fn request_signature(agent: &Agent) -> Result<()> { let Some(AuthorizationToken::Bearer(token)) = agent.inner().access_token().await else { bail!("not logged in"); }; let url = format!( "{}/xrpc/com.atproto.identity.requestPlcOperationSignature", agent.endpoint().await.as_str().trim_end_matches('/') ); let response = reqwest::Client::new() .post(&url) .bearer_auth(token.as_str()) .send() .await?; if !response.status().is_success() { bail!( "requestPlcOperationSignature failed: {} {}", response.status(), response.text().await? ); } Ok(()) } fn prompt(label: &str) -> Result { print!("{label}"); std::io::stdout().flush()?; let mut line = String::new(); std::io::stdin().read_line(&mut line)?; Ok(line.trim().to_owned()) }