feat(busvectors): add nkey + signed control-request vectors

Extend the cross-language vectors with the NATS user nkey derived from the
Ed25519 public key, and a signed control-plane request (CanonicalRequest +
Ed25519 signature). These let the TypeScript busauth port verify it authenticates
on both planes exactly like the Go client (issue uniweb/0001, Phase 1).
This commit is contained in:
2026-06-13 22:49:20 +02:00
parent e058b324f4
commit 0088fb946b
+58 -6
View File
@@ -35,7 +35,9 @@ import (
cs "fn-registry/functions/cybersecurity"
"github.com/enmanuel/unibus/pkg/busauth"
"github.com/enmanuel/unibus/pkg/frame"
"github.com/enmanuel/unibus/pkg/membership"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
)
@@ -58,12 +60,14 @@ var (
// is hex except the frame wire bytes, which are base64 (the frame is JSON, so the
// TS side compares the exact UTF-8 bytes).
type vectors struct {
Note string `json:"note"`
Endpoint endpointVector `json:"endpoint_id"`
Sign signVector `json:"sign"`
AEAD aeadVector `json:"aead"`
KeyBox keyboxVector `json:"keybox"`
Frame frameVector `json:"frame"`
Note string `json:"note"`
Endpoint endpointVector `json:"endpoint_id"`
Nkey nkeyVector `json:"nkey"`
Sign signVector `json:"sign"`
AEAD aeadVector `json:"aead"`
KeyBox keyboxVector `json:"keybox"`
Frame frameVector `json:"frame"`
CtrlReq controlReqVector `json:"control_request"`
}
type endpointVector struct {
@@ -71,6 +75,22 @@ type endpointVector struct {
EndpointID string `json:"endpoint_id"` // base64url(sha256(sign_pub)), unpadded
}
type nkeyVector struct {
SignPubHex string `json:"sign_pub_hex"`
NkeyPublic string `json:"nkey_public"` // NATS user nkey ("U...") from the Ed25519 pubkey
}
type controlReqVector struct {
Method string `json:"method"`
Path string `json:"path"`
Ts string `json:"ts"`
Nonce string `json:"nonce"`
BodyHex string `json:"body_hex"` // raw request body (empty for GET)
CanonicalHex string `json:"canonical_hex"` // bytes that get signed
SigHex string `json:"sig_hex"` // Ed25519 over canonical, by the signer below
SignPrivHex string `json:"sign_priv_hex"`
}
type signVector struct {
SignPrivHex string `json:"sign_priv_hex"`
SignPubHex string `json:"sign_pub_hex"`
@@ -171,6 +191,24 @@ func run(out *os.File) error {
return fmt.Errorf("marshal frame: %w", err)
}
// NATS user nkey derived from the Ed25519 public key (the browser must produce
// the same "U..." string to authenticate on the data plane).
nkeyPub, err := busauth.NkeyPublicFromSignPub(signPub)
if err != nil {
return fmt.Errorf("nkey public: %w", err)
}
// A signed control-plane request vector: the browser signs CanonicalRequest the
// same way to authenticate every HTTP call to membershipd. A POST with a body
// exercises the sha256(body) term.
const ctrlMethod = "POST"
const ctrlPath = "/rooms"
const ctrlTs = "1700000000"
const ctrlNonce = "Zm9vYmFyMTIzNDU2Nzg5MA=="
ctrlBody := []byte(`{"subject":"room.parity"}`)
canonical := membership.CanonicalRequest(ctrlMethod, ctrlPath, ctrlTs, ctrlNonce, ctrlBody)
ctrlSig := ed25519.Sign(signPriv, canonical)
v := vectors{
Note: "Deterministic cross-language vectors for the unibus protocol. Generated by " +
"cmd/busvectors in the unibus repo; regenerate with `go run ./cmd/busvectors`. " +
@@ -179,6 +217,10 @@ func run(out *os.File) error {
SignPubHex: hex.EncodeToString(signPub),
EndpointID: frame.EndpointID(signPub),
},
Nkey: nkeyVector{
SignPubHex: hex.EncodeToString(signPub),
NkeyPublic: nkeyPub,
},
Sign: signVector{
SignPrivHex: hex.EncodeToString(signPriv),
SignPubHex: hex.EncodeToString(signPub),
@@ -210,6 +252,16 @@ func run(out *os.File) error {
SigningB64: base64.StdEncoding.EncodeToString(f.SigningBytes()),
SigHex: hex.EncodeToString(f.Sig),
},
CtrlReq: controlReqVector{
Method: ctrlMethod,
Path: ctrlPath,
Ts: ctrlTs,
Nonce: ctrlNonce,
BodyHex: hex.EncodeToString(ctrlBody),
CanonicalHex: hex.EncodeToString(canonical),
SigHex: hex.EncodeToString(ctrlSig),
SignPrivHex: hex.EncodeToString(signPriv),
},
// kexPub is unused in a vector field today but derived above to validate the
// scalar; reference it so the intent is documented.
}