Skip to main content

CryptoService

Struct CryptoService 

Source
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>
where H: AteccHal, C: Clock, H::Error: Debug,

Source

pub fn new(atecc: Atecc<H>, clock: C) -> Self

Wrap an existing [Atecc] handle and clock.

Source

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

See CryptoServiceError::Atecc.

Source

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

See CryptoServiceError::Atecc.

Source

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_reset to refresh every identity slot at once.
§Errors
Source

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
Source

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
Source

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 with ParseError 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, and LockConfig. Those are modified only via the dedicated UpdateExtra and Lock commands; the Write command 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 call UpdateExtra / Lock separately 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
Source

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
Source

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.

Source

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.

Source

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.

Source

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
  1. Verify the precondition: both Counter0 and Counter1 are at a multiple of their respective batch_size with value > 0.
  2. Regenerate ECC keys in slots 0, 1, 2, 3, 4, 7.
  3. Generate a fresh random 8-digit PUK on-chip, write its hash to slot 6, refresh Counter1.
  4. Reset slot 5 to SHA-256("0000" || pin_salt), refresh Counter0.
  5. 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 + 1 on 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
Source

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.

Source

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
Source

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
Source

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
Source

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
Source

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
Source

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
Source

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
Source

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
Source

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

See CryptoServiceError::Atecc.

Source

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.

Source

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.

Source

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
Source

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
Source

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
Source

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.

Source

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.

Source

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.

Source

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.

Source

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:

  1. Read a fresh 32-byte random from the chip and load it into TempKey via Nonce(passthrough).
  2. Issue GenDig(zone=Data, key_id=IO_KEY_SLOT). TempKey becomes the derived session key.
  3. Host computes the same session key locally.
  4. Host XOR-encrypts plaintext and computes the Write MAC.
  5. Write(slot, encrypted) with ciphertext || 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.

Auto Trait Implementations§

§

impl<H, C> Freeze for CryptoService<H, C>
where C: Freeze, H: Freeze,

§

impl<H, C> RefUnwindSafe for CryptoService<H, C>

§

impl<H, C> Send for CryptoService<H, C>
where C: Send, H: Send,

§

impl<H, C> Sync for CryptoService<H, C>
where C: Sync, H: Sync,

§

impl<H, C> Unpin for CryptoService<H, C>
where C: Unpin, H: Unpin,

§

impl<H, C> UnsafeUnpin for CryptoService<H, C>
where C: UnsafeUnpin, H: UnsafeUnpin,

§

impl<H, C> UnwindSafe for CryptoService<H, C>
where C: UnwindSafe, H: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.