| Document | Security Concept |
| Project | Safe Kiln Controller, PID kiln controller |
| Version | 0.1 (draft) |
| Date | 2026-10-09 |
| Status | For review |
| Derives from | 03_software_req.sdoc v0.1, architecture.md v0.1, safety.md and safety.sdoc v0.1 |
| License | GPL-3.0-or-later |
Safe Kiln Controller is designed for a trusted local network and nothing else (
SWR-NFR-20,SYS-ASM-05). It must not be exposed to the internet, port-forwarded, or placed on a network it shares with untrusted devices. There is no transport encryption: everything, the web password included, crosses the LAN in cleartext.This matters more here than on an ordinary appliance, because a security compromise of this device is a safety event. An attacker who can issue commands can start a multi-kilowatt heater. §7 is the part of this document that distinguishes it from a generic threat model, and it should be read before the controls.
safety.md analyses what happens when the equipment fails. This
document analyses what happens when somebody attacks it, and the two meet at
one hazard: HZ-12, a remote command putting the
kiln into a dangerous state.
It covers the firmware's network-facing surfaces, its stored secrets, its update path, and physical access to the board. It does not cover the security of the user's own network, their browser, or their WiFi infrastructure, except to say where Safe Kiln Controller depends on them.
The split between this document and security.sdoc follows the
one between safety.md and safety.sdoc. The
assets, adversaries, threats, security goals, residual risks and open questions
are nodes in security.sdoc, because they have identity and a chain between
them that is now checked when the document is built. The reasoning is here: the
scope, the trust boundaries, section 7 on why a security compromise is a safety
event, what the implementation already gets right, the obligations and the
verification status.
Identifiers introduced by this concept extend the scheme of
requirements §1.4 and of
safety.md §1, and are defined as nodes in
security.sdoc:
| Prefix | Meaning |
|---|---|
A |
Asset |
ADV |
Adversary |
TH |
Threat |
SEC |
Security goal |
SRR |
Security residual risk |
OQ-S |
Open question, security |
The trust boundaries B-1 to B-7 are not nodes, and that is deliberate:
they are the legend of the diagram in §3
and carry no chain through them, so splitting them from the picture would help
nobody.
Note for anyone reading 03_software_req.sdoc alongside this: its section anchors
used to be SEC-n, which collided with the security goals here once both became
StrictDoc documents. They are SECT-n now. SEC means security goal.
Status honesty. Every control below is marked with what is actually true today, and the marks are not decoration, most of this is not built yet:
| Mark | Meaning |
|---|---|
| [built] | Implemented and covered by an automated test |
| [partial] | Implemented in the host-testable core, but the piece that would enforce it on a device does not exist |
| [spec] | Required or designed, no implementation at all |
| [absent] | Not required, not designed, not present, a gap, named as one |
At the time of writing there is no HTTP transport on the target. The REST
API (kiln_web) is complete and host-tested,
but the esp_http_server adapter that would terminate TCP, parse headers, check
a password and apply rate limiting is not started
(README status table). Nearly every network control in
this document therefore reads [spec], and that is the single most important
fact in it.
The seven assets live in security.sdoc, not here, each with
what it is and why it matters. A-1, control of the heater, is the asset;
everything else is a means to it.
flowchart TB
subgraph UNTRUSTED["Untrusted"]
INET["The internet<br/>(must never reach the device)"]
end
subgraph LAN["Trusted by assumption, SYS-ASM-05"]
BROWSER["Operator's browser"]
OTHER["Other LAN devices<br/>(IoT, guests, malware)"]
end
subgraph VENDOR["Manufacturer"]
UPDSVC["update.bitcrushtesting.com<br/>signed static manifest"]
end
subgraph DEVICE["Safe Kiln Controller"]
HTTP["HTTP server<br/>**not implemented**"]
API["REST API, kiln_web<br/>built, host-tested"]
APP["kiln_app, commands"]
SAFE["safety supervisor"]
NVS["NVS: WiFi + web password<br/>**plaintext**"]
OTA["update client<br/>**not implemented**"]
HMI["local display + encoder"]
end
subgraph PHYS["Physical access"]
UART["UART console"]
JTAG["ESP32 JTAG<br/>off on a provisioned unit"]
SWD["Supervisor SWD<br/>**open, by decision**"]
FLASH["SPI flash readout"]
end
INET -. "must not be routed" .-x HTTP
BROWSER --> HTTP
OTHER --> HTTP
HTTP --> API --> APP --> SAFE
APP --> NVS
HTTP -. "no route, by design" .-x OTA
OTA -->|"daily GET, nothing about the device"| UPDSVC
UPDSVC -->|"signed manifest, then image"| OTA
OTA -->|"announces"| HMI
HMI -->|"operator confirms: the only authority"| OTA
UART --> APP
JTAG --> NVS
SWD --> SAFE
FLASH --> NVS
The trust boundaries, named:
| # | Boundary | Crossed by | Enforced by |
|---|---|---|---|
| B-1 | Internet → LAN | Nothing, by assumption | The user's router. Safe Kiln Controller has no control here and no defence if it is wrong. |
| B-2 | LAN → device | Every HTTP request | The HTTP transport, which does not exist. |
| B-3 | Unauthenticated → authenticated | State-changing requests | kiln_api_needs_auth decides which; the transport would decide whether. |
| B-4 | Request data → parser | Bodies, query strings, JSON | Bounded buffers in kiln_web [built]. |
| B-5 | API → control | Commands | kiln_app; the safety supervisor holds heat authority regardless (SWA-04). |
| B-6 | Physical → device | UART, JTAG, SWD, flash | Asymmetric, and §6.1 is why. On the ESP32, Secure Boot V2 and JTAG disable exist as a provisioning step and are not applied by a default build. On the supervisor, nothing, by decision. No flash encryption anywhere. |
| B-7 | Device → manufacturer's update service | One daily outbound HTTPS GET, originated by the device | Authenticity comes from an offline signing key, not from the connection (§6.2); the install needs a person at the display. Nothing inbound crosses this boundary, and nothing about the device goes out across it. [spec] |
The six adversaries live in security.sdoc, each with its
capability and an honest answer to whether it is realistic. ADV-1, another
device on the same LAN, is the normal case and the one SYS-ASM-05 quietly assumes
away.
The fifteen threats live in security.sdoc, rated by
consequence rather than likelihood. Each names the asset it is aimed at, the
adversary who can mount it, and, where it gets there, the safety hazard it
reaches: that last relation is the claim this document exists to make, and it is
now a link to a node in safety.sdoc rather than a sentence.
Three of them, TH-01 to TH-03, are Resolved, closed outright by the
read-only decision of 2026-10-06. They are kept rather than deleted, because why
a threat is closed is the most useful thing about it.
TH-04, hostile firmware, was the fourth until 2026-10-09 and is Active
again. It was closed by the absence of an update path, and a threat closed by
an absence reopens when the feature arrives: see §6.2.
TH-14 and TH-15, a hostile or impersonated update service and a downgrade to
a published vulnerability, are new with it, and are the honest price of the path.
The fifteen security goals live in security.sdoc, each with the
threats it addresses, the requirements that realise it, and an
IMPLEMENTATION field carrying what used to be a [built] / [partial] /
[spec] / [absent] mark in this table.
That field is the one to read first. At the time of writing it stands at 6 built, 1 partial, 5 spec, 2 absent, 1 not needed, and the reason is that there is still no HTTP transport on the target: the REST API is complete and host-tested, but the adapter that would terminate TCP, parse headers and apply rate limiting is not started. Nearly every network control is therefore a specification, and that remains the single most important fact in this document.
SEC-01 and SEC-04 are Withdrawn, dissolved on 2026-10-06 along with the
password itself.
B-6 is the only boundary where the two microcontrollers of
SWA-22 differ, and most of the difference is a
property of the silicon rather than a decision anybody is free to revisit. It is
written out here because a single "nothing" in the table above was true until
2026-10-08 and is now true of one half.
The ESP32-S3: Secure Boot V2, built and off by default.
sdkconfig.secure builds a signed
image, and tools/secure-boot.py signs it, flashes
it, burns the public-key digest, enables secure boot and disables JTAG, in that
order, refusing each irreversible step until a pre-flight check passes. A board
that has been through it will not run unsigned code and has no debug port. A
board that has not is exactly what B-6 used to say, and a default build is
not provisioned, so the honest status of this control is that the mechanism is
[built] and whether a given unit carries it is a production decision. Three
things it does not do, each deliberate and each argued in the header of
sdkconfig.secure: it does not burn DIS_DOWNLOAD_MODE, because SRR-11
leaves serial download as the only route a security fix can take; it does not
enable flash encryption, which is OQ-S3, so SRR-05 stands untouched; and it
does nothing whatever for the supervisor.
The supervisor: no secure boot exists to enable. The STM32G031K8T6 has nothing corresponding to Secure Boot V2. There is no ROM-verified signature chain on the G0 line, no one-time-programmable key store, and on this part no AES, no hardware random number generator and no public-key accelerator, so a signature chain would have to be written in software and keyed from ordinary flash, which an adversary able to reflash can replace along with the image it verifies. What the part does offer is readout and boot protection through option bytes:
| Mechanism | What it does | Reversible |
|---|---|---|
| RDP Level 1 | Flash readout blocked from the debugger and the system bootloader; debug can still attach | Yes, and regression to Level 0 mass-erases the flash |
| RDP Level 2 | SWD permanently dead, system bootloader gone, option bytes frozen | No. No vendor recovery, ever |
| WRP | Write and erase protection over a flash area | Yes, unless Level 2 is set |
Securable memory + BOOT_LOCK |
A region that runs at boot and is then inaccessible until reset, with boot forced from main flash | Clearing it needs an RDP regression on this family; confirm against RM0444 before putting it on a line |
nBOOT_SEL / nBOOT0 |
Makes the BOOT0 pin irrelevant and selects main flash |
Yes |
No readout protection is set, and RDP Level 2 must not be. Three reasons, in the order of their weight:
-
There is nothing on the supervisor to keep. It holds no WiFi credentials, no password and no key:
A-2andA-3live in the ESP32's NVS, which is why the diagram above routesSWDto the safety supervisor and not toNVS. Its firmware is published under the GPL. RDP is a confidentiality control, and here it would protect a public artefact. -
It would contradict the independence argument.
firmware/supervisor/README.mdrequires SWD onPA13/PA14brought out to test points rather than left as pads, "because the independence argument rests on this firmware being reviewable and flashable on its own". Level 2 removes bench bring-up of the transport code that has never run on silicon, read-back of the image as evidence for an assessment against EN IEC 60730-1 Annex H, diagnosis of a returned unit, and any safety fix short of replacing the part.SRR-11already records that there is no field update path; closing SWD would extend that from the controller to the safety function. -
It does not close the attack it appears to close. The threat is
ADV-3reflashing the supervisor so that it permits heat unconditionally. The same adversary, at the same board, can jumper the permit line instead, which is cheaper and needs no firmware at all, andSYS-HW-13's independent cutout is untouched either way (§7, L5). §10.1 already asks the owner to treat physical access as equivalent to knowing the WiFi password. Locking one of the two MCUs against an adversary this concept places outside its scope would buy a word in a table and cost the ability to service the part that holds heat authority.
What is done instead is two option-byte settings with a safety argument rather than a security one, and one runtime check that is already running:
- Boot forced from main flash, through
nBOOT_SELandnBOOT0[spec]. Not a security control: it removes a state in which a floating or noise-latchedBOOT0leaves the part in the system bootloader with the permit line high-impedance, which is a supervisor that is silently not running. The external pull-down the pin map already requires makes that state safe; this removes the state. - Write protection over the image, through WRP [spec]. The supervisor firmware contains no flash-write code at all, so this guards against a runaway rather than against a bug anybody can point to. It is reversible and it costs nothing.
- CRC-32 over the whole image, ten times a second [built]:
sup_flash_ok, against a digest stamped in after linking bytools/sup-crc.py, with an unstamped image treated as a failure rather than as "no expectation". This is the program-memory integrity control that actually runs, and it catches the realistic fault, which is corrupted flash and not hostile flash. No option byte does that, and it carries noSWR-of its own, which is a gap in the requirements rather than in the firmware.
OQ-S2 asked whether images should be signed and secure boot enabled, and is
Resolved by the two halves above. SRR-04 stands anyway, because a default
build is unsigned and an unprovisioned board authenticates nothing.
Decided 2026-10-09, resolving OQ-08 and OQ-R4. The device checks
update.bitcrushtesting.com once a day, announces an available release on its
local display, and installs it only when the operator confirms there. The web
interface stays read-only. SWR-UPD-09 to SWR-UPD-16 are the requirements;
this is why they are shaped that way.
The driver is regulatory and is not negotiable:
UR-REG-004, the Cyber Resilience Act, requires that
security fixes can reach a unit, and SRR-11 recorded that they could not reach
this one without a visit. OQ-R4 asked whether that made the sold-units route
impossible, and the answer is no, provided this gets built.
The direction is the whole design. The question OQ-08 left open offered two
shapes, and they are not equally priced:
| What it needs | What it costs | |
|---|---|---|
| Push: an image staged over the LAN, applied after a physical confirmation | One inbound POST route, live whenever the device is armed |
SEC-00 stops being "no network request changes any device state at all" and becomes "read-only except for firmware". The strongest control in this document acquires an exception |
| Pull: the device fetches, the operator confirms at the display | One outbound GET, and a service to run for the support period | SEC-00 keeps its absolute form. A new outbound connection, and a dependency on the manufacturer |
Pull wins because the exception is the expensive part. An inbound route exists
whether or not anybody is standing at the kiln; an outbound GET creates nothing
for an attacker on the LAN to talk to. SWR-WEB-26 and SEC-00 are therefore
unchanged by this decision, which is the single most useful sentence in this
subsection.
The mechanism, and what each step is for.
- A static, signed manifest. One file, byte-identical for every device,
fetched over HTTPS. It is signed with an offline key and verified against a
public key compiled into the image (
SWR-UPD-11). The version comparison happens on the device, which is what lets the request carry nothing about the device at all (SWR-UPD-10): no serial, no installed version, no query string. The service learns an IP and a time, and that isSRR-13. - TLS that is required and not load-bearing. Certificate validation is
mandatory, and the design assumes it will fail one day anyway. An attacker who
takes the host, spoofs the DNS or breaks the connection cannot produce a
manifest or an image that verifies, so what they get is silence: a fix
that never arrives rather than a hostile one that does. That failure mode is
SRR-12, not a pretence that it cannot happen. - The display is the only authority. No request can initiate, offer,
authorise, postpone or cancel an install (
SWR-UPD-08). The device decides when to ask; the person at the kiln decides whether to install (SWR-UPD-12,SWR-HMI-16). - Strictly greater versions only.
SWR-UPD-13refuses any image that is not newer than the running one, because a signature does not stop a replay: an old manifest is genuinely signed and its vulnerability is published. That isTH-15. - The new image earns its keep.
SWR-UPD-15lets a first boot cancel its rollback only after the supervisor link, both thermocouples, the stored configuration and the display have all proved themselves in the running system. Cancelling it in start-up code is named as a defect, because that is the usual way this feature is reduced to decoration. - USB stays (
SWR-UPD-14), as the recovery path for a unit whose service is unreachable, whose image will not confirm itself, or which has to be downgraded deliberately. It is also what survives the project losing the domain.
Why the install is not automatic. The CRA expects security updates to
install automatically by default where applicable, and this design deliberately
does not. The basis is the one in §7: this
device switches kilowatts into a kiln, and rebooting into untried firmware
unattended with a load inside is not an acceptable trade for promptness. The
check is automatic and on by default; only the install waits for a person. A
conformity file has to make that argument explicitly rather than leave the
omission to be found, which is why it is also written into SWR-UPD-12 and
UR-REG-004's assessment.
What this costs, stated as plainly as §6.1 states its own trade. TH-04 is
Active again after being closed on 2026-10-06; TH-14 and TH-15 are new;
SEC-06 narrows from "no outbound connection" to "one outbound connection, to
the manufacturer, carrying nothing about the device"; and SRR-12 and SRR-13
are new residual risks, one carried by the project and one by the owner. Against
that, SEC-00 is untouched, SRR-11 has a specified answer, and the sold-units
route stops being blocked by a decision nobody had taken.
The eFuse anti-rollback counter is deliberately not burnt. It would stop a
downgrade in hardware, which sounds strictly better than SWR-UPD-13 doing it in
software, and it is not: the counter has few increments, it cannot be undone, and
it would make the fuse rather than the operator the thing that decides whether a
unit can ever be recovered. The same reasoning as DIS_DOWNLOAD_MODE in §6.1,
and the same conclusion.
Status: all of this is [spec]. The HTTP client, the manifest verifier, the
update screen and the rollback discipline are unwritten, on a target that has no
HTTP transport either (SRR-01). SEC-12 carries the goal, SRR-11 stays open,
and the deadline that matters is 11 December 2027 for the CRA's main
obligations, with the reporting route live from 11 September 2026.
This is the part that makes a kiln controller different from a thermostat.
safety.md §5 layers the protections by how
little each depends on software being correct. An attacker does not defeat those
layers by breaking them, they defeat them by using them as designed, from
the wrong side of the trust boundary:
| Protection layer | What an attacker does to it |
|---|---|
| L1 control: setpoint clamped to the configured maximum | Raise the configured maximum (TH-02). Still bounded by SWR-SAF-23's compile-time 1350 °C ceiling, which no configuration and therefore no attacker can raise. |
| L2 safety supervisor: 18 detection rules | Widen the thresholds (TH-02). SWR-SAF-22 bounds every range and forbids disabling a detection outright, so this degrades detection rather than removing it, a bound that was written for a careless operator and happens to hold against a hostile one. |
| L3 charge pump and contactor | Nothing. It is hardware, and it does not care who asked. An attacker commanding heat gets heat, but a hung or crashed MCU still drops the contactor. |
| L4 watchdogs, brownout | Nothing. |
| L5 independent hardware cutout | Nothing, and this is the point. It has its own sensor and its own contacts, and it is reachable from no network. Against TH-01 to TH-05 it is the only layer that holds unconditionally. |
| L6 operator attendance | Everything, if the operator is not there. SYS-ASM-06 requires attendance during firing; an attack that starts a firing when nobody expects one removes L6 by surprise rather than by negligence. |
Four consequences worth stating plainly:
-
SYS-HW-13's independent cutout is a security control, not only a safety one. It is the one protection in the entire design that no network attacker can reach, degrade, or reason about. Every argument for it insafety.mdis strengthened, not duplicated, here. -
The bounded ranges of
SWR-SAF-22and the ceiling ofSWR-SAF-23do real security work. They were written so an operator could not disable a detection; the effect is that TH-02 degrades the safety envelope within known limits instead of removing it. A future change that relaxes a bound "because the operator knows what they are doing" would also be widening an attack. -
SWR-SAF-18blunts TH-03. A latched fault cannot be cleared while its triggering condition is still observably true, and the check reuses the detector's own rule function. An attacker can clear a stale fault; they cannot clear a live one and then heat. -
Firmware is the one route that reaches every layer at once, which is why L6 is where the update path is anchored. An attacker who replaces the firmware does not have to defeat L1, L2 or L4: they are L1, L2 and L4, and only L3 and L5 remain, both of which are hardware. That is
TH-04, and it is why the update path of §6.2 puts the decision on the local display rather than on the network. The person in front of the kiln is L6, and giving them the only authority over what firmware installs makes the weakest layer in the table the gate on the strongest attack. It also means the argument for not installing updates automatically is a safety argument, not a convenience one.
Recorded because a security document that lists only gaps misrepresents the design, and because each of these is a property worth not losing.
- Auth by method, not by list.
kiln_api_needs_authprotects every non-GETroute. A new state-changing endpoint is protected the moment it exists; the failure mode of the usual approach, an enumerated allow-list somebody forgets to update, cannot occur. - Secrets cannot be read back.
SWR-CFG-07is implemented and tested, and the config endpoint is generated from the same table that declares the flag, so a new secret field is redacted by construction rather than by a second edit. - No allocation proportional to input, anywhere. The whole firmware contains
zero
malloc/free. An attacker cannot exhaust a heap that does not exist. - The dev harness binds to loopback only.
host/webhostlistens onINADDR_LOOPBACK, notINADDR_ANY, and setsauthenticated = trueunconditionally, which is safe precisely because it is unreachable from the network. Worth never "fixing" intoINADDR_ANYfor convenience. - Fault injection is compile-gated. The UART console that can inject relay
and sensor faults exists only under
CONFIG_KILN_PLANT_SIM, not in a production image. - Control is isolated from the network by construction (SEC-08), which means the worst realistic DoS is a lost web interface, not a lost kiln.
The thirteen security residual risks live in security.sdoc, each
with why it stands, who carries it, and what mitigation exists today. Four
are worth naming here: SRR-01, that every network control is a specification
and nothing in this concept may be cited as evidence of a deployed control;
SRR-11, which used to say that removing OTA left no way to receive a security
fix and now says that the way is specified and unbuilt, and which still blocks
release; and SRR-12 and SRR-13, the two new costs of
§6.2, being the manufacturer's
service and signing key as a dependency, and the daily check disclosing that a
device exists. SRR-14 is the fourth and the least technical: the Cyber
Resilience Act obligations that are not firmware have no owner, which
§13 exists to stop being invisible.
There is no SRR-10. The numbering has a gap, and keeping it costs less than
renumbering identifiers other documents cite.
| Requirement | |
|---|---|
| Do not expose the device to the internet; do not port-forward to it | SWR-NFR-20, SRR-03 |
| Prefer an isolated network segment or VLAN over a flat home LAN | SRR-09 |
Set a web password; it is optional (SWR-WEB-23) and the device ships without one |
SEC-01 |
| Treat physical access to the controller as equivalent to knowing the WiFi password | SRR-05 |
| Erase flash before disposing of or selling a device | SRR-05, ADV-4 |
| Fit the independent hardware over-temperature cutout; it is the only protection no attacker can reach | SYS-HW-13, §7 |
Let the controller reach update.bitcrushtesting.com outbound, or mirror the manifest and point update.url at the mirror. A segment with no outbound route gets no security fixes |
SWR-UPD-09, SRR-13 |
| Install an offered update, and understand that it waits for you: nothing installs itself | SWR-UPD-12, SRR-11 |
| If you disable the check, take on the job of watching the release channel yourself | SWR-UPD-09, SRR-13 |
This is the component that turns most of this document from specification into fact. These are not suggestions.
| Constraint | |
|---|---|
| Resolve SRR-02 first: store a salted hash, never the password. Decide it before writing the comparison, not during. | SWR-NFR-19 |
| Constant-time comparison; increasing delay after repeated failures. | SWR-NFR-19, SEC-04 |
Validate Origin/Host on every state-changing request. The cost is trivial and SRR-06 is otherwise permanent. |
SRR-06 |
Set req.authenticated only after a successful check. It is an input the API trusts absolutely. |
SEC-01 |
Never add a back channel for the UI. SWA-16 is a security property, not a tidiness preference. |
SWR-WEB-19, SWA-16 |
| Enforce the declared body maximum before parsing, not during. | SWR-NFR-19, SEC-03 |
| Keep the server on core 0. Heat authority stays with the safety supervisor. | SWA-15, SWA-04, SEC-08 |
| Do not bind the dev harness to anything but loopback. | §8 |
Do not add an inbound firmware route while you are in there. The update path points outwards on purpose, and SEC-00's absolute form is what that bought. |
SWR-UPD-01, SWR-UPD-08, §6.2 |
| Constraint | |
|---|---|
A new state-changing route must not bypass kiln_api_needs_auth's method rule. |
SEC-01 |
A new configuration field holding a credential must carry KILN_CFG_F_SECRET. |
SWR-CFG-07, SEC-02 |
| No dependency may be fetched at build time; assets stay vendored. | UR-CON-04, ADV-6 |
| No telemetry, no analytics, no outbound connection, ever. | SWR-NFR-21, SEC-06 |
Relaxing a bound in SWR-SAF-22 or the ceiling in SWR-SAF-23 widens an attack surface, not just a safety envelope. |
§7 |
| RDP Level 2 must not be set on the supervisor. SWD stays reachable, and the reasons are not matters of taste. | §6.1, SRR-11 |
| Provisioning a unit with secure boot makes the key holder the only party who can ever update it. Decide whose unit it is before burning. | SRR-11, OQ-S2 |
| The manifest signing key stays offline and is not the secure boot key. Two keys, two blast radii. | SWR-UPD-11, SRR-12 |
update.bitcrushtesting.com and its domain are a commitment for the whole declared support period. Letting the domain lapse is worse than never having had it, because somebody else can buy it. |
SRR-12, SWR-UPD-16 |
The manifest stays static and identical for every device. A per-device URL, a version query or a user agent carrying the serial would turn the check into telemetry and break SEC-06. |
SWR-UPD-10, SRR-13 |
| Security releases ship separately from functional ones, marked as such, with an advisory. | SWR-UPD-16, UR-REG-004 |
| Do not make the install automatic later "because the CRA prefers it". The manual confirmation is a safety argument, recorded in §7; reversing it needs that argument answered, not just a flag flipped. | SWR-UPD-12 |
| Claim | How it is verified | Status |
|---|---|---|
| Secrets are never serialised outward | Host unit test over every SECRET-flagged field, plus the API suite |
Verified |
| Parsers survive hostile input | test_fuzz_logrec, the JSON and API suites, all under ASan/UBSan in CI |
Verified for the codec and API; the HTTP layer does not exist to fuzz |
| No allocation proportional to input | Structural, zero malloc/free in the firmware; tools/tidy.sh enforces the check set |
Verified |
| Control is unaffected by web load | SWR-NFR-02 timing tests |
Not performed: needs the transport and on-target instrumentation |
| Authentication is constant-time and rate-limited | , | Not performed: nothing to test |
| OTA rejects an invalid or unauthenticated image | , | Not performed: nothing to test |
| CSRF defences | , | None designed |
| Only signed firmware runs on the ESP32 | tools/secure-boot.py preflight reads the eFuse state back off the part; --self-test covers its parsing with no board attached |
Verified on a provisioned unit only; a default build is unsigned |
| The supervisor's program memory is intact | sup_flash_ok against the digest tools/sup-crc.py stamps in, with host tests over the CRC and over the unstamped case |
Verified |
| A manifest or image that does not verify is refused | Host tests over the verifier with a tampered manifest, a wrong key, a truncated image and a replayed older version | Not performed: the verifier does not exist |
| The update check carries nothing about the device | Test over the request the client builds: no identifier, no version, no query string, byte-identical for two differently configured devices | Not performed: nothing to test |
| Only the display can authorise an install | API suite: no route initiates, offers, authorises, postpones or cancels an update; plus inspection that no such handler exists | Not performed for the transport; the API suite already proves every non-GET route returns 403 read_only |
| A new image that cannot prove itself is rolled back | On-target test: install an image whose self-check fails, reset, confirm the previous image returns | Not performed: needs a device |
The pattern is the same one safety.md §10 reports: what exists in the
host-testable core is tested well, and what needs a device is untested because
it is unbuilt.
The five open questions live in security.sdoc, each related to
the residual risk it would settle, so a question cannot drift away from the risk
that motivated it. OQ-S1 and OQ-S5 are Resolved, answered by the read-only
decision of 2026-10-06, and OQ-S2 is Resolved as of 2026-10-09 by
§6.1: signing and
secure boot on the ESP32 as an opt-in production step, and neither on the
supervisor, where the part has no such mechanism and the readout protection it
does have is refused on purpose.
Two questions that are not nodes here, because they live in the requirement and
user-requirement documents, were answered on the same day and are the reason
§6.2 exists: OQ-08, what
replaces the removed over-the-web update, and OQ-R4, whether the Cyber
Resilience Act made the sold-units route impossible as designed. Both are
Resolved; neither is implemented.
The traceability tables that used to sit below them are gone. They were a threat
to security-goal to requirement grid and a goal to residual-risk grid, both
maintained by hand, and both had fallen out of step with the body: they still
routed threats through SEC-01 and SEC-04 after those were dissolved, and
carried no row for SEC-00, the strongest control in the document. Those
mappings are relations in security.sdoc now, generated in both
directions from the relations themselves, and a goal citing a requirement that
does not exist is an error in the requirements CI job rather than a stale row.
UR-REG-004 applies to the SoldUnits route only: on the
published-design route nobody places a product on the market, and unmonetised
open source is outside the Act. The dates are 11 September 2026 for reporting
and 11 December 2027 for the main obligations.
This section exists because the update path, answered on 2026-10-09, was one essential requirement out of many, and closing it made the rest easier to mistake for done. Each row below names where the work lives, not only that it is missing.
The essential requirements of Annex I Part I, against what this project has:
| Requirement | State |
|---|---|
| Security updates can be delivered | Specified, barely built. §6.2, SWR-UPD-09 to SWR-UPD-16. One of the seven implementation steps exists (the manifest format and its signing tool); SRR-11 still blocks release |
| Shipped in a secure default configuration | Closed 2026-10-09, and not in the way OQ-S4 expected: the provisioning access point that could start without a passphrase is removed rather than given a rule. Network setup is on the local display (SWR-NET-11, SWR-NET-12), so the device raises no second radio and TH-11 and SRR-08 close with it |
| No known exploitable vulnerabilities | Nothing known. Worth stating that the honest basis for this is a small codebase with no network transport yet, not a penetration test (SRR-01) |
| Confidentiality of stored data | OQ-S3, reframed from a preference into a requirement. net.wifi_pass is plaintext in NVS (SRR-05); the Annex asks for state-of-the-art protection. The routes may diverge, a self-build unencrypted and a sold unit not, as OQ-S2 let secure boot be per-unit. What cannot happen is the question staying open while units ship |
| Confidentiality of data in transit | SEC-11 is Absent by decision, with SWR-NFR-20's trusted-LAN assumption in its place (SRR-03). Defensible for a read-only dashboard of kiln temperatures; it is a position to argue in the file, not to leave unmentioned |
| Integrity of stored programs and configuration | Secure boot exists but is opt-in (§6.1). For a sold unit it should be on by default, which is a production-line decision nobody has recorded, and SRR-04 stands until the application verifies what it installs |
| Protection from unauthorised access | SEC-00 is the strongest answer available: no network request changes any state, the handlers are deleted rather than gated. Evidence, not work |
| Minimised attack surface | Evidence, not work. No inbound update route, no malloc, no telemetry, no outbound connection but the three named in SWR-NFR-21, fault injection compile-gated out of production images |
| Resilience against denial of service | SEC-08: control is isolated from the network by construction, so the worst realistic case is a lost web interface, not a lost kiln |
| Data minimisation | SWR-UPD-10 keeps the update check free of any device identity; there is no account, no cloud and no analytics |
| Security event logging, with a user opt-out | Was missing entirely; now SEC-13 and SWR-LOG-16. The event log recorded process events and nothing asked it to record a refused manifest, a rollback, a configuration change or a client joining the access point. The opt-out is required as explicitly as the log |
| Permanent removal of all data and settings | Was an instruction in a manual; now SEC-14 and SWR-CFG-09. A factory reset at the display, erasing bytes rather than unlinking them, leaving the production data block alone. ADV-4, the person who buys a second-hand controller, had no answer but a warning |
| Vulnerability handling that users can rely on | See 13.2: the firmware is the smaller half |
Annex I Part II and the Articles. Nothing here is discharged by writing code, and the project's instinct is to answer a requirement by building something:
| Obligation | State | Owner |
|---|---|---|
| Coordinated disclosure policy, and a single point of contact | Done 2026-10-09: SECURITY.md, with security@bitcrushtesting.com as the Article 13(17) contact. The mailbox has to exist and be read |
, |
| Machine-readable SBOM, kept for the support period | Done 2026-10-09: tools/sbom.py generates CycloneDX 1.6 and SPDX 2.3 from the build's own component list, with each licence scanned from the component's own SPDX-License-Identifier tags and anything untagged reported as NOASSERTION rather than guessed. CI generates it inside the IDF container, the release workflow publishes both formats as release assets. What is left is keeping them for the support period, which is a retention decision rather than a tool |
, |
| Monitoring vulnerabilities in third-party components | No process. ESP-IDF and mbedTLS advisories need a reader | , |
| Security updates free of charge, with an advisory | SWR-UPD-16 specifies it; the release channel does not exist |
, |
| Article 14 reporting: 24 h early warning, 72 h notification, 14-day final report, to ENISA and the CSIRT | No procedure, nobody named, and it applies from 11 September 2026. The sharp end of SRR-14 |
, |
| Users informed without undue delay in the same cases | Covered by the same procedure, which does not exist | , |
| Declared support period, at least five years, stated to the user | SWR-UPD-16 requires the declaration. The number has not been chosen, and the service life of a kiln controller is well beyond the floor |
, |
| Annex II information for users: contact, point of contact, intended use, limitations, how updates arrive, end of support, secure disposal | Not written. It belongs with the instructions that SYS-SAF-24 already shapes |
, |
| Annex VII technical documentation, including the cybersecurity risk assessment | Mostly already here. This document and security.sdoc are a risk assessment in all but name; they need framing as the Annex VII one and keeping current, which Article 13(2) requires rather than suggests |
, |
| Conformity assessment route, CE marking, EU declaration of conformity | Depends on classification. A kiln controller does not appear in Annex III, so self-assessment should be available, but that is a conclusion to record and defend: OQ-R5 |
, |
| Evidence of security testing and review | Evidence, not work: fuzzing, ASan and UBSan in CI, clang-tidy on host and target, MC/DC over the safety logic, requirement-to-source traceability. It needs citing in the file, not performing again |
, |
The Owner column is deliberately empty. Every row of the second table needs a name against it, and this document cannot supply one.
Not the longest item, the one that is a weakness today:
CloseDone 2026-10-09, by deleting the access point rather than regulating it. The remaining items are all either unbuilt or paperwork.OQ-S4.- Create and read the
SECURITY.mdmailbox, and write the Article 14 procedure behind it, because that clock starts in September 2026 and runs whether or not the firmware is finished. - Choose the support period. It gates
SWR-UPD-16, the Annex II information and the SBOM retention, and it is a sentence rather than a project. - Then the update path,
H2steps 2 to 7, which is the largest piece of engineering on the list and the one with the latest deadline.