check / check (push) Waiting to run
mfer ran the gpg binary to sign, export keys and verify, so it 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. The embedded block may hold no DSA key and no secret key, and an armored field must be one well-formed block. Model: opus-5-5
87 lines
2.4 KiB
Go
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
|
|
}
|