The module is one Go binary the runtime launches: the renderer (its instruction file held byte for byte to the TypeScript one it replaces), the credentials and identity files, the licence flow of ADR 0206 and the MCP servers in state. Keeps the TypeScript module's key files, so a node moving to it keeps its key. The npm package, its tests and its build go. Both binaries were run together under the real runtime on a test bus with postgres and a stub vendor: a login was adopted by one exchange, the node bound and handed an access token, its file left with no refresh token, and no token in either state.
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
package main
|
|
|
|
// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): the manager seals what it hands a consumer to
|
|
// the key that consumer sent, and a node seals a waiting login to the key the manager gives. The same box
|
|
// the agent module's TypeScript makes and opens, byte for byte — X25519 for the agreement, HKDF-SHA256 for
|
|
// the key, AES-256-GCM for the box — so `testdata/sealed-by-typescript.json` is opened here, and a test
|
|
// reopens what this seals with the same derivation.
|
|
//
|
|
// A box is `{ v: 1, eph, iv, tag, ct }`, every field base64; `eph` is the one-time public key as SPKI DER,
|
|
// and the key is bound to it and to the recipient's raw public key, so a box cannot be re-addressed.
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/ecdh"
|
|
"crypto/hkdf"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// SealedBox is a value sealed to one recipient.
|
|
type SealedBox struct {
|
|
V int `json:"v"`
|
|
Eph string `json:"eph"`
|
|
IV string `json:"iv"`
|
|
Tag string `json:"tag"`
|
|
Ct string `json:"ct"`
|
|
}
|
|
|
|
// KeyPair is a recipient's keypair as the two PEM strings it is kept and sent as.
|
|
type KeyPair struct {
|
|
PublicKey string `json:"publicKey"`
|
|
PrivateKey string `json:"privateKey"`
|
|
}
|
|
|
|
const sealInfo = "novox-mesh sealed box v1"
|
|
|
|
// GenerateKeyPair makes an X25519 keypair, PEM-encoded as the agent module's are.
|
|
func GenerateKeyPair() (KeyPair, error) {
|
|
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
pubDER, err := x509.MarshalPKIXPublicKey(priv.PublicKey())
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
return KeyPair{
|
|
PublicKey: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
|
|
PrivateKey: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
|
|
}, nil
|
|
}
|
|
|
|
func publicFromPEM(p string) (*ecdh.PublicKey, error) {
|
|
block, _ := pem.Decode([]byte(p))
|
|
if block == nil {
|
|
return nil, errors.New("not a PEM public key")
|
|
}
|
|
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pub, ok := k.(*ecdh.PublicKey)
|
|
if !ok || pub.Curve() != ecdh.X25519() {
|
|
return nil, errors.New("not an X25519 public key")
|
|
}
|
|
return pub, nil
|
|
}
|
|
|
|
func privateFromPEM(p string) (*ecdh.PrivateKey, error) {
|
|
block, _ := pem.Decode([]byte(p))
|
|
if block == nil {
|
|
return nil, errors.New("not a PEM private key")
|
|
}
|
|
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
priv, ok := k.(*ecdh.PrivateKey)
|
|
if !ok || priv.Curve() != ecdh.X25519() {
|
|
return nil, errors.New("not an X25519 private key")
|
|
}
|
|
return priv, nil
|
|
}
|
|
|
|
func boxKey(secret, ephDER, recipientRaw []byte) ([]byte, error) {
|
|
salt := append(append([]byte{}, ephDER...), recipientRaw...)
|
|
return hkdf.Key(sha256.New, secret, salt, sealInfo, 32)
|
|
}
|
|
|
|
// Seal seals plaintext to the recipient's public key.
|
|
func Seal(plaintext, recipientPEM string) (SealedBox, error) {
|
|
recipient, err := publicFromPEM(recipientPEM)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
secret, err := eph.ECDH(recipient)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
ephDER, err := x509.MarshalPKIXPublicKey(eph.PublicKey())
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
key, err := boxKey(secret, ephDER, recipient.Bytes())
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
gcm, err := newGCM(key)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
iv := make([]byte, 12)
|
|
if _, err := rand.Read(iv); err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
out := gcm.Seal(nil, iv, []byte(plaintext), nil)
|
|
ct, tag := out[:len(out)-gcm.Overhead()], out[len(out)-gcm.Overhead():]
|
|
b64 := base64.StdEncoding.EncodeToString
|
|
return SealedBox{V: 1, Eph: b64(ephDER), IV: b64(iv), Tag: b64(tag), Ct: b64(ct)}, nil
|
|
}
|
|
|
|
// Open opens a box with the recipient's private key; it fails for a box to another key or one tampered with.
|
|
func Open(box SealedBox, privatePEM string) (string, error) {
|
|
if box.V != 1 {
|
|
return "", errors.New("not a sealed box this module can open")
|
|
}
|
|
priv, err := privateFromPEM(privatePEM)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
d := base64.StdEncoding.DecodeString
|
|
ephDER, err := d(box.Eph)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the box's eph: %w", err)
|
|
}
|
|
ephKey, err := x509.ParsePKIXPublicKey(ephDER)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
eph, ok := ephKey.(*ecdh.PublicKey)
|
|
if !ok {
|
|
return "", errors.New("the box's eph is not an X25519 key")
|
|
}
|
|
secret, err := priv.ECDH(eph)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key, err := boxKey(secret, ephDER, priv.PublicKey().Bytes())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
iv, err1 := d(box.IV)
|
|
tag, err2 := d(box.Tag)
|
|
ct, err3 := d(box.Ct)
|
|
if err := errors.Join(err1, err2, err3); err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := newGCM(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
plain, err := gcm.Open(nil, iv, append(ct, tag...), nil)
|
|
if err != nil {
|
|
return "", errors.New("the box does not open with this key")
|
|
}
|
|
return string(plain), nil
|
|
}
|
|
|
|
func newGCM(key []byte) (cipher.AEAD, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cipher.NewGCM(block)
|
|
}
|