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 = Гілка A + SE identity: its runtime keys go to Protected Flash through the same SWD writes (since 2026-09-27, before that a Гілка-B unit got no Flash KEYL and bricked), but the SE step itself is EMIT-ONLY, textual legacy ATECC calls, no real transport, so 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\".
Use Factory Flashing (SEC.3) in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Factory Flashing (SEC.3) and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Factory Flashing (SEC.3) skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
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, freezes the IWDG and sets RDP — L0 or L1 only; L2 is burned outside the pipeline (step 6). 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(one invocation:-c+-r32 0x1FFF7590 12+-r32 0x0803E000 4, the first key-page word) runs FIRST; a passport-less board (a Queen) that already holds keys is erased only withREFLASH_ACK=<device_uid>(guard_unverified_reflash!— declared intent, like the bench script's typed «RDP2»); 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. ⚠️ The guard checks ONE line, and every line reconnects: on a station with several ST-LINK probes each line takes probeindex0 afresh, so passSTLINK_SN=<probe serial>—CommandBuilder.connectputssn=into every-c, the passport read included (alphanumeric only: the line reaches a shell throughOpen3.capture3). Nothing detects a multi-probe station without it — the--listoutput was never seen on silicon. - 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-page erase + key writes as whole doublewords + IWDG freeze [SEC.15] + RDP; the UID-read already ran as preflight — gotcha #11). ⚖️ The pipeline never burns L2 (SEC.2, founder 2026-09-28): one run = keys → IWDG → RDP, which leaves no room for the self-test and WRP the ratified burn order puts before L2, soProvisioningSession::RDP_LEVELS= {0, 1} andCommandBuilder::RDP_OPTION_BYTEhas no L2 byte (raw bytes, not level numbers —03_05 §3.6). Production runs the pipeline atRDP_LEVEL=0, then the03_05 §3.6steps 3–7 (NOT at L1: on L1 a failed self-test board cannot be reflashed without an L0 regression, i.e. a mass erase). WRP is per chip — the Queen's page 125 is her runtime OTA-SHA mirror, so only page 124 there. - 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: -c+UID-read in one invocation; flash_commands per гілка, block_words → flash_write_commands (page erase + whole doublewords), 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) (KOTA block in BOTH branches + the Гілка-B SE Slot-3 copy) |
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:CommandBuilder#flash_commandsemits the SAMEprotected_flash_commandsfor both branches (KEYL · LSED · KOTA · KEYB for a Tree; KEYL=KEYB-value · KEYC · EDSK for a Queen), because every runtime key has an MCU consumer (CRYP · Lorenz-VM · OTA-HMAC); the SE adds only the Ed25519 voice / cert / anti-clone serial (+ a Slot-3 K_ota copy for the open post-TRL-7 migration), NOT the LoRa key — Slot 0 is reserved and never written (⚖️ delegated 2026-09-27; verdict, price and weakest link —03_06 §1, callout «Набір ключів Гілки B»). 🔴 Until 2026-09-27gilka_b_commandsskipped every key write (legacy ATECC model): a Гілка-B Soldier had no Flash KEYL → brick, and a Гілка-B Queen had neither KEYC nor EDSK. ⛔ Do not read «Гілка B» as «keys live in the SE»: its runtime keys are protected by RDP exactly like Гілка A's (SEC.2/SEC.24) — the SE's non-extractability covers identity only. -
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 is bench-gated, not yet run on hardware. RDP Level 2 (SEC.2, irreversible chip lock) is not a bench-gated pipeline step but no pipeline step at all: the session refuses level 2 (step 6), and production L2 is the03_05 §3.6procedure, steps 5–7 — whose script does not exist yet (00_07SEC.2). -
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). ⚠️ The no-op is the RESOLVER's only. For a TREE the session is not: since 2026-09-28 every re-flash of a tree whoseHardwareKeyrow exists is a RE-PROVISION into a new key EPOCH (Session#reprovision_tree_key!, ⚖️ founder 2026-09-28,03_05 §3.8) — KEYL = the epoch root K0_e from the CURRENT master, a fresh Flash-KV journal (FlashKvImage: only0x15, pages 122 AND 123 erased — a surviving sibling with a higher seq would win the mount and resurrect the ratchet version; since 2026-09-29 on the FIRST provision of a tree too — a bench-used board carries a foreign journal), K_seed re-derived under the CURRENT master and written with the row (⚖️ founder 2026-09-29,03_06 §5— same master, same seed), and in the DBepoch+ 1 ·key_version0 ·downlink_frame_counter0 (the fresh journal carries no DLFC record0x12,03_05 §2.5) · the old key in grace until the first MIC under the new one. Dry-run only PLANS it: an epoch bumped without a flashed chip deafens the node. ⛔ Don't resetkey_versionorepochby hand to «fix» a tree — the re-provision is the fix. A Queen's re-flash still writes her row's key as is (it IS the delivery of a rotated KEYC). -
Every transcript line is its own CLI process, and WL Flash takes only whole erased doublewords [SEC.3, 2026-09-28] —
STM32_Programmer_CLIholds SWD only within one invocation, soCommandBuilder::CONNECT(-c port=SWD mode=UR, the vendor WL script's form; UR = hardware reset, so NRST must be wired) prefixes EVERY line — a «disconnect» line does not exist, and UR is needed because after-ea keyless firmware resets every ~100 ms.-w32does not erase (STM32CubeProgrammer manual) and WL programs a whole doubleword + ECC (HAL), soflash_write_commandserases the touched pages first and writes each touched doubleword exactly once, padding with0xFFFFFFFF; the erase is irreversible, hence the passport-less guard (step 2). 🔴 Before 2026-09-28 only the KOTA|KEYB boundary was aligned (+40) — magic + key word and the KEYL|LSED boundary still shared doublewords across commands, and the execute-path shim stayed green because it accepts any args: a shim proves orchestration, not CLI semantics. Adding a key block → the pins «форма запису Flash WL» incommand_builder_specjudge it (incl. «redact hides every key word»); a runtime-written page (Soldier 122–123/126, Queen 122–123/125) must never enter the erase set, and the erase also wipes the Soldier's role word on page 124.Executor.redactkeeps-w32data out of the dry-run print AND the persistederror_message; dry-run never persists the Queen's voice pubkey. ⚠️ Keys still sit in the CLI's argv — a factory-host requirement (03_06 §5). -
Dry-run is a PLAN, not a flash — and it is the default of
factory:execute[SEC.3, 2026-09-28] — it runs the real derivation from the real master key, so nothing it produces may be persisted as if it had been flashed, and nothing it prints may carry key data. Today's carriers:confirm_gateway_key_delivery!(grace) andgateway_voice_seed(the Queen's voice pubkey) both skip ondry_run?;Executorprintsredact(command). 🔴 Until 2026-09-28 a Queen dry-run replaced her pubkey with one of a seed that lived only in the terminal output — the real Queen's L1 signatures stopped verifying — and two specs pinned exactly that, because they ran dry-run and asserted the pubkey was persisted. Adding a side effect toSession? Ask «does it describe the chip?» — if yes, gate it on a live run and pin the dry-run negative.
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
- 23
- Forks
- 1
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
factory-flashing- Source
- github.com/alexey-lukin/silken_net
github.com/alexey-lukin/silken_net
Related picks
Skill · posthog
The pick for Rubyvigilante-issue-implementation-on-ruby
Skill · aliengiraffe
The pick for Rubysocial
Skill · coreyhaines31
More in Productivitygws-calendar
Skill · googleworkspace
More in Productivitylark-minutes
Skill · larksuite
More in Productivitylark-workflow-standup-report
Skill · larksuite
More in Productivity