michal/tit
Browse tree · Show commit · Download archive
Diff
280a5e37c1e9 → a99a15017f7a
docs/adr/0011-web-login-sessions.md
Mode 100644 → 100644; object 27c315912321 → e19a35ef3ffa
@@ -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 100644 → 100644; object 4755eea70bce → 0a4221f4d548
@@ -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 100644 → 100644; object 9fba7cee6a12 → aa9ce9ead824
@@ -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),
],