fetch_ml/internal/crypto/signing.go
Jeremie Fraeys 4cdb68907e
refactor(utilities): update supporting modules for scheduler integration
Update utility modules:
- File utilities with secure file operations
- Environment pool with resource tracking
- Error types with scheduler error categories
- Logging with audit context support
- Network/SSH with connection pooling
- Privacy/PII handling with tenant boundaries
- Resource manager with scheduler allocation
- Security monitor with audit integration
- Tracking plugins (MLflow, TensorBoard) with auth
- Crypto signing with tenant keys
- Database init with multi-user support
2026-02-26 12:07:15 -05:00

184 lines
5.4 KiB
Go

// Package crypto provides cryptographic utilities for FetchML
package crypto
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"github.com/jfraeys/fetch_ml/internal/fileutil"
)
// ManifestSigner provides Ed25519 signing for run manifests
type ManifestSigner struct {
keyID string
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
}
// SigningResult contains the signature and metadata
type SigningResult struct {
Signature string `json:"signature"`
KeyID string `json:"key_id"`
Algorithm string `json:"algorithm"`
}
// GenerateSigningKeys creates a new Ed25519 keypair for manifest signing
// This should be done once and the keys stored securely
func GenerateSigningKeys() (publicKey, privateKey []byte, err error) {
return ed25519.GenerateKey(rand.Reader)
}
// NewManifestSigner creates a signer from a private key
func NewManifestSigner(privateKey []byte, keyID string) (*ManifestSigner, error) {
if len(privateKey) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d, got %d",
ed25519.PrivateKeySize, len(privateKey))
}
// Extract public key from private key
pubKey := make([]byte, ed25519.PublicKeySize)
copy(pubKey, privateKey[32:])
return &ManifestSigner{
privateKey: ed25519.PrivateKey(privateKey),
publicKey: ed25519.PublicKey(pubKey),
keyID: keyID,
}, nil
}
// SignManifest signs a manifest and returns the signing result
// The manifest is canonicalized to JSON before signing
func (s *ManifestSigner) SignManifest(manifest any) (*SigningResult, error) {
// Marshal to canonical JSON (sorted keys)
data, err := json.Marshal(manifest)
if err != nil {
return nil, fmt.Errorf("failed to marshal manifest: %w", err)
}
// Sign the data
signature := ed25519.Sign(s.privateKey, data)
return &SigningResult{
Signature: base64.StdEncoding.EncodeToString(signature),
KeyID: s.keyID,
Algorithm: "Ed25519",
}, nil
}
// SignManifestBytes signs raw bytes directly (for pre-serialized manifests)
func (s *ManifestSigner) SignManifestBytes(data []byte) (*SigningResult, error) {
signature := ed25519.Sign(s.privateKey, data)
return &SigningResult{
Signature: base64.StdEncoding.EncodeToString(signature),
KeyID: s.keyID,
Algorithm: "Ed25519",
}, nil
}
// VerifyManifest verifies a manifest signature
func VerifyManifest(manifest any, result *SigningResult, publicKey []byte) (bool, error) {
if result.Algorithm != "Ed25519" {
return false, fmt.Errorf("unsupported algorithm: %s", result.Algorithm)
}
// Marshal manifest to same format used for signing
data, err := json.Marshal(manifest)
if err != nil {
return false, fmt.Errorf("failed to marshal manifest: %w", err)
}
// Decode signature
signature, err := base64.StdEncoding.DecodeString(result.Signature)
if err != nil {
return false, fmt.Errorf("failed to decode signature: %w", err)
}
// Verify
return ed25519.Verify(ed25519.PublicKey(publicKey), data, signature), nil
}
// VerifyManifestBytes verifies raw bytes directly
func VerifyManifestBytes(data []byte, result *SigningResult, publicKey []byte) (bool, error) {
if result.Algorithm != "Ed25519" {
return false, fmt.Errorf("unsupported algorithm: %s", result.Algorithm)
}
signature, err := base64.StdEncoding.DecodeString(result.Signature)
if err != nil {
return false, fmt.Errorf("failed to decode signature: %w", err)
}
return ed25519.Verify(ed25519.PublicKey(publicKey), data, signature), nil
}
// GetPublicKey returns the signer's public key
func (s *ManifestSigner) GetPublicKey() []byte {
return s.publicKey
}
// GetKeyID returns the signer's key ID
func (s *ManifestSigner) GetKeyID() string {
return s.keyID
}
// SavePrivateKeyToFile saves a private key to a file with restricted permissions and crash safety (fsync)
func SavePrivateKeyToFile(key []byte, path string) error {
// Write with restricted permissions (owner read/write only) and fsync
if err := fileutil.WriteFileSafe(path, key, 0600); err != nil {
return fmt.Errorf("failed to write private key: %w", err)
}
return nil
}
// LoadPrivateKeyFromFile loads a private key from a file
func LoadPrivateKeyFromFile(path string) ([]byte, error) {
key, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read private key: %w", err)
}
if len(key) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d, got %d",
ed25519.PrivateKeySize, len(key))
}
return key, nil
}
// SavePublicKeyToFile saves a public key to a file with crash safety (fsync)
func SavePublicKeyToFile(key []byte, path string) error {
if err := fileutil.WriteFileSafe(path, key, 0644); err != nil {
return fmt.Errorf("failed to write public key: %w", err)
}
return nil
}
// LoadPublicKeyFromFile loads a public key from a file
func LoadPublicKeyFromFile(path string) ([]byte, error) {
key, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read public key: %w", err)
}
if len(key) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key size: expected %d, got %d",
ed25519.PublicKeySize, len(key))
}
return key, nil
}
// EncodeKeyToBase64 encodes a key to base64 for storage/transmission
func EncodeKeyToBase64(key []byte) string {
return base64.StdEncoding.EncodeToString(key)
}
// DecodeKeyFromBase64 decodes a key from base64
func DecodeKeyFromBase64(encoded string) ([]byte, error) {
return base64.StdEncoding.DecodeString(encoded)
}