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.
trussed-auth derives a per-PIN key with a single HMAC-SHA256
invocation (backend/src/data.rs:491-501):
No iteration count, no PBKDF2, no Argon2. The author's design
rationale at backend/src/data.rs:65 explicitly states:
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):
Impact.
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.
protection.
high-entropy PINs resist brute force.
Recommended remediation.
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.
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.
minimum-charset policies in the API of set_pin, refusing weak
PINs with a clear error.