Refresh agent signing keys from repository (Refs #64)

This commit is contained in:
2026-10-09 21:39:03 -04:00
parent 6f71822d41
commit a93ddb5ef9
14 changed files with 420 additions and 214 deletions
+19 -8
View File
@@ -96,11 +96,6 @@ def trusted_json(path):
trusted_directories(root)
trusted_directories(config.parent)
keyring = config.with_name('release-signing.gpg')
try:
trusted_file(keyring)
except FileNotFoundError:
raise SystemExit('Provision the publisher public key at /etc/electric-cherry/release-signing.gpg through a trusted channel before installing. Never obtain the trust key from the controller.') from None
mountinfo = pathlib.Path('/proc/1/mountinfo')
if mountinfo.exists():
for line in mountinfo.read_text().splitlines():
@@ -147,7 +142,7 @@ PY
if [ -f /etc/electric-cherry/agent.json ]; then existing_enrollment=true; fi
if [ -d "$data_dir" ]; then existing_data_root=true; fi
installation_preflight
temporary_dir=$(mktemp -d)
temporary_dir=$(mktemp -d /tmp/electric-cherry-install.XXXXXXXXXX)
updater_timer_was_active=false
update_lock_held=false
activity_lock_held=false
@@ -172,23 +167,39 @@ installation_cleanup() {
exit "$ec_install_status"
}
trap installation_cleanup EXIT
# INSTALLATION_TRUST: always fetch directly from the repository, never the controller.
key_status=$(curl --proto '=https' --tlsv1.2 --fail --silent --show-error \
--connect-timeout 25 --max-time 120 --max-filesize 262144 \
--header 'Cache-Control: no-cache' --write-out '%{http_code}' \
'https://gitea.thewrightserver.net/josh/electric-cherry/raw/branch/main/agent/release-signing.gpg' \
-o "$temporary_dir/release-signing.gpg")
[ "$key_status" = 200 ] && [ -s "$temporary_dir/release-signing.gpg" ] || {
echo 'The release-signing repository did not return a public key. Installation stopped.' >&2
exit 1
}
# INSTALLATION_TRUST_END
controller="${controller%/}"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/manifest.json" -o "$temporary_dir/manifest.json"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/manifest.json.sig" -o "$temporary_dir/manifest.json.sig"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/ec-agent.py" -o "$temporary_dir/ec-agent.py"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/ec-agent-update.py" -o "$temporary_dir/ec-agent-update.py"
python3 - "$temporary_dir/manifest.json" "$temporary_dir/ec-agent.py" "$temporary_dir/ec-agent-update.py" /etc/electric-cherry/release-signing.gpg <<'PY'
python3 - "$temporary_dir/manifest.json" "$temporary_dir/ec-agent.py" "$temporary_dir/ec-agent-update.py" "$temporary_dir/release-signing.gpg" <<'PY'
# INSTALLATION_RELEASE: check both downloads before installing or executing either.
import ast, hashlib, json, pathlib, re, stat, subprocess, sys, tempfile
manifest_path = pathlib.Path(sys.argv[1])
manifest_bytes = manifest_path.read_bytes()
signature = manifest_path.with_name('manifest.json.sig')
keyring = pathlib.Path(sys.argv[4])
for path in (*reversed(keyring.parents), keyring):
# The key lives in our private root-owned mktemp directory under sticky /tmp.
# /tmp itself is intentionally writable; require protection of the directory
# and file we created, rather than accepting a pre-existing host keyring.
for path in (keyring.parent, keyring):
info = path.lstat()
expected_type = stat.S_ISREG if path == keyring else stat.S_ISDIR
if not expected_type(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
raise SystemExit('Release signing key must be a regular root-owned file in protected directories.')
if not 0 < keyring.stat().st_size <= 256 * 1024:
raise SystemExit('Repository public key is empty or exceeds its size limit.')
if len(manifest_bytes) > 16 * 1024 or signature.stat().st_size > 16 * 1024:
raise SystemExit('Release signing metadata exceeds its size limit.')
try: