Factory Flashing (SEC.3)
SkillProductivityUse when working on the SEC.3 factory-flashing pipeline, provisioning per-device keys into an STM32 at manufacture (app/services/factory_flashing/, lib/tasks/factory.rake): the one-pass silicon UID→DID derivation [FW.54], the wrong-board guard, UID collision → quarantine, the six Flash key blocks and their magics, word→big-endian byte order [FW.30], the chain-hashed audit trail, and the 2-Person supervisor rule. ⚠️ Гілка B / SE05x is EMIT-ONLY today, the provisioner writes textual legacy ATECC calls and no real transport, so a unit flashed by it gets no Flash KEYL and bricks; treat any SE05x request as blocked, not supported. Routes to 03_06 §5 + 03_01 §7, does not restate. Examples: \"why did factory:flash raise CollisionError\", \"add a key slot\", \"the backend DID does not match the silicon\", \"provision a new board\", \"what does the flashing session do, in order\", \"why is Гілка B not usable\".
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Factory Flashing (SEC.3) skill
What this skill tells your AI
The instructions your AI receives, as published by alexey-lukin/silken_net in .claude/skills/factory-flashing/SKILL.md and read by ahel’s review.
Burns per-device keys into a Soldier/Queen at manufacture, then locks the chip (RDP). Navigation aid — the SSOT is the code + docs below; this skill points, it does not restate.
SSOT Documents — Read These First
| Document | What it covers |
|---|---|
docs/03_06_Factory_Flashing_and_Key_Provisioning.md | THE factory home (split from 03_05 §3.4): pipeline Гілки A/B, HKDF per-device derivation, K_ota, §5 ops-security (2-Person Rule, master-key delivery variants, SEC.3 status) |
docs/03_05_Hardware_Symmetric_Crypto_and_Security.md | Crypto modes, SE050 (SEC.6), key rotation, RDP (SEC.2) |
docs/00_07_Action_Plan_Tracker.md | SEC.3 (bench SWD + Bitwarden live — residuals), SEC.2 (RDP Level 2) |
Pipeline (entry → exit)
Entry: lib/tasks/factory.rake → [FW.54] one-pass UID→DID at factory:flash:
for a Tree the device_uid arg = 24-hex silicon UID (NOT a DID) →
SilkenNet::DidDerivation.wire_did_from_uid_hex → TreeResolver.resolve!
(create with CLUSTER_ID+TREE_FAMILY_ID env / re-flash / bind legacy /
DID-collision → CollisionError = quarantine, 03_01 §7); the session's
device_uid = derived wire-DID. Bare SNET- DID accepted only for a Tree
that already has trees.silicon_uid_hex; Gateway path unchanged.
Then FactoryFlashing::Session.run (after supervisor-approved). One ActiveRecord::Base.transaction:
- Preflight — session
may_start?+ device exists + master key fetched into@master_key(fail fast before the tx; the result is NOT discarded — SEC.3 DI). - Wrong-board guard [FW.54] —
CommandBuilder.preflight_commands(connect +-r32 0x1FFF7590 12) runs FIRST; live mode parses stdout viaUidReadoutand compares the board's UID totrees.silicon_uid_hexbefore any derivation or-w32— mismatch/unparseable →WrongBoardError(not even a HardwareKey row materializes). dry-run or passport-less device → skip. - Master key —
MasterKeySource(Env or Bitwarden adapter);WeakKeyDetectorrefuses a weak key. The fetched key threads asmaster_key:param into every derivation below (runtime callers of the same services use the ENV fallback instead). - HardwareKey —
HardwareKeyService.provision(device, master_key:)(the SINGLE HKDF source — same derivation the firmware runs; never derive keys elsewhere). - ATECC (Гілка B + Tree only) —
SecureElementProvisioneremits the I²C ATCA write-zone transcript. - Commands —
CommandBuilder#flash_commands(key writes + RDP + disconnect; connect/UID-read already ran as preflight). - Execute —
Executor(dry-run prints;--executespawns subprocesses). - Audit —
AuditTrail.record!→ chain-hashedAuditLog(metadata incl.silicon_uid_hex) +MaintenanceRecord;complete!(orfail_with!+ rollback).
Key Components
| Component | Role |
|---|---|
app/services/factory_flashing/session.rb | Orchestrator (run, preflight!, verify_silicon_uid! wrong-board guard, AASM start!/complete!/fail_with!) |
app/services/factory_flashing/tree_resolver.rb | [FW.54] UID→DID→Tree: create / re-flash / bind / collision→quarantine; deliberately does NOT enqueue peaq (offline factory) |
app/services/factory_flashing/uid_readout.rb | [FW.54] tolerant -r32 stdout parser (keyed on 1FFF7590); live format = bench-confirm (RUNBOOK 1.3) |
app/services/factory_flashing/command_builder.rb | STM32_Programmer_CLI emission (preflight_commands class-method: connect+UID-read; flash_commands per гілка, write_block, rdp_command) |
app/services/factory_flashing/secure_element_provisioner.rb | ATECC608B data-zone provisioning (Гілка B) |
app/services/factory_flashing/executor.rb | dry-run vs live subprocess (programmer_available?) |
app/services/factory_flashing/master_key_source.rb | Base / EnvAdapter / BitwardenAdapter master-key fetch — the fetched key feeds HKDF via Session (SEC.3 DI), not just the preflight gate |
app/services/factory_flashing/audit_trail.rb | chain-hashed audit log record |
ota_hmac_key_service.rb | per-cluster OTA HMAC key fetch_for(cluster_id, master_key: nil) (Гілка A KOTA block + Гілка B ATECC provisioning) |
Line numbers drift every commit — grep/read
Sessionfor live locations rather than a hardcoded table ([[feedback_no_volatile_counts]]).
Gotchas Not Obvious From Code
- Гілка A vs Гілка B (ARCH.42 variants, post-SEC.14 2026-07-03) — A = keys written into Protected Flash via SWD (
-w32). B = Гілка A + SE05x identity-chip: KEYL still goes to Protected Flash in BOTH branches (03_06 §1); the SE adds only the Ed25519 voice / cert / anti-clone serial, NOT the LoRa key (SEC.14 = provisioning-only). ⚠️ The currentgilka_b_commandsis still the legacy ATECC-model (skip-key-writes, only RDP-lock + disconnect) — a known code-lag pending the SE050 eval-kit (00_07SE050-MIGRATION); a Гілка-B unit flashed by today's code would have no Flash KEYL → brick. - Flash layout MUST match firmware —
CommandBuilderaddresses/magics mirrorfirmware/soldier/main.cFLASH_KEY_ADDR(post-ARCH.42):KEYL(0x4B45594C)+aes@0x0803E000,LSED(0x4C534544)+k_seed@0x0803E014(Tree),KEYC(0x4B455943)+coap@0x0803E040,EDSK(0x4544534B)+ed25519_seed@0x0803E064(Gateway, L1 QATT),KOTA(0x4B4F5441)+k_ota@0x0803E800(Tree, FW.23),KEYB(0x4B455942)+bcast@0x0803E828(Tree, FW.2 (в) cluster control-plane). Drift here ⇒ device can't read its own key. Change one side → change both + host tests. Word→BE-bytes convention (FW.30): firmware unpacks each-w32word MSB-first — naive memcpy on LE Cortex-M4 reverses every word. - Dry-run is the default; live is the SEC.3 residual — real
STM32_Programmer_CLIexecution + RDP Level 2 (SEC.2, irreversible chip lock) are bench-gated, not yet run on hardware. - Key shapes — Tree: 32-hex AES-128 LoRa key + 64-hex Lorenz
K_seed; Gateway: 64-hex AES-256 CoAP key + 32-hex broadcast key written to the LoRaKEYLslot (FW.2 (в), post-2026-07-03 — Queen's single control-plane key = theKEYBbroadcast value; the pre-fix «LoRa slot unused» bricked her at boot) + optional 64-hex Ed25519 seed (L1 QATT «голос Королеви» — generated bySessionon the factory host, NOT HKDF; only the pubkey persists inHardwareKey).CommandBuilder#validate!enforces this. - Transaction + chain-hash integrity — the whole run is one AR transaction; a downstream raise rolls back HardwareKey + audit rows together, so rolled-back rows never enter the chain-hashed
AuditLog(the chain stays intact). - Supervisor gate —
Sessionrefuses to run unless theProvisioningSessionissupervisor_approved(preflightmay_start?). - Never add WeakKeyDetector to the ENV-fallback branch of the derivation services — the rails_helper test pin (
silken-net-test-master-key-32b!!) is itself a placeholder needle in the detector, so validating insidehkdf_derive/fetch_for/derive_seedfails the whole suite. Coverage is already two-layer by design:EnvAdapterguards the factory path, the boot initializer (master_key_strength_check.rb) guards runtime. - [FW.54] UID wire-form is frozen — 24 hex = three
%08Xwords in register order (0x1FFF7590first), exactly how firmwaredid_derive.hreads them; golden pair0039002F3138511538323634 → SNET-80B12004frozen indid_derivation_spec↔firmware/test/test_soldier_logic.c. Reordering/re-endianing the parse = a different DID on backend vs silicon (keys diverge silently). - DID is derived EVERYWHERE by one function —
wire_did_from_uid_hexfeeds both the factory (TreeResolver) and the fieldProvisioningController#register(the oldlast(8)-suffix DID + the dead tree double-init guard were a real prod bug, fixed 2026-07-03). Never invent a third derivation path. - Collision ≠ re-flash — same derived DID + different
silicon_uid_hex= birthday collision or wrong chip →CollisionError, unit goes to quarantine (03_01 §7); same UID = legit re-flash (idempotent no-op).
How to Explore
- grep/read
Session— orchestrator callers/callees - grep
factory flashingacrossapp/services/— related flows - Read
command_builder.rbfor the exact CLI sequence;03_06 §5for the threat model
Signals
- GitHub stars
- 22
- Forks
- 1
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
factory-flashing- Source
- github.com/alexey-lukin/silken_net