michal/tit

Browse tree · Show commit · Download archive

Diff

280a5e37c1e9a99a15017f7a

docs/adr/0011-web-login-sessions.md

Mode 100644100644; object 27c315912321e19a35ef3ffa

@@ -19,8 +19,10 @@
 
 The server verifies the origin, username, key fingerprint, time, namespace,
 signature algorithm, and signature. The user can paste the envelope or upload
-the signature file. In one SQLite transaction, the server consumes the nonce
-and creates a seven-day session. The response stores an opaque session
+the signature file. An HTML form changes challenge line endings to CRLF. The
+HTTP interface changes these line endings back to LF before signature
+verification. In one SQLite transaction, the server consumes the nonce and
+creates a seven-day session. The response stores an opaque session
 token in an `HttpOnly` cookie and a CSRF token in a second cookie. Both cookies
 use `SameSite=Strict`. HTTPS responses also use `Secure`. SQLite stores only
 SHA-256 hashes of both values.
@@ -57,8 +59,9 @@
 between issue and verification, rejects challenge replay and a bad CSRF token,
 checks the stored hashes, invalidates a session after key addition, and tests
 the function that ends all sessions. The executable test completes login and
-logout through HTTP. It rejects an incorrect upload content type and malformed
-multipart content. It also confirms CSRF rejection and session invalidation.
+logout through HTTP with browser CRLF line endings. It rejects an incorrect
+upload content type and malformed multipart content. It also confirms CSRF
+rejection and session invalidation.
 
 ## Consequences
 

src/http/mod.rs

Mode 100644100644; object 4755eea70bce0a4221f4d548

@@ -775,6 +775,7 @@
     login_csrf: String,
 ) -> Response {
     let display_username = username.clone();
+    let challenge = normalize_browser_newlines(challenge);
     let secure = state.secure_cookies;
     let correlation_id = request_id.to_owned();
     let result = login_job(state, move |login| {
@@ -827,6 +828,14 @@
             &display_username,
             "The signature is not valid or the challenge has expired.",
         ),
+    }
+}
+
+fn normalize_browser_newlines(value: String) -> String {
+    if value.contains("\r\n") {
+        value.replace("\r\n", "\n")
+    } else {
+        value
     }
 }
 

tests/serve.rs

Mode 100644100644; object 9fba7cee6a12aa9ce9ead824

@@ -177,6 +177,8 @@
         "</textarea>",
     );
     let signature = sign_challenge(instance.path(), &private_key, challenge);
+    let browser_challenge = challenge.replace('\n', "\r\n");
+    let browser_signature = signature.replace('\n', "\r\n");
     let login_csrf_cookies = response_cookies(&login_challenge);
     let login_csrf = cookie_value(&login_csrf_cookies, "tit-login-csrf");
     let rejected_login = http_form_with_headers(
@@ -185,8 +187,8 @@
         &[
             ("username", "alice"),
             ("public-key", public_key.trim()),
-            ("challenge", challenge),
-            ("signature", &signature),
+            ("challenge", &browser_challenge),
+            ("signature", &browser_signature),
             ("login-csrf", &"0".repeat(64)),
         ],
         &[("Cookie", &login_csrf_cookies)],
@@ -199,8 +201,8 @@
         &[
             ("username", "alice"),
             ("public-key", public_key.trim()),
-            ("challenge", challenge),
-            ("signature", &signature),
+            ("challenge", &browser_challenge),
+            ("signature", &browser_signature),
             ("login-csrf", login_csrf),
         ],
         &[("Cookie", &login_csrf_cookies)],
@@ -265,6 +267,7 @@
         "</textarea>",
     );
     let upload_signature = sign_challenge(instance.path(), &private_key, upload_challenge);
+    let browser_upload_challenge = upload_challenge.replace('\n', "\r\n");
     let upload_csrf_cookies = response_cookies(&upload_challenge_page);
     let upload_csrf = cookie_value(&upload_csrf_cookies, "tit-login-csrf");
     let wrong_upload_type = http_form_with_headers(
@@ -288,7 +291,7 @@
         &[
             ("username", "alice"),
             ("public-key", public_key.trim()),
-            ("challenge", upload_challenge),
+            ("challenge", &browser_upload_challenge),
             ("signature-file", &upload_signature),
             ("login-csrf", upload_csrf),
         ],