]> git.proxmox.com Git - proxmox-backup.git/commitdiff
cleanup auth code, verify CSRF prevention token
authorDietmar Maurer <dietmar@proxmox.com>
Sat, 16 Feb 2019 14:52:55 +0000 (15:52 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Sat, 16 Feb 2019 14:52:55 +0000 (15:52 +0100)
src/auth_helpers.rs
src/server/rest.rs

index fa0217dd3f50e3f7d8e3886c811b5d8c22c31daa..d2e0a9fa4eab0f5ae7d3fd5cd59b99332a728bad 100644 (file)
@@ -9,24 +9,79 @@ use openssl::sha;
 
 use std::path::PathBuf;
 
-pub fn assemble_csrf_prevention_token(
+fn compute_csrf_secret_digest(
+    timestamp: i64,
     secret: &[u8],
     username: &str,
 ) -> String {
 
-    let epoch = std::time::SystemTime::now().duration_since(
-        std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs();
-
     let mut hasher = sha::Sha256::new();
-    let data = format!("{:08X}:{}:", epoch, username);
+    let data = format!("{:08X}:{}:", timestamp, username);
     hasher.update(data.as_bytes());
     hasher.update(secret);
 
-    let digest = base64::encode_config(&hasher.finish(), base64::STANDARD_NO_PAD);
+    base64::encode_config(&hasher.finish(), base64::STANDARD_NO_PAD)
+}
+
+pub fn assemble_csrf_prevention_token(
+    secret: &[u8],
+    username: &str,
+) -> String {
+
+    let epoch = std::time::SystemTime::now().duration_since(
+        std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
+
+    let digest = compute_csrf_secret_digest(epoch, secret, username);
 
     format!("{:08X}:{}", epoch, digest)
 }
 
+pub fn verify_csrf_prevention_token(
+    secret: &[u8],
+    username: &str,
+    token: &str,
+    min_age: i64,
+    max_age: i64,
+) -> Result<i64, Error> {
+
+    use std::collections::VecDeque;
+
+    let mut parts: VecDeque<&str> = token.split(':').collect();
+
+    try_block!({
+
+        if parts.len() != 2 {
+            bail!("format error - wrong number of parts.");
+        }
+
+        let timestamp = parts.pop_front().unwrap();
+        let sig = parts.pop_front().unwrap();
+
+        let ttime = i64::from_str_radix(timestamp, 16).
+            map_err(|err| format_err!("timestamp format error - {}", err))?;
+
+        let digest = compute_csrf_secret_digest(ttime, secret, username);
+
+        if digest != sig {
+            bail!("invalid signature.");
+        }
+
+        let now = std::time::SystemTime::now().duration_since(
+            std::time::SystemTime::UNIX_EPOCH)?.as_secs() as i64;
+
+        let age = now - ttime;
+        if age < min_age {
+            bail!("timestamp newer than expected.");
+        }
+
+        if age > max_age {
+            bail!("timestamp too old.");
+        }
+
+        Ok(age)
+    }).map_err(|err| format_err!("invalid csrf token - {}", err))
+}
+
 pub fn generate_csrf_key() -> Result<(), Error> {
 
     let path = PathBuf::from(configdir!("/csrf.key"));
index 0910f6f455901a1bd7fa26655063ef8822e7a5c1..5d0f8249dc7ac7da7f851d5ca9946171b898c7eb 100644 (file)
@@ -422,6 +422,48 @@ fn handle_static_file_download(filename: PathBuf) ->  BoxFut {
     return Box::new(response);
 }
 
+fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
+
+    let mut ticket = None;
+    if let Some(raw_cookie) = headers.get("COOKIE") {
+        if let Ok(cookie) = raw_cookie.to_str() {
+            ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie");
+        }
+    }
+
+    let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
+        Some(Ok(v)) => Some(v.to_owned()),
+        _ => None,
+    };
+
+    (ticket, token)
+}
+
+fn check_auth(method: &hyper::Method, ticket: Option<String>, token: Option<String>) -> Result<String, Error> {
+
+    let ticket_lifetime = 3600*2; // 2 hours
+
+    let username = match ticket {
+        Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) {
+            Ok((_age, Some(username))) => username.to_owned(),
+            Ok((_, None)) => bail!("ticket without username."),
+            Err(err) => return Err(err),
+        }
+        None => bail!("missing ticket"),
+    };
+
+    if method != hyper::Method::GET {
+        if let Some(token) = token {
+            println!("CSRF prev token: {:?}", token);
+            verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?;
+        } else {
+            bail!("");
+        }
+    }
+
+    Ok(username)
+}
+
 pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
 
     let (parts, body) = req.into_parts();
@@ -457,20 +499,9 @@ pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
 
     let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
 
-    if let Some(raw_cookie) = parts.headers.get("COOKIE") {
-        if let Ok(cookie) = raw_cookie.to_str() {
-            if let Some(ticket) = tools::extract_auth_cookie(cookie, "PBSAuthCookie") {
-                if let Ok((_, Some(username))) = tools::ticket::verify_rsa_ticket(
-                    public_auth_key(), "PBS", &ticket, None, -300, 3600*2) {
-                    rpcenv.set_user(Some(username));
-                }
-            }
-        }
-    }
-
-
     if comp_len >= 1 && components[0] == "api2" {
         println!("GOT API REQUEST");
+
         if comp_len >= 2 {
             let format = components[1];
             let formatter = match format {
@@ -486,16 +517,24 @@ pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
             if comp_len == 4 && components[2] == "access" && components[3] == "ticket" {
                 // explicitly allow those calls without auth
             } else {
-                if let Some(_username) = rpcenv.get_user() {
-                    // fixme: check permissions
-                } else {
-                    // always delay unauthorized calls by 3 seconds (from start of request)
-                    let resp = (formatter.format_error)(http_err!(UNAUTHORIZED, "permission check failed.".into()));
-                    let delayed_response = tokio::timer::Delay::new(delay_unauth_time)
-                        .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
-                        .and_then(|_| Ok(resp));
-
-                    return Box::new(delayed_response);
+                let (ticket, token) = extract_auth_data(&parts.headers);
+                match check_auth(&method, ticket, token) {
+                    Ok(username) => {
+
+                        // fixme: check permissions
+
+                        rpcenv.set_user(Some(username));
+                    }
+                    Err(err) => {
+                        // always delay unauthorized calls by 3 seconds (from start of request)
+                        let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err));
+                        let resp = (formatter.format_error)(err);
+                        let delayed_response = tokio::timer::Delay::new(delay_unauth_time)
+                            .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
+                            .and_then(|_| Ok(resp));
+
+                        return Box::new(delayed_response);
+                    }
                 }
             }