Necropolis v1 release

This commit is contained in:
2026-07-07 04:37:29 +01:00
commit cf9c51a3df
69 changed files with 15056 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
// Cryptographic key types, generation, loading, and box encryption helpers. Uses Ed25519
// for signing and NaCl box (Curve25519) for payload encryption.
package cryptography
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
)
// KeyPair holds an Ed25519 private/public keypair used for libp2p identity and signing.
type KeyPair struct {
PrivateKey crypto.PrivKey
PublicKey crypto.PubKey
}
// BoxKeyPair holds a NaCl box (Curve25519) keypair for anonymous sealed-box encryption.
type BoxKeyPair struct {
PrivateKey *[32]byte
PublicKey *[32]byte
}
// OperatorKey bundles the Ed25519 identity, NaCl box keys for message decryption,
// and a 32-byte auth token shared with implants at build time.
type OperatorKey struct {
KeyPair
PeerID peer.ID
BoxKeys *BoxKeyPair
AuthToken []byte
}
// ImplantKey bundles the Ed25519 identity and stores the operator's public key for
// verifying incoming command signatures.
type ImplantKey struct {
KeyPair
PeerID peer.ID
OperatorPubKey crypto.PubKey
}
// GenerateBoxKey creates a fresh NaCl box keypair for anonymous sealed-box encryption.
func GenerateBoxKey() (*BoxKeyPair, error) {
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate box key: %w", err)
}
return &BoxKeyPair{PrivateKey: priv, PublicKey: pub}, nil
}
// LoadBoxKey derives a box keypair from a 32-byte private key seed.
func LoadBoxKey(data []byte) *BoxKeyPair {
var priv [32]byte
copy(priv[:], data)
pub, err := curve25519.X25519(priv[:], curve25519.Basepoint)
if err != nil {
return nil
}
var pubArr [32]byte
copy(pubArr[:], pub)
return &BoxKeyPair{PrivateKey: &priv, PublicKey: &pubArr}
}
// Marshal returns the box private key as 32 raw bytes.
func (kp *BoxKeyPair) Marshal() []byte {
return kp.PrivateKey[:]
}
// GenerateOperatorKey creates a new Ed25519 identity, box keypair, and 32-byte auth token.
func GenerateOperatorKey() (*OperatorKey, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate operator key: %w", err)
}
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from pubkey: %w", err)
}
boxKeys, err := GenerateBoxKey()
if err != nil {
return nil, fmt.Errorf("generate box key: %w", err)
}
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return nil, fmt.Errorf("generate auth token: %w", err)
}
return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
BoxKeys: boxKeys,
AuthToken: token,
}, nil
}
// GenerateImplantKey creates a fresh Ed25519 keypair and stores a reference to the
// operator's public key for signature verification.
func GenerateImplantKey(operatorPub crypto.PubKey) (*ImplantKey, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate implant key: %w", err)
}
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from pubkey: %w", err)
}
return &ImplantKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
OperatorPubKey: operatorPub,
}, nil
}
// LoadPrivateKey deserializes an Ed25519 private key from protobuf wire format.
func LoadPrivateKey(data []byte) (crypto.PrivKey, error) {
return crypto.UnmarshalPrivateKey(data)
}
// MarshalPrivateKey serializes a private key to protobuf wire format.
func MarshalPrivateKey(priv crypto.PrivKey) ([]byte, error) {
return crypto.MarshalPrivateKey(priv)
}
// PubKeyFromBytes deserializes a public key from protobuf wire format.
func PubKeyFromBytes(data []byte) (crypto.PubKey, error) {
return crypto.UnmarshalPublicKey(data)
}
// Verify checks an Ed25519 signature against the given public key and data.
func Verify(pub crypto.PubKey, data []byte, sig []byte) (bool, error) {
return pub.Verify(data, sig)
}
// BoxKeyPath returns the path to the operator's box private key file.
func BoxKeyPath(dir string) string {
return filepath.Join(dir, "operator.boxkey")
}
// BoxPubKeyPath returns the path to the operator's box public key file.
func BoxPubKeyPath(dir string) string {
return filepath.Join(dir, "operator.boxpub")
}
// AuthTokenPath returns the path to the operator's auth token file.
func AuthTokenPath(dir string) string {
return filepath.Join(dir, "operator.authtoken")
}
// LoadOrGenerateOperatorKey tries to load an existing key pair from disk; if not found,
// generates a new one and persists it alongside the box keypair and auth token.
func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("create key directory: %w", err)
}
data, err := os.ReadFile(path)
if err == nil {
priv, err := LoadPrivateKey(data)
if err != nil {
return nil, fmt.Errorf("load private key from %s: %w", path, err)
}
pub := priv.GetPublic()
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from loaded key: %w", err)
}
boxPrivData, boxErr := os.ReadFile(BoxKeyPath(dir))
var boxKeys *BoxKeyPair
if boxErr == nil && len(boxPrivData) == 32 {
boxKeys = LoadBoxKey(boxPrivData)
}
if boxKeys == nil {
boxKeys, boxErr = GenerateBoxKey()
if boxErr == nil {
os.WriteFile(BoxKeyPath(dir), boxKeys.Marshal(), 0600)
os.WriteFile(BoxPubKeyPath(dir), boxKeys.BoxPubKeyBytes(), 0644)
}
}
tokenData, tokenErr := os.ReadFile(AuthTokenPath(dir))
var authToken []byte
if tokenErr == nil && len(tokenData) == 32 {
authToken = tokenData
} else {
authToken = make([]byte, 32)
if _, err := rand.Read(authToken); err != nil {
return nil, fmt.Errorf("generate auth token: %w", err)
}
os.WriteFile(AuthTokenPath(dir), authToken, 0600)
}
return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
BoxKeys: boxKeys,
AuthToken: authToken,
}, nil
}
if !os.IsNotExist(err) {
return nil, fmt.Errorf("read key file %s: %w", path, err)
}
key, err := GenerateOperatorKey()
if err != nil {
return nil, err
}
marshaled, err := MarshalPrivateKey(key.PrivateKey)
if err != nil {
return nil, fmt.Errorf("marshal key: %w", err)
}
if err := os.WriteFile(path, marshaled, 0600); err != nil {
return nil, fmt.Errorf("write key to %s: %w", path, err)
}
if err := os.WriteFile(BoxKeyPath(dir), key.BoxKeys.Marshal(), 0600); err != nil {
return nil, fmt.Errorf("write box key: %w", err)
}
if err := os.WriteFile(BoxPubKeyPath(dir), key.BoxKeys.BoxPubKeyBytes(), 0644); err != nil {
return nil, fmt.Errorf("write box pubkey: %w", err)
}
if err := os.WriteFile(AuthTokenPath(dir), key.AuthToken, 0600); err != nil {
return nil, fmt.Errorf("write auth token: %w", err)
}
return key, nil
}
// BoxPubKeyBytes returns the box public key as 32 raw bytes.
func (kp *BoxKeyPair) BoxPubKeyBytes() []byte {
return kp.PublicKey[:]
}
// EncryptMessage encrypts plaintext using NaCl box anonymous sealed boxes.
func EncryptMessage(plaintext []byte, boxPub *[32]byte) ([]byte, error) {
if len(plaintext) == 0 {
return plaintext, nil
}
sealed, err := box.SealAnonymous(nil, plaintext, boxPub, rand.Reader)
if err != nil {
return nil, fmt.Errorf("seal anonymous: %w", err)
}
return sealed, nil
}
// DecryptMessage decrypts a NaCl box anonymous sealed box using the provided keypair.
func DecryptMessage(ciphertext []byte, boxKeys *BoxKeyPair) ([]byte, error) {
if len(ciphertext) == 0 {
return ciphertext, nil
}
opened, ok := box.OpenAnonymous(nil, ciphertext, boxKeys.PublicKey, boxKeys.PrivateKey)
if !ok {
return nil, fmt.Errorf("open sealed box failed")
}
return opened, nil
}
+134
View File
@@ -0,0 +1,134 @@
package cryptography
import (
"bytes"
"testing"
)
func TestBoxEncryptDecrypt(t *testing.T) {
bk, err := GenerateBoxKey()
if err != nil {
t.Fatalf("GenerateBoxKey: %v", err)
}
plain := []byte("necropolis secure message")
cipher, err := EncryptMessage(plain, bk.PublicKey)
if err != nil {
t.Fatalf("EncryptMessage: %v", err)
}
got, err := DecryptMessage(cipher, bk)
if err != nil {
t.Fatalf("DecryptMessage: %v", err)
}
if !bytes.Equal(got, plain) {
t.Fatalf("roundtrip mismatch: got %q, want %q", got, plain)
}
if len(cipher) > 0 {
cipher[0] ^= 0x01
}
_, err = DecryptMessage(cipher, bk)
if err == nil {
t.Fatal("expected error on tampered ciphertext, got nil")
}
}
func TestSignVerify(t *testing.T) {
op, err := GenerateOperatorKey()
if err != nil {
t.Fatalf("GenerateOperatorKey: %v", err)
}
data := []byte("necropolis command")
sig, err := op.PrivateKey.Sign(data)
if err != nil {
t.Fatalf("Sign: %v", err)
}
ok, err := Verify(op.PublicKey, data, sig)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if !ok {
t.Fatal("signature verification failed")
}
data[0] ^= 0x01
ok, err = Verify(op.PublicKey, data, sig)
if err != nil {
t.Fatalf("Verify tampered: %v", err)
}
if ok {
t.Fatal("expected verification failure on tampered data")
}
}
func TestBoxKeyPersistenceRoundtrip(t *testing.T) {
orig, err := GenerateBoxKey()
if err != nil {
t.Fatalf("GenerateBoxKey: %v", err)
}
privBytes := orig.Marshal()
if len(privBytes) != 32 {
t.Fatalf("Marshal returned %d bytes, want 32", len(privBytes))
}
pubBytes := orig.BoxPubKeyBytes()
if len(pubBytes) != 32 {
t.Fatalf("BoxPubKeyBytes returned %d bytes, want 32", len(pubBytes))
}
reloaded := LoadBoxKey(privBytes)
if reloaded == nil {
t.Fatal("LoadBoxKey returned nil")
}
if !bytes.Equal(reloaded.PublicKey[:], pubBytes) {
t.Fatal("reloaded public key does not match original public key")
}
plain := []byte("necropolis beacon payload")
cipher, err := EncryptMessage(plain, reloaded.PublicKey)
if err != nil {
t.Fatalf("EncryptMessage with reloaded pub: %v", err)
}
got, err := DecryptMessage(cipher, reloaded)
if err != nil {
t.Fatalf("DecryptMessage with reloaded keys: %v", err)
}
if !bytes.Equal(got, plain) {
t.Fatalf("reloaded roundtrip mismatch: got %q, want %q", got, plain)
}
got2, err := DecryptMessage(cipher, orig)
if err != nil {
t.Fatalf("DecryptMessage with original keys: %v", err)
}
if !bytes.Equal(got2, plain) {
t.Fatalf("original roundtrip mismatch: got %q, want %q", got2, plain)
}
}
func TestKeyPersistence(t *testing.T) {
orig, err := GenerateOperatorKey()
if err != nil {
t.Fatalf("GenerateOperatorKey: %v", err)
}
marshaled, err := MarshalPrivateKey(orig.PrivateKey)
if err != nil {
t.Fatalf("MarshalPrivateKey: %v", err)
}
loaded, err := LoadPrivateKey(marshaled)
if err != nil {
t.Fatalf("LoadPrivateKey: %v", err)
}
if !orig.PublicKey.Equals(loaded.GetPublic()) {
t.Fatal("public key mismatch after marshal roundtrip")
}
}