From 9debd20b243647282b3f96d76914cc1ab1362139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:27:43 +0800 Subject: [PATCH 1/2] chore(deps): sha2 0.11, whose digest no longer formats as hex on its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Sha256::digest` returns a `hybrid_array::Array` under digest 0.11, and that type has no `LowerHex` — so `format!("{:x}", ..)` in `watch_token_hash` stopped compiling, which is why dependabot's own bump failed to build. `hex::encode` writes the same lowercase hex those rows already hold, so the `watch_tokens` records written by a running agent stay valid across the upgrade. Supersedes #1321, which moved the version and left the call site. --- Cargo.lock | 3 ++- monitor/Cargo.toml | 4 +++- monitor/src/api/server.rs | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) 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..b8f4d927f5 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> { From c8ef3d0d767e001a304cb6ed2a72d15ff97c1d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:36:11 +0800 Subject: [PATCH 2/2] test(monitor): seed the watch token row with a hash this crate did not produce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test hashed the token through `watch_token_hash` on both sides, so it agreed with itself whatever the encoding became — which is exactly what this branch changes. An agent's `watch_tokens` rows outlive the build that wrote them, and a changed encoding invalidates every paired watch with no error. Seeded with the published SHA-256 of `abc` instead. --- monitor/src/api/server.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/monitor/src/api/server.rs b/monitor/src/api/server.rs index b8f4d927f5..fe9d319c94 100644 --- a/monitor/src/api/server.rs +++ b/monitor/src/api/server.rs @@ -1224,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) @@ -1243,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)