Authenticate config bundles: Ed25519, per-box identity, no plaintext
The hash in a bundle only proved it was undamaged. Anyone who could write the stick could write both files, so a Proxy Box would install a config handed to it by whoever got physical access - including a config pointing the box at a different cloud. Encryption alone does not fix that either. While plaintext was accepted an attacker simply did not encrypt, and the box took it. So a Box now refuses plaintext outright, and the bundle carries a signature. A third entry, `config.toml.sig`: strict TOML with `box_id`, `config_version` and a raw Ed25519 signature. What is signed is `signed_message()` - a domain tag, the length-prefixed box id, the version big-endian, then the raw `config.toml` bytes. Raw bytes rather than the archive, so the signature is independent of ZIP framing, compression, entry order and encryption. The box id and version are inside the signed message rather than beside it, so neither can be edited without breaking it, and the length prefix stops a byte-shifting collision between the id and the config. Past decryption the order is hash, signature, box id, version, then parse and validate. The claims in the `.sig` are attacker-controlled text until the signature over them verifies, so nothing is concluded from them before then - and nothing about this box's identity is revealed before then either, which is why unsigned junk cannot be used to probe which box it is talking to. Which error wins when a box cannot even open a bundle: a signature over the plaintext is only checkable after decryption, so a box with the wrong key cannot distinguish "meant for another box" from "forged". Wrong key wins. Neither bundle installs either way, it is nearly always the true cause, and calling something forged that we could not open would be an accusation we cannot support. Replay is enforced rather than documented away: `config_version` must not go backwards, recorded in a sidecar beside the installed config. An equal version is accepted on purpose, because the version is written before the config, so a power cut between the two leaves "new version, old config" and re-inserting the same stick repairs it. The other write order would leave a replay window instead. Both key sets are lists, so the cloud's signing key and a box's own key can be rotated. `load_provisioning()` is the single seam where a box's identity and keys come from; a TPM or secure element replaces that function and nothing else. A missing file means unprovisioned, and the box then says loudly what it cannot check - but an empty or malformed one is an error, because quietly reading a broken key file as absent would turn a provisioning mistake into a box that accepts unsigned bundles. Not verified: interop with the real cloud signer. These tests prove the box accepts what this code defines as correct, not that the cloud will produce the same bytes - the message layout, the pure-Ed25519-no-prehash choice and the `.sig` shape all need a cross-check against the configurator's own crypto library once it exists. A compromised cloud signing key still breaks every box trusting it; revocation is not built.
This commit is contained in:
+8
-10
@@ -5,16 +5,14 @@
|
||||
#
|
||||
# PC / server: copy to continuum.toml next to the proxy and edit it. Nothing
|
||||
# else is needed; removable media is never read.
|
||||
# Proxy Box: put this file in a ZIP named continuum-config.zip together
|
||||
# with its SHA-256, and insert the stick:
|
||||
#
|
||||
# cp continuum.example.toml config.toml # edit it
|
||||
# sha256sum config.toml > config.toml.sha256
|
||||
# zip continuum-config.zip config.toml config.toml.sha256
|
||||
#
|
||||
# The hash catches a damaged or hand-edited stick. It does NOT
|
||||
# prove who made it - anyone who can write the stick can write
|
||||
# both files. See src/config_import.rs.
|
||||
# Proxy Box: this file is NOT written by hand. Continuum Cloud's
|
||||
# configurator produces continuum-config.zip: config.toml,
|
||||
# config.toml.sha256 and config.toml.sig (an Ed25519 signature
|
||||
# naming this box and a config_version), every entry
|
||||
# AES-256-encrypted under the box's own key. A Box refuses
|
||||
# plaintext, unsigned, other-box and older-version bundles.
|
||||
# See the module docs of src/config_import.rs for the exact
|
||||
# format the cloud must produce.
|
||||
#
|
||||
# Unknown keys are rejected on purpose, so a typo cannot silently fall back to
|
||||
# a default. On a Proxy Box the file must be complete by itself.
|
||||
|
||||
+740
-141
File diff suppressed because it is too large
Load Diff
+21
-6
@@ -9,7 +9,7 @@ mod go2rtc;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use continuum_proxy::bootstrap::Bootstrap;
|
||||
use continuum_proxy::bootstrap::{self, Bootstrap};
|
||||
use continuum_proxy::config::Config;
|
||||
use continuum_proxy::config_import;
|
||||
use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
|
||||
@@ -27,12 +27,27 @@ async fn main() -> anyhow::Result<()> {
|
||||
// the installed config by the time it is read. A rejected stick is
|
||||
// logged inside and does not stop startup - the box runs on what it has.
|
||||
let bootstrap = Bootstrap::discover()?;
|
||||
let policy = config_import::BundlePolicy {
|
||||
key: config_import::load_bundle_key()?,
|
||||
// Plaintext bundles are accepted (and logged at warn on install);
|
||||
// see the config_import module docs for what that costs.
|
||||
accept_unencrypted: true,
|
||||
// Plaintext bundles are refused on a Proxy Box and signatures are
|
||||
// mandatory once a signing key is provisioned; see the config_import
|
||||
// module docs. An unprovisioned box is allowed to start - it runs on its
|
||||
// installed config - but says loudly what it cannot check.
|
||||
//
|
||||
// Loaded only on a box: on a PC nothing imports from removable media, and
|
||||
// a stray or malformed file under /etc/continuum must not stop a PC user's
|
||||
// proxy starting over provisioning it never uses.
|
||||
let policy = if bootstrap.mode == bootstrap::Mode::ProxyBox {
|
||||
config_import::load_provisioning()?
|
||||
} else {
|
||||
config_import::BundlePolicy::unencrypted_only()
|
||||
};
|
||||
if bootstrap.mode == bootstrap::Mode::ProxyBox {
|
||||
if policy.signing_keys.is_empty() {
|
||||
tracing::warn!("no signing key provisioned: config bundles will NOT be authenticated");
|
||||
}
|
||||
if policy.box_id.is_none() {
|
||||
tracing::warn!("no box id provisioned: signed config bundles cannot be accepted");
|
||||
}
|
||||
}
|
||||
for outcome in config_import::import_from_pendrives(&bootstrap, &policy) {
|
||||
config_import::log_outcome(&outcome);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user