// Package keypair provides Ed25519 keypair generation and signing helpers // over opaque []byte blobs. The package is network-free, storage-free, // and TinyGo-friendly: it does not import `crypto/x509`, `encoding/pem`, // or `os`. Random bytes are not read internally; callers pass an io.Reader // (typically `crypto/rand.Reader` on host builds, or a `crypto.getRandomValues` // adapter on WASM). // // Public APIs return raw byte blobs (32-byte public keys, 64-byte private // keys, 64-byte signatures) so the WASM bridge in later phases can hand // them back and forth across the JS boundary as Uint8Array. The package // never re-exports `crypto/ed25519` types in its surface. package keypair import ( "bytes" "crypto/ed25519" "encoding/base64" "errors" "fmt" "io" ) var ( // ErrInvalidPrivateKey reports that a private key blob does not have the // required Ed25519 private-key length. ErrInvalidPrivateKey = errors.New("private_key must be a 64-byte Ed25519 private key") // ErrInvalidPublicKey reports that a public key blob does not have the // required Ed25519 public-key length. ErrInvalidPublicKey = errors.New("public_key must be a 32-byte Ed25519 public key") // ErrInvalidPublicKeyEncoding reports that a marshaled public key is not a // strict base64 encoding of a 32-byte Ed25519 public key. ErrInvalidPublicKeyEncoding = errors.New("public_key is not a valid base64-encoded Ed25519 public key") ) // Generate reads 32 seed bytes from reader and derives an Ed25519 keypair. // The returned slices are independent copies; callers may retain or zero // them without affecting subsequent calls. func Generate(reader io.Reader) (privateKey, publicKey []byte, err error) { if reader == nil { return nil, nil, errors.New("keypair.Generate: reader must not be nil") } pub, priv, err := ed25519.GenerateKey(reader) if err != nil { return nil, nil, fmt.Errorf("keypair.Generate: %w", err) } return bytes.Clone(priv), bytes.Clone(pub), nil } // Sign returns the raw 64-byte Ed25519 signature of message under privateKey. func Sign(privateKey, message []byte) ([]byte, error) { if len(privateKey) != ed25519.PrivateKeySize { return nil, ErrInvalidPrivateKey } signature := ed25519.Sign(ed25519.PrivateKey(privateKey), message) return bytes.Clone(signature), nil } // Verify reports whether signature authenticates message under publicKey. // It returns false if any input has the wrong length. func Verify(publicKey, message, signature []byte) bool { if len(publicKey) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize { return false } return ed25519.Verify(ed25519.PublicKey(publicKey), message, signature) } // MarshalPublicKey returns the base64 (StdEncoding) representation of the raw // 32-byte Ed25519 public key. The encoding matches docs/ARCHITECTURE.md ยง15: // the backend stores client public keys in this exact form and the gateway // reads them out of session cache as base64 strings. func MarshalPublicKey(publicKey []byte) (string, error) { if len(publicKey) != ed25519.PublicKeySize { return "", ErrInvalidPublicKey } return base64.StdEncoding.EncodeToString(publicKey), nil } // UnmarshalPublicKey decodes a strict base64 (StdEncoding) representation of // a raw 32-byte Ed25519 public key. func UnmarshalPublicKey(value string) ([]byte, error) { decoded, err := base64.StdEncoding.Strict().DecodeString(value) if err != nil { return nil, ErrInvalidPublicKeyEncoding } if len(decoded) != ed25519.PublicKeySize { return nil, ErrInvalidPublicKey } return decoded, nil } // PublicKeyFromPrivate extracts the Ed25519 public key embedded in privateKey. // The returned slice is an independent copy. func PublicKeyFromPrivate(privateKey []byte) ([]byte, error) { if len(privateKey) != ed25519.PrivateKeySize { return nil, ErrInvalidPrivateKey } pub, _ := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey) return bytes.Clone(pub), nil }