pub struct CryptoService<H, C>where
H: AteccHal,
C: Clock,{
atecc: Atecc<H>,
clock: C,
session: Session,
serial: Option<[u8; 9]>,
}Expand description
High-level crypto orchestrator.
Fields§
§atecc: Atecc<H>§clock: C§session: Session§serial: Option<[u8; 9]>Cached chip serial, populated on first use.
Implementations§
Source§impl<H, C> CryptoService<H, C>
impl<H, C> CryptoService<H, C>
Sourcepub async fn info(&mut self) -> Result<DeviceInfo, CryptoServiceError<H::Error>>
pub async fn info(&mut self) -> Result<DeviceInfo, CryptoServiceError<H::Error>>
Return chip revision, serial, and provisioning state.
Opens one channel for the revision read, then reuses a second channel for the config-zone reads that produce serial and lock status. The two channels are independent because there is no volatile state to share.
§Errors
Sourcepub async fn get_pubkey(
&mut self,
slot: Slot,
) -> Result<[u8; 64], CryptoServiceError<H::Error>>
pub async fn get_pubkey( &mut self, slot: Slot, ) -> Result<[u8; 64], CryptoServiceError<H::Error>>
Read the 64-byte public key from slot.
Does not require an active PIN session: the public key is, well,
public. Internally calls GenKey in mode 0 (compute pubkey from
existing private key, no mutation).
§Errors
Sourcepub async fn genkey_create(
&mut self,
slot: Slot,
) -> Result<[u8; 64], CryptoServiceError<H::Error>>
pub async fn genkey_create( &mut self, slot: Slot, ) -> Result<[u8; 64], CryptoServiceError<H::Error>>
Generate a fresh ECC P-256 private key on chip in the specified slot. The old key (if any) is destroyed. Returns the corresponding public key (64 bytes: X || Y).
The chip enforces the slot’s policy: a slot configured as
Locked rejects this command, and a slot whose KeyConfig does
not allow GenKey(create) is also rejected.
In this project, genkey_create is used:
- During provisioning to populate the primary identity key in slot 0 (and optionally extra user slots in 1..=4 and 7).
- In
Self::emergency_resetto refresh every identity slot at once.
§Errors
CryptoServiceError::Ateccon chip-level failures.
Sourcepub async fn read_config_slot(
&mut self,
slot: Slot,
) -> Result<[u8; 4], CryptoServiceError<H::Error>>
pub async fn read_config_slot( &mut self, slot: Slot, ) -> Result<[u8; 4], CryptoServiceError<H::Error>>
Read the per-slot configuration bytes (SlotConfig + KeyConfig).
Returns 4 bytes: [SlotConfig_lo, SlotConfig_hi, KeyConfig_lo, KeyConfig_hi]. This is useful when the host wants to inspect a
slot’s policy without re-downloading the full 128-byte config
zone (which read_config_zone already does block by block).
SlotConfig for slot N lives at config byte 20 + 2*N and
KeyConfig at 96 + 2*N. This method reads the whole config
zone and extracts those 4 bytes. The chip cost is identical to
read_config_zone (4 reads of 32 bytes each).
§Errors
CryptoServiceError::Ateccon chip-level failures.
Sourcepub async fn read_config_block(
&mut self,
block: u8,
) -> Result<[u8; 32], CryptoServiceError<H::Error>>
pub async fn read_config_block( &mut self, block: u8, ) -> Result<[u8; 32], CryptoServiceError<H::Error>>
Read one 32-byte block of the configuration zone.
block must be in 0..=3. Each block covers a fixed 32-byte
region:
- block 0 : factory area +
SlotConfig[0..6] (bytes 0..32) - block 1 :
SlotConfig[6..16] + Counter0 + start of Counter1 (32..64) - block 2 : end of Counter1 + UseLock..ChipOptions..X509format (64..96)
- block 3 :
KeyConfig[0..16] (96..128)
§Errors
CryptoServiceError::InvalidFormatifblock > 3.CryptoServiceError::Ateccon chip-level failures.
Sourcepub async fn write_config_block(
&mut self,
block: u8,
data: &[u8; 32],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn write_config_block( &mut self, block: u8, data: &[u8; 32], ) -> Result<(), CryptoServiceError<H::Error>>
Write one 32-byte block of the configuration zone (provisioning).
The ATECC608B’s Write command refuses to touch certain regions of
the configuration zone, even before lock. The reference Microchip
routine calib_write_bytes_zone in CryptoAuthLib
(lib/calib/calib_basic.c) handles this by switching from 32-byte
to 4-byte transfers on the affected blocks and by skipping the
non-writable words entirely. We follow the same strategy.
Concretely:
- Block 0 (chip-side bytes 0..32). Words 0..=3 (bytes 0..16) are
the read-only factory area (serial,
RevNum, reserved). Any 32-byte write that includes them is rejected withParseError 0x03. Words 4..=7 (bytes 16..32) are written one at a time in 4-byte mode. - Block 1 (bytes 32..64). All writable. Single 32-byte write.
- Block 2 (bytes 64..96). Word 5 (bytes 84..88) covers
UserExtra, Selector,LockValue, andLockConfig. Those are modified only via the dedicatedUpdateExtraandLockcommands; theWritecommand rejects the whole 32-byte transfer if word 5 is part of it. Words 0..=4 and 6..=7 are written one at a time in 4-byte mode; word 5 is skipped entirely. The host-side blob’s bytes 84..88 are therefore ignored by the chip, make sure the factory defaults (0x00 0x00 0x55 0x55) match what the blob contains for these positions, or callUpdateExtra/Lockseparately if a different value is desired. - Block 3 (bytes 96..128). All writable. Single 32-byte write.
This operation is reversible while the config zone is unlocked
(LockConfig != 0). Once LockConfigZone has been issued, every
chip-side Write here will return a chip error.
block must be in 0..=3.
§Errors
CryptoServiceError::InvalidFormatifblock > 3.CryptoServiceError::Ateccon chip-level failures.
Sourcepub async fn verify_pin(
&mut self,
pin: &[u8; 4],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn verify_pin( &mut self, pin: &[u8; 4], ) -> Result<(), CryptoServiceError<H::Error>>
Open a PIN session if the provided PIN is correct.
The CheckMac of slot 5 bumps Counter0 regardless of the outcome,
so each call costs one count. On success, the service bumps the
counter additionally to bring it back up to the next multiple of
PIN_MAX_RETRIES, granting the user a fresh batch of 5 attempts.
§Errors
CryptoServiceError::InvalidFormatifpinis not 4 ASCII digits.CryptoServiceError::PinIncorrectwith tries remaining.CryptoServiceError::PinBlockedif Counter0 already at threshold.CryptoServiceError::Ateccfor I/O / chip errors.
Sourcepub async fn unblock_pin(
&mut self,
puk: &[u8; 8],
new_pin: &[u8; 4],
io_key: &[u8; 32],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn unblock_pin( &mut self, puk: &[u8; 8], new_pin: &[u8; 4], io_key: &[u8; 32], ) -> Result<(), CryptoServiceError<H::Error>>
Reset the PIN slot via the PUK.
On success, slot 5 is rewritten with the SHA-256 of the new PIN and
Counter0 is refreshed back to a fresh batch.
io_key is the 32-byte I/O Protection Key (slot 8 content) known
to the host that performed provisioning. The service uses it to
build the encrypted-write payload. The key is never stored in the
service. It lives only for the duration of this call.
§Errors
See CryptoServiceError variants.
Sourcepub async fn set_pin(
&mut self,
old_pin: &[u8; 4],
new_pin: &[u8; 4],
io_key: &[u8; 32],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn set_pin( &mut self, old_pin: &[u8; 4], new_pin: &[u8; 4], io_key: &[u8; 32], ) -> Result<(), CryptoServiceError<H::Error>>
Change the PIN.
Defence in depth: the caller must supply both the current PIN
(old_pin) and the new one. The current PIN is re-checked
against slot 5 via the same CheckMac flow as Self::verify_pin.
This protects against the case where a USB session is hijacked
while a PIN session is open. Knowing the active session is not
enough to rotate the PIN.
On success, the PIN session is opened (or refreshed). Slot 5 is
rewritten with SHA-256(new_pin || pin_salt) via the
encrypted-write protocol. Counter0 is refreshed.
§Errors
See CryptoServiceError variants.
Sourcepub async fn set_puk(
&mut self,
old_puk: &[u8; 8],
new_puk: &[u8; 8],
io_key: &[u8; 32],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn set_puk( &mut self, old_puk: &[u8; 8], new_puk: &[u8; 8], io_key: &[u8; 32], ) -> Result<(), CryptoServiceError<H::Error>>
Change the PUK.
Requires an active PIN session AND the current PUK. Defence in
depth: knowing the active session is not enough; the current PUK
is re-verified via CheckMac on slot 6, consuming one Counter1
attempt internally (refreshed on success).
§Errors
See CryptoServiceError variants.
Sourcepub async fn emergency_reset(
&mut self,
io_key: &[u8; 32],
) -> Result<[u8; 8], CryptoServiceError<H::Error>>
pub async fn emergency_reset( &mut self, io_key: &[u8; 32], ) -> Result<[u8; 8], CryptoServiceError<H::Error>>
Last-chance reset for the case where the user has forgotten both
the PIN and the PUK and has exhausted both LimitedUse batches.
This is the recovery path of last resort. Discards everything user-owned and rebuilds a clean baseline. The user loses the ECC private keys in slots 0..=4 and 7 forever.
§Preconditions
The service refuses to perform this operation unless both counters report zero attempts remaining. This is the hard guarantee that prevents the call from being a back door:
- If the user has forgotten only the PIN but not the PUK, they
should use
Self::unblock_pin. - Only the combined “PIN forgotten + PUK forgotten + both
batches exhausted” state authorises
emergency_reset.
§What it does
- Verify the precondition: both Counter0 and Counter1 are at a
multiple of their respective
batch_sizewith value > 0. - Regenerate ECC keys in slots 0, 1, 2, 3, 4, 7.
- Generate a fresh random 8-digit PUK on-chip, write its hash to slot 6, refresh Counter1.
- Reset slot 5 to
SHA-256("0000" || pin_salt), refresh Counter0. - Return the new PUK to the caller for one-time display.
§What it does NOT do
- It cannot reset Counter0 or Counter1 to zero. Both counters
are bumped further during the recovery (one increment to
reach
multiple + 1on each). The user is granted one fresh batch of PIN attempts and one fresh batch of PUK attempts. - It does not rewrite slot 8 (IO key). The caller must supply the IO key, which is stored host-side (the host knows it from provisioning).
- If the chip’s hardware counter limit (2^21) is reached during the refresh increments, the operation reports an error and the chip is genuinely bricked. There is nothing more software can do.
§Errors
CryptoServiceError::EmergencyResetNotPermittedif either counter still has attempts remaining.CryptoServiceError::Brickedif the chip’s hardware counter limit is hit during refresh.CryptoServiceError::Ateccfor chip-level errors.
Sourceasync fn generate_random_puk(
&mut self,
) -> Result<[u8; 8], CryptoServiceError<H::Error>>
async fn generate_random_puk( &mut self, ) -> Result<[u8; 8], CryptoServiceError<H::Error>>
Pull 8 ASCII digits from the chip’s RNG to produce a new PUK.
The distribution is uniform over [b'0'..=b'9'], obtained by
modulo on each random byte (one random byte yields one digit).
32 bytes of random are pulled for an 8-byte PUK, which leaves
ample headroom if any byte ever turned out to be unusable.
Sourcepub async fn lock_config_zone(
&mut self,
expected_crc: u16,
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn lock_config_zone( &mut self, expected_crc: u16, ) -> Result<(), CryptoServiceError<H::Error>>
Permanently lock the configuration zone.
expected_crc is the CRC-16/CCITT of the configuration zone as
the host expects it to be. The chip recomputes its own and
rejects the lock if the two disagree.
Irreversible. See [atecc608b::AteccChannel::lock_config_zone].
§Errors
CryptoServiceError::Ateccif the chip refuses (typically because the CRC does not match).
Sourcepub async fn lock_data_zone(
&mut self,
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn lock_data_zone( &mut self, ) -> Result<(), CryptoServiceError<H::Error>>
Permanently lock the data + OTP zones.
Irreversible. See [atecc608b::AteccChannel::lock_data_zone].
No CRC is checked at the chip level: secret-bearing slots
(IsSecret=1) cannot be read back to compute one. The double
confirmation prompt in the host CLI and the magic-word check in
the firmware are the only guards.
§Errors
CryptoServiceError::Ateccif the chip refuses.
Sourcepub async fn lock_slot(
&mut self,
slot: Slot,
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn lock_slot( &mut self, slot: Slot, ) -> Result<(), CryptoServiceError<H::Error>>
Permanently lock an individual slot. The slot’s config in the
configuration zone must have Lockable=1 for this to succeed.
Irreversible. See [atecc608b::AteccChannel::lock_slot].
§Errors
CryptoServiceError::Ateccif the chip refuses (slot not lockable, or already locked).
Sourcepub async fn provision_slot(
&mut self,
slot: Slot,
value: &[u8; 32],
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn provision_slot( &mut self, slot: Slot, value: &[u8; 32], ) -> Result<(), CryptoServiceError<H::Error>>
Write a 32-byte value in cleartext into one of the data slots.
Only legal while the data zone is unlocked. After
Self::lock_data_zone, writes to data slots must go through
Self::write_slot_encrypted.
Restricted by policy to the three slots that hold project-level secrets and need to be initialised before lock:
- Slot 5 (PIN hash). Written with
SHA-256("0000" || pin_salt)to set the default PIN. Salt is derived from the chip serial. - Slot 6 (PUK hash). Written with
SHA-256(random_puk || puk_salt). - Slot 8 (IO Protection Key). Written with a random 32-byte value, kept secret host-side for later encrypted writes.
Other slots are rejected at the service layer to avoid mistakes.
ECC slots (0..=4, 7) are populated via GenKey, not by write.
Reserve slots (9..=15) are kept unprovisioned for V2.
§Errors
CryptoServiceError::InvalidSlotif the slot is not one of the three policy-allowed targets.CryptoServiceError::Ateccif the chip refuses the write (most likely because the data zone is already locked).
Sourcepub async fn provision_initial_puk(
&mut self,
) -> Result<[u8; 8], CryptoServiceError<H::Error>>
pub async fn provision_initial_puk( &mut self, ) -> Result<[u8; 8], CryptoServiceError<H::Error>>
Generate a fresh random PUK and write its hash into slot 6, in cleartext. Returns the PUK to the caller for one-time display.
Only legal while the data zone is unlocked: used during provisioning to set the initial PUK before locking.
§Errors
CryptoServiceError::Ateccfor chip-level errors.
Sourcepub async fn provision_initial_pin(
&mut self,
) -> Result<(), CryptoServiceError<H::Error>>
pub async fn provision_initial_pin( &mut self, ) -> Result<(), CryptoServiceError<H::Error>>
Write the hash of the default PIN (“0000”) into slot 5 in cleartext. Used at provisioning, before data lock.
§Errors
CryptoServiceError::Ateccfor chip-level errors.
Sourcepub async fn provision_initial_io_key(
&mut self,
) -> Result<[u8; 32], CryptoServiceError<H::Error>>
pub async fn provision_initial_io_key( &mut self, ) -> Result<[u8; 32], CryptoServiceError<H::Error>>
Generate a fresh random 32-byte I/O Protection Key from the chip’s RNG, write it into slot 8 in cleartext, and return it to the caller.
The caller must persist this value immediately: it is the only opportunity to learn the IO key. After data lock, slot 8 becomes write-only via the encrypted-write protocol, which itself depends on knowing the IO key. Losing the IO key turns the chip into a permanently degraded device (no more PIN/PUK changes possible).
Only legal while the data zone is unlocked.
§Errors
CryptoServiceError::Ateccfor chip-level errors.
Sourcepub async fn sign(
&mut self,
slot: Slot,
digest: &[u8; 32],
) -> Result<[u8; 64], CryptoServiceError<H::Error>>
pub async fn sign( &mut self, slot: Slot, digest: &[u8; 32], ) -> Result<[u8; 64], CryptoServiceError<H::Error>>
Sign a 32-byte digest with the private key in slot.
Requires an active PIN session.
Internally this is a two-step chip workflow on the ATECC608:
Nonce(passthrough, target=MsgDigBuf) then
Sign(external, source=MsgDigBuf, slot). Both steps run inside a
single chip channel so the MsgDigBuf register survives between
them. The MsgDigBuf source (rather than the older TempKey-based
path used by the 508A) is required on the 608 to make the chip
sign the supplied digest verbatim, without blending in any other
chip state. Returned R || S is 64 bytes big-endian.
§Errors
CryptoServiceError::PinRequiredif no session is active.CryptoServiceError::Ateccfor chip-level errors (slot misconfigured, authorization missing, etc).
Sourcepub async fn get_pin_status(
&mut self,
) -> Result<PinStatus, CryptoServiceError<H::Error>>
pub async fn get_pin_status( &mut self, ) -> Result<PinStatus, CryptoServiceError<H::Error>>
Report the current PIN / PUK retry counters and session state.
Reads both counters within a single chip channel for efficiency.
§Errors
Sourcepub fn close_session(&mut self)
pub fn close_session(&mut self)
Terminate the active PIN session immediately, without waiting for the 30 s inactivity timeout.
Idempotent: closing an already-closed session is a no-op. Useful when the user wants to lock the dongle proactively after a signing burst, instead of letting the timeout expire.
Sourcepub fn is_session_active(&self) -> bool
pub fn is_session_active(&self) -> bool
Return whether a PIN session is currently active.
Used by command handlers that need to refuse pre-touch on
session-gated operations (notably Sign): without this early
check the firmware would arm the touch wait then time out 30
seconds later instead of failing immediately with
CryptoServiceError::PinRequired.
Sourcepub async fn read_slot_block(
&mut self,
slot: Slot,
block: u8,
) -> Result<[u8; 32], CryptoServiceError<H::Error>>
pub async fn read_slot_block( &mut self, slot: Slot, block: u8, ) -> Result<[u8; 32], CryptoServiceError<H::Error>>
Read one 32-byte block from a data slot.
Used by CommandOpcode::ReadSlotBlock for bring-up diagnostics
(verifying what ProvisionSlot wrote) and to inspect the IO key
in slot 8 before locking the data zone.
No PIN session check here: the chip itself enforces slot policy.
A slot configured IsSecret or with EncryptRead returns a chip
error, which surfaces here as CryptoServiceError::Atecc.
§Errors
CryptoServiceError::Ateccon chip-level errors (forbidden read, bad address, etc).
Sourcepub async fn read_slot_word(
&mut self,
slot: Slot,
block: u8,
offset_words: u8,
) -> Result<[u8; 4], CryptoServiceError<H::Error>>
pub async fn read_slot_word( &mut self, slot: Slot, block: u8, offset_words: u8, ) -> Result<[u8; 4], CryptoServiceError<H::Error>>
Read one 4-byte word from a data slot.
See Self::read_slot_block for context. Same policy enforcement.
§Errors
CryptoServiceError::Ateccon chip-level errors.
Sourcepub async fn read_counter(
&mut self,
counter: CounterId,
) -> Result<u32, CryptoServiceError<H::Error>>
pub async fn read_counter( &mut self, counter: CounterId, ) -> Result<u32, CryptoServiceError<H::Error>>
Read the raw value of one of the chip’s monotonic counters.
Returns the binary count as decoded by the chip (the chip’s
Counter(mode=Read) command performs the popcount-style decoding
of its 8-byte storage into a u32). The returned value starts at
0 on a factory-fresh chip and increments by 1 on every key-usage
event for keys whose SlotConfig.LimitedUse == 1.
Used internally by Self::verify_pin and the PIN/PUK retry
arithmetic; exposed publicly so the host CLI can read the raw
value for bring-up diagnostics without going through
retries_remaining.
§Errors
CryptoServiceError::Ateccon chip-level failures.
Sourceasync fn cached_serial(
&mut self,
) -> Result<[u8; 9], CryptoServiceError<H::Error>>
async fn cached_serial( &mut self, ) -> Result<[u8; 9], CryptoServiceError<H::Error>>
Read the chip’s 9-byte serial number, caching it on first call.
Sourceasync fn is_provisioned(&mut self) -> Result<bool, CryptoServiceError<H::Error>>
async fn is_provisioned(&mut self) -> Result<bool, CryptoServiceError<H::Error>>
Check whether the chip is in its operational locked state.
Inspects the config zone lock byte at offset 87. 0x55 = unlocked,
0x00 = locked. Data zone lock is at offset 86.
Sourceasync fn checkmac_with_hash(
&mut self,
slot: Slot,
expected_slot_value: &[u8; 32],
serial: &[u8; 9],
) -> Result<bool, CryptoServiceError<H::Error>>
async fn checkmac_with_hash( &mut self, slot: Slot, expected_slot_value: &[u8; 32], serial: &[u8; 9], ) -> Result<bool, CryptoServiceError<H::Error>>
Issue a CheckMac against slot with the host-computed hash that
should match its content, and return whether the chip confirmed.
Random + CheckMac run inside the same chip channel: this avoids two
wake / idle round-trips and matches the natural “ask for a challenge,
then use it” flow.
Sourceasync fn refresh_counter_batch(
&mut self,
counter: CounterId,
batch_size: u8,
) -> Result<(), CryptoServiceError<H::Error>>
async fn refresh_counter_batch( &mut self, counter: CounterId, batch_size: u8, ) -> Result<(), CryptoServiceError<H::Error>>
Increment counter until its value lands one past a multiple of
batch_size, i.e. count % batch_size == 1.
Called after a successful PIN or PUK verify to grant the user a
fresh batch of attempts while keeping count % batch == 0 as
an unambiguous saturation indicator: if a future
retries_remaining observes count % batch == 0 with
count > 0, it can conclude that the user has consumed a full
batch without any successful verify in between (and route to
emergency recovery).
The cost is one attempt per refresh: a PIN batch effectively
allows 4 tries (rather than 5), a PUK batch 9 (rather than 10).
In exchange the host gains a reliable way to detect saturation,
which enables Self::emergency_reset.
Reads the counter and all increments share one channel.
Sourceasync fn write_slot_encrypted(
&mut self,
target_slot: Slot,
plaintext: &[u8; 32],
io_key: &[u8; 32],
chip_serial: &[u8; 9],
) -> Result<(), CryptoServiceError<H::Error>>
async fn write_slot_encrypted( &mut self, target_slot: Slot, plaintext: &[u8; 32], io_key: &[u8; 32], chip_serial: &[u8; 9], ) -> Result<(), CryptoServiceError<H::Error>>
Perform an encrypted 32-byte write into target_slot.
Sequence:
- Read a fresh 32-byte random from the chip and load it into
TempKeyviaNonce(passthrough). - Issue
GenDig(zone=Data, key_id=IO_KEY_SLOT).TempKeybecomes the derived session key. - Host computes the same session key locally.
- Host XOR-encrypts
plaintextand computes the Write MAC. Write(slot, encrypted)withciphertext || mac.
All chip commands run within a single channel because TempKey
must survive from Nonce to the encrypted Write.
Used by Self::set_pin and Self::unblock_pin to update the
PIN hash in slot 5.