Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
default (5)
- Fix the inverted parsing of the touch requirement in SET MANAGEMENT KEY's P2
(0xFF means no touch, 0xFE means touch required)
- Let a malformed PIN of the right length burn a retry instead of rejecting it as a
format error
- Validate the new PIN in RESET RETRY COUNTER

## [v0.7.0][] (2026-08-17)

Expand Down
9 changes: 5 additions & 4 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ impl TryFrom<VerifyArguments<'_>> for Verify {
}
Ok(match (logout.0, data.len()) {
(false, 0) => Verify::Status(key_reference),
(false, 8) => Verify::Login(VerifyLogin::PivPin(
data.try_into()
.map_err(|_| Status::IncorrectDataParameter)?,
)),
// A malformed PIN of the right length can never match the stored
// one, so it burns a retry like a wrong PIN
(false, 8) => Verify::Login(VerifyLogin::PivPin(Pin(data
.try_into()
.map_err(|_| Status::IncorrectDataParameter)?))),
(false, _) => return Err(Status::IncorrectDataParameter),
(true, 0) => Verify::Logout(key_reference),
(true, _) => return Err(Status::IncorrectDataParameter),
Expand Down
9 changes: 4 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,11 +1016,10 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
}

fn reset_retry_counter(&mut self, data: ResetRetryCounter) -> Result {
let res = self.state.persistent.reset_retry_counter(
&Puk(data.puk),
&Pin(data.pin),
self.trussed,
)?;
let res =
self.state
.persistent
.reset_retry_counter(&Puk(data.puk), &data.pin, self.trussed)?;
if !res {
return Err(Status::RemainingRetries(
self.state.persistent.remaining_puk_retries(self.trussed),
Expand Down
9 changes: 7 additions & 2 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,17 @@ impl Persistent {
pub fn reset_retry_counter<T: crate::Client>(
&mut self,
puk: &Puk,
new_pin: &Pin,
new_pin: &[u8; 8],
client: &mut T,
) -> Result<bool, Status> {
let Some(puk_key) = self.get_puk_key(puk, client)? else {
return Ok(false);
};
// The PUK is compared first, like VERIFY would; a stored PIN has to be well formed
let Ok(new_pin) = Pin::try_from(new_pin.as_slice()) else {
syscall!(client.delete(puk_key));
return Err(Status::IncorrectDataParameter);
};

let Some(pin_key) = syscall!(client.unwrap_key_from_file(
Mechanism::Chacha8Poly1305,
Expand All @@ -669,7 +674,7 @@ impl Persistent {
return Err(Status::UnspecifiedNonpersistentExecutionError);
};

self.reset_pin(*new_pin, pin_key, client)?;
self.reset_pin(new_pin, pin_key, client)?;
syscall!(client.delete(pin_key));
syscall!(client.delete(puk_key));
Ok(true)
Expand Down
15 changes: 15 additions & 0 deletions tests/command_response.ron
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
Select
]
),
IoTest(
name: "Malformed pin burns a retry",
cmd_resp: [
// eight 0xFF bytes can never match, but count like a wrong pin
VerifyApplicationPin(pin: "FFFFFFFFFFFFFFFF", expected_status: RemainingRetries(2)),
VerifyApplicationPin(pin: "FFFFFFFFFFFFFFFF", expected_status: RemainingRetries(1)),
// the correct pin still verifies and resets the counter
VerifyApplicationPin(),
VerifyApplicationPin(pin: "FFFFFFFFFFFFFFFF", expected_status: RemainingRetries(2)),
]
),
IoTest(
name: "Bad management key",
cmd_resp: [
Expand Down Expand Up @@ -389,6 +400,10 @@
VerifyApplicationPin(pin: "313233343536FFFF", expected_status: RemainingRetries(2)),
ResetRetryCounter(puk: "FFFFFFFFFFFFFFFF", new_pin: "3132333435363738", expected_status: RemainingRetries(4)),
VerifyApplicationPin(pin: "3132333435363738", expected_status: RemainingRetries(1)),
// a wrong puk burns a retry whatever the new pin looks like (how ykman blocks the puk)
ResetRetryCounter(puk: "FFFFFFFFFFFFFFFF", new_pin: "FFFFFFFFFFFFFFFF", expected_status: RemainingRetries(3)),
// a malformed new pin is rejected after the puk matched, before anything is stored
ResetRetryCounter(puk: "0102030405060708", new_pin: "FFFFFFFFFFFFFFFF", expected_status: IncorrectDataParameter),
ResetRetryCounter(puk: "0102030405060708", new_pin: "3132333435363738"),
VerifyApplicationPin(pin: "3132333435363738"),
ChangePuk(
Expand Down
Loading