- 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>
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package control
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestHTTPEchoReflectsRequest(t *testing.T) {
|
|
s := &Server{}
|
|
req := httptest.NewRequest("POST", "/v1/echo", strings.NewReader("payload-bytes"))
|
|
req.Header.Set("X-Injected", "canary")
|
|
rr := httptest.NewRecorder()
|
|
s.httpEcho(rr, req)
|
|
|
|
var resp struct {
|
|
RequestHeadB64 string `json:"request_head_b64"`
|
|
BodyB64 string `json:"body_b64"`
|
|
BodyLen int `json:"body_len"`
|
|
Scheme string `json:"scheme"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
head, _ := base64.StdEncoding.DecodeString(resp.RequestHeadB64)
|
|
if !strings.Contains(string(head), "X-Injected: canary") {
|
|
t.Fatalf("echo did not reflect the injected header:\n%s", head)
|
|
}
|
|
body, _ := base64.StdEncoding.DecodeString(resp.BodyB64)
|
|
if string(body) != "payload-bytes" || resp.BodyLen != 13 {
|
|
t.Fatalf("body mismatch: %q len=%d", body, resp.BodyLen)
|
|
}
|
|
if resp.Scheme != "http" { // httptest requests carry no TLS
|
|
t.Fatalf("scheme = %s, want http", resp.Scheme)
|
|
}
|
|
}
|
|
|
|
func TestTLSReferenceReturnsChain(t *testing.T) {
|
|
s := &Server{PinB64: "TESTPIN", CertChain: [][]byte{{0x30, 0x82, 0x01}, {0xAA, 0xBB}}}
|
|
rr := httptest.NewRecorder()
|
|
s.tlsReference(rr, httptest.NewRequest("GET", "/v1/tls-reference", nil))
|
|
|
|
var resp struct {
|
|
PinSHA256 string `json:"pin_sha256"`
|
|
ChainDER []string `json:"chain_der"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.PinSHA256 != "TESTPIN" || len(resp.ChainDER) != 2 {
|
|
t.Fatalf("bad tls-reference: %+v", resp)
|
|
}
|
|
first, _ := base64.StdEncoding.DecodeString(resp.ChainDER[0])
|
|
if len(first) != 3 || first[0] != 0x30 {
|
|
t.Fatalf("leaf DER not round-tripped: %x", first)
|
|
}
|
|
}
|
|
|