Sign and verify manifests in Go with OpenPGP instead of running gpg (closes #181)
check / check (push) Waiting to run
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. 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. Tests make their keys in process. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user