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]>
100 lines
3.7 KiB
Go
100 lines
3.7 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// auth.go — envelope-signature verification for ZAP secret opcodes.
|
|
//
|
|
// The envelope shape lives in pkg/envelope so the client and server
|
|
// share one source of truth for the wire format. This file is a thin
|
|
// re-export so package consumers don't need to know about the split.
|
|
//
|
|
// Every secret-opcode request rides an Envelope: the JSON payload plus
|
|
// the signer's typed NodeID + 48-byte commitment + ML-DSA-65
|
|
// signature. The verifier:
|
|
//
|
|
// 1. Parses the envelope from the inbound frame.
|
|
// 2. Reconstructs the signed digest from (path, name, env, op,
|
|
// timestamp, nonce) and verifies the signature against the
|
|
// envelope's bound public key.
|
|
// 3. Returns the verified Identity to the dispatcher; authorization
|
|
// (consensus-native) runs separately.
|
|
//
|
|
// There is NO ACL. Authority lives in consensus (consensus_auth.go).
|
|
// If the envelope is missing or fails verification, the request is
|
|
// rejected at the wire layer with statusForbid.
|
|
|
|
package zapserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/luxfi/keys"
|
|
"github.com/luxfi/kms/pkg/envelope"
|
|
)
|
|
|
|
// Envelope re-exports the canonical wire shape.
|
|
type Envelope = envelope.Envelope
|
|
|
|
// EnvelopeIdentity re-exports the inner identity block.
|
|
type EnvelopeIdentity = envelope.Identity
|
|
|
|
// EnvelopeVersion re-exports the canonical version constant.
|
|
const EnvelopeVersion = envelope.Version
|
|
|
|
// EnvelopeMaxClockSkew re-exports the canonical freshness window.
|
|
const EnvelopeMaxClockSkew = envelope.MaxClockSkew
|
|
|
|
// NonceLedger re-exports the envelope-side anti-replay ledger interface
|
|
// so kmsd boot can wire one without importing pkg/envelope directly.
|
|
type NonceLedger = envelope.NonceLedger
|
|
|
|
// MemoryNonceLedger re-exports the canonical in-process ledger impl.
|
|
type MemoryNonceLedger = envelope.MemoryNonceLedger
|
|
|
|
// MemoryNonceLedgerConfig re-exports the ledger config struct.
|
|
type MemoryNonceLedgerConfig = envelope.MemoryNonceLedgerConfig
|
|
|
|
// NewMemoryNonceLedger constructs an in-process ledger. Production kmsd
|
|
// wires exactly one of these at boot and hands it to Server.Config.
|
|
func NewMemoryNonceLedger(cfg MemoryNonceLedgerConfig) *MemoryNonceLedger {
|
|
return envelope.NewMemoryNonceLedger(cfg)
|
|
}
|
|
|
|
// ErrEnvelopeReplay re-exports envelope.ErrReplay so handlers can match
|
|
// the structured replay-detected outcome without importing pkg/envelope.
|
|
var ErrEnvelopeReplay = envelope.ErrReplay
|
|
|
|
// ParseEnvelope re-exports envelope.Parse.
|
|
func ParseEnvelope(raw []byte) (*Envelope, error) {
|
|
return envelope.Parse(raw)
|
|
}
|
|
|
|
// verifyEnvelopeWithLedger is the canonical wire-side verify. The Server
|
|
// constructs a VerifierWithLedger at boot and routes every envelope
|
|
// through this method. Returns ErrEnvelopeReplay on duplicate nonce.
|
|
func verifyEnvelopeWithLedger(ctx context.Context, v *envelope.VerifierWithLedger, env *Envelope, now time.Time, bind []byte) (Identity, error) {
|
|
ident, err := v.Verify(ctx, env, now, bind)
|
|
if err != nil {
|
|
return Identity{}, err
|
|
}
|
|
return Identity{
|
|
NodeID: ident.NodeID,
|
|
FullDigest: ident.FullDigest,
|
|
ServicePath: ident.ServicePath,
|
|
}, nil
|
|
}
|
|
|
|
// BuildEnvelope is the canonical helper for callers that hold a
|
|
// *keys.ServiceIdentity. Lifts the public identity into the envelope
|
|
// header and delegates to envelope.Build.
|
|
func BuildEnvelope(ident *keys.ServiceIdentity, op uint16, req json.RawMessage, nonce string, bind []byte, now time.Time) (*Envelope, error) {
|
|
hdr := envelope.IdentityHeader{
|
|
NodeID: ident.NodeID,
|
|
FullDigest: ident.FullDigest,
|
|
ServicePath: ident.ServicePath,
|
|
PublicKey: ident.PublicKey,
|
|
}
|
|
return envelope.Build(hdr, ident, op, req, nonce, bind, now)
|
|
}
|