server: HTTP echo + TLS reference (control-plane security measurements)
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 28s

- POST /v1/echo: returns the received request head + body (base64) and the
  observed TLS parameters (version, cipher, SNI, ALPN, resumed). The client
  diffs against what it sent to detect header injection/stripping,
  transparent proxying, or TLS interception (sec.http_echo). http-echo
  added to the capability set.
- GET /v1/tls-reference: the served leaf-first DER chain + pin, so the app
  can compare an out-of-band copy against its own handshake (sec.tls_reference).
  Always available, no auth — public handshake info.
- Optional CLEARTEXT http-echo listener (ECHOLOT_HTTP_ECHO_LISTEN, default
  off) exposing only /v1/echo for the plaintext-path tampering test.

Live-smoke-tested (HTTPS echo reflected an injected header + observed
TLS1.3; cleartext variant reports tls:none); httptest unit tests added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-07-31 20:34:03 +02:00
co-authored by Claude Opus 5
parent 379153219e
commit 38fb73c34e
5 changed files with 188 additions and 3 deletions
+94
View File
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package control
import (
"crypto/tls"
"encoding/base64"
"io"
"net/http"
"strings"
)
// httpEcho implements spec §4 HTTP echo: return the exact received request
// (request line + headers + body, base64) plus the TLS parameters the server
// observed. The client diffs this against what it sent to detect header
// injection/stripping, transparent proxying, or TLS interception
// (sec.http_echo). Served on the control HTTPS listener and, optionally, on a
// cleartext listener to test plaintext-path tampering.
func (s *Server) httpEcho(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
// Reconstruct the received request head verbatim (as close as net/http
// exposes it — header order is lost, but names/values and the request
// line survive, which is what tampering changes).
var head strings.Builder
head.WriteString(r.Method + " " + r.RequestURI + " " + r.Proto + "\r\n")
head.WriteString("Host: " + r.Host + "\r\n")
for name, vals := range r.Header {
for _, v := range vals {
head.WriteString(name + ": " + v + "\r\n")
}
}
head.WriteString("\r\n")
resp := map[string]any{
"observed_src": r.RemoteAddr,
"request_head_b64": base64.StdEncoding.EncodeToString([]byte(head.String())),
"body_b64": base64.StdEncoding.EncodeToString(body),
"body_len": len(body),
"scheme": schemeOf(r),
}
if r.TLS != nil {
resp["tls"] = tlsParams(r.TLS)
}
writeJSON(w, http.StatusOK, resp)
}
func schemeOf(r *http.Request) string {
if r.TLS != nil {
return "https"
}
return "http"
}
func tlsParams(cs *tls.ConnectionState) map[string]any {
return map[string]any{
"version": tlsVersionName(cs.Version),
"cipher": tls.CipherSuiteName(cs.CipherSuite),
"sni": cs.ServerName,
"alpn": cs.NegotiatedProtocol,
"resumed": cs.DidResume,
}
}
func tlsVersionName(v uint16) string {
switch v {
case tls.VersionTLS13:
return "TLS1.3"
case tls.VersionTLS12:
return "TLS1.2"
case tls.VersionTLS11:
return "TLS1.1"
case tls.VersionTLS10:
return "TLS1.0"
}
return "unknown"
}
// tlsReference implements spec §4: return the exact certificate chain this
// server serves (DER, base64), so the app can compare it against a copy it
// obtained out-of-band and against what its own direct handshake yielded
// (sec.tls_reference). Not a capability — always available on the control
// plane. No auth: the chain is public information a handshake already reveals.
func (s *Server) tlsReference(w http.ResponseWriter, r *http.Request) {
chain := make([]string, 0, len(s.CertChain))
for _, der := range s.CertChain {
chain = append(chain, base64.StdEncoding.EncodeToString(der))
}
writeJSON(w, http.StatusOK, map[string]any{
"pin_sha256": s.PinB64,
"chain_der": chain, // leaf first, as served
})
}