#[cfg(feature = "net")]
use std::collections::HashMap;
#[cfg(feature = "net")]
use std::sync::Mutex;
#[cfg(feature = "net")]
use std::time::{Duration, Instant};
#[cfg(feature = "net")]
use async_trait::async_trait;
use base64::Engine;
use k256::ecdsa::signature::{Signer, Verifier};
use k256::ecdsa::{Signature, SigningKey, VerifyingKey};
use serde_json::{json, Map, Value};
pub const LABEL_KEY_ID: &str = "atproto_label";
pub const LABELER_SERVICE_ID: &str = "atproto_labeler";
pub const LABELER_SERVICE_TYPE: &str = "AtprotoLabeler";
pub const LABEL_VERSION: i64 = 1;
const B64: base64::engine::general_purpose::GeneralPurpose = base64::engine::general_purpose::STANDARD_NO_PAD;
pub fn unsigned(label: &Value) -> Value {
let mut m = Map::new();
m.insert("ver".into(), json!(label["ver"].as_i64().unwrap_or(LABEL_VERSION)));
for field in ["src", "uri", "cid", "val", "cts", "exp"] {
if let Some(v) = label.get(field).filter(|v| !v.is_null()) {
m.insert(field.into(), v.clone());
}
}
if label["neg"].as_bool() == Some(true) {
m.insert("neg".into(), json!(true));
}
Value::Object(m)
}
pub fn signing_bytes(label: &Value) -> anyhow::Result<Vec<u8>> {
Ok(techne_lexicon_check::dag_cbor::canonical_encode_json(&unsigned(label))?)
}
pub fn sign(label: &Value, key: &SigningKey) -> anyhow::Result<Value> {
let sig: Signature = key.sign(&signing_bytes(label)?);
let sig = sig.normalize_s().unwrap_or(sig);
let mut signed = unsigned(label);
signed["sig"] = json!({"$bytes": B64.encode(sig.to_bytes())});
Ok(signed)
}
pub fn sig_of(label: &Value) -> Option<Vec<u8>> {
let b64 = label["sig"]["$bytes"].as_str()?;
B64.decode(b64.trim_end_matches('=')).ok()
}
pub fn verify(label: &Value, key: &VerifyingKey) -> bool {
let (Some(sig), Ok(bytes)) = (sig_of(label), signing_bytes(label)) else { return false };
let Ok(sig) = Signature::from_slice(&sig) else { return false };
if sig.normalize_s().is_some() {
return false;
}
key.verify(&bytes, &sig).is_ok()
}
pub fn did_key(key: &VerifyingKey) -> String {
format!("did:key:{}", multibase(key))
}
pub fn multibase(key: &VerifyingKey) -> String {
let mut bytes = vec![0xe7, 0x01];
bytes.extend_from_slice(key.to_encoded_point(true).as_bytes());
format!("z{}", bs58::encode(bytes).into_string())
}
pub fn parse_key(s: &str) -> Option<VerifyingKey> {
let z = s.strip_prefix("did:key:").unwrap_or(s).strip_prefix('z')?;
let bytes = bs58::decode(z).into_vec().ok()?;
let point = bytes.strip_prefix(&[0xe7, 0x01])?;
VerifyingKey::from_sec1_bytes(point).ok()
}
pub fn label_key_in_doc(doc: &Value, did: &str) -> Option<VerifyingKey> {
doc["verificationMethod"].as_array()?.iter().find_map(|m| {
let id = m["id"].as_str()?;
let ours = id == format!("#{LABEL_KEY_ID}") || id == format!("{did}#{LABEL_KEY_ID}");
if !ours {
return None;
}
parse_key(m["publicKeyMultibase"].as_str()?)
})
}
pub fn labeler_endpoint_in_doc(doc: &Value, did: &str) -> Option<String> {
doc["service"].as_array()?.iter().find_map(|s| {
let id = s["id"].as_str()?;
let ours = id == format!("#{LABELER_SERVICE_ID}") || id == format!("{did}#{LABELER_SERVICE_ID}");
(ours && s["type"].as_str() == Some(LABELER_SERVICE_TYPE)).then(|| s["serviceEndpoint"].as_str().map(str::to_string))?
})
}
pub fn derive_key(secret: &[u8], did: &str) -> SigningKey {
use sha2::{Digest, Sha256};
(0u32..)
.find_map(|n| {
let mut h = Sha256::new();
h.update(b"atproto_label\0");
h.update((secret.len() as u64).to_be_bytes());
h.update(secret);
h.update(did.as_bytes());
h.update(n.to_be_bytes());
SigningKey::from_slice(&h.finalize()).ok()
})
.expect("a valid scalar turns up")
}
pub fn did_doc_update(recommended: &Value, endpoint: &str, key: &VerifyingKey) -> Value {
let mut methods = recommended["verificationMethods"].as_object().cloned().unwrap_or_default();
methods.insert(LABEL_KEY_ID.into(), json!(did_key(key)));
let mut services = recommended["services"].as_object().cloned().unwrap_or_default();
services.insert(LABELER_SERVICE_ID.into(), json!({"type": LABELER_SERVICE_TYPE, "endpoint": endpoint}));
json!({
"rotationKeys": recommended["rotationKeys"],
"alsoKnownAs": recommended["alsoKnownAs"],
"verificationMethods": methods,
"services": services,
})
}
pub fn did_doc_declares(doc: &Value, did: &str, endpoint: &str, key: &VerifyingKey) -> bool {
label_key_in_doc(doc, did).as_ref() == Some(key)
&& labeler_endpoint_in_doc(doc, did).is_some_and(|e| e.trim_end_matches('/') == endpoint.trim_end_matches('/'))
}
pub fn plc_token_in(text: &str) -> Option<String> {
text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-')).find_map(|w| {
let (a, b) = w.split_once('-')?;
(a.len() == 5 && b.len() == 5 && a.chars().chain(b.chars()).all(|c| c.is_ascii_alphanumeric())).then(|| w.to_string())
})
}
#[cfg(feature = "net")]
pub async fn plc_token_from_mailpit(
http: &reqwest::Client,
base: &str,
address: &str,
after: chrono::DateTime<chrono::Utc>,
) -> anyhow::Result<Option<String>> {
let base = base.trim_end_matches('/');
let found: Value = http
.get(format!("{base}/api/v1/search"))
.query(&[("query", format!("to:\"{address}\""))])
.send()
.await?
.error_for_status()?
.json()
.await?;
for m in found["messages"].as_array().into_iter().flatten() {
let created = m["Created"].as_str().and_then(|c| chrono::DateTime::parse_from_rfc3339(c).ok());
if created.is_some_and(|c| c < after) {
continue;
}
let Some(id) = m["ID"].as_str() else { continue };
let msg: Value = http.get(format!("{base}/api/v1/message/{id}")).send().await?.error_for_status()?.json().await?;
let text = format!("{} {}", msg["Text"].as_str().unwrap_or_default(), msg["Subject"].as_str().unwrap_or_default());
if let Some(token) = plc_token_in(&text) {
return Ok(Some(token));
}
}
Ok(None)
}
#[cfg(feature = "net")]
#[async_trait]
pub trait LabelKeys: Send + Sync {
async fn label_key(&self, did: &str) -> anyhow::Result<Option<VerifyingKey>>;
}
#[cfg(feature = "net")]
pub async fn verified(keys: &dyn LabelKeys, labels: &[Value]) -> anyhow::Result<Vec<Value>> {
let mut cache: HashMap<String, VerifyingKey> = HashMap::new();
let mut out = Vec::with_capacity(labels.len());
for l in labels {
let Some(src) = l["src"].as_str() else { continue };
if !cache.contains_key(src) {
let key = keys.label_key(src).await?.ok_or_else(|| {
anyhow::anyhow!("labeler {src} declares no #{LABEL_KEY_ID} key in its DID document yet; its labels wait")
})?;
cache.insert(src.to_string(), key);
}
if verify(l, &cache[src]) {
out.push(l.clone());
} else {
tracing::warn!(%src, uri = %l["uri"], val = %l["val"], "a label whose signature does not verify was dropped");
}
}
Ok(out)
}
#[cfg(feature = "net")]
#[derive(Default)]
pub struct StaticLabelKeys(pub HashMap<String, VerifyingKey>);
#[cfg(feature = "net")]
#[async_trait]
impl LabelKeys for StaticLabelKeys {
async fn label_key(&self, did: &str) -> anyhow::Result<Option<VerifyingKey>> {
Ok(self.0.get(did).copied())
}
}
#[cfg(feature = "net")]
pub struct DidDocLabelKeys {
http: reqwest::Client,
plc_url: String,
ttl: Duration,
cache: Mutex<HashMap<String, (Instant, Option<VerifyingKey>)>>,
}
#[cfg(feature = "net")]
impl DidDocLabelKeys {
pub fn new(plc_url: impl Into<String>) -> anyhow::Result<Self> {
Ok(Self {
http: reqwest::Client::builder().timeout(Duration::from_secs(10)).build()?,
plc_url: plc_url.into().trim_end_matches('/').to_string(),
ttl: Duration::from_secs(300),
cache: Mutex::new(HashMap::new()),
})
}
pub async fn doc(&self, did: &str) -> anyhow::Result<Option<Value>> {
let url = if did.starts_with("did:plc:") {
format!("{}/{did}", self.plc_url)
} else if let Some(host) = did.strip_prefix("did:web:") {
format!("https://{}/.well-known/did.json", host.replace("%3A", ":"))
} else {
return Ok(None);
};
let resp = self.http.get(url).send().await?;
if resp.status() == reqwest::StatusCode::NOT_FOUND || resp.status() == reqwest::StatusCode::GONE {
return Ok(None);
}
anyhow::ensure!(resp.status().is_success(), "resolving {did} returned {}", resp.status());
Ok(Some(resp.json().await?))
}
}
#[cfg(feature = "net")]
#[async_trait]
impl LabelKeys for DidDocLabelKeys {
async fn label_key(&self, did: &str) -> anyhow::Result<Option<VerifyingKey>> {
if let Some((at, key)) = self.cache.lock().unwrap_or_else(|p| p.into_inner()).get(did) {
if at.elapsed() < self.ttl {
return Ok(*key);
}
}
let key = self.doc(did).await?.and_then(|doc| label_key_in_doc(&doc, did));
self.cache.lock().unwrap_or_else(|p| p.into_inner()).insert(did.to_string(), (Instant::now(), key));
Ok(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(b: u8) -> SigningKey {
SigningKey::from_slice(&[b; 32]).unwrap()
}
fn label() -> Value {
json!({"src": "did:plc:rulefeatures", "uri": "at://did:plc:c/x.y.z/1", "cid": "bafyx", "val": "spam", "cts": "2026-10-09T00:00:00.000Z"})
}
#[test]
fn a_signed_label_verifies_and_any_change_breaks_it() {
let k = key(7);
let signed = sign(&label(), &k).unwrap();
assert_eq!(signed["ver"], 1);
assert!(signed.get("neg").is_none(), "neg is omitted unless true");
assert_eq!(sig_of(&signed).unwrap().len(), 64);
assert!(verify(&signed, k.verifying_key()));
assert!(!verify(&signed, key(8).verifying_key()), "another key");
for (field, v) in [("val", json!("obscene")), ("uri", json!("at://other/x.y.z/1")), ("neg", json!(true)), ("cts", json!("2026-10-10T00:00:00.000Z"))] {
let mut forged = signed.clone();
forged[field] = v;
assert!(!verify(&forged, k.verifying_key()), "{field} changed");
}
let mut unsigned_label = signed.clone();
unsigned_label.as_object_mut().unwrap().remove("sig");
assert!(!verify(&unsigned_label, k.verifying_key()));
let mut neg_false = signed.clone();
neg_false["neg"] = json!(false);
assert!(verify(&neg_false, k.verifying_key()));
}
#[test]
fn keys_round_trip_through_did_key_and_a_did_document() {
let k = key(9);
let vk = *k.verifying_key();
assert_eq!(parse_key(&did_key(&vk)), Some(vk));
let did = "did:plc:rulefeatures";
let doc = json!({
"id": did,
"verificationMethod": [
{"id": format!("{did}#atproto"), "type": "Multikey", "controller": did, "publicKeyMultibase": multibase(key(1).verifying_key())},
{"id": format!("{did}#atproto_label"), "type": "Multikey", "controller": did, "publicKeyMultibase": multibase(&vk)},
],
"service": [{"id": "#atproto_labeler", "type": "AtprotoLabeler", "serviceEndpoint": "https://rules.example"}],
});
assert_eq!(label_key_in_doc(&doc, did), Some(vk), "the #atproto_label key, not the repo key");
assert_eq!(labeler_endpoint_in_doc(&doc, did).as_deref(), Some("https://rules.example"));
assert_eq!(label_key_in_doc(&json!({"verificationMethod": []}), did), None);
}
#[test]
fn derived_keys_are_per_did_and_stable() {
let secret = [5u8; 32];
assert_eq!(derive_key(&secret, "did:plc:a").to_bytes(), derive_key(&secret, "did:plc:a").to_bytes());
assert_ne!(derive_key(&secret, "did:plc:a").to_bytes(), derive_key(&secret, "did:plc:b").to_bytes());
assert_ne!(derive_key(&secret, "did:plc:a").to_bytes(), derive_key(&[6u8; 32], "did:plc:a").to_bytes());
}
#[cfg(feature = "net")]
#[tokio::test]
async fn readers_keep_only_labels_that_verify_against_the_declared_key() {
let k = key(3);
let mut keys = StaticLabelKeys::default();
keys.0.insert("did:plc:rulefeatures".into(), *k.verifying_key());
let good = sign(&label(), &k).unwrap();
let forged = sign(&label(), &key(4)).unwrap();
let mut stranger = sign(&label(), &k).unwrap();
stranger["src"] = json!("did:plc:stranger");
let kept = verified(&keys, &[good.clone(), forged, label()]).await.unwrap();
assert_eq!(kept, vec![good], "the forged and the unsigned label are dropped");
assert!(verified(&keys, &[stranger]).await.is_err(), "a source with no declared key holds the poll");
}
#[test]
fn a_did_doc_update_adds_the_labeler_and_keeps_the_rest() {
let k = key(2);
let recommended = json!({
"rotationKeys": ["did:key:zRot"],
"alsoKnownAs": ["at://rule-features.example"],
"verificationMethods": {"atproto": "did:key:zRepo"},
"services": {"atproto_pds": {"type": "AtprotoPersonalDataServer", "endpoint": "https://pds.example"}},
});
let update = did_doc_update(&recommended, "https://rules.example", k.verifying_key());
assert_eq!(update["rotationKeys"], json!(["did:key:zRot"]));
assert_eq!(update["alsoKnownAs"], json!(["at://rule-features.example"]));
assert_eq!(update["verificationMethods"]["atproto"], "did:key:zRepo");
assert_eq!(update["verificationMethods"]["atproto_label"], json!(did_key(k.verifying_key())));
assert_eq!(update["services"]["atproto_pds"]["endpoint"], "https://pds.example");
assert_eq!(update["services"]["atproto_labeler"], json!({"type": "AtprotoLabeler", "endpoint": "https://rules.example"}));
let did = "did:plc:rulefeatures";
let doc = json!({
"verificationMethod": [{"id": "#atproto_label", "publicKeyMultibase": multibase(k.verifying_key())}],
"service": [{"id": format!("{did}#atproto_labeler"), "type": "AtprotoLabeler", "serviceEndpoint": "https://rules.example/"}],
});
assert!(did_doc_declares(&doc, did, "https://rules.example", k.verifying_key()));
assert!(!did_doc_declares(&doc, did, "https://elsewhere.example", k.verifying_key()));
assert!(!did_doc_declares(&doc, did, "https://rules.example", key(3).verifying_key()));
}
#[test]
fn a_plc_token_is_found_in_mail_text() {
assert_eq!(plc_token_in("Your code: AB1CD-EF2GH. Thanks").as_deref(), Some("AB1CD-EF2GH"));
assert_eq!(plc_token_in("no code in ABCD-EFGHI here"), None);
assert_eq!(plc_token_in(""), None);
}
#[test]
fn keys_parse_only_secp256k1() {
assert_eq!(parse_key("did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo"), None, "a P-256 key");
assert_eq!(parse_key("not a key"), None);
let vk = *key(4).verifying_key();
assert_eq!(parse_key(&multibase(&vk)), Some(vk));
}
#[cfg(feature = "net")]
async fn serve(app: axum::Router) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
format!("http://{addr}")
}
#[cfg(feature = "net")]
#[tokio::test]
async fn label_keys_come_from_the_plc_document() {
let k = key(6);
let did = "did:plc:rulefeatures";
let doc = json!({"id": did, "verificationMethod": [{"id": "#atproto_label", "publicKeyMultibase": multibase(k.verifying_key())}]});
let plc = serve(axum::Router::new().route(
&format!("/{did}"),
axum::routing::get(move || {
let doc = doc.clone();
async move { axum::Json(doc) }
}),
))
.await;
let keys = DidDocLabelKeys::new(plc).unwrap();
assert_eq!(keys.label_key(did).await.unwrap(), Some(*k.verifying_key()));
assert_eq!(keys.label_key("did:plc:missing").await.unwrap(), None, "404: no document");
assert_eq!(keys.doc("did:key:zx").await.unwrap(), None, "unsupported method");
}
#[cfg(feature = "net")]
#[tokio::test]
async fn the_newest_plc_token_comes_from_mailpit() {
let app = axum::Router::new()
.route(
"/api/v1/search",
axum::routing::get(|| async {
axum::Json(json!({"messages": [
{"ID": "old", "Created": "2026-01-01T00:00:00Z"},
{"ID": "new", "Created": "2026-10-20T00:00:00Z"},
]}))
}),
)
.route(
"/api/v1/message/{id}",
axum::routing::get(|axum::extract::Path(id): axum::extract::Path<String>| async move {
let code = if id == "old" { "OLD11-OLD11" } else { "NEW22-NEW22" };
axum::Json(json!({"Text": format!("Your PLC code is {code}"), "Subject": "PLC"}))
}),
);
let base = serve(app).await;
let after = chrono::DateTime::parse_from_rfc3339("2026-10-01T00:00:00Z").unwrap().with_timezone(&chrono::Utc);
let token = plc_token_from_mailpit(&reqwest::Client::new(), &base, "a@example.test", after).await.unwrap();
assert_eq!(token.as_deref(), Some("NEW22-NEW22"));
}
}