server: refuse unsigned releases and polluted reserved addresses
Self-update now verifies SHA256SUMS.sig (ed25519, relsign package) against a public key baked into the binary; the private key exists only in the CI secret store, so a compromised release host can withhold updates but not inject one. CI signs on every server-v* tag and hard-fails without the secret. Operators with their own pipeline override the key via ECHOLOT_SELF_UPDATE_PUBKEY (mint a pair with release-sign -gen). Startup also now proves 80/443 are actually free on the reserved measurement addresses by asking the OS (throwaway bind), not the config - CheckReserved could never see a stray process, and the adb-beacon receiver on 0.0.0.0:443 was exactly that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
20cfecf566
commit
a49bef5821
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package selftest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ReservedWebPortsFree verifies that nothing on this host — this process or any other — is
|
||||
// listening on 80/443 of the reserved measurement addresses.
|
||||
//
|
||||
// config.CheckReserved keeps *our own* listeners off those ports, but our configuration is not
|
||||
// the host: the adb-beacon receiver, a separate Python process wildcard-bound to 0.0.0.0:443,
|
||||
// silently voided the IPv4 interception proof for as long as it ran, and nothing in this server's
|
||||
// config could have seen it. Asking the OS is the only check that covers processes we did not
|
||||
// start.
|
||||
//
|
||||
// The mechanism is a throwaway bind: if the bind succeeds the port was provably free (closed
|
||||
// again immediately — nothing is served), and if it fails with EADDRINUSE something is listening
|
||||
// there. Any other failure (address not assigned to this host, missing privilege) means the
|
||||
// question could not be answered, which is reported separately rather than pretending either way.
|
||||
func ReservedWebPortsFree(ips []net.IP) (occupied, unverifiable []string) {
|
||||
return portsFree(ips, []string{"80", "443"})
|
||||
}
|
||||
|
||||
func portsFree(ips []net.IP, ports []string) (occupied, unverifiable []string) {
|
||||
for _, ip := range ips {
|
||||
for _, port := range ports {
|
||||
addr := net.JoinHostPort(ip.String(), port)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err == nil {
|
||||
ln.Close()
|
||||
continue
|
||||
}
|
||||
if isAddrInUse(err) {
|
||||
occupied = append(occupied, addr)
|
||||
} else {
|
||||
unverifiable = append(unverifiable, fmt.Sprintf("%s (%v)", addr, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return occupied, unverifiable
|
||||
}
|
||||
Reference in New Issue
Block a user