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" cs "fn-registry/functions/cybersecurity"
"github.com/enmanuel/unibus/pkg/busauth"
"github.com/enmanuel/unibus/pkg/frame" "github.com/enmanuel/unibus/pkg/frame"
"github.com/enmanuel/unibus/pkg/membership"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519" "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 // is hex except the frame wire bytes, which are base64 (the frame is JSON, so the
// TS side compares the exact UTF-8 bytes). // TS side compares the exact UTF-8 bytes).
type vectors struct { type vectors struct {
Note string `json:"note"` Note string `json:"note"`
Endpoint endpointVector `json:"endpoint_id"` Endpoint endpointVector `json:"endpoint_id"`
Sign signVector `json:"sign"` Nkey nkeyVector `json:"nkey"`
AEAD aeadVector `json:"aead"` Sign signVector `json:"sign"`
KeyBox keyboxVector `json:"keybox"` AEAD aeadVector `json:"aead"`
Frame frameVector `json:"frame"` KeyBox keyboxVector `json:"keybox"`
Frame frameVector `json:"frame"`
CtrlReq controlReqVector `json:"control_request"`
} }
type endpointVector struct { type endpointVector struct {
@@ -71,6 +75,22 @@ type endpointVector struct {
EndpointID string `json:"endpoint_id"` // base64url(sha256(sign_pub)), unpadded 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 { type signVector struct {
SignPrivHex string `json:"sign_priv_hex"` SignPrivHex string `json:"sign_priv_hex"`
SignPubHex string `json:"sign_pub_hex"` SignPubHex string `json:"sign_pub_hex"`
@@ -171,6 +191,24 @@ func run(out *os.File) error {
return fmt.Errorf("marshal frame: %w", err) 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{ v := vectors{
Note: "Deterministic cross-language vectors for the unibus protocol. Generated by " + Note: "Deterministic cross-language vectors for the unibus protocol. Generated by " +
"cmd/busvectors in the unibus repo; regenerate with `go run ./cmd/busvectors`. " + "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), SignPubHex: hex.EncodeToString(signPub),
EndpointID: frame.EndpointID(signPub), EndpointID: frame.EndpointID(signPub),
}, },
Nkey: nkeyVector{
SignPubHex: hex.EncodeToString(signPub),
NkeyPublic: nkeyPub,
},
Sign: signVector{ Sign: signVector{
SignPrivHex: hex.EncodeToString(signPriv), SignPrivHex: hex.EncodeToString(signPriv),
SignPubHex: hex.EncodeToString(signPub), SignPubHex: hex.EncodeToString(signPub),
@@ -210,6 +252,16 @@ func run(out *os.File) error {
SigningB64: base64.StdEncoding.EncodeToString(f.SigningBytes()), SigningB64: base64.StdEncoding.EncodeToString(f.SigningBytes()),
SigHex: hex.EncodeToString(f.Sig), 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 // kexPub is unused in a vector field today but derived above to validate the
// scalar; reference it so the intent is documented. // scalar; reference it so the intent is documented.
} }