Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/rust/rules/no-hmac-equality-compare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
id: no-hmac-equality-compare
message: "Use constant_time_eq() for HMAC comparison, not == or !=. Non-constant-time comparison leaks timing information."
severity: error
language: rust
note: |
HMAC challenge IDs must be compared using constant_time_eq() to prevent
timing side-channel attacks. Using == or != leaks information about how
many leading bytes match, which can allow an attacker to forge valid HMACs.

Replace:
if credential.challenge.id != expected_id { ... }
With:
if !constant_time_eq(&credential.challenge.id, &expected_id) { ... }

To disable this rule:
- Line: add `// ast-grep-ignore: no-hmac-equality-compare` with justification
rule:
any:
- pattern: $A == expected_id
- pattern: $A != expected_id
- pattern: expected_id == $A
- pattern: expected_id != $A
Comment on lines +19 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match borrowed expected_id comparisons too

The rule only matches comparisons where the RHS/LHS is the bare identifier expected_id, so it misses common Rust forms like foo == &expected_id, &foo == expected_id_ref, or foo == *expected_id when expected_id is stored as a reference. In those cases the non-constant-time comparison still happens, but this lint will not fire, which undermines the stated goal of preventing HMAC timing-regression patterns from slipping into PRs.

Useful? React with 👍 / 👎.

45 changes: 45 additions & 0 deletions src/rust/tests/__snapshots__/no-hmac-equality-compare-snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
id: no-hmac-equality-compare
snapshots:
? |
if credential.challenge.id != expected_id {
return Err("mismatch");
}
: labels:
- source: credential.challenge.id != expected_id
style: primary
start: 3
end: 41
? |
if credential.challenge.id == expected_id {
return Ok(());
}
: labels:
- source: credential.challenge.id == expected_id
style: primary
start: 3
end: 41
? |
if expected_id != credential.challenge.id {
return Err("mismatch");
}
: labels:
- source: expected_id != credential.challenge.id
style: primary
start: 3
end: 41
? |
if expected_id == credential.challenge.id {
return Ok(());
}
: labels:
- source: expected_id == credential.challenge.id
style: primary
start: 3
end: 41
? |
let matches = self.id == expected_id;
: labels:
- source: self.id == expected_id
style: primary
start: 14
end: 36
32 changes: 32 additions & 0 deletions src/rust/tests/no-hmac-equality-compare-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
id: no-hmac-equality-compare
valid:
- |
if !constant_time_eq(&credential.challenge.id, &expected_id) {
return Err("mismatch");
}
- |
constant_time_eq(&self.id, &expected_id)
- |
let expected_id = compute_challenge_id(secret, realm, method, intent, request, None, None, None);
- |
let is_valid = hmac_verify(&id, &expected_id);

invalid:
- |
if credential.challenge.id != expected_id {
return Err("mismatch");
}
- |
if credential.challenge.id == expected_id {
return Ok(());
}
- |
if expected_id != credential.challenge.id {
return Err("mismatch");
}
- |
if expected_id == credential.challenge.id {
return Ok(());
}
- |
let matches = self.id == expected_id;