2026-05-02 11:43:41 -04:00
|
|
|
[package]
|
|
|
|
|
name = "esp32-blinky"
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
edition = "2021"
|
|
|
|
|
resolver = "2"
|
|
|
|
|
rust-version = "1.77"
|
|
|
|
|
|
|
|
|
|
[[bin]]
|
|
|
|
|
name = "esp32-blinky"
|
|
|
|
|
harness = false
|
|
|
|
|
|
|
|
|
|
[profile.release]
|
|
|
|
|
opt-level = "s"
|
2026-05-02 15:39:33 -04:00
|
|
|
lto = true # link-time optimization across all crates
|
|
|
|
|
codegen-units = 1 # one CGU = more cross-fn opt at the cost of build speed
|
|
|
|
|
strip = true # drop ELF symbols not needed at runtime
|
|
|
|
|
panic = "abort" # smaller panic handler; matches build-std=panic_abort
|
2026-05-02 11:43:41 -04:00
|
|
|
|
|
|
|
|
[profile.dev]
|
|
|
|
|
debug = true
|
|
|
|
|
opt-level = "z"
|
|
|
|
|
|
|
|
|
|
[features]
|
|
|
|
|
default = ["std", "esp-idf-svc/native", "esp-idf-svc/binstart"]
|
|
|
|
|
pio = ["esp-idf-svc/pio"]
|
|
|
|
|
std = ["esp-idf-svc/std"]
|
|
|
|
|
|
|
|
|
|
[dependencies]
|
cloud_log: NVS-loaded config, tracing layer, queue, stub sender
First commit of cloud-logging work (logs-plan.md). Foundation only:
the actual NTP/JWT/RSA/HTTPS-POST sender is the next commit; this
sender is a stub that writes 'would POST' to serial.
src/cloud_log.rs:
- GcpConfig::load reads the optional 'gcp' NVS namespace. If any
required key (project_id / sa_email / sa_key_id / sa_key_pem) is
missing, returns Ok(None) — cloud logging is opt-in per device.
- LogQueue: Mutex<VecDeque>-backed bounded ring buffer (256 entries),
drops oldest when full and surfaces the drop count on the next push.
- CloudLogLayer: tracing_subscriber Layer that captures events,
extracts structured fields via field::Visit, applies the configured
min_severity filter, and pushes onto the queue. Uses wall-clock
time when SystemTime::now() is past 2020 (NTP synced); else None
so Cloud Logging assigns server-side timestamps.
main.rs:
- Take NVS first, before any tracing events fire.
- If the gcp NVS namespace is populated, install CloudLogLayer as
the global tracing subscriber and spawn the sender thread (32 KB
stack).
- tracing now uses the 'log-always' feature so events still emit
log records even with a subscriber installed — keeps EspLogger
writing to serial regardless.
tools/provision/:
- Optional [gcp] section in provisioning.toml. Tool emits the gcp
namespace into the NVS CSV when present, validates min_severity
spelling early.
ota.md: gcp namespace added to NVS schema docs.
provisioning.toml.example: commented [gcp] block.
Firmware size: 1.52 MB -> 1.60 MB (tracing-subscriber + cloud_log code).
Plenty of slot headroom remaining (1.94 MB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:06:00 -04:00
|
|
|
# `log-always` makes tracing emit log records even when a Subscriber is
|
|
|
|
|
# set, so EspLogger (a log::Logger) keeps writing to the serial console
|
|
|
|
|
# in addition to whatever subscriber layers we install (cloud_log).
|
|
|
|
|
tracing = { version = "0.1", features = ["log-always"] }
|
|
|
|
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["registry"] }
|
2026-05-02 11:43:41 -04:00
|
|
|
anyhow = "1"
|
2026-05-02 20:35:56 +00:00
|
|
|
esp-idf-svc = "0.52"
|
|
|
|
|
embedded-svc = "0.29"
|
Add device-side OTA loop (OTA phase 2)
src/ota.rs polls ghcr.io every 60s on a dedicated 32 KB pthread,
fetches the manifest with an anonymous Bearer token, and on a digest
mismatch streams the layer into the inactive OTA partition while
verifying SHA256 against the descriptor as it goes. Aborts and
bails on size or SHA mismatch; otherwise sets the boot partition,
persists pending_digest to NVS, and reboots.
main.rs detects PENDING_VERIFY on boot and only calls
esp_ota_mark_app_valid_cancel_rollback() after Wi-Fi + the existing
HTTPS bringup checks pass -- so an OTA that breaks networking
auto-reverts on the bootloader's next boot. After mark-valid,
pending_digest is promoted to last_digest in NVS.
GIT_SHA is baked into the firmware via env!() at compile time
(set by the Makefile from `git rev-parse --short HEAD`) and logged
on boot, so we can see which build is running.
Bumped CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT to 16 KB; 8 KB
stack-overflowed during HTTPS + JSON + SHA work.
`make monitor` now auto-passes --non-interactive when stdout is
not a TTY, so it works in scripts and background tasks.
Verified end-to-end against ghcr.io/imjasonh/esp32 -- published v1,
flashed via USB, bumped a visible version string, published v2,
and the device polled, downloaded, SHA-verified, rebooted, and
marked the new image valid after bringup. Zero USB intervention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:53:19 -04:00
|
|
|
serde = { version = "1", features = ["derive"] }
|
|
|
|
|
serde_json = "1"
|
2026-05-02 20:35:56 +00:00
|
|
|
sha2 = "0.11"
|
2026-05-02 16:51:14 -04:00
|
|
|
# `hex::encode` to render SHA digests; sha2 0.11's output type
|
|
|
|
|
# (`Array<u8, _>`) doesn't implement `LowerHex` like `GenericArray`
|
|
|
|
|
# did, so `format!("{:x}", ...)` no longer works.
|
|
|
|
|
hex = "0.4"
|
Add cosign signature verification (OTA phase 4a)
Each OTA fetch is now gated on a Sigstore Bundle signature whose Fulcio
cert identifies a member of an allowlist hardcoded in src/trust.rs
(currently imjasonh@gmail.com / accounts.google.com). The allowlist
cannot be changed via OTA -- only by editing source and reflashing
over USB.
Publisher (Makefile): make publish now runs cosign sign --yes after
the publisher push. Cosign keyless OIDC pops a browser the first time;
subsequent signs reuse the cached token within ~10min.
Firmware:
- src/trust.rs: TRUSTED_IDENTITIES list + bundled Sigstore root and
intermediate CA PEMs (trust/fulcio_root.pem, fulcio_intermediate.pem).
- src/sig.rs: parse Sigstore Bundle v0.3 (DSSE envelope), verify (a)
Fulcio cert SAN email + OID 1.3.6.1.4.1.57264.1.1 issuer match the
allowlist, (b) leaf cert chains to bundled Sigstore root via
P-384 ECDSA-SHA384, (c) DSSE signature verifies via P-256 ECDSA-SHA256
over the PAE, (d) in-toto Statement subject digest binds to our
manifest digest.
- src/ota.rs: fetch_manifest now returns the manifest's own SHA256 (the
digest cosign signed), not just the parsed body. New
fetch_signature_bundle walks the OCI 1.1 referrers layout cosign uses
(image index -> inner manifest -> bundle blob). Sig is fetched and
verified before the firmware download starts.
Crates: p256, p384, x509-cert (with pem feature), base64. Adds
~350 KB to the firmware -- repartitioned ota slots from 1.5MB to
1.75MB to fit (USB-only migration). Bumped OTA thread stack to
48KB for cert-parsing headroom.
Verified end-to-end: Jason signed :latest with cosign keyless,
device polled, all four verification steps passed (identity, chain,
DSSE sig, in-toto binding), then downloaded and applied as before.
Phase 4b (Rekor SET / transparency log inclusion proof) and 4c
(operational hardening) remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:50:03 -04:00
|
|
|
# Phase 4a: cosign Sigstore Bundle verification.
|
|
|
|
|
p256 = { version = "0.13", features = ["ecdsa"] } # leaf signs DSSE
|
|
|
|
|
p384 = { version = "0.13", features = ["ecdsa"] } # intermediate signs leaf
|
|
|
|
|
x509-cert = { version = "0.2", features = ["pem"] }
|
|
|
|
|
base64 = "0.22"
|
cloud_log: real sender — JWT, RSA-SHA256, OAuth2, POST to logging.googleapis.com
Replaces the stub eprintln sender with the real implementation:
- mint_access_token: builds + signs a service-account JWT (RS256 over
base64url(header).base64url(claims)), POSTs to oauth2.googleapis.com
with the standard grant_type=...jwt-bearer form body, parses the
access_token + expires_in.
- CachedToken: held in the sender thread's local state. Refreshes
300 seconds before expiry (Google issues 1h tokens; we re-mint at
T+55min).
- post_batch: serializes the WriteEntriesRequest with logName,
resource (generic_node + project + MAC node_id), and entries
(severity + jsonPayload + optional timestamp), POSTs to
logging.googleapis.com/v2/entries:write with the bearer.
- Backoff on failure: sleep doubles on each consecutive failure
(capped at 5 min). Batches dropped on POST failure rather than
re-enqueued; loss is surfaced via dropped_before on the next entry.
Crates added: rsa 0.9 (with sha2 feature for AssociatedOid; PKCS#1
v1.5 needs the DigestInfo prefix), time 0.3 (RFC3339 formatting).
main.rs: SNTP startup is now gated on gcp.is_some(). The JWT auth
needs a real wall-clock for / (Google rejects ~5min skew),
so devices with cloud logging pay a one-time ~few-second sync at
boot. Devices without [gcp] skip it. Log entry timestamps themselves
are still optional — when omitted, GCP server-side assigns them.
Cloud Logging severity mapping: tracing TRACE/DEBUG -> DEBUG,
INFO -> INFO, WARN -> WARNING, ERROR -> ERROR (matches GCP's
LogSeverity enum).
device_mac() reads via esp_efuse_mac_get_default and uses the lower
6 bytes as the node_id resource label, so multi-device logs can be
filtered by MAC.
Firmware size: 1.60 MB -> 1.71 MB (+rsa, +time, +sender code).
Still under the 1.94 MB slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:13:03 -04:00
|
|
|
# Cloud Logging: RSA-PKCS#1v1.5 SHA-256 signing for service-account JWTs.
|
|
|
|
|
rsa = { version = "0.9", default-features = false, features = ["std", "pem", "sha2"] }
|
|
|
|
|
# RFC 3339 timestamp formatting for Cloud Logging entries.
|
|
|
|
|
time = { version = "0.3", default-features = false, features = ["std", "formatting"] }
|
2026-05-02 11:43:41 -04:00
|
|
|
|
|
|
|
|
[build-dependencies]
|
|
|
|
|
embuild = "0.33"
|