check / check (push) Waiting to run
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 OpenPGP secret key; a protected key's passphrase comes from MFER_SIGN_KEY_PASSPHRASE or a prompt on the terminal, and is checked before any file is read. Verification keeps the rules of the --require-signature fix: one primary key in the embedded block, counted from its packets so that keys the library skips count too, exactly one signature, made by that key or one of its subkeys, and signer equal to its fingerprint. An armored key or signature must be one block and nothing else. Model: opus-5-5
88 lines
2.3 KiB
Go
88 lines
2.3 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. 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 {
|
|
return opts, nil
|
|
}
|
|
|
|
opts.Passphrase, err = mfa.readPassphrase(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// gen and freshen read the signing options before any file, so 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
|
|
}
|