chiark / gitweb /
ssh keys update: Log when we find the hardlink situation
[otter.git] / src / sshkeys.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 visible_slotmap_key!{ Id(b'k') }
8
9 static RESTRICTIONS: &str =
10   concat!("restrict,no-agent-forwarding,no-port-forwarding,",
11           "no-pty,no-user-rc,no-X11-forwarding");
12
13 static MAGIC_BANNER: &str = 
14   "# WARNING - FILE AUTOMATICALLY GENERATED BY OTTER - DO NOT EDIT";
15
16 #[derive(Copy,Clone,Serialize,Deserialize)]
17 #[derive(Eq,PartialEq,Hash,Ord,PartialOrd)]
18 #[serde(transparent)]
19 // This will detecte if the slotmap in Accounts gets rewound, without
20 // updating the authorzed keys.  That might reuse Id values but it
21 // can't reuse Nonces.
22 pub struct Nonce([u8; 32]);
23
24 // States of a key wrt a particular scope:
25 //
26 //                   PerScope       in authorized_keys     leftover key
27 //                   core    file           a.k._dirty     refcount==0
28 //                                       if us only        core   file
29 //
30 //    ABSENT         -       -         maybe[1] -          -      -
31 //    GARBAGE        -       -         maybe[1] -          -      y [2]
32 // **undesriable**   -       -         -                   y      -
33 //  **illegal**      -       y
34 //    UNSAVED        y       -         maybe    true       n/a    n/a
35 //    BROKEN         y       y         maybe    true       n/a    n/a
36 //    PRESENT        y       y         y        -          n/a    n/a
37 //
38 // [1] garbage in the authorised_keys is got rid of next time we
39 //     write it, so it does not persist indefinitely.
40 // [2] garbage in Global is deleted when we do the key hashing (which
41 //     iterates over all keys), which will happen before we add any
42 //     key.
43
44 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
45 pub struct Global {
46   keys: DenseSlotMap<Id, Key>,
47   authkeys_dirty: bool,
48   #[serde(skip)] fps: Option<FingerprintMap>,
49 }
50 type FingerprintMap = HashMap<Arc<Fingerprint>, Id>;
51
52 #[derive(Debug,Clone,Serialize,Deserialize)]
53 pub struct Key {
54   refcount: usize,
55   data: PubData,
56   nonce: Nonce,
57   #[serde(skip)] fp: Option<Result<Arc<Fingerprint>, KeyError>>,
58 }
59
60 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
61 pub struct PerScope {
62   authorised: Vec<Option<ScopeKey>>,
63 }
64 #[derive(Debug,Clone,Serialize,Deserialize)]
65 pub struct ScopeKey {
66   id: Id, // owns a refcount
67   comment: Comment,
68 }
69
70 #[derive(Debug,Clone,Serialize,Deserialize)]
71 #[derive(Eq,PartialEq,Hash,Ord,PartialOrd)]
72 pub struct KeySpec {
73   pub id: sshkeys::Id,
74   pub nonce: sshkeys::Nonce,
75 }
76
77 #[derive(Error,Copy,Clone,Debug,Hash,Serialize,Deserialize)]
78 #[error("ssh authorized_keys manipulation failed")]
79 pub struct AuthKeysManipError { }
80 impl From<anyhow::Error> for AuthKeysManipError {
81   fn from(ae: anyhow::Error) -> AuthKeysManipError {
82     error!("authorized_keys manipulation error: {}: {}",
83            &config().authorized_keys, ae.d());
84     AuthKeysManipError { }
85   }
86 }
87 impl From<AuthKeysManipError> for MgmtError {
88   fn from(akme: AuthKeysManipError) -> MgmtError {
89     IE::from(akme).into()
90   }
91 }
92
93 mod veneer {
94   // openssh_keys's API is a little odd.  We make our own mini-API.
95   use crate::prelude::*;
96   extern crate openssh_keys;
97   use openssh_keys::errors::OpenSSHKeyError;
98
99   // A line in nssh authorized keys firmat.  Might have comment or
100   // options.  Might or might not have a newline.  String inside
101   // this newtype has not necessarily been syntax checked.
102   #[derive(Debug,Clone,Serialize,Deserialize)]
103   #[serde(transparent)]
104   pub struct AuthkeysLine(pub String);
105
106   // In nssh authorized keys firmat.  No options, no comment.
107   #[derive(Debug,Clone,Serialize,Deserialize)]
108   #[serde(transparent)]
109   pub struct PubData(String);
110
111   #[derive(Debug,Clone,Serialize,Deserialize)]
112   #[serde(transparent)]
113   pub struct Comment(String /* must not contain newline/cr */);
114
115   // Not Serialize,Deserialize, so we can change the hash, etc.
116   #[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
117   pub struct Fingerprint(String);
118
119   #[derive(Error,Debug,Clone,Serialize,Deserialize)]
120   pub enum KeyError {
121     #[error("bad key data: {0}")]                        BadData(String),
122     #[error("whitespace in public key data!")]           Whitespace,
123     #[error("failed to save key data, possibly broken")] Dirty,
124   }
125
126   impl From<OpenSSHKeyError> for KeyError {
127     fn from(e: OpenSSHKeyError) -> Self { KeyError::BadData(e.to_string()) }
128   }
129
130   impl Display for Comment {
131     #[throws(fmt::Error)]
132     fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
133   }
134   impl Display for PubData {
135     #[throws(fmt::Error)]
136     fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
137   }
138
139   impl AuthkeysLine {
140     #[throws(KeyError)]
141     pub fn parse(&self) -> (PubData, Comment) {
142       let openssh_keys::PublicKey {
143         options:_, data, comment
144       } = self.0.parse()?;
145       let data = openssh_keys::PublicKey {
146         data,
147         options: None,
148         comment: None,
149       };
150       let data = PubData(data.to_string());
151       if data.0.chars().any(|c| c !=' ' && c.is_whitespace()) {
152         throw!(KeyError::Whitespace);
153       }
154       (data, Comment(comment.unwrap_or_default()))
155     }
156   }
157
158   impl PubData {
159     #[throws(KeyError)]
160     pub fn fingerprint(&self) -> Fingerprint {
161       let k: openssh_keys::PublicKey = self.0.parse()?;
162       Fingerprint(k.fingerprint())
163     }
164   }
165
166   impl Display for Fingerprint {
167     #[throws(fmt::Error)]
168     fn fmt(&self, f: &mut Formatter) { write!(f, "{}", self.0)? }
169   }
170
171 }
172 pub use veneer::*;
173
174 format_by_fmt_hex!{Display, for Nonce, .0}
175 impl Debug for Nonce {
176   #[throws(fmt::Error)]
177   fn fmt(&self, f: &mut Formatter) {
178     write!(f,"Nonce[")?;
179     fmt_hex(f, &self.0)?;
180     write!(f,"]")?;
181   }
182 }
183
184 impl FromStr for KeySpec {
185   type Err = anyhow::Error;
186   #[throws(anyhow::Error)]
187   fn from_str(s: &str) -> KeySpec {
188     (||{
189       let (id, nonce) = s.split_once(':')
190         .ok_or_else(|| anyhow!("missing `:`"))?;
191       let id    = id.try_into().context("bad id")?;
192       let nonce = nonce.parse().context("bad nonce")?;
193       Ok::<_,AE>(KeySpec { id, nonce })
194     })().context("failed to parse ssh key spec")?
195   }
196 }
197
198 impl FromStr for Nonce {
199   type Err = anyhow::Error;
200   #[throws(anyhow::Error)]
201   fn from_str(s: &str) -> Nonce {
202     Nonce(parse_fixed_hex(s).ok_or_else(|| anyhow!("bad nonce syntax"))?)
203   }
204 }
205
206 impl PerScope {
207   pub fn check(&self, ag: &AccountsGuard, authed_key: &KeySpec,
208                auth_in: Authorisation<KeySpec>)
209                -> Option<Authorisation<AccountScope>> {
210     let gl = &ag.get().ssh_keys;
211     for sk in &self.authorised {
212       if_chain!{
213         if let Some(sk) = sk;
214         if sk.id == authed_key.id;
215         if let Some(want_key) = gl.keys.get(sk.id);
216         if &want_key.nonce == &authed_key.nonce;
217         then {
218           // We have checked id and nonce, against those allowed
219           let auth = auth_in.so_promise();
220           return Some(auth);
221         }
222       }
223     }
224     None
225   }
226 }
227
228 #[derive(Debug,Clone,Serialize,Deserialize)]
229 pub struct MgmtKeyReport {
230   pub key: KeySpec,
231   pub data: PubData,
232   pub comment: Comment,
233   pub problem: Option<KeyError>,
234 }
235
236 impl Display for KeySpec {
237   #[throws(fmt::Error)]
238   fn fmt(&self, f: &mut fmt::Formatter) {
239     write!(f, "{}:{}", self.id, &self.nonce)?;
240   }
241 }
242
243 impl Display for MgmtKeyReport {
244   #[throws(fmt::Error)]
245   fn fmt(&self, f: &mut fmt::Formatter) {
246     if let Some(problem) = &self.problem {
247       write!(f, "# PROBLEM {} # ", &problem)?;
248     }
249     write!(f, "{} {} # {}", &self.data, &self.comment, &self.key)?;
250   }
251 }
252
253 macro_rules! def_pskeys_get {
254   ($trait:ident, $f:ident, $get:ident, $($mut:tt)?) => {
255     #[ext(name=$trait)]
256     impl DenseSlotMap<AccountId, AccountRecord> {
257       #[throws(MgmtError)]
258       fn $f(& $($mut)? self, acctid: AccountId) -> & $($mut)? PerScope {
259         let record = self.$get(acctid).ok_or(AccountNotFound)?;
260         if ! record.account.subaccount.is_empty() {
261           throw!(ME::NoSshKeysForSubaccount)
262         }
263         // We do *not* check that the account is of scope kind Ssh.
264         // Installing ssh keys for other scopes is fine.
265         // otter(1) will check this.
266
267         & $($mut)? record.ssh_keys
268       }
269     }
270   }
271 }
272
273 def_pskeys_get!{ RecordsExtImm, pskeys_get, get    ,     }
274 def_pskeys_get!{ RecordsExtMut, pskeys_mut, get_mut, mut }
275
276 type Auth = Authorisation<AccountScope>;
277
278 impl AccountsGuard {
279   #[throws(MgmtError)]
280   pub fn sshkeys_report(&self, acctid: AccountId, _:Auth)
281                         -> Vec<MgmtKeyReport> {
282     let accounts = self.get();
283     let gl = &accounts.ssh_keys;
284     let ps = &accounts.records.pskeys_get(acctid)?;
285     let dirty_error =
286       if gl.authkeys_dirty { Some(KeyError::Dirty) }
287       else { None };
288     ps.authorised.iter().filter_map(|sk| Some({
289       let sk = sk.as_ref()?;
290       let key = gl.keys.get(sk.id)?;
291       let problem = if let Some(Err(ref e)) = key.fp { Some(e) }
292                     else { dirty_error.as_ref() };
293       MgmtKeyReport {
294         key: KeySpec {
295         id:      sk.id,
296         nonce:   key.nonce.clone(),
297         },
298         data:    key.data.clone(),
299         comment: sk.comment.clone(),
300         problem: problem.cloned(),
301       }
302     }))
303       .collect()
304   }
305
306   // not a good idea to speicfy a problem, but "whatever"
307   #[throws(ME)]
308   pub fn sshkeys_add(&mut self, acctid: AccountId,
309                      new_akl: AuthkeysLine, _:Auth) -> (usize, Id) {
310     let accounts = self.get_mut();
311     let gl = &mut accounts.ssh_keys;
312     let ps = accounts.records.pskeys_mut(acctid)?;
313     let (data, comment) = new_akl.parse()?;
314     let fp = data.fingerprint().map_err(KeyError::from)?;
315     let fp = Arc::new(fp);
316     let _ = gl.fps();
317     let fpe = gl.fps.as_mut().unwrap().entry(fp.clone());
318     // ABSENT
319     let id = {
320       let keys = &mut gl.keys;
321       *fpe.or_insert_with(||{
322         keys.insert(Key {
323           data,
324           refcount: 0,
325           nonce: Nonce(thread_rng().gen()),
326           fp: Some(Ok(fp.clone())),
327         })
328       })
329     };
330     // **undesirable**
331     let key = gl.keys.get_mut(id)
332       .ok_or_else(|| internal_error_bydebug(&(id, &fp)))?;
333     // GARBAGE
334     key.refcount += 1;
335     let new_sk = Some(ScopeKey { id, comment });
336     let index =
337       if let Some((index,_)) = ps.authorised.iter()
338           .find_position(|sk| sk.is_none())
339       {
340         ps.authorised[index] = new_sk;
341         index
342       } else {
343         let index = ps.authorised.len();
344         ps.authorised.push(new_sk);
345         index
346       };
347     gl.authkeys_dirty = true;
348     // UNSAVED
349     self.save_accounts_now()?;
350     // BROKEN
351     self.get_mut().ssh_keys.rewrite_authorized_keys()?;
352     // PRESENT
353     (index, id)
354   }
355
356   #[throws(ME)]
357   pub fn sshkeys_remove(&mut self, acctid: AccountId,
358                         index: usize, id: Id, _:Auth) {
359     let accounts = self.get_mut();
360     let gl = &mut accounts.ssh_keys;
361     let ps = accounts.records.pskeys_mut(acctid)?;
362     if id == default() {
363       throw!(ME::InvalidSshKeyId);
364     }
365     match ps.authorised.get(index) {
366       Some(&Some(ScopeKey{id:tid,..})) if tid == id => { },
367       _ => throw!(ME::SshKeyNotFound), /* [ABSEMT..GARBAGE] */
368     }
369     let key = gl.keys.get_mut(id).ok_or_else(|| internal_logic_error(
370       format!("corrupted accounts db: key id {} missing", id)))?;
371
372     // [UNSAVED..PRESENT]
373     
374     key.refcount -= 1;
375     let previously = mem::take(&mut ps.authorised[index]);
376     // Now **illegal**, briefly, don't leave it like this!  No-one can
377     // observe the illegal state since we have the accounts lock.  If
378     // we abort, the in-core version vanishes, leaving a legal state.
379     let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
380       || self.save_accounts_now()
381     ));
382
383     // Must re-borrow everything since save_accounts_now needed a
384     // reference to all of it.
385     let accounts = self.get_mut();
386     let gl = &mut accounts.ssh_keys;
387     let ps = accounts.records.pskeys_mut(acctid).expect("aargh!");
388     let key = gl.keys.get_mut(id).expect("aargh!");
389
390     if ! matches!(r, Ok(Ok(()))) {
391       key.refcount += 1;
392       ps.authorised[index] = previously;
393       // [UNSAVED..PRESENT]
394       match r {
395         Err(payload) => std::panic::resume_unwind(payload),
396         Ok(Err(e)) => throw!(e),
397         Ok(Ok(())) => panic!(), // handled that earlier
398       }
399     }
400     // [ABSENT..GARBAGE]
401
402     if key.refcount == 0 {
403       let key = gl.keys.remove(id).unwrap();
404       if let Some(Ok(ref fp)) = key.fp {
405         gl.fps().remove(fp);
406       }
407     }
408     // ABSENT
409   }
410 }
411
412 impl Global {
413   fn fps(&mut self) -> &mut FingerprintMap {
414     let keys = &mut self.keys;
415     self.fps.get_or_insert_with(||{
416
417       let mut fps = FingerprintMap::default();
418       let mut garbage = vec![];
419
420       for (id, key) in keys.iter_mut() {
421         if key.refcount == 0 { garbage.push(id); continue; }
422
423         if_let!{
424           Ok(fp) = {
425             let data = &key.data;
426             key.fp.get_or_insert_with(
427               || Ok(Arc::new(data.fingerprint()?))
428             )
429           };
430           else continue;
431         }
432
433         use hash_map::Entry::*;
434         match fps.entry(fp.clone()) {
435           Vacant(ve) => { ve.insert(id); },
436           Occupied(mut oe) => {
437             error!("ssh key fingerprint collision! \
438                     fp={} newid={} oldid={} newdata={:?}",
439                    &fp, id, oe.get(), &key.data);
440             oe.insert(Id::default());
441           },
442         }
443       }
444
445       for id in garbage { keys.remove(id); }
446
447       fps
448
449     })
450   }
451
452   #[throws(AuthKeysManipError)]
453   fn write_keys(&self, w: &mut BufWriter<File>) {
454     for (id, key) in &self.keys {
455       let fp = match key.fp { Some(Ok(ref fp)) => fp, _ => continue };
456       if key.refcount == 0 { continue }
457       writeln!(w,
458  r#"{},command="{} mgmtchannel-proxy --restrict-ssh {}:{}" {} {}:{}"#, 
459                RESTRICTIONS,
460                &config().ssh_proxy_bin, id, key.nonce,
461                &key.data,
462                key.refcount, &fp)
463         .context("write new auth keys")?;
464     }
465   }
466
467   // Caller should make sure accounts are saved first, to avoid
468   // getting the authkeys_dirty bit wrong.
469   #[throws(AuthKeysManipError)]
470   fn rewrite_authorized_keys(&mut self) {
471     let config = config();
472     let path = &config.authorized_keys;
473     let tmp = format!("{}.tmp", &path);
474     let include = &config.authorized_keys_include;
475
476     let staticf = match File::open(include) {
477       Ok(y) => Some(y),
478       Err(e) if e.kind() == ErrorKind::NotFound => None,
479       Err(e) => throw!(AE::from(e).context(include.clone())
480                        .context("open static auth keys")),
481     };
482
483     (||{
484       let mut f = match File::open(path) {
485         Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
486         x => x,
487       }.context("open")?;
488
489       let l = BufReader::new(&mut f).lines().next()
490         .ok_or_else(|| anyhow!("no first line!"))?
491         .context("read first line")?;
492       if l == MAGIC_BANNER {
493         return Ok(());
494       }
495       if let Some(staticf) = &staticf {
496         let devino = |f: &File| f.metadata().map(|m| (m.dev(), m.ino()));
497         if devino(staticf).context("fstat static auth keys")? ==
498            devino(&f).context("fstat existing auth keys")? {
499              info!("auth keys files hardlinked, doing first install");
500              return Ok(());
501            }
502       }
503       Err(anyhow!(
504           "first line is not as expected (manually written/edited?) \
505            (before first run, make static include be a hardlink to real file)"
506       ))
507     })()
508       .context("check authorized_keys magic/banner")?;
509
510     let mut f = fs::OpenOptions::new()
511       .write(true).truncate(true).create(true)
512       .mode(0o644)
513       .open(&tmp)
514       .context("open new auth keys file (.tmp)")?;
515
516     (||{
517       let mut f = BufWriter::new(&mut f);
518       writeln!(f, "{}", MAGIC_BANNER)?;
519       writeln!(f, "# You can edit {:?} instead - that is included here:",
520                include)?;
521       f.flush()?;
522       Ok::<_,io::Error>(())
523     })().context("write header (to .tmp)")?;
524
525     if let Some(mut sf) = staticf {
526       io::copy(&mut sf, &mut f).context("copy data into new auth keys")?;
527       writeln!(f).context("write newline into new auth keys")?;
528     }
529
530     let mut f = BufWriter::new(f);
531     self.write_keys(&mut f)?;
532     f.flush().context("finish writing new auth keys")?;
533
534     fs::rename(&tmp, &path).with_context(|| path.clone())
535       .context("install new auth keys")?;
536
537     self.authkeys_dirty = false;
538   }
539 }