package archiver import ( "context" "fmt" "os" "sync" "git.unkin.net/unkin/logarchiver/internal/config" "git.unkin.net/unkin/logarchiver/internal/crypto" "git.unkin.net/unkin/logarchiver/internal/vaultgpg" ) // PubkeyLoader fetches the current armored public key from the configured source. type PubkeyLoader func(ctx context.Context) (*crypto.PublicKey, error) // PubkeyProvider caches the current public key and supports periodic refresh so // key rotation in the Vault GPG engine is picked up without a restart. type PubkeyProvider struct { load PubkeyLoader mu sync.RWMutex key *crypto.PublicKey } // NewPubkeyProvider builds a provider and loads the key once. func NewPubkeyProvider(ctx context.Context, load PubkeyLoader) (*PubkeyProvider, error) { p := &PubkeyProvider{load: load} if err := p.Refresh(ctx); err != nil { return nil, err } return p, nil } // Refresh reloads the public key. func (p *PubkeyProvider) Refresh(ctx context.Context) error { key, err := p.load(ctx) if err != nil { return err } p.mu.Lock() p.key = key p.mu.Unlock() return nil } // Current returns the cached public key. func (p *PubkeyProvider) Current() *crypto.PublicKey { p.mu.RLock() defer p.mu.RUnlock() return p.key } // PubkeyLoaderFromConfig builds a loader for the configured source. For // pubkey_source=vault it also verifies the parsed key's fingerprint matches the // fingerprint the engine reports, catching armor corruption. func PubkeyLoaderFromConfig(cfg config.CryptoConfig, vc *vaultgpg.Client) (PubkeyLoader, error) { switch cfg.Source { case config.PubkeyFile: path := cfg.PubkeyFile return func(_ context.Context) (*crypto.PublicKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read pubkey file %s: %w", path, err) } return crypto.LoadPublicKey(data) }, nil case config.PubkeyVault: if vc == nil { return nil, fmt.Errorf("pubkey_source=vault requires a vault client") } name := cfg.KeyName return func(ctx context.Context) (*crypto.PublicKey, error) { pk, err := vc.FetchPublicKey(ctx, name) if err != nil { return nil, err } key, err := crypto.LoadPublicKey([]byte(pk.Armored)) if err != nil { return nil, err } if pk.Fingerprint != "" && key.Fingerprint != pk.Fingerprint { return nil, fmt.Errorf("pubkey fingerprint mismatch: engine=%s parsed=%s", pk.Fingerprint, key.Fingerprint) } return key, nil }, nil default: return nil, fmt.Errorf("unknown pubkey_source %q", cfg.Source) } }