pub shapelibs: Option<Vec<shapelib::Config1>>,
pub specs_dir: Option<String>,
pub sendmail: Option<String>,
+ pub ssh_proxy_bin: Option<String>,
+ pub authorized_keys: Option<String>,
+ pub authorized_keys_include: Option<String>,
pub debug_js_inject_file: Option<String>,
#[serde(default)] pub fake_rng: FakeRngSpec,
/// Disable this for local testing only. See LICENCE.
pub shapelibs: Vec<shapelib::Config1>,
pub specs_dir: String,
pub sendmail: String,
+ pub ssh_proxy_bin: String,
+ pub authorized_keys: String,
+ pub authorized_keys_include: String,
pub debug_js_inject: Arc<String>,
pub check_bundled_sources: bool,
pub game_rng: RngWrap,
template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir, usvg_bin,
log, bundled_sources, shapelibs, sendmail,
debug_js_inject_file, check_bundled_sources, fake_rng,
+ ssh_proxy_bin, authorized_keys, authorized_keys_include,
} = self;
let game_rng = fake_rng.make_game_rng();
+ let home = || env::var("HOME").context("HOME");
let prctx;
if let Some(ref cd) = change_directory {
specd.unwrap_or_else(|| format!("{}/{}", &libexec_dir, leaf))
};
let usvg_bin = in_libexec(usvg_bin, "usvg" );
+ let ssh_proxy_bin = in_libexec(ssh_proxy_bin,"otter-ssh-proxy" );
+
+ let authorized_keys = if let Some(ak) = authorized_keys { ak } else {
+ let home = home().context("for authorized_keys")?;
+ // we deliberately don't create the ~/.ssh dir
+ format!("{}/.ssh/authorized_keys", home)
+ };
+ let authorized_keys_include = authorized_keys_include.unwrap_or_else(
+ || format!("{}.static", authorized_keys)
+ );
let shapelibs = shapelibs.unwrap_or_else(||{
let glob = defpath(None, DEFAULT_LIBRARY_GLOB);
template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir,
bundled_sources, shapelibs, sendmail, usvg_bin,
debug_js_inject, check_bundled_sources, game_rng, prctx,
+ ssh_proxy_bin, authorized_keys, authorized_keys_include,
};
trace_dbg!("config resolved", &server);
Ok(WholeServerConfig {
--- /dev/null
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+visible_slotmap_key!{ Id(b'k') }
+
+static RESTRICTIONS: &str =
+ concat!("restrict,no-agent-forwarding,no-port-forwarding,",
+ "no-pty,no-user-rc,no-X11-forwarding");
+
+static MAGIC_BANNER: &str =
+ "# WARNING - AUTOMATICALLY GENERAED FILE - DO NOT EDIT\n";
+
+#[derive(Copy,Clone,Serialize,Deserialize,Eq,PartialEq)]
+#[serde(transparent)]
+// This will detecte if the slotmap in Accounts gets rewound, without
+// updating the authorzed keys. That might reuse Id values but it
+// can't reuse Nonces.
+pub struct Nonce([u8; 32]);
+
+// States of a key wrt a particular scope:
+//
+// PerScope in authorized_keys leftover key
+// core file a.k._dirty refcount==0
+// if us only core file
+//
+// ABSENT - - maybe[1] - - -
+// GARBAGE - - maybe[1] - - y [2]
+// **undesriable** - - - y -
+// **illegal** - y
+// UNSAVED y - maybe true n/a n/a
+// BROKEN y y maybe true n/a n/a
+// PRESENT y y y - n/a n/a
+//
+// [1] garbage in the authorised_keys is got rid of next time we
+// write it, so it does not persist indefinitely.
+// [2] garbage in Global is deleted when we do the key hashing (which
+// iterates over all keys), which will happen before we add any
+// key.
+
+#[derive(Debug,Clone,Serialize,Deserialize,Default)]
+pub struct Global {
+ keys: DenseSlotMap<Id, Key>,
+ authkeys_dirty: bool,
+ #[serde(skip)] fps: Option<FingerprintMap>,
+}
+type FingerprintMap = HashMap<Arc<Fingerprint>, Id>;
+
+#[derive(Debug,Clone,Serialize,Deserialize)]
+pub struct Key {
+ refcount: usize,
+ data: PubData,
+ nonce: Nonce,
+ #[serde(skip)] fp: Option<Result<Arc<Fingerprint>, KeyError>>,
+}
+
+#[derive(Debug,Clone,Serialize,Deserialize,Default)]
+pub struct PerScope {
+ authorised: Vec<Option<ScopeKey>>,
+}
+#[derive(Debug,Clone,Serialize,Deserialize)]
+pub struct ScopeKey {
+ id: Id, // owns a refcount
+ comment: Comment,
+}
+
+mod veneer {
+ // openssh_keys's API is a little odd. We make our own mini-API.
+ use crate::prelude::*;
+ extern crate openssh_keys;
+ use openssh_keys::errors::OpenSSHKeyError;
+
+ // A line in nssh authorized keys firmat. Might have comment or
+ // options. Might or might not have a newline. String inside
+ // this newtype has not necessarily been syntax checked.
+ #[derive(Debug,Clone,Serialize,Deserialize)]
+ #[serde(transparent)]
+ pub struct AuthkeysLine(pub String);
+
+ // In nssh authorized keys firmat. No options, no comment.
+ #[derive(Debug,Clone,Serialize,Deserialize)]
+ #[serde(transparent)]
+ pub struct PubData(String);
+
+ #[derive(Debug,Clone,Serialize,Deserialize)]
+ #[serde(transparent)]
+ pub struct Comment(String /* must not contain newline/cr */);
+
+ // Not Serialize,Deserialize, so we can change the hash, etc.
+ #[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
+ pub struct Fingerprint(String);
+
+ #[derive(Error,Debug,Clone,Serialize,Deserialize)]
+ pub enum KeyError {
+ #[error("bad key data: {0}")] BadData(String),
+ #[error("failed to save key data, possibly broken")] Dirty,
+ }
+
+ impl From<OpenSSHKeyError> for KeyError {
+ fn from(e: OpenSSHKeyError) -> Self { KeyError::BadData(e.to_string()) }
+ }
+
+ impl Display for PubData {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
+ }
+
+ impl AuthkeysLine {
+ #[throws(KeyError)]
+ pub fn parse(&self) -> (PubData, Comment) {
+ let openssh_keys::PublicKey {
+ options:_, data, comment
+ } = self.0.parse()?;
+ let data = openssh_keys::PublicKey {
+ data,
+ options: None,
+ comment: None,
+ };
+ (PubData(data.to_string()), Comment(comment.unwrap_or_default()))
+ }
+ }
+
+ impl PubData {
+ #[throws(KeyError)]
+ pub fn fingerprint(&self) -> Fingerprint {
+ let k: openssh_keys::PublicKey = self.0.parse()?;
+ Fingerprint(k.fingerprint())
+ }
+ }
+
+ impl Display for Fingerprint {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut Formatter) { write!(f, "{}", self.0)? }
+ }
+
+}
+pub use veneer::*;
+
+format_by_fmt_hex!{Display, for Nonce, .0}
+impl Debug for Nonce {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut Formatter) {
+ write!(f,"Nonce[")?;
+ fmt_hex(f, &self.0)?;
+ write!(f,"]")?;
+ }
+}
+
+impl PerScope {
+ pub fn check(&self, gl: &Global, id: Id, nonce: Nonce)
+ -> Option<Authorisation<AccountScope>> {
+ for sk in &self.authorised {
+ if_chain!{
+ if let Some(sk) = sk;
+ if sk.id == id;
+ if let Some(key) = gl.keys.get(sk.id);
+ if key.nonce == nonce;
+ then { return Some(Authorisation::authorise_any()) }
+ }
+ }
+ None
+ }
+}
+
+#[derive(Debug,Clone,Serialize,Deserialize)]
+pub struct MgmtKeyReport {
+ id: Id,
+ data: PubData,
+ comment: Comment,
+ problem: Option<KeyError>,
+}
+
+impl PerScope {
+ pub fn keys(&self, gl: &Global) -> Vec<MgmtKeyReport> {
+ let dirty_error =
+ if gl.authkeys_dirty { Some(KeyError::Dirty) }
+ else { None };
+ self.authorised.iter().filter_map(|sk| Some({
+ let sk = sk.as_ref()?;
+ let key = gl.keys.get(sk.id)?;
+ let problem = if let Some(Err(ref e)) = key.fp { Some(e) }
+ else { dirty_error.as_ref() };
+ MgmtKeyReport {
+ id: sk.id,
+ data: key.data.clone(),
+ comment: sk.comment.clone(),
+ problem: problem.cloned(),
+ }
+ }))
+ .collect()
+ }
+
+ // not a good idea to speicfy a problem, but "whatever"
+ #[throws(ME)]
+ pub fn add_key(&mut self, accounts: &mut AccountsGuard,
+ new_akl: AuthkeysLine) -> (usize, Id) {
+ let (data, comment) = new_akl.parse()?;
+ let fp = data.fingerprint().map_err(KeyError::from)?;
+ let gl = &mut accounts.ssh_keys_mut();
+ let fp = Arc::new(fp);
+ let _ = gl.fps();
+ let fpe = gl.fps.as_mut().unwrap().entry(fp.clone());
+ // ABSENT
+ let id = {
+ let keys = &mut gl.keys;
+ *fpe.or_insert_with(||{
+ keys.insert(Key {
+ data,
+ refcount: 0,
+ nonce: Nonce(thread_rng().gen()),
+ fp: Some(Ok(fp.clone())),
+ })
+ })
+ };
+ // **undesirable**
+ let key = gl.keys.get_mut(id)
+ .ok_or_else(|| internal_error_bydebug(&(id, &fp)))?;
+ // GARBAGE
+ key.refcount += 1;
+ let new_sk = Some(ScopeKey { id, comment });
+ let index =
+ if let Some((index,_)) = self.authorised.iter()
+ .find_position(|sk| sk.is_none())
+ {
+ self.authorised[index] = new_sk;
+ index
+ } else {
+ let index = self.authorised.len();
+ self.authorised.push(new_sk);
+ index
+ };
+ gl.authkeys_dirty = true;
+ // UNSAVED
+ accounts.save_accounts_now()?;
+ // BROKEN
+ accounts.ssh_keys_mut().rewrite_authorized_keys()?;
+ // PRESENT
+ (index, id)
+ }
+
+ #[throws(ME)]
+ pub fn remove_key(&mut self, accounts: &mut AccountsGuard,
+ index: usize, id: Id) {
+ let gl = &mut accounts.ssh_keys_mut();
+ if id == default() {
+ throw!(ME::InvalidSshKeyId);
+ }
+ match self.authorised.get(index) {
+ Some(&Some(ScopeKey{id:tid,..})) if tid == id => { },
+ _ => throw!(ME::SshKeyNotFound), /* [ABSEMT..GARBAGE] */
+ }
+ let key = gl.keys.get_mut(id).ok_or_else(|| internal_logic_error(
+ format!("corrupted accounts db: key id {} missing", id)))?;
+
+ // [UNSAVED..PRESENT]
+
+ key.refcount -= 1;
+ let previously = mem::take(&mut self.authorised[index]);
+ // Now **illegal**, briefly, don't leave it like this! No-one can
+ // observe the illegal state since we have the accounts lock. If
+ // we abort, the in-core version vanishes, leaving a legal state.
+ let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
+ || accounts.save_accounts_now()
+ ));
+ let gl = &mut accounts.ssh_keys_mut();
+ let key = gl.keys.get_mut(id).expect("aargh!");
+
+ if ! matches!(r, Ok(Ok(()))) {
+ key.refcount += 1;
+ self.authorised[index] = previously;
+ // [UNSAVED..PRESENT]
+ match r {
+ Err(payload) => std::panic::resume_unwind(payload),
+ Ok(Err(e)) => throw!(e),
+ Ok(Ok(())) => panic!(), // handled that earlier
+ }
+ }
+ // [ABSENT..GARBAGE]
+
+ if key.refcount == 0 {
+ let key = gl.keys.remove(id).unwrap();
+ if let Some(Ok(ref fp)) = key.fp {
+ gl.fps().remove(fp);
+ }
+ }
+ // ABSENT
+ }
+}
+
+impl Global {
+ fn fps(&mut self) -> &mut FingerprintMap {
+ let keys = &mut self.keys;
+ self.fps.get_or_insert_with(||{
+
+ let mut fps = FingerprintMap::default();
+ let mut garbage = vec![];
+
+ for (id, key) in keys.iter_mut() {
+ if key.refcount == 0 { garbage.push(id); continue; }
+
+ if_let!{
+ Ok(fp) = {
+ let data = &key.data;
+ key.fp.get_or_insert_with(
+ || Ok(Arc::new(data.fingerprint()?))
+ )
+ };
+ else continue;
+ }
+
+ use hash_map::Entry::*;
+ match fps.entry(fp.clone()) {
+ Vacant(ve) => { ve.insert(id); },
+ Occupied(mut oe) => {
+ error!("ssh key fingerprint collision! \
+ fp={} newid={} oldid={} newdata={:?}",
+ &fp, id, oe.get(), &key.data);
+ oe.insert(Id::default());
+ },
+ }
+ }
+
+ for id in garbage { keys.remove(id); }
+
+ fps
+
+ })
+ }
+
+ #[throws(InternalError)]
+ fn write_keys(&self, w: &mut BufWriter<File>) {
+ for (id, key) in &self.keys {
+ let fp = match key.fp { Some(Ok(ref fp)) => fp, _ => continue };
+ if key.refcount == 0 { continue }
+ writeln!(w, r#"{},command="{} --ssh-proxy {} {}" {} {}:{}"#,
+ RESTRICTIONS,
+ &config().ssh_proxy_bin, id, key.nonce,
+ &key.data,
+ key.refcount, &fp)
+ .context("write new auth keys")?;
+ }
+ }
+
+ // Caller should make sure accounts are saved first, to avoid
+ // getting the authkeys_dirty bit wrong.
+ #[throws(InternalError)]
+ fn rewrite_authorized_keys(&mut self) {
+ let config = config();
+ let path = &config.authorized_keys;
+ let tmp = format!("{}.tmp", &path);
+
+ (||{
+ let f = File::open(path).context("open")?;
+ let l = BufReader::new(f).lines().next()
+ .ok_or_else(|| anyhow!("no first line!"))?
+ .context("read first line")?;
+ if l != MAGIC_BANNER {
+ throw!(anyhow!(
+ "first line is not as expected (manually written/edited?)"
+ ));
+ }
+ Ok::<_,AE>(())
+ })()
+ .with_context(|| path.clone())
+ .context("check authorized_keys magic/banner")?;
+
+ let mut f = fs::OpenOptions::new()
+ .write(true).truncate(true).create(true)
+ .mode(0o644)
+ .open(&tmp)
+ .with_context(|| tmp.clone()).context("open new auth keys file")?;
+
+ let include = &config.authorized_keys_include;
+
+ (||{
+ let mut f = BufWriter::new(&mut f);
+ write!(f, "{}", MAGIC_BANNER)?;
+ writeln!(f, "# YOU MAY EDIT {:?} INSTEAD - THAT IS INCLUDED HERE",
+ include)?;
+ f.flush()?;
+ Ok::<_,io::Error>(())
+ })().with_context(|| tmp.clone()).context("write header")?;
+
+ if let Some(mut sf) = match File::open(include) {
+ Ok(y) => Some(y),
+ Err(e) if e.kind() == ErrorKind::NotFound => None,
+ Err(e) => throw!(AE::from(e).context(include.clone())
+ .context("open static auth keys")),
+ } {
+ io::copy(&mut sf, &mut f).context("copy data into new auth keys")?;
+ }
+
+ let mut f = BufWriter::new(f);
+ self.write_keys(&mut f)?;
+ f.flush().context("finish writing new auth keys")?;
+
+ fs::rename(&tmp, &path).with_context(|| path.clone())
+ .context("install new auth keys")?;
+
+ self.authkeys_dirty = false;
+ }
+}