Files
zeekayandHanzo Dev a20ace0dd0 a request names the channel it is for, and is honoured only there
Key agreement gives two endpoints a private channel. It does not tell
either of them who is on the far end, so a signature on a request proves
who wrote it and not who they wrote it to. Anyone able to agree keys with
both sides can therefore carry a signed request from one channel to
another and be served on it.

The handshake now derives a second value alongside the session key, under
its own label: a binding that names the channel. Only the two endpoints
that ran that handshake can compute it. The caller signs it into every
envelope; the peer checks it against its own side of the channel the
envelope arrived on. A request written for one channel is inert on any
other — it cannot be re-signed without the caller's key, and a channel
cannot be made to produce a name it did not derive.

It is not optional anywhere it could be skipped:

  - NewSession takes the handshake result, so a session always carries
    the binding derived with its key.
  - envelope.Build requires one, so an envelope addressed to nobody
    cannot be signed; Parse requires one, so it cannot be read.
  - envelope.Verify takes the arriving channel as a parameter, so there
    is no way to verify without saying where the request came from.
  - Both transports supply their own: the ZAP wire its session's, the
    HTTP surface the TLS exporter. A connection with no keys of its own
    has no binding and can carry no envelope.

zapclient loses SkipHandshake and RequireSession. With the binding
required, a client without a session has nothing to sign and cannot
speak, so the dial either agrees keys or fails, and a peer that answers
classical-only when ML-KEM-768 was offered is refused outright rather
than by configuration. VerifyEnvelope goes with them: a verify entry
that checked neither the ledger nor the channel had no callers and no
business existing.

Tests: an attacker that terminates and re-originates both halves of the
handshake, built from the shipping client and server, is refused — both
when it forwards the envelope verbatim and when it rewrites the name to
its own. A caller reaching the KMS directly still gets its secret.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 04:49:47 -07:00

590 lines
21 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// http_test.go — the HTTP /v1/sdk surface. These tests drive the real
// http.Handler end-to-end (httptest), so envelope verification,
// consensus authorization, the nonce ledger, and the secret-store /
// sign-backend dispatch all run exactly as they do in production. The
// only in-memory piece is the authority snapshot (a StaticAuthority
// provider) and, for sign/verify, a recorder SignBackend that records
// delegation WITHOUT faking threshold crypto.
package zapserver
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"math/big"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
"github.com/luxfi/kms/pkg/envelope"
"github.com/luxfi/kms/pkg/store"
"github.com/luxfi/log"
badger "github.com/luxfi/zapdb"
)
// httpTestClock is the wall-clock the test server pins. Envelopes sign
// under the same value so freshness passes unless a test deliberately
// skews.
var httpTestClock = time.Unix(1_717_200_000, 0)
// recorderSigner is a SignBackend test double. It RECORDS every
// delegation so a test can assert the KMS handed the backend the right
// (validatorID, keyType, message) — it does NOT fake threshold math. The
// real t-of-n signature correctness lives in luxfi/mpc; this proves only
// the KMS-side auth + dispatch contract.
type recorderSigner struct {
mu sync.Mutex
signCalls []signCall
verifyCalls []signCall
verifyRet bool
}
type signCall struct {
validatorID string
keyType string
msg []byte
sig []byte
}
func (r *recorderSigner) Sign(_ context.Context, validatorID, keyType string, msg []byte) (SignResult, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.signCalls = append(r.signCalls, signCall{validatorID: validatorID, keyType: keyType, msg: append([]byte(nil), msg...)})
return SignResult{Signature: "sig:" + validatorID + ":" + keyType}, nil
}
func (r *recorderSigner) Verify(_ context.Context, validatorID, keyType string, msg, sig []byte) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.verifyCalls = append(r.verifyCalls, signCall{validatorID: validatorID, keyType: keyType, msg: append([]byte(nil), msg...), sig: append([]byte(nil), sig...)})
return r.verifyRet, nil
}
func (r *recorderSigner) signCount() int {
r.mu.Lock()
defer r.mu.Unlock()
return len(r.signCalls)
}
// newHTTPServer wires a Server with an in-memory SecretStore and an
// optional signer, returning the server and its /v1/sdk handler.
func newHTTPServer(t *testing.T, validators, operators []ids.NodeID, signer SignBackend) (*Server, http.Handler) {
t.Helper()
opts := badger.DefaultOptions("").WithInMemory(true)
db, err := badger.Open(opts)
if err != nil {
t.Fatalf("open zapdb: %v", err)
}
t.Cleanup(func() { db.Close() })
mk := make([]byte, 32)
for i := range mk {
mk[i] = byte(i + 1)
}
authz, err := NewInProcessAuthorizer(InProcessAuthorizerConfig{
Validators: NewStaticAuthorityProvider(validators),
Operator: NewStaticAuthorityProvider(operators),
})
if err != nil {
t.Fatalf("authorizer: %v", err)
}
srv := New(Config{
Store: store.NewSecretStore(db),
MasterKey: mk,
Authorizer: authz,
Signer: signer,
Logger: log.NewNoOpLogger(),
Now: func() time.Time { return httpTestClock },
})
return srv, srv.HTTPHandler()
}
// tlsChannel runs a real TLS handshake over an in-memory pipe and hands
// back the server's view of it. Both ends of that handshake are the
// test, so the exporter it yields is the same value a real client on the
// far end would compute — which is what lets these tests address an
// envelope to the connection they are about to send it on.
func tlsChannel(t *testing.T) *tls.ConnectionState {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("gen key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "kms-test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
DNSNames: []string{"kms-test"},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv)
if err != nil {
t.Fatalf("cert: %v", err)
}
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv}
cliPipe, srvPipe := net.Pipe()
t.Cleanup(func() { cliPipe.Close(); srvPipe.Close() })
srvConn := tls.Server(srvPipe, &tls.Config{Certificates: []tls.Certificate{cert}})
cliConn := tls.Client(cliPipe, &tls.Config{InsecureSkipVerify: true, ServerName: "kms-test"})
done := make(chan error, 1)
go func() { done <- srvConn.Handshake() }()
if err := cliConn.Handshake(); err != nil {
t.Fatalf("client handshake: %v", err)
}
if err := <-done; err != nil {
t.Fatalf("server handshake: %v", err)
}
state := srvConn.ConnectionState()
return &state
}
// bindOf is the binding the KMS derives for a connection — the same one
// the caller must name in its envelope.
func bindOf(t *testing.T, state *tls.ConnectionState) []byte {
t.Helper()
b, err := state.ExportKeyingMaterial(bindLabel, nil, envelope.BindSize)
if err != nil {
t.Fatalf("exporter: %v", err)
}
return b
}
// do issues a signed-envelope POST to /v1/sdk/secrets over a TLS
// connection, with the envelope addressed to that connection, and
// returns the recorder.
func do(t *testing.T, h http.Handler, ident *keys.ServiceIdentity, op uint16, inner any, nonce string, now time.Time) *httptest.ResponseRecorder {
t.Helper()
state := tlsChannel(t)
raw := envelopeFor(t, ident, op, buildInner(t, inner), now, nonce, bindOf(t, state))
return send(t, h, raw, state)
}
// send posts already-built envelope bytes over the given connection.
// Tests that send twice reuse one connection, which is what a replay
// actually looks like.
func send(t *testing.T, h http.Handler, raw []byte, state *tls.ConnectionState) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/v1/sdk/secrets", bytes.NewReader(raw))
req.TLS = state
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
// seedHTTP puts a sealed secret directly into the server's store.
func seedHTTP(t *testing.T, s *Server, path, name, env, value string) {
t.Helper()
sec, err := store.Seal(s.masterKey, path, name, env, []byte(value))
if err != nil {
t.Fatalf("seal: %v", err)
}
if err := s.store.Put(sec); err != nil {
t.Fatalf("put: %v", err)
}
}
// ---- secret plane ----
func TestHTTP_ValidatorGet_200(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
srv, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
seedHTTP(t, srv, "hanzo/auto", "api-key", "prod", "sk_live_42")
rec := do(t, h, ident, OpSecretGet, getReq{Path: "hanzo/auto", Name: "api-key", Env: "prod"}, "n1", httpTestClock)
if rec.Code != http.StatusOK {
t.Fatalf("code=%d body=%s", rec.Code, rec.Body.String())
}
var out getResp
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode: %v", err)
}
pt, err := base64.StdEncoding.DecodeString(out.Value)
if err != nil {
t.Fatalf("b64: %v", err)
}
if string(pt) != "sk_live_42" {
t.Fatalf("value=%q want sk_live_42", pt)
}
}
func TestHTTP_NonValidator_403(t *testing.T) {
stranger := newIdentity(t, "stranger/svc")
defer stranger.Wipe()
known := newIdentity(t, "hanzo/auto")
defer known.Wipe()
srv, h := newHTTPServer(t, []ids.NodeID{known.NodeID}, nil, nil)
seedHTTP(t, srv, "hanzo/auto", "api-key", "prod", "v")
rec := do(t, h, stranger, OpSecretGet, getReq{Path: "hanzo/auto", Name: "api-key", Env: "prod"}, "n1", httpTestClock)
if rec.Code != http.StatusForbidden {
t.Fatalf("stranger Get code=%d want 403 body=%s", rec.Code, rec.Body.String())
}
}
func TestHTTP_ValidatorWrite_403(t *testing.T) {
// A validator that is NOT in the operator set may read but not write.
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
put := putReq{Path: "hanzo/auto", Name: "k", Env: "prod", Value: base64.StdEncoding.EncodeToString([]byte("v"))}
rec := do(t, h, ident, OpSecretPut, put, "n1", httpTestClock)
if rec.Code != http.StatusForbidden {
t.Fatalf("validator Put code=%d want 403", rec.Code)
}
del := delReq{Path: "hanzo/auto", Name: "k", Env: "prod"}
rec = do(t, h, ident, OpSecretDelete, del, "n2", httpTestClock)
if rec.Code != http.StatusForbidden {
t.Fatalf("validator Delete code=%d want 403", rec.Code)
}
}
func TestHTTP_OperatorPut_Then_Get(t *testing.T) {
op := newIdentity(t, "hanzo/kms-operator")
defer op.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{op.NodeID}, []ids.NodeID{op.NodeID}, nil)
put := putReq{Path: "hanzo/commerce", Name: "api-key", Env: "prod", Value: base64.StdEncoding.EncodeToString([]byte("sk_live_xxx"))}
rec := do(t, h, op, OpSecretPut, put, "n1", httpTestClock)
if rec.Code != http.StatusOK {
t.Fatalf("operator Put code=%d body=%s", rec.Code, rec.Body.String())
}
rec = do(t, h, op, OpSecretGet, getReq{Path: "hanzo/commerce", Name: "api-key", Env: "prod"}, "n2", httpTestClock)
if rec.Code != http.StatusOK {
t.Fatalf("operator Get code=%d", rec.Code)
}
var out getResp
_ = json.Unmarshal(rec.Body.Bytes(), &out)
pt, _ := base64.StdEncoding.DecodeString(out.Value)
if string(pt) != "sk_live_xxx" {
t.Fatalf("round-trip value=%q", pt)
}
}
// Rotate is not a separate opcode: rotating a secret value is an
// operator Put (upsert). This pins that contract — a second Put replaces
// the value and a Get returns the latest.
func TestHTTP_Rotate_IsUpsertPut(t *testing.T) {
op := newIdentity(t, "hanzo/kms-operator")
defer op.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{op.NodeID}, []ids.NodeID{op.NodeID}, nil)
for i, val := range []string{"v1", "v2"} {
put := putReq{Path: "p", Name: "k", Env: "prod", Value: base64.StdEncoding.EncodeToString([]byte(val))}
rec := do(t, h, op, OpSecretPut, put, "put-"+val, httpTestClock)
if rec.Code != http.StatusOK {
t.Fatalf("put[%d] code=%d", i, rec.Code)
}
}
rec := do(t, h, op, OpSecretGet, getReq{Path: "p", Name: "k", Env: "prod"}, "get", httpTestClock)
var out getResp
_ = json.Unmarshal(rec.Body.Bytes(), &out)
pt, _ := base64.StdEncoding.DecodeString(out.Value)
if string(pt) != "v2" {
t.Fatalf("rotate: got %q want v2", pt)
}
}
func TestHTTP_GetMissing_404(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
rec := do(t, h, ident, OpSecretGet, getReq{Path: "hanzo/auto", Name: "nope", Env: "prod"}, "n1", httpTestClock)
if rec.Code != http.StatusNotFound {
t.Fatalf("missing Get code=%d want 404", rec.Code)
}
}
func TestHTTP_Replay_403(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
srv, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
seedHTTP(t, srv, "hanzo/auto", "api-key", "prod", "v")
state := tlsChannel(t)
raw := envelopeFor(t, ident, OpSecretGet, buildInner(t, getReq{Path: "hanzo/auto", Name: "api-key", Env: "prod"}), httpTestClock, "replay-nonce", bindOf(t, state))
first := send(t, h, raw, state)
if first.Code != http.StatusOK {
t.Fatalf("first code=%d body=%s", first.Code, first.Body.String())
}
second := send(t, h, raw, state)
if second.Code != http.StatusForbidden {
t.Fatalf("replay code=%d want 403", second.Code)
}
// Replay must be masked as a generic "forbidden" — no ledger probe.
var e map[string]string
_ = json.Unmarshal(second.Body.Bytes(), &e)
if e["error"] != "forbidden" {
t.Fatalf("replay body=%q want generic forbidden", second.Body.String())
}
}
func TestHTTP_Stale_403(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
// Sign 10 minutes in the past; server clock is httpTestClock.
old := httpTestClock.Add(-10 * time.Minute)
rec := do(t, h, ident, OpSecretGet, getReq{Path: "hanzo/auto", Name: "k", Env: "prod"}, "stale", old)
if rec.Code != http.StatusForbidden {
t.Fatalf("stale code=%d want 403", rec.Code)
}
}
func TestHTTP_Malformed_400(t *testing.T) {
_, h := newHTTPServer(t, nil, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/sdk/secrets", strings.NewReader("{not json"))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("malformed code=%d want 400", rec.Code)
}
}
func TestHTTP_Tampered_403(t *testing.T) {
ident := newIdentity(t, "hanzo/kms-operator")
defer ident.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, []ids.NodeID{ident.NodeID}, nil)
raw := signedEnvelopeBytes(t, ident, OpSecretPut, buildInner(t, putReq{
Path: "p", Name: "k", Env: "prod", Value: base64.StdEncoding.EncodeToString([]byte("ok")),
}), httpTestClock, "tamper")
// Swap the inner value AFTER signing; signature no longer covers it.
var env Envelope
if err := json.Unmarshal(raw, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
env.Req = buildInner(t, putReq{Path: "p", Name: "k", Env: "prod", Value: base64.StdEncoding.EncodeToString([]byte("EVIL"))})
tampered, _ := json.Marshal(env)
req := httptest.NewRequest(http.MethodPost, "/v1/sdk/secrets", bytes.NewReader(tampered))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("tampered code=%d want 403", rec.Code)
}
}
func TestHTTP_Oversize_413(t *testing.T) {
_, h := newHTTPServer(t, nil, nil, nil)
big := bytes.Repeat([]byte("A"), MaxEnvelopeBytes+2)
req := httptest.NewRequest(http.MethodPost, "/v1/sdk/secrets", bytes.NewReader(big))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("oversize code=%d want 413", rec.Code)
}
}
func TestHTTP_UnknownOpcode_403(t *testing.T) {
// A validly-signed envelope carrying an opcode the authorizer does
// not recognise is denied at authorization (not dispatched).
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, []ids.NodeID{ident.NodeID}, nil)
rec := do(t, h, ident, 0x9999, map[string]string{"path": "p"}, "n1", httpTestClock)
if rec.Code != http.StatusForbidden {
t.Fatalf("unknown op code=%d want 403", rec.Code)
}
}
// ---- threshold sign / verify plane ----
func TestHTTP_Sign_OperatorDelegates(t *testing.T) {
op := newIdentity(t, "hanzo/kms-operator")
defer op.Wipe()
rec := &recorderSigner{}
_, h := newHTTPServer(t, []ids.NodeID{op.NodeID}, []ids.NodeID{op.NodeID}, rec)
msg := []byte("block-header-bytes")
req := signReq{ValidatorID: "val-1", KeyType: "bls", Message: base64.StdEncoding.EncodeToString(msg)}
resp := do(t, h, op, OpSign, req, "n1", httpTestClock)
if resp.Code != http.StatusOK {
t.Fatalf("operator Sign code=%d body=%s", resp.Code, resp.Body.String())
}
if rec.signCount() != 1 {
t.Fatalf("backend sign calls=%d want 1", rec.signCount())
}
got := rec.signCalls[0]
if got.validatorID != "val-1" || got.keyType != "bls" || !bytes.Equal(got.msg, msg) {
t.Fatalf("delegated wrong params: %+v", got)
}
var out SignResult
_ = json.Unmarshal(resp.Body.Bytes(), &out)
if out.Signature != "sig:val-1:bls" {
t.Fatalf("sig=%q", out.Signature)
}
}
func TestHTTP_Sign_Validator_403_NoDelegation(t *testing.T) {
// A read-only validator (not in operator set) must NOT be able to
// sign, and the backend must NEVER be reached.
val := newIdentity(t, "hanzo/auto")
defer val.Wipe()
rec := &recorderSigner{}
_, h := newHTTPServer(t, []ids.NodeID{val.NodeID}, nil, rec)
req := signReq{ValidatorID: "val-1", KeyType: "bls", Message: base64.StdEncoding.EncodeToString([]byte("m"))}
resp := do(t, h, val, OpSign, req, "n1", httpTestClock)
if resp.Code != http.StatusForbidden {
t.Fatalf("validator Sign code=%d want 403", resp.Code)
}
if rec.signCount() != 0 {
t.Fatalf("backend was called %d times on a forbidden sign — auth must gate BEFORE dispatch", rec.signCount())
}
}
func TestHTTP_Sign_Replay_403(t *testing.T) {
op := newIdentity(t, "hanzo/kms-operator")
defer op.Wipe()
rec := &recorderSigner{}
_, h := newHTTPServer(t, []ids.NodeID{op.NodeID}, []ids.NodeID{op.NodeID}, rec)
state := tlsChannel(t)
raw := envelopeFor(t, op, OpSign, buildInner(t, signReq{
ValidatorID: "val-1", KeyType: "corona", Message: base64.StdEncoding.EncodeToString([]byte("m")),
}), httpTestClock, "sign-replay", bindOf(t, state))
r1 := send(t, h, raw, state)
if r1.Code != http.StatusOK {
t.Fatalf("first sign code=%d", r1.Code)
}
r2 := send(t, h, raw, state)
if r2.Code != http.StatusForbidden {
t.Fatalf("replayed sign code=%d want 403", r2.Code)
}
// The replayed sign must not have reached the backend a second time.
if rec.signCount() != 1 {
t.Fatalf("backend sign calls=%d want 1 (replay must be blocked before dispatch)", rec.signCount())
}
}
func TestHTTP_Sign_NotConfigured_400(t *testing.T) {
// Operator auth passes but no SignBackend is wired → statusError → 400.
op := newIdentity(t, "hanzo/kms-operator")
defer op.Wipe()
_, h := newHTTPServer(t, []ids.NodeID{op.NodeID}, []ids.NodeID{op.NodeID}, nil)
req := signReq{ValidatorID: "val-1", KeyType: "bls", Message: base64.StdEncoding.EncodeToString([]byte("m"))}
resp := do(t, h, op, OpSign, req, "n1", httpTestClock)
if resp.Code != http.StatusBadRequest {
t.Fatalf("unconfigured Sign code=%d want 400", resp.Code)
}
if !strings.Contains(resp.Body.String(), "signing not configured") {
t.Fatalf("body=%s", resp.Body.String())
}
}
func TestHTTP_Verify_Validator_200(t *testing.T) {
// Verify is a READ op — any validator may call it.
val := newIdentity(t, "hanzo/auto")
defer val.Wipe()
rec := &recorderSigner{verifyRet: true}
_, h := newHTTPServer(t, []ids.NodeID{val.NodeID}, nil, rec)
req := verifyReq{
ValidatorID: "val-1", KeyType: "corona",
Message: base64.StdEncoding.EncodeToString([]byte("m")),
Signature: base64.StdEncoding.EncodeToString([]byte("s")),
}
resp := do(t, h, val, OpVerify, req, "n1", httpTestClock)
if resp.Code != http.StatusOK {
t.Fatalf("validator Verify code=%d body=%s", resp.Code, resp.Body.String())
}
var out map[string]bool
_ = json.Unmarshal(resp.Body.Bytes(), &out)
if !out["valid"] {
t.Fatalf("verify body=%s want valid:true", resp.Body.String())
}
if len(rec.verifyCalls) != 1 {
t.Fatalf("verify calls=%d want 1", len(rec.verifyCalls))
}
}
func TestHTTP_Verify_NonValidator_403(t *testing.T) {
stranger := newIdentity(t, "stranger/svc")
defer stranger.Wipe()
known := newIdentity(t, "hanzo/auto")
defer known.Wipe()
rec := &recorderSigner{verifyRet: true}
_, h := newHTTPServer(t, []ids.NodeID{known.NodeID}, nil, rec)
req := verifyReq{ValidatorID: "val-1", KeyType: "bls", Message: "", Signature: ""}
resp := do(t, h, stranger, OpVerify, req, "n1", httpTestClock)
if resp.Code != http.StatusForbidden {
t.Fatalf("stranger Verify code=%d want 403", resp.Code)
}
if len(rec.verifyCalls) != 0 {
t.Fatalf("backend verify reached on forbidden request")
}
}
// An envelope that reaches this surface over a connection with no keys
// of its own is refused. Without that, the HTTP door would launder any
// envelope an on-path attacker lifted off the ZAP wire: it would arrive
// signed, fresh, unreplayed, and addressed to somewhere else entirely.
func TestHTTPWithoutAChannelIsRefused(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
srv, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
seedHTTP(t, srv, "hanzo/auto", "api-key", "prod", "the-secret")
state := tlsChannel(t)
raw := envelopeFor(t, ident, OpSecretGet,
buildInner(t, getReq{Path: "hanzo/auto", Name: "api-key", Env: "prod"}),
httpTestClock, "no-channel", bindOf(t, state))
// Same bytes, presented on nothing.
rec := send(t, h, raw, nil)
if rec.Code != http.StatusForbidden {
t.Fatalf("code=%d want 403 body=%s", rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), "the-secret") {
t.Fatalf("the secret came back: %s", rec.Body.String())
}
}
// The same, one step subtler: a real TLS connection, but not the one the
// envelope was written for.
func TestHTTPOnAnotherChannelIsRefused(t *testing.T) {
ident := newIdentity(t, "hanzo/auto")
defer ident.Wipe()
srv, h := newHTTPServer(t, []ids.NodeID{ident.NodeID}, nil, nil)
seedHTTP(t, srv, "hanzo/auto", "api-key", "prod", "the-secret")
mine, theirs := tlsChannel(t), tlsChannel(t)
if bytes.Equal(bindOf(t, mine), bindOf(t, theirs)) {
t.Fatal("two TLS connections produced the same binding")
}
raw := envelopeFor(t, ident, OpSecretGet,
buildInner(t, getReq{Path: "hanzo/auto", Name: "api-key", Env: "prod"}),
httpTestClock, "other-channel", bindOf(t, mine))
rec := send(t, h, raw, theirs)
if rec.Code != http.StatusForbidden {
t.Fatalf("code=%d want 403 body=%s", rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), "the-secret") {
t.Fatalf("the secret came back: %s", rec.Body.String())
}
}