claude-licence-manager holds the anthropic-licence-manager seat: it reads every node's holdings state, adopts a login it does not hold by refreshing it (newest first, once per account), keeps each grant alive under a lease, publishes what each consumer should hold as its bindings state with a generation, and answers current sealed to the consumer's key. Postgres store prepared by a run-once step; grants encrypted with the vault's key. claude-code reports what its node holds (fingerprints and account, never a token), hands its grant over only when the manager asks, watches its binding and fetches the token on a newer generation, and writes access-token-only. Its ask now reads the runtime's answer as a value and addresses seats as seats.
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)
|
|
}
|