Skip to content

feat(server): check box certificates - #536

Merged
SciLor merged 2 commits into
toniebox-reverse-engineering:developfrom
Oaradla:box-cert-check
Oct 16, 2026
Merged

SciLor merged 2 commits into
toniebox-reverse-engineering:developfrom
Oaradla:box-cert-check

Conversation

@Oaradla

@Oaradla Oaradla commented Oct 7, 2026

Copy link
Copy Markdown

Builds on #534 (its commit is included here until that one is merged).

Until now the box port accepted any client certificate whose common name looked like a box. Now:

  • Certificates issued by a TeddyCloud CA (TB1, TB2) are trusted. Such a box is pinned too, so no self-made
    certificate can take its place later.
  • Original Boxine certificates have no checkable chain (the box only sends its own certificate, issued by "Boxine
    Factory SubCA nn", which is not available). So they are checked against the real client certificate: if the
    original client.der of the box is uploaded, the box has to present exactly that certificate (also when another
    one was pinned before); without it, the first certificate a known box presents is pinned in toniebox.certPin,
    also with core.allowNewBox off, so nobody is locked out by the update. allowNewBox still decides about unknown
    boxes.
  • The existing core.boxCertAuth (default on) refuses boxes whose certificate does not match; off, they are only
    logged.
  • Result per box in internal.boxCertStatus (trusted / pinned / mismatch) and internal.boxCertSha256; a pin is
    reset with /api/settings/reset/toniebox.certPin?overlay=<box>.
  • Same check for the MQTT server: a refused box gets CONNACK "not authorized". An unknown box id only counts as a
    box there while allowNewBox is on (before, the MQTT server created an overlay for any box id).

Issuer check and box id from the certificate subject in box_cert.c (C tests), the check in box_cert_check.c;
the TLS context keeps the SHA-256 fingerprint of the box certificate.

Tests: test_box_cert_check.py (trusted, pinned on first contact, mismatch refused, only logged without
boxCertAuth, pin reset, known box pinned with allowNewBox off, uploaded client.der wins over an earlier pin, a box
with a TeddyCloud certificate refuses a self-made one).

Device-tested with the original firmware of a CC3200 box (3.1.0 BF2, 3.3.0, 3.4.0: pinned on first contact,
refused with a wrong pin) and an ESP32 box (v5.237.0: pinned, the same certificate as the client.der extracted
from its flash).

Refs #85

@henryk86

henryk86 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

So in short just for my understanding.

A box is still identified by its mac, but now it's additionally checked that the boxes certificate matches the pinned one (first contact original tonies cert or teddycloud cert). So if some bad guy knows the Mac and creates a fake cert it will reject?

@Oaradla

Oaradla commented Oct 7, 2026 •

Copy link
Copy Markdown
Author

Yes, exactly. The MAC from the certificate's common name still decides which box it is. On top of that, the certificate itself now has to be the right one:

  • Certificate issued by TeddyCloud (e.g. TB2): it is verified against the TeddyCloud CA, so it can't be faked without the CA key. The box is pinned to it as well, so a self-made certificate for that MAC is refused later.
  • Original Boxine certificate: there is no CA to check against (the "Boxine Factory SubCA" isn't available), so it is compared with the real one. That is either the client.der you uploaded or extracted for that box, or, without one, the certificate the box presented on its first
    contact after the update (pinned in toniebox.certPin).

So someone who knows the MAC and builds their own certificate gets a different fingerprint, which counts as a mismatch: 401 on the box port, "not authorized" on MQTT (with core.boxCertAuth, which is on by default). Copying the real certificate doesn't help either: the TLS handshake
needs the box's private key.

One gap remains: if no client.der is uploaded and an attacker connects with that MAC before the real box does, the attacker's certificate gets pinned. The real box is then refused and shows up as "mismatch" in the log and the web UI. Resetting the pin
(/api/settings/reset/toniebox.certPin?overlay=) or uploading the original client.der fixes that. Uploading the client.der closes this window from the start.

@SciLor

SciLor commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

#533, #534, #535 and #537 are merged. Could you rebase this onto develop? The #534 commit can be dropped, and #535 touches the same three lines in Makefile, tests/c/test_runner.c and tests/c/tests.h. After that I'll merge it.

@Oaradla

Oaradla commented Oct 9, 2026

Copy link
Copy Markdown
Author

Rebased onto develop: one commit now, the #534 commit is dropped. Makefile, tests/c/test_runner.c and tests/c/tests.h keep both the esp32_port entries from #535 and the box_cert ones. The rest of the diff is unchanged. Build and tests pass (C tests, box_cert_check, box_cert_upload, box_sim).

@SciLor

SciLor commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Could you please rebase again / merge as needed?

@Oaradla

Oaradla commented Oct 15, 2026

Copy link
Copy Markdown
Author

Rebased onto develop again (after #545).

Oaradla and others added 2 commits October 16, 2026 11:58
Until now any client certificate whose common name looked like a box was
accepted. Now:

- Certificates issued by a TeddyCloud CA (TB1, TB2) are trusted.
- Original Boxine certificates cannot be checked against a CA (the box
  only sends its own certificate, issued by "Boxine Factory SubCA nn",
  which is not available). If the original client.der of the box is
  uploaded, the box has to present exactly that certificate; without it,
  the first certificate a known box presents is pinned in
  toniebox.certPin (also with core.allowNewBox off, so nobody is locked
  out by the update). A box with a trusted certificate is pinned too, so
  no self-made certificate takes its place later.
- core.boxCertAuth (default on) refuses boxes whose certificate does not
  match; off, they are only logged.
- Result per box in internal.boxCertStatus and internal.boxCertSha256.
- Same check for the MQTT server; a refused box gets CONNACK "not
  authorized". An unknown box id only counts as a box there while
  core.allowNewBox is on.

The issuer check and the box id from the certificate subject are in
box_cert.c, the check in box_cert_check.c. The TLS context keeps the
SHA-256 fingerprint of the box certificate.

Refs toniebox-reverse-engineering#85
The global setting raced with the box overlay (401 instead of 200 in about 1 of 8 runs). Setting it on the overlay of the box is reliable (0 of 26 runs failed).
@SciLor
SciLor merged commit 8291cda into toniebox-reverse-engineering:develop Oct 16, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants