mirror of
https://github.com/luxfi/kms.git
synced 2026-10-30 16:23:36 +00:00
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]>
73 lines
1.9 KiB
Go
73 lines
1.9 KiB
Go
package zapclient
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
// A client with no session has no binding to sign and no key to seal
|
|
// under, so it has nothing to send. call() refuses before it builds or
|
|
// transmits anything, and it refuses unconditionally — there is no
|
|
// field, flag or environment that relaxes it. A bare Client (no node, no
|
|
// signer) exercises the check without touching the network.
|
|
func TestCall_NoSessionIsRefused(t *testing.T) {
|
|
c := &Client{} // session == nil
|
|
_, err := c.call(context.Background(), OpSecretGet, []byte(`{}`))
|
|
if err == nil {
|
|
t.Fatal("a sessionless client transmitted a secret opcode")
|
|
}
|
|
if !strings.Contains(err.Error(), "no session") {
|
|
t.Fatalf("wrong error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpcodes_MatchServer(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
op uint16
|
|
want uint16
|
|
}{
|
|
{"Get", OpSecretGet, 0x0040},
|
|
{"Put", OpSecretPut, 0x0041},
|
|
{"List", OpSecretList, 0x0042},
|
|
{"Delete", OpSecretDelete, 0x0043},
|
|
}
|
|
for _, c := range cases {
|
|
if c.op != c.want {
|
|
t.Errorf("%s = 0x%04x, want 0x%04x", c.name, c.op, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusBytes(t *testing.T) {
|
|
if statusOK != 0x00 || statusNotFound != 0x01 || statusError != 0x02 || statusForbid != 0x03 {
|
|
t.Fatalf("status bytes drift: ok=%d nf=%d err=%d forbid=%d",
|
|
statusOK, statusNotFound, statusError, statusForbid)
|
|
}
|
|
}
|
|
|
|
func TestBuildMessageWithType_Parseable(t *testing.T) {
|
|
body := []byte("hello-zap")
|
|
msg := buildMessageWithType(0x0040, body)
|
|
if msg == nil {
|
|
t.Fatal("buildMessageWithType returned nil")
|
|
}
|
|
if msg.Size() == 0 {
|
|
t.Fatal("message size is zero")
|
|
}
|
|
reparsed, err := zap.Parse(msg.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("not re-parseable: %v", err)
|
|
}
|
|
if reparsed == nil {
|
|
t.Fatal("re-parsed message is nil")
|
|
}
|
|
// Verify the flags encode our opcode
|
|
if got := reparsed.Flags() >> 8; got != 0x0040 {
|
|
t.Fatalf("flags>>8 = 0x%04x, want 0x0040", got)
|
|
}
|
|
}
|