diff --git a/Cargo.lock b/Cargo.lock index 2455ad7de1..0c1d6e858c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3986,6 +3986,7 @@ dependencies = [ "dotenvy", "futures", "getrandom 0.4.3", + "hex", "hostname", "jsonwebtoken", "libc", @@ -4003,7 +4004,7 @@ dependencies = [ "sbm_parser", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "sqlx", "tempfile", "thiserror 2.0.20", diff --git a/monitor/Cargo.toml b/monitor/Cargo.toml index 8c892e7189..5a1cf19dcd 100644 --- a/monitor/Cargo.toml +++ b/monitor/Cargo.toml @@ -43,7 +43,9 @@ sqlx = { version = "0.9", features = [ # rust_crypto instead of aws_lc_rs: pure Rust, no cmake/assembly dependency for static musl builds jsonwebtoken = { version = "11.0", default-features = false, features = ["rust_crypto"] } bcrypt = "0.19" -sha2 = "0.10" +# sha2 0.11 returns a `hybrid_array::Array`, which has no `LowerHex` — hence hex +sha2 = "0.11" +hex = "0.4" # Configuration dotenvy = "0.15" diff --git a/monitor/src/api/server.rs b/monitor/src/api/server.rs index d2b934eaef..fe9d319c94 100644 --- a/monitor/src/api/server.rs +++ b/monitor/src/api/server.rs @@ -414,8 +414,10 @@ async fn login( })) } +/// Lowercase hex of the SHA-256, which is what the `watch_tokens` rows already +/// hold — `hex::encode` writes the same bytes the `{:x}` of digest 0.10 did. fn watch_token_hash(token: &str) -> String { - format!("{:x}", Sha256::digest(token.as_bytes())) + hex::encode(Sha256::digest(token.as_bytes())) } fn validate_watch_client_id(client_id: &str) -> Result<&str> { @@ -1222,17 +1224,27 @@ mod watch_token_tests { pool } + /// The published SHA-256 of `abc`, so the row below is seeded with a value + /// this crate did not produce. Hashing the token through + /// `watch_token_hash` on both sides would agree with itself whatever the + /// encoding became, and an agent's `watch_tokens` rows outlive the build + /// that wrote them: a change there invalidates every paired watch, silently. + const TOKEN: &str = "abc"; + const TOKEN_HASH: &str = + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + #[tokio::test] async fn watch_tokens_are_hashed_expiring_and_revocable() { let pool = pool().await; - let token = "sbw_secret"; + let token = TOKEN; + assert_eq!(watch_token_hash(token), TOKEN_HASH); sqlx::query( "INSERT INTO watch_tokens(subject, client_id, token_hash, created_at, expires_at) \ VALUES (?, ?, ?, ?, ?)", ) .bind("admin") .bind("watch:one") - .bind(watch_token_hash(token)) + .bind(TOKEN_HASH) .bind(10_i64) .bind(20_i64) .execute(&pool) @@ -1241,12 +1253,6 @@ mod watch_token_tests { assert_eq!(verify_watch_token(&pool, token, 19).await.unwrap(), "admin"); assert!(verify_watch_token(&pool, token, 20).await.is_err()); - let stored: String = sqlx::query_scalar("SELECT token_hash FROM watch_tokens") - .fetch_one(&pool) - .await - .unwrap(); - assert_ne!(stored, token); - sqlx::query("DELETE FROM watch_tokens WHERE client_id = ?") .bind("watch:one") .execute(&pool)