Chip #1 (0123f3acfd2a826bee) runs Microchip’s test table (test_ecc608_configdata), inherited
through Austin’s Pi code. It works, but it was never meant for a wallet:
Every chip from #2 on gets quorum v1, a table written for one job: a P-256 key that is one owner
of a Safe. The table lives in firmware/chipcfg.py. The ceremony that
writes and seals it lives in firmware/provision.py and runs from the
ceremony image.
0: 01 23 00 00 00 00 60 00 00 00 00 00 ee 00 00 00 the chip's own bytes (never written)
16: c0 00 00 00 a1 2f 9f 8f 9f 8f 9f 8f 9f 8f 9f 8f
32: 9f 8f 9f 8f 9f 8f 9f 8f 9f 8f 9f 8f 9f 8f 9f 8f
48: 9f 8f 9f 8f ff ff ff ff 00 00 00 00 ff ff ff ff
64: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
80: 00 00 00 00 00 00 55 55 ff ff 00 00 00 00 00 00
96: 33 00 1c 00 1c 00 1c 00 1c 00 1c 00 1c 00 1c 00
112: 1c 00 1c 00 1c 00 1c 00 1c 00 1c 00 1c 00 1c 00
sha256 aadcf1ea51b86a8f71c19823c5eea2adbd683221180496622db2c54b66e8abef
0x2FA1, KeyConfig 0x0033This is chip #1’s proven slot 0 (0x2FAF/0x0033) with internal sign and ECDH taken away.
SlotConfig:
| Bits | Field | Value | Meaning |
|---|---|---|---|
| 0-3 | ReadKey | 0001 |
External Sign only. No internal sign, no ECDH. |
| 5 | LimitedUse | 1 | Every signature raises counter 0 first. At the cap, Sign fails. |
| 7 | IsSecret | 1 | Required for Sign. |
| 12-15 | WriteConfig | 0010 |
The Write command never. GenKey allowed. PrivWrite forbidden, so no key can be imported. |
KeyConfig:
The slot is sealed by the slot lock after GenKey (C6). Datasheet §2.4.3: after that, “modification via PrivWrite, Write, GenKey, and/or DeriveKey is permanently prohibited”.
0x8F9F, KeyConfig 0x001CThis is chip #1’s slot 6 row, which has run locked since 2026-09-17.
1000: the Write command never, and not a DeriveKey target.F, because 0 would make a slot a copy source for CheckMac.The ceremony zeroes them before the data lock, and nothing can write them after it.
| Byte(s) | Field | Value | Why |
|---|---|---|---|
| 16 | I2C address | C0 |
0x60, unchanged. |
| 18 | CountMatch | 00 |
Off: no slot-10 trap. |
| 19 | ChipMode | 00 |
1.3 s watchdog, which the driver assumes. |
| 52-67 | Counters | 0 / 0 | Both start at zero. |
| 68-83 | UseLock, VolatileKeyPermission, SecureBoot, KdfIv | 00 |
Off. |
| 88-89 | SlotLocked | FF FF |
Required. A cleared bit for slot 0 at the config lock would lock the slot before it ever had a key. |
| 90-91 | ChipOptions | 00 00 |
No IO protection and no KDF AES. IO protection only covers ECDH, KDF and Verify-MAC outputs, none of which this chip allows. |
| 92-95 | X509 | 00 |
Unused. |
chipcfg.py asserts the table’s fingerprint and shape at import.python3 tools/check_config decodes every slot again from the datasheet’s bit tables,
independently of atecc.decode_slot.0000.A lock without a CRC is how a table bug bricked a chip on 2026-09-15. Both zone locks here carry a summary CRC, and the chip refuses to lock unless what it holds matches the summary exactly.
0x00, summary = CRC-16 of the 128 bytes the zone must hold.
55 55.0x01, summary = CRC-16 of every slot that is not a private key (slots
1-15, zeroed at C8), then the 64-byte OTP zone. Private keys are never in this CRC (datasheet,
Lock).0x82. It ignores the summary.python3 tools/check_config --summary <snapshot> prints the config CRC for a chip, from the
factory dump that C1 saves. Compare it with what the device shows at C2.
The OTP record is written at C8 and frozen by C9. It is 64 bytes, integers big-endian:
| Offset | Size | Content |
|---|---|---|
| 0 | 2 | PQ |
| 2 | 1 | Version 1 |
| 3 | 1 | Profile (1 = quorum v1) |
| 4 | 9 | Serial |
| 13 | 1 | Key slot |
| 14 | 2 | Flags |
| 16 | 16 | SHA-256(qx‖qy), first 16 bytes |
| 32 | 20 | Owner address |
| 52 | 4 | Date |
| 56 | 4 | Build |
| 60 | 4 | Counter 0 |
The record is advisory: the console always checks the live key.
It runs on the ceremony image: tools/fw push ceremony.
secrets.py pins a chip (QUORUM_EXPECT), so take the pin out
first.Getting the board ready:
tools/fw push dual, with the ALLOW_* flags still False and the old chip still on. The dual
image holds both the console and the ceremony. At every boot it reads the chip on the bus:
So you can swap chips back and forth during the provisioning. Once the new chip is sealed,
enrolled and pinned, the board goes back to tools/fw push console.
(tools/fw push ceremony --pinned-chip-ok is the ceremony alone.)
chips.json).tools/fw allow on and tools/fw restart. allow on refuses while the pinned
chip is on the bus.tools/fw allow off.Permanent steps also need the B+Y arm (held 3 s, lasts 60 s) and A held for 3 s.
| Step | What | Checks | Permanent? |
|---|---|---|---|
| C1 | Pre-flight | At 0x60, revision 6002/6003 matching bytes 4-7, zones 55 55, SlotLocked FF FF, byte 16 C0, Random’s unlocked pattern, self-test, the factory table. Saves snapshot-<serial>.bin |
no |
| C2 | Write quorum v1 | Readback is quorum v1; shows the config CRC | no, rewritable |
| C3 | Config lock with CRC | Byte 87 = 00, still quorum v1, Random live, slot 0 decodes right |
yes |
| C4 | GenKey slot 0 | KeyValid, the public key reads back, GenKey on slot 1 refused | replaceable until C6 |
| C5 | Two test signatures, a plain digest and a WebAuthn assertion | Both verify on the Pico, counter +2, Sign on slot 1 refused | no |
| C6 | Slot-lock slot 0 | FE FF, the same public key, signs and verifies |
yes |
| C7 | Owner address | Compare with tools/fw verify --baseline on the laptop, and with the factory’s getSigner on Base |
no |
| C8 | Zero-fill slots 1-15, record into OTP | The data CRC it will need | rewritable until C9 |
| C9 | Data lock with CRC | Byte 86 = 00, OTP holds the record, slot 1 unreadable, OTP frozen, signs and verifies |
yes |
| C10 | Summary | quorum v1 locked 00/00, slot 0 sealed, the record matches the key; set ALLOW_* back to False |
no |
The slot lock comes before the data lock on purpose. While the data zone is open, a plaintext PrivWrite could put a known key into slot 0 (LOCKING.md).
Then, each with its own explicit yes:
tools/quorum live enroll deploys the signer proxy.swapOwner on the Base Safe. It needs 2 of 3; chip #1 can co-sign its own replacement, and the
console decodes the owner change in red.tools/fw push console, then tools/fw pin with the new chip.node test-vectors/run-ceremony.mjs runs all of C1-C10 on the emulator’s virtual chip (608A and
608B), with a reboot after every step. It also runs the refusals:
ALLOW_LOCKThen it has the console sign with the sealed chip.
What only silicon can settle:
crc[0] | crc[1] << 8, as cryptoauthlib
sends it). A wrong guess makes the chip refuse the lock, which locks nothing.0x81), by explicit decision
only.A spare breakout (about $6) run through C1-C9 first settles all three. Without one, every unknown is either harmless (a CRC lock that refuses locks nothing) or has a fallback before anything irreversible.
Delta from quorum v1:
B3 09: ReqAuth, with AuthKey slot 9BF 8F: LimitedUse, so failed attempts count too5C 00: ReqRandomBefore the data lock, slot 9 would get K = PBKDF2-HMAC-SHA256(PIN, “pico-quorum pin v1” ‖ serial).
What it would need:
Why not now:
With a PIN, the WebAuthn UV flag (webauthn.FLAGS) would be enforced by the chip. Without it, UV is
the firmware’s word that someone held A on the device that showed the transaction.