1use std::collections::BTreeSet;
13
14pub use tesserax_keytree as keytree;
15
16pub mod grants {
18 pub const SEND: &str = "m4a.send_as";
19 pub const RECV: &str = "m4a.recv_inbox";
20 pub const LEGACY: &[(&str, &str)] = &[("send_as", SEND), ("recv_inbox", RECV)];
22
23 pub fn canonical(name: &str) -> &str {
24 LEGACY.iter().find(|(l, _)| *l == name).map(|(_, g)| *g).unwrap_or(name)
25 }
26 pub fn wire(name: &str) -> &str {
28 let g = canonical(name);
29 LEGACY.iter().find(|(_, x)| *x == g).map(|(l, _)| *l).unwrap_or(g)
30 }
31 pub fn is_m4a(name: &str) -> bool {
32 canonical(name).starts_with("m4a.")
33 }
34}
35
36pub mod env {
38 pub const SESSION_HANDLE: &str = "M4A_SESSION_HANDLE";
39 pub const SIGNER_SOCK: &str = "M4A_SIGNER_SOCK";
40}
41
42pub enum Authority {
44 Own(keytree::Authority),
46 Delegated,
48}
49
50impl Authority {
51 pub fn standalone() -> Self {
52 Self::Own(keytree::Authority::generate())
53 }
54 pub fn is_delegated(&self) -> bool {
55 matches!(self, Self::Delegated)
56 }
57
58 pub fn issue_session(&self, nick: &str, grants: &BTreeSet<String>) -> Result<keytree::Issued, keytree::Error> {
60 let Self::Own(root) = self else { return Err(keytree::Error::Denied) };
61 if !grants.iter().all(|g| grants::is_m4a(g)) {
62 return Err(keytree::Error::Denied);
63 }
64 let g: BTreeSet<String> = grants.iter().map(|g| grants::canonical(g).to_owned()).collect();
65 root.issue(&[("nick", nick)], nick, &g)
66 }
67
68 pub fn verify_session(&self, token: &str, op: &str) -> Result<String, keytree::Error> {
70 let Self::Own(root) = self else { return Err(keytree::Error::Denied) };
71 let v = root.verify(token, grants::canonical(op), "nick", &[])?;
72 v.facts.get("nick").cloned().ok_or(keytree::Error::BadToken)
73 }
74}
75
76pub fn letter_message(domain: &[u8], nick: &str, op: &str, payload: &[u8]) -> Vec<u8> {
78 keytree::domain_message(domain, &[nick.as_bytes(), grants::wire(op).as_bytes(), payload])
79}
80
81pub fn verify_letter(domain: &[u8], public_key: &[u8; 32], nick: &str, op: &str, payload: &[u8], signature: &[u8; 64]) -> bool {
83 keytree::verify_signature(public_key, &letter_message(domain, nick, op, payload), signature)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 const DOMAIN: &[u8] = b"test-domain/v1";
91
92 #[test]
93 fn letters_bind_nick_and_op() {
94 let h = keytree::Secret::random().signing_handle();
95 let sig = h.sign(&letter_message(DOMAIN, "a-1", grants::SEND, b"hi"));
96 assert!(verify_letter(DOMAIN, &h.public_key(), "a-1", "send_as", b"hi", &sig));
97 assert!(!verify_letter(DOMAIN, &h.public_key(), "a-2", grants::SEND, b"hi", &sig));
98 assert!(!verify_letter(b"other", &h.public_key(), "a-1", grants::SEND, b"hi", &sig));
99 }
100
101 #[test]
102 fn modes() {
103 let own = Authority::standalone();
104 let g: BTreeSet<String> = [grants::SEND.to_owned()].into();
105 let t = own.issue_session("a-1", &g).unwrap();
106 assert_eq!(own.verify_session(&t.token, "send_as").unwrap(), "a-1");
107 assert!(own.verify_session(&t.token, grants::RECV).is_err());
108 assert!(own.issue_session("a-1", &["node.uplink".to_owned()].into()).is_err());
109 let del = Authority::Delegated;
110 assert!(del.issue_session("a-1", &g).is_err(), "no root when delegated");
111 }
112}