diff options
Diffstat (limited to 'rust/src/auth.rs')
-rw-r--r-- | rust/src/auth.rs | 233 |
1 files changed, 147 insertions, 86 deletions
diff --git a/rust/src/auth.rs b/rust/src/auth.rs index b88a72b0..3a6271e5 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -5,7 +5,7 @@ //! role-based authentication (RBAC). use data_encoding::BASE64; -use macaroon::{Format, Macaroon, Verifier}; +use macaroon::{ByteString, Caveat, Format, Macaroon, MacaroonError, MacaroonKey, Verifier}; use std::fmt; use swagger::auth::{AuthData, Authorization, Scopes}; @@ -23,10 +23,10 @@ use std::collections::HashMap; use std::env; use std::str::FromStr; -// 32 bytes max (!) -static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; +// 32 bytes exactly +static DUMMY_KEY_BYTES: &[u8; 32] = b"dummy-key-a-one-two-three-a-la!!"; -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FatcatRole { Public, Editor, @@ -178,8 +178,23 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { pub struct AuthConfectionary { pub location: String, pub identifier: String, - pub key: Vec<u8>, - pub root_keys: HashMap<String, Vec<u8>>, + pub key: MacaroonKey, + pub root_keys: HashMap<String, MacaroonKey>, +} + +fn parse_macaroon_key(key_base64: &str) -> Result<MacaroonKey> { + // instead of creating a [u8; 32], we decode into an arbitrary Vec (after checking the input + // length first), because the MacaroonKey 'From' trait is implemented differently for [u8] and + // [u8; 32] (sigh). + if key_base64.len() != 44 { + bail!("bad base64-padded-encoded key for macaroons"); + } + let key_bytes = BASE64 + .decode(key_base64.as_bytes()) + .expect("base64 key decode"); + let bytes_ref: &[u8] = key_bytes.as_ref(); + let key = MacaroonKey::from(bytes_ref); + Ok(key) } impl AuthConfectionary { @@ -188,10 +203,10 @@ impl AuthConfectionary { identifier: String, key_base64: &str, ) -> Result<AuthConfectionary> { - macaroon::initialize().unwrap(); - let key = BASE64.decode(key_base64.as_bytes())?; + macaroon::initialize().expect("initializing macaroon library"); + let key = parse_macaroon_key(key_base64)?; let mut root_keys = HashMap::new(); - root_keys.insert(identifier.clone(), key.clone()); + root_keys.insert(identifier.clone(), key); Ok(AuthConfectionary { location, identifier, @@ -204,13 +219,13 @@ impl AuthConfectionary { AuthConfectionary::new( "test.fatcat.wiki".to_string(), "dummy".to_string(), - &BASE64.encode(DUMMY_KEY), + &BASE64.encode(DUMMY_KEY_BYTES), ) - .unwrap() + .expect("creating dummy AuthConfectionary") } pub fn add_keypair(&mut self, identifier: String, key_base64: &str) -> Result<()> { - let key = BASE64.decode(key_base64.as_bytes())?; + let key = parse_macaroon_key(key_base64)?; self.root_keys.insert(identifier, key); Ok(()) } @@ -220,23 +235,31 @@ impl AuthConfectionary { editor_id: FatcatId, duration: Option<chrono::Duration>, ) -> Result<String> { - let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier) - .expect("Macaroon creation"); - mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); + let mut mac = Macaroon::create( + Some(self.location.clone()), + &self.key, + self.identifier.clone().into(), + ) + .expect("Macaroon creation"); + mac.add_first_party_caveat(format!("editor_id = {}", editor_id).into()); let now_utc = Utc::now(); let now = now_utc.to_rfc3339_opts(SecondsFormat::Secs, true); - mac.add_first_party_caveat(&format!("time > {}", now)); + mac.add_first_party_caveat(format!("time > {}", now).into()); if let Some(duration) = duration { let expires = now_utc + duration; - mac.add_first_party_caveat(&format!( - "time < {}", - &expires.to_rfc3339_opts(SecondsFormat::Secs, true) - )); + mac.add_first_party_caveat( + format!( + "time < {}", + &expires.to_rfc3339_opts(SecondsFormat::Secs, true) + ) + .into(), + ); }; let raw = mac.serialize(Format::V2).expect("macaroon serialization"); Ok(BASE64.encode(&raw)) } + /// Takes a macaroon as a base64-encoded string, deserializes it pub fn parse_macaroon_token( &self, conn: &DbConn, @@ -255,23 +278,19 @@ impl AuthConfectionary { .into()); } }; - let mac = match mac.validate() { - Ok(m) => m, - Err(e) => { - // TODO: should be "chaining" here - return Err(FatcatError::InvalidCredentials(format!( - "macaroon validate error: {:?}", - e - )) - .into()); - } - }; - let mut verifier = Verifier::new(); + let mut verifier = Verifier::default(); let mut editor_id: Option<FatcatId> = None; for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("editor_id = ") { - editor_id = Some(FatcatId::from_str(caveat.predicate().get(12..).unwrap())?); - break; + if let Caveat::FirstParty(fp) = caveat { + let predicate_str = String::from_utf8(fp.predicate().as_ref().to_vec())?; + if predicate_str.starts_with("editor_id = ") { + editor_id = Some(FatcatId::from_str( + predicate_str + .get(12..) + .expect("parsing editor_id from macaroon"), + )?); + break; + } } } let editor_id = match editor_id { @@ -283,25 +302,28 @@ impl AuthConfectionary { .into()); } }; - verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); + verifier.satisfy_exact(format!("editor_id = {}", editor_id).into()); if let Some(endpoint) = endpoint { // API endpoint - verifier.satisfy_exact(&format!("endpoint = {}", endpoint)); + verifier.satisfy_exact(format!("endpoint = {}", endpoint).into()); } let mut created: Option<DateTime<Utc>> = None; for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("time > ") { - let ts: chrono::ParseResult<DateTime<Utc>> = - DateTime::parse_from_rfc3339(caveat.predicate().get(7..).unwrap()) - .map(|x| x.with_timezone(&Utc)); - if let Ok(ts) = ts { - created = Some(ts); - break; - } else { - info!( - "couldn't parse macaroon time constraint: {}", - caveat.predicate() - ); + if let Caveat::FirstParty(fp) = caveat { + let predicate_str = String::from_utf8(fp.predicate().as_ref().to_vec())?; + if predicate_str.starts_with("time > ") { + let ts: chrono::ParseResult<DateTime<Utc>> = DateTime::parse_from_rfc3339( + predicate_str + .get(7..) + .expect("parsing timestamp from macaroon"), + ) + .map(|x| x.with_timezone(&Utc)); + if let Ok(ts) = ts { + created = Some(ts); + break; + } else { + info!("couldn't parse macaroon time constraint: {}", predicate_str); + } } } } @@ -314,23 +336,25 @@ impl AuthConfectionary { .into()); } }; - verifier.satisfy_exact(&format!( - "time > {}", - created.to_rfc3339_opts(SecondsFormat::Secs, true) - )); + verifier.satisfy_exact( + format!( + "time > {}", + created.to_rfc3339_opts(SecondsFormat::Secs, true) + ) + .into(), + ); // not finding the editor_id is an auth issue, not a 404 - let editor: EditorRow = - match Editor::db_get(conn, editor_id).map_err(|e| FatcatError::from(e)) { - Ok(ed) => ed, - Err(FatcatError::NotFound(_, _)) => { - return Err(FatcatError::InvalidCredentials(format!( - "editor_id not found: {}", - editor_id - )) - .into()); - } - other_db_err => other_db_err?, - }; + let editor: EditorRow = match Editor::db_get(conn, editor_id).map_err(FatcatError::from) { + Ok(ed) => ed, + Err(FatcatError::NotFound(_, _)) => { + return Err(FatcatError::InvalidCredentials(format!( + "editor_id not found: {}", + editor_id + )) + .into()); + } + other_db_err => other_db_err?, + }; let auth_epoch = DateTime::<Utc>::from_utc(editor.auth_epoch, Utc); // allow a second of wiggle room for precision and, eg, tests if created < (auth_epoch - chrono::Duration::seconds(1)) { @@ -340,39 +364,48 @@ impl AuthConfectionary { ) .into()); } - verifier.satisfy_general(|p: &str| -> bool { - // not expired (based on time) - if p.starts_with("time < ") { - let expires: chrono::ParseResult<DateTime<Utc>> = - DateTime::parse_from_rfc3339(p.get(7..).unwrap()) - .map(|x| x.with_timezone(&Utc)); - if let Ok(when) = expires { - //info!("checking time constraint: {} < {}", Utc::now(), when); - Utc::now() < when + verifier.satisfy_general(|p_bstr: &ByteString| -> bool { + if let Ok(p) = String::from_utf8(p_bstr.as_ref().to_vec()) { + // not expired (based on time) + if p.starts_with("time < ") { + let expires: chrono::ParseResult<DateTime<Utc>> = DateTime::parse_from_rfc3339( + p.get(7..).expect("parsing datetime from macaroon"), + ) + .map(|x| x.with_timezone(&Utc)); + if let Ok(when) = expires { + //info!("checking time constraint: {} < {}", Utc::now(), when); + Utc::now() < when + } else { + info!("couldn't parse macaroon time constraint: {}", p); + false + } } else { - info!("couldn't parse macaroon time constraint: {}", p); false } } else { false } }); - let verify_key = match self.root_keys.get(mac.identifier()) { + let verify_key = match self + .root_keys + .get(std::str::from_utf8(mac.identifier().as_ref())?) + { Some(key) => key, None => { return Err(FatcatError::InvalidCredentials(format!( "no valid auth signing key for identifier: {}", - mac.identifier() + std::str::from_utf8(mac.identifier().as_ref())? )) .into()); } }; - match mac.verify(verify_key, &mut verifier) { - Ok(true) => (), - Ok(false) => { - return Err(FatcatError::InvalidCredentials( - "auth token (macaroon) not valid (signature and/or caveats failed)".to_string(), - ) + match verifier.verify(&mac, verify_key, Default::default()) { + Ok(()) => (), + Err(MacaroonError::InvalidMacaroon(em)) => { + return Err(FatcatError::InvalidCredentials(format!( + "auth token (macaroon) not valid (signature and/or caveats failed): {}", + em + )) .into()); } Err(e) => { @@ -448,9 +481,15 @@ impl AuthConfectionary { let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); println!("current time: {}", now); println!("domain (location): {:?}", mac.location()); - println!("signing key name (identifier): {}", mac.identifier()); + println!( + "signing key name (identifier): {}", + std::str::from_utf8(mac.identifier().as_ref())? + ); for caveat in mac.first_party_caveats() { - println!("caveat: {}", caveat.predicate()); + if let Caveat::FirstParty(fp) = caveat { + let predicate_str = String::from_utf8(fp.predicate().as_ref().to_vec())?; + println!("caveat: {}", predicate_str); + } } println!("verify: {:?}", self.parse_macaroon_token(conn, token, None)); Ok(()) @@ -488,7 +527,7 @@ pub fn print_editors(conn: &DbConn) -> Result<()> { for e in all_editors { println!( "{}\t{}/{}/{}\t{}\t{}\t{:?}", - FatcatId::from_uuid(&e.id).to_string(), + FatcatId::from_uuid(&e.id), e.is_superuser, e.is_admin, e.is_bot, @@ -519,3 +558,25 @@ pub fn env_confectionary() -> Result<AuthConfectionary> { }; Ok(confectionary) } + +#[test] +fn test_macaroon_keys() { + assert!(parse_macaroon_key("blah").is_err()); + + let key_bytes: [u8; 32] = [ + 231, 158, 121, 231, 158, 121, 231, 158, 121, 231, 158, 121, 231, 158, 121, 231, 158, 121, + 231, 158, 121, 231, 158, 121, 231, 158, 121, 231, 158, 121, 198, 107, + ]; + + // old way of parsing keys + let old_key = BASE64 + .decode(b"5555555555555555555555555555555555555555xms=") + .unwrap(); + assert_eq!(old_key.len(), 32); + assert_eq!(old_key, key_bytes); + let old_macaroon_key: MacaroonKey = old_key.as_slice().into(); + + // new (2022) way of parsing keys + let key = parse_macaroon_key("5555555555555555555555555555555555555555xms=").unwrap(); + assert_eq!(old_macaroon_key, key); +} |