Files
unibus/pkg/busauth/nkey.go
T
egutierrez 413dd61041 feat(busauth): Ed25519<->NATS nkey conversion with round-trip test
A NATS nkey is an Ed25519 keypair, so the bus reuses each peer's signing
identity for the data plane instead of minting new key material. ClientNkey
derives the user nkey public string and a nonce-signing callback from the
peer's Ed25519 private key (its first 32 bytes are the nkey seed);
SignPubHexFromNkey maps a presented nkey back to the allowlist's hex key;
NkeyPublicFromSignPub is the public-only derivation.

This is NATS-specific transport glue kept in the app, not promoted to the
registry, to avoid pulling nats-io/nkeys into the multi-domain registry
module. The dedicated round-trip test runs first (spec requirement): it
proves the nkey signature equals the identity's raw Ed25519 signature and
that the nkey maps back to the identity's hex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:37:46 +02:00

77 lines
3.2 KiB
Go

// Package busauth bridges a unibus peer's Ed25519 identity to NATS nkey
// authentication. A NATS nkey IS an Ed25519 keypair, so the bus reuses the
// peer's existing signing identity for the data plane instead of minting new
// key material — one identity authenticates both planes (HTTP request signatures
// and NATS connections), keyed in the user allowlist by the same Ed25519 public
// key.
//
// This is transport glue specific to NATS + unibus, not a general-purpose
// registry primitive: it deliberately lives in the app to avoid pulling
// github.com/nats-io/nkeys into the multi-domain registry module. The Ed25519
// signing/verification it relies on comes from the registry cybersecurity
// package; this package never reimplements a primitive.
package busauth
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"github.com/nats-io/nkeys"
)
// ClientNkey derives, from a peer's Ed25519 private key, the NATS user nkey
// public string ("U...") and a signature callback suitable for
// nats.Nkey(pub, sign). The callback signs the server-presented nonce with the
// same Ed25519 key, so the server can verify it and map it back to the bus user.
//
// signPriv must be a 64-byte Ed25519 private key (as produced by the registry's
// GenerateIdentity). Its first 32 bytes are the seed nkeys needs.
func ClientNkey(signPriv []byte) (pub string, sign func([]byte) ([]byte, error), err error) {
if len(signPriv) != ed25519.PrivateKeySize {
return "", nil, fmt.Errorf("busauth: signPriv must be %d bytes, got %d", ed25519.PrivateKeySize, len(signPriv))
}
seed := ed25519.PrivateKey(signPriv).Seed() // 32-byte Ed25519 seed
kp, err := nkeys.FromRawSeed(nkeys.PrefixByteUser, seed)
if err != nil {
return "", nil, fmt.Errorf("busauth: derive nkey from seed: %w", err)
}
pub, err = kp.PublicKey()
if err != nil {
return "", nil, fmt.Errorf("busauth: nkey public key: %w", err)
}
sign = func(nonce []byte) ([]byte, error) {
return kp.Sign(nonce)
}
return pub, sign, nil
}
// NkeyPublicFromSignPub derives the NATS user nkey public string from a 32-byte
// Ed25519 public key. It is the inverse view of the identity used by callers
// that have only the public key (e.g. to display or pre-register an nkey).
func NkeyPublicFromSignPub(signPub []byte) (string, error) {
if len(signPub) != ed25519.PublicKeySize {
return "", fmt.Errorf("busauth: signPub must be %d bytes, got %d", ed25519.PublicKeySize, len(signPub))
}
pub, err := nkeys.Encode(nkeys.PrefixByteUser, signPub)
if err != nil {
return "", fmt.Errorf("busauth: encode nkey public: %w", err)
}
return string(pub), nil
}
// SignPubHexFromNkey decodes a NATS user nkey public string ("U...") back to the
// lowercase hex of its 32-byte Ed25519 public key — the identity key used to
// look a peer up in the bus user allowlist. The server calls this to map the
// nkey a client presented to the users table.
func SignPubHexFromNkey(nkeyPub string) (string, error) {
raw, err := nkeys.Decode(nkeys.PrefixByteUser, []byte(nkeyPub))
if err != nil {
return "", fmt.Errorf("busauth: decode nkey %q: %w", nkeyPub, err)
}
if len(raw) != ed25519.PublicKeySize {
return "", fmt.Errorf("busauth: decoded nkey is %d bytes, want %d", len(raw), ed25519.PublicKeySize)
}
return hex.EncodeToString(raw), nil
}