Skip to main content

max / makenotwork

server: upgrade sha2 to 0.11, sha1 to 0.11, hmac to 0.13 The RustCrypto hash/MAC wave, minus chacha20poly1305 — that one is the ciphertext envelope and wants its own decrypt-old-under-new check. Only API break is new_from_slice, which moved from Mac to KeyInit, so KeyInit joins the hmac import at every call site. No format!("{:x}", ..) sites here to convert; this crate already went through hex::encode. Pin two HMAC outputs against independently computed known answers, since both are contracts with a counterparty that no round-trip test can check: the Stripe webhook signature (Stripe signs, we verify) and the internal request signature (we sign, multithreaded verifies — same vector is now pinned on both ends). Also tie mt_client's production signer to the message layout the test reimplements. 1888 lib tests and 1190 integration tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 01:18 UTC
Commit: ad9f8e34f04e63ad915cca5143f8e1253ec10b98
Parent: de46409
9 files changed, +55 insertions, -27 deletions
@@ -4343,7 +4343,7 @@ dependencies = [
4343 4343 "goblin 0.10.7",
4344 4344 "governor",
4345 4345 "hex",
4346 - "hmac 0.12.1",
4346 + "hmac 0.13.0",
4347 4347 "http-body-util",
4348 4348 "infer",
4349 4349 "jsonwebtoken",
@@ -4364,8 +4364,8 @@ dependencies = [
4364 4364 "semver",
4365 4365 "serde",
4366 4366 "serde_json",
4367 - "sha1 0.10.6",
4368 - "sha2 0.10.9",
4367 + "sha1 0.11.0",
4368 + "sha2 0.11.0",
4369 4369 "sqlx",
4370 4370 "subtle",
4371 4371 "syntect",
@@ -71,9 +71,9 @@ openssl = { version = "0.10", features = ["vendored"] }
71 71
72 72 # Security
73 73 rand = "0.10"
74 - hmac = "0.12.1"
75 - sha1 = "0.10.6"
76 - sha2 = "0.10.9"
74 + hmac = "0.13.0"
75 + sha1 = "0.11.0"
76 + sha2 = "0.11.0"
77 77 subtle = "2.6"
78 78 chacha20poly1305 = "0.10.1"
79 79 hex = "0.4.3"
@@ -118,7 +118,7 @@ done
118 118
119 119 /// Compute a per-repo HMAC so the global token never touches disk.
120 120 pub fn repo_hmac(token: &str, owner: &str, repo: &str) -> String {
121 - use hmac::{Hmac, Mac};
121 + use hmac::{Hmac, KeyInit, Mac};
122 122 use sha2::Sha256;
123 123 let mut mac =
124 124 Hmac::<Sha256>::new_from_slice(token.as_bytes()).expect("HMAC accepts any key length");
@@ -14,7 +14,7 @@ const TOTP_ENC_PREFIX: &str = "enc:v1:";
14 14 /// backup-code HMAC, session tokens), so reuse in one context can't weaken
15 15 /// another.
16 16 fn totp_encryption_key(signing_secret: &str) -> [u8; 32] {
17 - use hmac::{Hmac, Mac};
17 + use hmac::{Hmac, KeyInit, Mac};
18 18 use sha2::Sha256;
19 19
20 20 let mut mac = Hmac::<Sha256>::new_from_slice(signing_secret.as_bytes())
@@ -191,7 +191,7 @@ pub fn mint_internal_actor_token(
191 191 expiry_unix: i64,
192 192 signing_secret: &str,
193 193 ) -> String {
194 - use hmac::{Hmac, Mac};
194 + use hmac::{Hmac, KeyInit, Mac};
195 195 use sha2::Sha256;
196 196 let message = format!("{INTERNAL_ACTOR_DOMAIN}:{user_id}:{expiry_unix}");
197 197 let mut mac = Hmac::<Sha256>::new_from_slice(signing_secret.as_bytes())
@@ -210,7 +210,7 @@ pub fn verify_internal_actor_token(
210 210 signing_secret: &str,
211 211 now_unix: i64,
212 212 ) -> Option<crate::db::UserId> {
213 - use hmac::{Hmac, Mac};
213 + use hmac::{Hmac, KeyInit, Mac};
214 214 use sha2::Sha256;
215 215
216 216 let (user_part, rest) = token.split_once('.')?;
@@ -231,7 +231,7 @@ pub fn verify_internal_actor_token(
231 231
232 232 /// Compute the hex HMAC-SHA256 over `feed:{user_id}:{version}` with `secret`.
233 233 fn feed_signature(user_id: crate::db::UserId, version: i32, secret: &str) -> String {
234 - use hmac::{Hmac, Mac};
234 + use hmac::{Hmac, KeyInit, Mac};
235 235 use sha2::Sha256;
236 236
237 237 let message = format!("feed:{user_id}:{version}");
@@ -76,7 +76,7 @@ pub fn generate_verification_url(
76 76 email: &str,
77 77 secret: &str,
78 78 ) -> String {
79 - use hmac::{Hmac, Mac};
79 + use hmac::{Hmac, KeyInit, Mac};
80 80 use sha2::Sha256;
81 81
82 82 let expires = chrono::Utc::now().timestamp() + constants::EMAIL_VERIFICATION_EXPIRY_SECS;
@@ -151,7 +151,7 @@ pub fn verify_email_signature(
151 151 signature: &str,
152 152 secret: &str,
153 153 ) -> bool {
154 - use hmac::{Hmac, Mac};
154 + use hmac::{Hmac, KeyInit, Mac};
155 155 use sha2::Sha256;
156 156
157 157 if expires < chrono::Utc::now().timestamp() {
@@ -184,7 +184,7 @@ pub fn generate_unsubscribe_url(
184 184 target: &str,
185 185 secret: &str,
186 186 ) -> String {
187 - use hmac::{Hmac, Mac};
187 + use hmac::{Hmac, KeyInit, Mac};
188 188 use sha2::Sha256;
189 189
190 190 let message = format!("unsub:{}:{}:{}", user_id, action, target);
@@ -207,7 +207,7 @@ pub fn verify_unsubscribe_signature(
207 207 signature: &str,
208 208 secret: &str,
209 209 ) -> bool {
210 - use hmac::{Hmac, Mac};
210 + use hmac::{Hmac, KeyInit, Mac};
211 211 use sha2::Sha256;
212 212
213 213 let message = format!("unsub:{}:{}:{}", user_id, action, target);
@@ -234,7 +234,7 @@ pub fn generate_unsubscribe_url_for_email(
234 234 target: &str,
235 235 secret: &str,
236 236 ) -> String {
237 - use hmac::{Hmac, Mac};
237 + use hmac::{Hmac, KeyInit, Mac};
238 238 use sha2::Sha256;
239 239
240 240 let email = email.to_lowercase();
@@ -262,7 +262,7 @@ pub fn verify_email_unsubscribe_signature(
262 262 signature: &str,
263 263 secret: &str,
264 264 ) -> bool {
265 - use hmac::{Hmac, Mac};
265 + use hmac::{Hmac, KeyInit, Mac};
266 266 use sha2::Sha256;
267 267
268 268 let email = email.to_lowercase();
@@ -293,7 +293,7 @@ pub fn generate_deletion_signature(
293 293 expires: i64,
294 294 email: &str,
295 295 ) -> String {
296 - use hmac::{Hmac, Mac};
296 + use hmac::{Hmac, KeyInit, Mac};
297 297 use sha2::Sha256;
298 298
299 299 let message = format!("delete:{}:{}:{}", user_id, expires, email);
@@ -319,7 +319,7 @@ pub fn generate_issue_reply_address(
319 319 secret: &str,
320 320 ) -> String {
321 321 use base64::engine::{Engine, general_purpose::URL_SAFE_NO_PAD};
322 - use hmac::{Hmac, Mac};
322 + use hmac::{Hmac, KeyInit, Mac};
323 323 use sha2::Sha256;
324 324
325 325 let message = format!("issue-reply:{}:{}", issue_id, user_id);
@@ -342,7 +342,7 @@ pub fn parse_issue_reply_token(
342 342 secret: &str,
343 343 ) -> Option<(crate::db::IssueId, UserId)> {
344 344 use base64::engine::{Engine, general_purpose::URL_SAFE_NO_PAD};
345 - use hmac::{Hmac, Mac};
345 + use hmac::{Hmac, KeyInit, Mac};
346 346 use sha2::Sha256;
347 347
348 348 let payload = local_part.strip_prefix("issue+")?;
@@ -880,7 +880,7 @@ mod tests {
880 880 let now = chrono::Utc::now().timestamp();
881 881
882 882 // Generate a sig for an EXPIRED timestamp.
883 - use hmac::{Hmac, Mac};
883 + use hmac::{Hmac, KeyInit, Mac};
884 884 use sha2::Sha256;
885 885 let expires_past = now - 60;
886 886 let message = format!("verify:{}:{}:{}", user_id, expires_past, email);
@@ -904,7 +904,7 @@ mod tests {
904 904 let secret = "secret";
905 905 let expires_future = chrono::Utc::now().timestamp() + 3600;
906 906
907 - use hmac::{Hmac, Mac};
907 + use hmac::{Hmac, KeyInit, Mac};
908 908 use sha2::Sha256;
909 909 let message = format!("verify:{}:{}:{}", user_id, expires_future, email);
910 910 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
@@ -2,7 +2,7 @@
2 2 //!
3 3 //! Signs requests with HMAC-SHA256 and communicates with MT's `/internal/*` endpoints.
4 4
5 - use hmac::{Hmac, Mac};
5 + use hmac::{Hmac, KeyInit, Mac};
6 6 use serde::{Deserialize, Serialize};
7 7 use sha2::Sha256;
8 8 use uuid::Uuid;
@@ -254,7 +254,7 @@ mod tests {
254 254 /// nonce are all bound (a mutation dropping any field would collide).
255 255 #[test]
256 256 fn signed_message_binds_method_path_nonce() {
257 - use hmac::{Hmac, Mac};
257 + use hmac::{Hmac, KeyInit, Mac};
258 258 use sha2::Sha256;
259 259
260 260 fn sig(
@@ -274,6 +274,21 @@ mod tests {
274 274 hex::encode(mac.finalize().into_bytes())
275 275 }
276 276
277 + // The verifier is multithreaded's compute_internal_signature_v2, so the
278 + // exact bytes are a cross-repo contract that neither repo can catch by
279 + // agreeing with itself. Both ends pin this same independently computed
280 + // HMAC-SHA256: key "secret", message "100\nPOST\n/x\nn\nbody".
281 + assert_eq!(
282 + sig("secret", "100", "POST", "/x", "n", "body"),
283 + "0e97f90cb4e4ca7aaa5499e67a22fb5b7ad45ad3cc966f37d225f39da2728098"
284 + );
285 +
286 + // Tie the production signer to that layout, so the pinned vector above
287 + // constrains sign_request and not just this local reimplementation.
288 + let client = MtClient::new("http://localhost".to_string(), "s".to_string());
289 + let (ts, nonce, produced) = client.sign_request("POST", "/a", "body");
290 + assert_eq!(produced, sig("s", &ts, "POST", "/a", &nonce, "body"));
291 +
277 292 let base = sig("s", "100", "POST", "/a", "n1", "body");
278 293 assert_ne!(
279 294 base,
@@ -4,7 +4,7 @@
4 4 //! and a thin `UntypedEvent` envelope. The webhook dispatcher matches on
5 5 //! `type_` and consumes `data_object` (no per-extractor clones).
6 6
7 - use hmac::{Hmac, Mac};
7 + use hmac::{Hmac, KeyInit, Mac};
8 8 use sha2::Sha256;
9 9
10 10 use super::StripeClient;
@@ -724,6 +724,19 @@ mod tests {
724 724 }
725 725
726 726 #[test]
727 + fn signature_matches_the_reference_hmac() {
728 + // Stripe is the counterparty and its HMAC is fixed, so these bytes are
729 + // an external contract no round-trip test can check — signing and
730 + // verifying with the same crate agrees with itself even if the crate
731 + // changed. Pinned against an independent HMAC-SHA256 over Stripe's
732 + // documented signed payload, "{timestamp}.{body}".
733 + assert_eq!(
734 + sign_at(r#"{"id":"evt_1"}"#, "whsec_test", 1_700_000_000),
735 + "t=1700000000,v1=c89214b5b5da833daed6f0b8c5bb6bd58cea9022bd80ccc78230f3942d632925"
736 + );
737 + }
738 +
739 + #[test]
727 740 fn verify_signature_valid_current() {
728 741 let header = sign_at(r#"{"id":"evt_1"}"#, "whsec_test", now_secs());
729 742 assert!(verify_signature(r#"{"id":"evt_1"}"#, &header, "whsec_test").is_ok());
@@ -343,7 +343,7 @@ pub(crate) fn hash_backup_code(code: &str, _secret: &str) -> String {
343 343 /// stored hash isn't an Argon2 PHC string. Do not call from new code paths —
344 344 /// any newly issued backup code goes through `hash_backup_code` (Argon2).
345 345 pub(crate) fn legacy_hmac_backup_code(code: &str, secret: &str) -> String {
346 - use hmac::{Hmac, Mac};
346 + use hmac::{Hmac, KeyInit, Mac};
347 347 use sha2::Sha256;
348 348
349 349 let mut mac =
@@ -1,6 +1,6 @@
1 1 //! Stripe test helpers — webhook signature computation and mock payment provider.
2 2
3 - use hmac::{Hmac, Mac};
3 + use hmac::{Hmac, KeyInit, Mac};
4 4 use sha2::Sha256;
5 5 use std::sync::Mutex;
6 6 use std::time::{SystemTime, UNIX_EPOCH};