Skip to content

Single-HMAC PIN derivation enables fast offline brute force #53

@curious-rabbit

Description

@curious-rabbit

trussed-auth derives a per-PIN key with a single HMAC-SHA256
invocation (backend/src/data.rs:491-501):

      fn derive_key(id: PinId, pin: &Pin, salt: &Salt,
                    application_key: &[u8; 32]) -> Hash {
          let mut hmac = Hmac::<Sha256>::new_from_slice(application_key)
              .expect("Slice will always be of acceptable size");
          hmac.update(&[u8::from(id)]);
          hmac.update(&[pin_len(pin)]);
          hmac.update(pin);
          hmac.update(&**salt);
          let tmp: [_; HASH_LEN] = hmac.finalize().into_bytes().into();
          tmp.into()
      }

No iteration count, no PBKDF2, no Argon2. The author's design
rationale at backend/src/data.rs:65 explicitly states:

  // We can't use PBKDF or argon2 here because of limited hardware.
  // Ideally such a step would be done on the host

No host-side stretching is performed by gpg, OpenSC, or nitropy; the
raw PIN bytes are sent to the device.

The application_key is itself derived as
HKDF(salt = global_salt, ikm = hw_key, info = client_id), where
global_salt is in plain on internal flash and hw_key on nRF52
platforms is FICR.ER. After persistent-storage extraction, an
adversary holds all derivation inputs (hw_key, global_salt,
per-PIN salt, ciphertext to test against) except the PIN itself.
Per-attempt cost is one HMAC-SHA256 plus one short
ChaCha8Poly1305 verification.

The OpenPGP card specification mandates a three-attempt retry counter
for PINs. trussed-auth implements this counter, but the counter sits
on the same flash as the rest of the state. An offline adversary
extracting the flash dump simply ignores the counter when testing
PIN candidates.

Brute-force time estimates for typical PIN charsets and lengths
on commodity hardware (CPU with SHA-NI; consumer GPU):

- 6-digit numeric (10^6):       milliseconds.
  The OpenPGP-card default for PW1 is 6-digit.
- 8-digit numeric (10^8):       seconds to minutes.
- 6-character mixed alnum (62^6 ≈ 5.7e10):
                                seconds (GPU); ~30 minutes (CPU).
- 8-character mixed alnum (62^8 ≈ 2.2e14):
                                hours to days (GPU);
                                weeks (CPU).
- 16-character random alnum:    not feasible with current
                                commodity hardware.
- Common-password dictionaries: seconds to minutes
                                (regardless of length).

Impact.

  • The OpenPGP card threat model expects the three-attempt retry
    counter to provide effective resistance to PIN guessing on a lost
    or stolen device. Once persistent storage is extracted, this
    counter is bypassed; the only remaining defense is the strength of
    the PIN under offline brute force.
  • Default OpenPGP PINs and short numeric PINs offer effectively no
    protection.
  • User passphrases of typical length fall within hours; only long,
    high-entropy PINs resist brute force.

Recommended remediation.

  • Implement on-device key stretching using PBKDF2-HMAC-SHA256 with a
    high iteration count, or Argon2id if memory permits. 100 000
    iterations of PBKDF2 raises brute-force cost by 5 orders of
    magnitude at the cost of approximately one second of PIN-
    verification latency on the target hardware.
  • Alternatively, define a host-side stretching protocol (Argon2id or
    high-iteration PBKDF2) and document it. Coordinate with downstream
    callers (gpg-agent via scdaemon, OpenSC, nitropy) to apply
    stretching before the PIN reaches the device. This is a wire-format
    change.
  • As a non-cryptographic mitigation: enforce minimum-PIN-length and
    minimum-charset policies in the API of set_pin, refusing weak
    PINs with a clear error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions