Skip to content

Use constant-time comparison for security-sensitive MAC/PIN checks - #388

Open
schnuartz-ai wants to merge 4 commits into
cryptoadvance:masterfrom
Schnuartz:upstream/pr14-constant-time
Open

schnuartz-ai wants to merge 4 commits into
cryptoadvance:masterfrom
Schnuartz:upstream/pr14-constant-time

Conversation

@schnuartz-ai

Copy link
Copy Markdown
Contributor

Problem

Security-sensitive MAC and PIN checks used ordinary byte equality. Such comparisons may stop at the first differing byte and can expose timing information.

Resolution

Add a centralized fixed-work consteq() helper for fixed-length authentication values and use it for storage-encryption MACs, PIN verification, and all JavaCard SecureChannel HMAC checks. The existing cryptographic protocols, key derivation, and failure behavior are otherwise unchanged.

Tests

The port includes regression coverage for consteq(), AEAD/MAC validation, PIN verification, and SecureChannel decrypt/open handshakes.

Previous testing and development history

This change was originally developed and tested in Schnuartz/specter-diy#14. The original PR contains the full development history, test results, review discussion, and implementation details. The results below refer to the original source revision. Fresh CI on this upstream PR validates the cleanly ported revision against the current upstream master.

  • Native tests: 5/5 passed
  • MicroPython make test: 28/28 passed
  • AEAD/MAC tests, PIN verification, and SecureChannel tests passed
  • Production firmware make disco: successful
  • python3 -m compileall src test: successful

Full original test history and implementation discussion: Schnuartz/specter-diy#14

Replace plain == / != on PIN HMACs, storage-encryption MACs, and
JavaCard secure-channel HMACs with a new consteq() helper in
helpers.py, since ordinary byte comparisons can short-circuit on the
first differing byte and create a timing side channel.

hmac.compare_digest() is not available: the hmac module here is a
custom C usermod (usermods/uhashlib/uhmac.c) exposing only new()/HMAC,
so consteq() walks every byte and accumulates differences without
early exit instead.
Address review feedback on PR #14:
- consteq()'s docstring overclaimed "compares every byte
  unconditionally", which isn't true for the length-mismatch branch;
  clarify that the length check is not constant-time and that the
  helper is only meant for fixed, public-length authentication values.
- SecureChannel's three HMAC comparisons had no regression coverage.
  decrypt() only depends on card_aes_key/card_mac_key/iv, so it can be
  tested directly without a smartcard: valid MAC decrypts, a flipped
  MAC byte (first/middle/last), a tampered ciphertext byte, or the
  wrong key are all rejected.
- native_support.py was missing a pyb.Pin stub, needed because the new
  test file transitively imports keystore.javacard.applets.securechannel,
  which through ..util pulls in `from pyb import Pin`.
A functional test can't distinguish consteq() from a plain a == b,
since both return identical booleans - only a timing measurement
could, and those are flaky in CI. As a cheap guard against a future
"simplification" silently reintroducing the L6 timing side channel,
mark consteq() and each of its five call sites with a comment
explaining why the comparison must not be swapped back to ==/!=.
Fixes a real CI regression from the previous commit: test.yml's
native-tests job checks out this repo without submodules (unlike the
'tests'/make-test job, which auto-inits them via the Makefile), so
test_securechannel.py's import chain (securechannel -> ..util ->
uscard) failed with ModuleNotFoundError: No module named 'uscard'.
Verified locally by importing with the f469-disco submodule paths
removed from sys.path, matching what native-tests actually sees.

Also close the coverage gap flagged in review: only decrypt() had
regression tests, not the two open() HMAC checks ("ee"/"se" handshake
modes). Added SecureChannelOpenTest, which drives open() against a
mock applet that plays the card's side of the handshake using real
secp256k1 ECDH/ECDSA (not just stubs) - valid response opens the
channel, a tampered HMAC is rejected, for both modes.
@netlify

netlify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploy Preview for specter-diy-docs ready!

Name Link
🔨 Latest commit 3a01a1a
🔍 Latest deploy log https://app.netlify.com/projects/specter-diy-docs/deploys/6a8a94f57accc0000894ed59
😎 Deploy Preview https://deploy-preview-388--specter-diy-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Schnuartz

Copy link
Copy Markdown
Contributor

@al-munazzim can you review?

@al-munazzim al-munazzim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the constant-time comparison change and the SecureChannel/PIN/storage-MAC call sites. The implementation keeps the fixed-length caveat explicit, uses the helper only for fixed-size authentication values, and CI is green.\n\nNo objections from my side.

Comment thread src/helpers.py
Comment thread test/tests/test_helpers.py

@Schnuartz Schnuartz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This makes sense.

Reviewed it. And it adds a security benefit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants