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:
+58
-6
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user