1use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18use sha2::{Digest, Sha256};
19
20pub struct Issuer {
22 key: [u8; 32],
23}
24
25impl std::fmt::Debug for Issuer {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.write_str("Issuer { key: [redacted] }")
29 }
30}
31
32impl Issuer {
33 #[must_use]
35 pub fn new() -> Option<Self> {
36 let mut key = [0u8; 32];
37 getrandom::fill(&mut key).ok()?;
38 Some(Self { key })
39 }
40
41 fn mac(&self, root: &Path, argvs: &[Vec<String>], interactive: bool) -> String {
42 const BLOCK: usize = 64;
43 let mut k = [0u8; BLOCK];
44 k[..self.key.len()].copy_from_slice(&self.key);
45 let message = serde_json::to_vec(&(root.display().to_string(), argvs, interactive))
46 .unwrap_or_default();
47 let pad = |b: u8| k.iter().map(|x| x ^ b).collect::<Vec<u8>>();
48 let inner = Sha256::new()
49 .chain_update(pad(0x36))
50 .chain_update(&message)
51 .finalize();
52 let outer = Sha256::new()
53 .chain_update(pad(0x5c))
54 .chain_update(inner)
55 .finalize();
56 outer.iter().map(|b| format!("{b:02x}")).collect()
57 }
58
59 #[must_use]
62 pub fn issued(
63 &self,
64 root: &Path,
65 argvs: &[Vec<String>],
66 interactive: bool,
67 token: &str,
68 ) -> bool {
69 !argvs.is_empty()
70 && argvs.iter().all(|a| !a.is_empty())
71 && crate::security::constant_time_eq(
72 self.mac(root, argvs, interactive).as_bytes(),
73 token.as_bytes(),
74 )
75 }
76
77 pub fn signing<T>(self: &Arc<Self>, root: PathBuf, f: impl FnOnce() -> T) -> T {
80 let issuer = Arc::clone(self);
81 speccoder_core::step::signing(
82 std::rc::Rc::new(move |argvs: &[Vec<String>], interactive: bool| {
83 issuer.mac(&root, argvs, interactive)
84 }),
85 f,
86 )
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use speccoder_core::step::{How, Step, person};
93
94 use super::*;
95
96 fn s(v: &[&str]) -> Vec<String> {
97 v.iter().map(|w| (*w).to_owned()).collect()
98 }
99
100 fn token_of(step: &Step) -> String {
101 match &step.how {
102 How::Command { token: Some(t), .. } => t.clone(),
103 other => panic!("{other:?}"),
104 }
105 }
106
107 #[test]
108 fn only_what_it_signed_runs() {
109 let issuer = Arc::new(Issuer::new().unwrap_or_else(|| panic!("no random source")));
110 let root = Path::new("/p");
111 let step = issuer.signing(root.to_path_buf(), || person(vec![s(&["npm", "install"])]));
112 let token = token_of(&step);
113 assert_eq!(token.len(), 64);
114 assert!(issuer.issued(root, &[s(&["npm", "install"])], false, &token));
115 assert!(!issuer.issued(root, &[s(&["npm", "install", "evil"])], false, &token));
118 assert!(!issuer.issued(Path::new("/q"), &[s(&["npm", "install"])], false, &token));
119 assert!(!issuer.issued(root, &[s(&["npm", "install"])], true, &token));
120 let other = Issuer::new().unwrap_or_else(|| panic!("no random source"));
121 assert!(!other.issued(root, &[s(&["npm", "install"])], false, &token));
122 assert!(!issuer.issued(root, &[s(&["npm", "install"])], false, ""));
123 assert!(!issuer.issued(root, &[], false, &issuer.mac(root, &[], false)));
124 let login = issuer.signing(root.to_path_buf(), || {
126 person(vec![s(&["copilot", "login"])])
127 });
128 let t = token_of(&login);
129 assert!(issuer.issued(root, &[s(&["copilot", "login"])], true, &t));
130 assert!(!issuer.issued(root, &[s(&["copilot", "login"])], false, &t));
131 }
132
133 #[test]
134 fn json_from_elsewhere_is_never_signed() {
135 let issuer = Arc::new(Issuer::new().unwrap_or_else(|| panic!("no random source")));
136 let v = issuer.signing(PathBuf::from("/p"), || {
137 serde_json::from_str::<serde_json::Value>(
138 r#"{"how":"command","argvs":[["rm","-rf","/"]],"interactive":false}"#,
139 )
140 .unwrap_or_default()
141 });
142 assert!(v.get("token").is_none());
143 assert!(!format!("{issuer:?}").contains(&format!("{:?}", issuer.key)));
144 }
145}