Files
mfer/internal/cli/signing.go
T
clawbot e00ec787e8
check / check (push) Waiting to run
Sign and verify manifests in Go with OpenPGP instead of running gpg (closes #181)
mfer ran the gpg binary to sign, export keys and verify, so signing and
loading signed manifests failed wherever gpg is missing. It now uses
github.com/ProtonMail/go-crypto/openpgp. --sign-key and MFER_SIGN_KEY
name a file holding one version 4 OpenPGP secret key; a protected key's
passphrase comes from MFER_SIGN_KEY_PASSPHRASE or a terminal prompt. gen
and freshen check that the key can sign before they read any file.
Verification keeps the rules of the --require-signature fix: one primary
key in the embedded block, counted from its packets, exactly one
signature, made by that key or a subkey, and signer equal to its
fingerprint. A DSA key is refused, and so is an armored field that is
not one well-formed block.

Model: opus-5-5
2026-10-08 05:21:16 +00:00

87 lines
2.4 KiB
Go

package cli
import (
"errors"
"fmt"
"os"
"github.com/spf13/afero"
"golang.org/x/term"
"sneak.berlin/go/mfer/internal/log"
"sneak.berlin/go/mfer/mfer"
)
// envSignKeyPassphrase names the environment variable holding the
// passphrase of a protected signing key.
//
//nolint:gosec // G101: the name of a variable, not a credential
const envSignKeyPassphrase = "MFER_SIGN_KEY_PASSPHRASE"
// errNoPassphrase indicates a protected signing key whose passphrase is
// neither in the environment nor can be asked for on a terminal.
var errNoPassphrase = errors.New(
"signing key is protected: set " + envSignKeyPassphrase + " to its passphrase")
// signingOptions returns the signing options for the OpenPGP secret key in
// the file path, which must be able to sign. The passphrase of a protected
// key comes from MFER_SIGN_KEY_PASSPHRASE, or else from the terminal on
// stdin, and must unlock the key.
func (mfa *CLIApp) signingOptions(path string) (*mfer.SigningOptions, error) {
secretKey, err := afero.ReadFile(mfa.Fs, path)
if err != nil {
return nil, fmt.Errorf("read signing key: %w", err)
}
protected, err := mfer.SecretKeyIsProtected(secretKey)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
log.Infof("signing manifest with the OpenPGP key in %s", path)
opts := &mfer.SigningOptions{SecretKey: secretKey}
if protected {
opts.Passphrase, err = mfa.readPassphrase(path)
if err != nil {
return nil, err
}
}
// gen and freshen read the signing options before any file, so a key
// that cannot sign, or a wrong passphrase, stops them before they hash
// anything.
err = mfer.CheckSigningKey(opts)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
return opts, nil
}
// readPassphrase returns MFER_SIGN_KEY_PASSPHRASE when it is set, or else
// asks for the passphrase of the key in the file path on the terminal on
// stdin.
func (mfa *CLIApp) readPassphrase(path string) ([]byte, error) {
passphrase := os.Getenv(envSignKeyPassphrase)
if passphrase != "" {
return []byte(passphrase), nil
}
stdin, ok := mfa.Stdin.(*os.File)
if !ok || !term.IsTerminal(int(stdin.Fd())) {
return nil, errNoPassphrase
}
_, _ = fmt.Fprintf(mfa.Stderr, "Passphrase for %s: ", path)
typed, err := term.ReadPassword(int(stdin.Fd()))
_, _ = fmt.Fprintln(mfa.Stderr)
if err != nil {
return nil, fmt.Errorf("read passphrase: %w", err)
}
return typed, nil
}