fetch_ml/internal/crypto/signing.go
Jeremie Fraeys 4b2782f674
feat(domain): add task visibility and supporting infrastructure
Core domain and utility updates:

- domain/task.go: Task model with visibility system
  * Visibility enum: private, lab, institution, open
  * Group associations for lab-scoped access
  * CreatedBy tracking for ownership
  * Sharing metadata with expiry

- config/paths.go: Group-scoped data directories and audit log paths
- crypto/signing.go: Key management for audit sealing, token signature verification
- container/supply_chain.go: Image provenance tracking, vulnerability scanning
- fileutil/filetype.go: MIME type detection and security validation
- fileutil/secure.go: Protected file permissions, secure deletion
- jupyter/: Package and service manager updates
- experiment/manager.go: Visibility cascade from experiments to tasks
- network/ssh.go: SSH tunneling improvements
- queue/: Filesystem queue enhancements
2026-03-08 13:03:27 -04:00

186 lines
5.6 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) {
// #nosec G304 -- path is controlled by configuration, not arbitrary user input
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) {
// #nosec G304 -- path is controlled by configuration, not arbitrary user input
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)
}