1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
7 visible_slotmap_key!{ Id(b'k') }
9 static RESTRICTIONS: &str =
10 concat!("restrict,no-agent-forwarding,no-port-forwarding,",
11 "no-pty,no-user-rc,no-X11-forwarding");
13 static MAGIC_BANNER: &str =
14 "# WARNING - FILE AUTOMATICALLY GENERATED BY OTTER - DO NOT EDIT";
16 #[derive(Copy,Clone,Serialize,Deserialize)]
17 #[derive(Eq,PartialEq,Hash,Ord,PartialOrd)]
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]);
24 // States of a key wrt a particular scope:
26 // PerScope in authorized_keys leftover key
27 // core file a.k._dirty refcount==0
28 // if us only core file
30 // ABSENT - - maybe[1] - - -
31 // GARBAGE - - maybe[1] - - y [2]
32 // **undesriable** - - - 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
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
44 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
46 keys: DenseSlotMap<Id, Key>,
48 #[serde(skip)] fps: Option<FingerprintMap>,
50 type FingerprintMap = HashMap<Arc<Fingerprint>, Id>;
52 #[derive(Debug,Clone,Serialize,Deserialize)]
57 #[serde(skip)] fp: Option<Result<Arc<Fingerprint>, KeyError>>,
60 #[derive(Debug,Clone,Serialize,Deserialize,Default)]
62 authorised: Vec<Option<ScopeKey>>,
64 #[derive(Debug,Clone,Serialize,Deserialize)]
66 id: Id, // owns a refcount
70 #[derive(Debug,Clone,Serialize,Deserialize)]
71 #[derive(Eq,PartialEq,Hash,Ord,PartialOrd)]
74 pub nonce: sshkeys::Nonce,
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 { }
87 impl From<AuthKeysManipError> for MgmtError {
88 fn from(akme: AuthKeysManipError) -> MgmtError {
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;
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);
106 // In nssh authorized keys firmat. No options, no comment.
107 #[derive(Debug,Clone,Serialize,Deserialize)]
108 #[serde(transparent)]
109 pub struct PubData(String);
111 #[derive(Debug,Clone,Serialize,Deserialize)]
112 #[serde(transparent)]
113 pub struct Comment(String /* must not contain newline/cr */);
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);
119 #[derive(Error,Debug,Clone,Serialize,Deserialize)]
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,
126 impl From<OpenSSHKeyError> for KeyError {
127 fn from(e: OpenSSHKeyError) -> Self { KeyError::BadData(e.to_string()) }
130 impl Display for Comment {
131 #[throws(fmt::Error)]
132 fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
134 impl Display for PubData {
135 #[throws(fmt::Error)]
136 fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
141 pub fn parse(&self) -> (PubData, Comment) {
142 let openssh_keys::PublicKey {
143 options:_, data, comment
145 let data = openssh_keys::PublicKey {
150 let data = PubData(data.to_string());
151 if data.0.chars().any(|c| c !=' ' && c.is_whitespace()) {
152 throw!(KeyError::Whitespace);
154 (data, Comment(comment.unwrap_or_default()))
160 pub fn fingerprint(&self) -> Fingerprint {
161 let k: openssh_keys::PublicKey = self.0.parse()?;
162 Fingerprint(k.fingerprint())
166 impl Display for Fingerprint {
167 #[throws(fmt::Error)]
168 fn fmt(&self, f: &mut Formatter) { write!(f, "{}", self.0)? }
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) {
179 fmt_hex(f, &self.0)?;
184 impl FromStr for KeySpec {
185 type Err = anyhow::Error;
186 #[throws(anyhow::Error)]
187 fn from_str(s: &str) -> KeySpec {
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")?
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"))?)
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 {
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;
218 // We have checked id and nonce, against those allowed
219 let auth = auth_in.so_promise();
228 #[derive(Debug,Clone,Serialize,Deserialize)]
229 pub struct MgmtKeyReport {
232 pub comment: Comment,
233 pub problem: Option<KeyError>,
236 impl Display for KeySpec {
237 #[throws(fmt::Error)]
238 fn fmt(&self, f: &mut fmt::Formatter) {
239 write!(f, "{}:{}", self.id, &self.nonce)?;
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)?;
249 write!(f, "{} {} # {}", &self.data, &self.comment, &self.key)?;
253 macro_rules! def_pskeys_get {
254 ($trait:ident, $f:ident, $get:ident, $($mut:tt)?) => {
256 impl DenseSlotMap<AccountId, AccountRecord> {
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)
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.
267 & $($mut)? record.ssh_keys
273 def_pskeys_get!{ RecordsExtImm, pskeys_get, get , }
274 def_pskeys_get!{ RecordsExtMut, pskeys_mut, get_mut, mut }
276 type Auth = Authorisation<AccountScope>;
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)?;
286 if gl.authkeys_dirty { Some(KeyError::Dirty) }
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() };
296 nonce: key.nonce.clone(),
298 data: key.data.clone(),
299 comment: sk.comment.clone(),
300 problem: problem.cloned(),
306 // not a good idea to speicfy a problem, but "whatever"
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);
317 let fpe = gl.fps.as_mut().unwrap().entry(fp.clone());
320 let keys = &mut gl.keys;
321 *fpe.or_insert_with(||{
325 nonce: Nonce(thread_rng().gen()),
326 fp: Some(Ok(fp.clone())),
331 let key = gl.keys.get_mut(id)
332 .ok_or_else(|| internal_error_bydebug(&(id, &fp)))?;
335 let new_sk = Some(ScopeKey { id, comment });
337 if let Some((index,_)) = ps.authorised.iter()
338 .find_position(|sk| sk.is_none())
340 ps.authorised[index] = new_sk;
343 let index = ps.authorised.len();
344 ps.authorised.push(new_sk);
347 gl.authkeys_dirty = true;
349 self.save_accounts_now()?;
351 self.get_mut().ssh_keys.rewrite_authorized_keys()?;
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)?;
363 throw!(ME::InvalidSshKeyId);
365 match ps.authorised.get(index) {
366 Some(&Some(ScopeKey{id:tid,..})) if tid == id => { },
367 _ => throw!(ME::SshKeyNotFound), /* [ABSEMT..GARBAGE] */
369 let key = gl.keys.get_mut(id).ok_or_else(|| internal_logic_error(
370 format!("corrupted accounts db: key id {} missing", id)))?;
372 // [UNSAVED..PRESENT]
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()
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!");
390 if ! matches!(r, Ok(Ok(()))) {
392 ps.authorised[index] = previously;
393 // [UNSAVED..PRESENT]
395 Err(payload) => std::panic::resume_unwind(payload),
396 Ok(Err(e)) => throw!(e),
397 Ok(Ok(())) => panic!(), // handled that earlier
402 if key.refcount == 0 {
403 let key = gl.keys.remove(id).unwrap();
404 if let Some(Ok(ref fp)) = key.fp {
413 fn fps(&mut self) -> &mut FingerprintMap {
414 let keys = &mut self.keys;
415 self.fps.get_or_insert_with(||{
417 let mut fps = FingerprintMap::default();
418 let mut garbage = vec![];
420 for (id, key) in keys.iter_mut() {
421 if key.refcount == 0 { garbage.push(id); continue; }
425 let data = &key.data;
426 key.fp.get_or_insert_with(
427 || Ok(Arc::new(data.fingerprint()?))
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());
445 for id in garbage { keys.remove(id); }
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 }
458 r#"{},command="{} mgmtchannel-proxy --restrict-ssh {}:{}" {} {}:{}"#,
460 &config().ssh_proxy_bin, id, key.nonce,
463 .context("write new auth keys")?;
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;
476 let staticf = match File::open(include) {
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")),
484 let mut f = match File::open(path) {
485 Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
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 {
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")? {
503 "first line is not as expected (manually written/edited?) \
504 (before first run, make static include be a hardlink to real file)"
507 .context("check authorized_keys magic/banner")?;
509 let mut f = fs::OpenOptions::new()
510 .write(true).truncate(true).create(true)
513 .context("open new auth keys file (.tmp)")?;
516 let mut f = BufWriter::new(&mut f);
517 writeln!(f, "{}", MAGIC_BANNER)?;
518 writeln!(f, "# You can edit {:?} instead - that is included here:",
521 Ok::<_,io::Error>(())
522 })().context("write header (to .tmp)")?;
524 if let Some(mut sf) = staticf {
525 io::copy(&mut sf, &mut f).context("copy data into new auth keys")?;
526 writeln!(f).context("write newline into new auth keys")?;
529 let mut f = BufWriter::new(f);
530 self.write_keys(&mut f)?;
531 f.flush().context("finish writing new auth keys")?;
533 fs::rename(&tmp, &path).with_context(|| path.clone())
534 .context("install new auth keys")?;
536 self.authkeys_dirty = false;