Skip to main content

config_generator/
counter_encoding.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Encoding of the 8-byte initial value for the ATECC608B monotonic
17//! counters (`Counter0` at bytes 52..60 of the config zone, `Counter1` at
18//! bytes 60..68).
19//!
20//! The chip stores each counter as a redundant 8-byte structure split
21//! into two 16-bit linear ("lin") halves and two 16-bit binary ("bin")
22//! halves. Linear halves encode the low 5 bits of the count by clearing
23//! one bit per increment (popcount-style); binary halves encode the high
24//! 16 bits as a normal big-endian unsigned 16-bit integer. The two halves
25//! are offset by 16 increments so a corruption of either is detected by
26//! the chip.
27//!
28//! Writing `0xFF` to all 8 bytes does **not** represent "count = 0". It
29//! leaves `bin_a` and `bin_b` at `0xFFFF`, which the chip interprets as
30//! a high-bit count near the hardware ceiling (`2^21 - 1`). A chip that
31//! is config-locked with this stray initialization comes out of the lock
32//! with `count ≈ 2_097_120` (= `0xFFFF * 32`), losing virtually all of
33//! its `2^21` lifetime increments.
34//!
35//! The correct factory initialization is `FF FF FF FF 00 00 00 00`,
36//! which decodes to `count = 0`. The function in this module produces
37//! that, plus any other target count up to the maximum.
38//!
39//! # Reference
40//!
41//! Translation of `calib_write_config_counter` in
42//! `lib/calib/calib_basic.c` of Microchip CryptoAuthLib. The formula:
43//!
44//! ```text
45//!     lin_a = 0xFFFF >> (counter_value % 32)
46//!     lin_b = 0xFFFF >> ((counter_value - 16) % 32)     if counter_value >= 16 else 0xFFFF
47//!     bin_a = counter_value / 32
48//!     bin_b = (counter_value - 16) / 32                  if counter_value >= 16 else 0
49//! ```
50//!
51//! is serialized big-endian as:
52//!
53//! ```text
54//!     bytes = [lin_a_hi, lin_a_lo, lin_b_hi, lin_b_lo,
55//!              bin_a_hi, bin_a_lo, bin_b_hi, bin_b_lo]
56//! ```
57
58/// Maximum supported counter value on the ATECC608B (`2^21 - 1`).
59///
60/// Each successful "key-usage event" against a slot whose `LimitedUse`
61/// bit is set bumps the counter by one. When the counter reaches this
62/// ceiling the chip refuses further increments. The value is fixed by
63/// the chip's storage size (21 bits encoded in the 8-byte structure).
64pub const COUNTER_MAX_VALUE: u32 = 2_097_151;
65
66/// Number of bytes consumed by one counter in the config zone.
67pub const COUNTER_STORAGE_SIZE: usize = 8;
68
69/// Encode a target counter value into the 8-byte storage representation
70/// the ATECC608B expects in the configuration zone.
71///
72/// `value == 0` produces the factory initialization, the canonical
73/// "fresh chip" state. Any value in `0..=COUNTER_MAX_VALUE` is accepted
74/// and produces a valid storage; values above are clamped to
75/// `COUNTER_MAX_VALUE` because the chip cannot represent more.
76///
77/// Mirrors `calib_write_config_counter` from CryptoAuthLib exactly. The
78/// `[u8; 8]` returned is what gets written to bytes 52..60 (Counter0)
79/// or 60..68 (Counter1) of the configuration zone blob.
80///
81/// # Examples
82///
83/// ```ignore
84/// // Factory init: count = 0 → FF FF FF FF 00 00 00 00.
85/// assert_eq!(encode_counter_value(0), [0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0]);
86/// ```
87#[must_use]
88pub fn encode_counter_value(value: u32) -> [u8; COUNTER_STORAGE_SIZE]
89{
90    let value = value.min(COUNTER_MAX_VALUE);
91
92    let lin_a: u16 = (0xFFFFu32 >> (value % 32)) as u16;
93    let lin_b: u16 = if value >= 16
94    {
95        (0xFFFFu32 >> ((value - 16) % 32)) as u16
96    }
97    else
98    {
99        0xFFFF
100    };
101    // `bin_a` and `bin_b` fit in 16 bits because `value <= 2^21 - 1` and
102    // `bin_a = value / 32` is therefore at most `(2^21 - 1) / 32 = 65535`.
103    let bin_a: u16 = (value / 32) as u16;
104    let bin_b: u16 = if value >= 16
105    {
106        ((value - 16) / 32) as u16
107    }
108    else
109    {
110        0
111    };
112
113    [
114        (lin_a >> 8) as u8, (lin_a & 0xFF) as u8,
115        (lin_b >> 8) as u8, (lin_b & 0xFF) as u8,
116        (bin_a >> 8) as u8, (bin_a & 0xFF) as u8,
117        (bin_b >> 8) as u8, (bin_b & 0xFF) as u8,
118    ]
119}
120
121#[cfg(test)]
122mod tests
123{
124    use super::*;
125
126    /// Reference vectors generated from `encode_counter_cryptoauthlib`
127    /// (the Python translation we used to validate the chip's behavior
128    /// during bring-up). If this list ever diverges from CryptoAuthLib,
129    /// the test fails and we know we've drifted from the spec.
130    #[test]
131    fn matches_cryptoauthlib_reference_vectors()
132    {
133        let cases: &[(u32, [u8; 8])] =
134        &[
135            (0,        [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
136            (1,        [0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
137            (5,        [0x07, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
138            (31,       [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]),
139            (32,       [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]),
140            (100,      [0x0F, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02]),
141            (2_097_120,[0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE]),
142            (COUNTER_MAX_VALUE, [0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF]),
143        ];
144        for (value, expected) in cases
145        {
146            let got = encode_counter_value(*value);
147            assert_eq!(
148                got, *expected,
149                "encode_counter_value({value}) = {got:02X?}, expected {expected:02X?}",
150            );
151        }
152    }
153
154    #[test]
155    fn factory_zero_is_all_ff_then_all_zero()
156    {
157        // The most important case: a freshly initialized chip must see
158        // its counter at 0. Any other value here means we ship a chip
159        // with a degraded budget, which is exactly the bug this module
160        // was created to fix.
161        assert_eq!
162        (
163            encode_counter_value(0),
164            [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
165        );
166    }
167
168    #[test]
169    fn values_above_max_clamp()
170    {
171        // u32::MAX is way above the chip's 2^21 - 1 ceiling. Encoding
172        // such a value must produce the same byte pattern as encoding
173        // exactly `COUNTER_MAX_VALUE`, never a wraparound that the chip
174        // would interpret as a small count.
175        assert_eq!(encode_counter_value(u32::MAX), encode_counter_value(COUNTER_MAX_VALUE));
176    }
177}