The bundle is authored by Continuum Cloud's configurator and downloaded by
the customer, so it travels over the internet and then on a USB stick while
holding the gateway token and possibly a wifi PSK. That makes it a secret at
rest, and confidentiality is the benefit encryption actually delivers here.
WinZip AES-256 ("AE-x"), via the zip crate's `aes-crypto` feature. That
pulls in aes, hmac, pbkdf2, sha1, constant_time_eq and zeroize - all
permissive (MIT/Apache-2.0, and CC0-1.0 OR MIT-0 OR Apache-2.0 for
constant_time_eq), each checked against docs/LICENSES.md, and
`cargo deny check licenses` passes.
**Legacy ZipCrypto is refused outright**, whatever key is configured: it
falls to a known-plaintext attack. The test is on metadata, not on a failed
decrypt - `encrypted()` is set for ZipCrypto and AES alike, but only AES
carries the AE-x extra field, so encrypted-without-it is ZipCrypto and the
whole bundle is rejected. Judged in the first pass, before any decrypt call,
so the crate's ZipCrypto path never runs on our input. Mixed bundles - one
entry encrypted, the other not - are rejected too.
The caps now bound *decrypted* output, so a small ciphertext that inflates
is capped exactly as a plaintext bomb is. Everything else holds unchanged
for an encrypted bundle: zip-slip, the inner config.toml.sha256, the
no-environment validation, atomic install, and a rejected bundle never
touching the installed config.
Failures are distinguished, because "wrong key" and "damaged" need different
answers: no key on the box and a wrong key both say "not for this box", while
tampered or damaged ciphertext says it failed its AES integrity check. A
wrong key passes AES's 16-bit password check 1 time in 65536 and then fails
the HMAC, so that message names a wrong key as a possibility.
**The limitation to be clear about: with plaintext bundles still accepted,
encryption does nothing against tampering.** An attacker supplies a
plaintext bundle and never touches the crypto. The tamper goal only begins to
hold once `accept_unencrypted` is false, which is one line in main.rs and is
deliberately not flipped yet - doing so breaks every stick made before the
configurator encrypts, and every hand-made development stick. That is a
product decision, not a code one.
More generally, encryption is not authenticity: the AES HMAC detects
modification only by someone without the password, and the password has to
live on the box, so extracting it from one image breaks every box sharing it.
It raises the bar from "anyone with a text editor" to "anyone who can read a
firmware image". The module docs say exactly that and do not call it
tamper-proof. `check_authenticity` is a named no-op where an Ed25519 check
over the config, verified against a *public* key in the box image, belongs -
it verifies nothing and says so, so the missing authenticity is a searchable
gap rather than an implicit one.
`load_bundle_key` is the single marked seam where provisioning lands. Its
current `$CONTINUUM_BUNDLE_KEY_FILE` / `/etc/continuum/bundle.key` lookup is
a placeholder, not a decision: shared-vs-per-box keys, where the key lives,
and rotation are open product questions and the mechanism depends on none of
them.
Encrypted fixtures are genuine AES ZIPs written by the zip crate itself, and
one test confirms the config text is absent from the ciphertext. The
ZipCrypto fixture sets the encrypted flag without an AE-x field, which is the
metadata shape this must refuse. Unproven: interop with whatever ZIP writer
the cloud actually uses (only archives written by this same crate version
were read back), AES-128/192, and `load_bundle_key` itself, which reads
process env and so has no test.
90 lines
3.3 KiB
TOML
90 lines
3.3 KiB
TOML
[package]
|
|
name = "continuum-proxy"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
description = "Continuum edge gateway daemon - runs on-site, bridges printers to the cloud control plane"
|
|
# Proprietary: deliberately not an SPDX id, so cargo-deny treats it as
|
|
# first-party. See docs/LICENSES.md in continuum-common.
|
|
license = "LicenseRef-Proprietary"
|
|
publish = false
|
|
|
|
[lib]
|
|
name = "continuum_proxy"
|
|
path = "src/lib.rs"
|
|
|
|
[[bin]]
|
|
name = "continuum-proxy"
|
|
path = "src/main.rs"
|
|
|
|
[[example]]
|
|
name = "printer_polymorphism"
|
|
path = "examples/printer_polymorphism.rs"
|
|
|
|
[[example]]
|
|
name = "test_bambu_certs"
|
|
path = "examples/test_bambu_certs.rs"
|
|
|
|
[[example]]
|
|
name = "auto_connect_bambu"
|
|
path = "examples/auto_connect_bambu.rs"
|
|
|
|
[[example]]
|
|
name = "bambu_commands_demo"
|
|
path = "examples/bambu_commands_demo.rs"
|
|
|
|
[[example]]
|
|
name = "live_reports"
|
|
path = "examples/live_reports.rs"
|
|
|
|
[[example]]
|
|
name = "bambu_idle_checks"
|
|
path = "examples/bambu_idle_checks.rs"
|
|
|
|
[dependencies]
|
|
# Real printer protocol clients (MQTT for Bambu, HTTP for PrusaLink/
|
|
# Moonraker, FTPS for gcode transfer) go here once you're ready to build
|
|
# past the stubs in src/printer/ - see that module's doc comment.
|
|
continuum-rust-types = { path = "../continuum-schemas/packages/rust-types" }
|
|
# FOR NON-LOCAL DEV
|
|
# continuum-rust-types = { git = "https://git.octoturge.com/Continuum/continuum-schemas.git" }
|
|
|
|
# Builds a JSON instance straight from the schema files in
|
|
# continuum-schemas/schemas/ - used for Bambu's raw MQTT JSON messages
|
|
# instead of hand-written serde structs (see bambu.rs).
|
|
continuum-schema-instance = { path = "../continuum-common/packages/rust-schema-instance" }
|
|
|
|
tokio = { version = "1.40", features = ["full"] }
|
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
|
futures-util = "0.3"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
toml = "0.8"
|
|
native-tls = "0.2"
|
|
base64 = "0.22"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
|
anyhow = "1"
|
|
thiserror = "1"
|
|
dotenvy = "0.15"
|
|
tokio-serial = { version = "5.5.0", default-features = false }
|
|
rumqttc = { version = "0.25.1", default-features = false, features = ["use-native-tls"] }
|
|
# Blocking FTPS client for pushing project files to a printer's own
|
|
# storage (see src/printer/bambu_ftps.rs for why the blocking one, not
|
|
# the async one). `deprecated` is what gates `connect_secure_implicit`:
|
|
# Bambu speaks implicit FTPS on 990, which the RFCs deprecated in favour
|
|
# of explicit AUTH TLS. The printers do not care.
|
|
# MIT OR Apache-2.0 - checked against docs/LICENSES.md before adding.
|
|
suppaftp = { version = "12.1.1", features = ["native-tls", "deprecated"] }
|
|
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2", "aes-crypto"] }
|
|
# Reads the subject CN out of the certificate a printer presents on 8883,
|
|
# which is where its serial number is (see src/discovery.rs). Already in the
|
|
# tree - native-tls is openssl on Linux - so naming it directly adds nothing
|
|
# to the build.
|
|
# Apache-2.0 - checked against docs/LICENSES.md before adding.
|
|
openssl = "0.10"
|
|
flate2 = "1.1.10"
|
|
# HTTPS GET of a presigned job-file URL for the `start_print` command (see
|
|
# src/uplink/download.rs). Features are the bare minimum: TLS via rustls, no
|
|
# openssl-sys; no json/gzip/cookies/etc, since the body is an opaque binary.
|
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] }
|