Files
podman/cmd/podman/common/sign.go
Miloslav Trmač 9e2850d0a8 Add --sign-by-sq-fingerprint to push operations
This adds a new feature that allows signing using Sequoia-backed
keys.  The existing options to sign using GPG-backed keys (and sigstore)
remain unchanged, and continue to use the same backends as usual.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
2025-09-11 20:32:32 +02:00

144 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package common
import (
"fmt"
"os"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
"go.podman.io/common/pkg/completion"
"go.podman.io/common/pkg/ssh"
"go.podman.io/image/v5/pkg/cli"
"go.podman.io/image/v5/pkg/cli/sigstore"
"go.podman.io/image/v5/signature/signer"
"go.podman.io/image/v5/signature/simplesequoia"
)
// SigningCLIOnlyOptions contains signing-related CLI options.
// Some other options are defined in entities.ImagePushOptions.
type SigningCLIOnlyOptions struct {
signPassphraseFile string
signBySequoiaFingerprint string
signBySigstoreParamFile string
}
func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, pushOpts *entities.ImagePushOptions) {
flags := cmd.Flags()
signByFlagName := "sign-by"
flags.StringVar(&pushOpts.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key")
_ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone)
signBySequoiaFingerprintFlagName := "sign-by-sq-fingerprint"
flags.StringVar(&cliOpts.signBySequoiaFingerprint, signBySequoiaFingerprintFlagName, "", "Sign the image using a Sequoia-PGP key with the specified `FINGERPRINT`")
_ = cmd.RegisterFlagCompletionFunc(signBySequoiaFingerprintFlagName, completion.AutocompleteNone)
signBySigstoreFlagName := "sign-by-sigstore"
flags.StringVar(&cliOpts.signBySigstoreParamFile, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault)
signBySigstorePrivateKeyFlagName := "sign-by-sigstore-private-key"
flags.StringVar(&pushOpts.SignBySigstorePrivateKeyFile, signBySigstorePrivateKeyFlagName, "", "Sign the image using a sigstore private key at `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signBySigstorePrivateKeyFlagName, completion.AutocompleteDefault)
signPassphraseFileFlagName := "sign-passphrase-file"
flags.StringVar(&cliOpts.signPassphraseFile, signPassphraseFileFlagName, "", "Read a passphrase for signing an image from `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signPassphraseFileFlagName, completion.AutocompleteDefault)
if registry.IsRemote() {
_ = flags.MarkHidden(signByFlagName)
_ = flags.MarkHidden(signBySequoiaFingerprintFlagName)
_ = flags.MarkHidden(signBySigstoreFlagName)
_ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
_ = flags.MarkHidden(signPassphraseFileFlagName)
}
}
// PrepareSigning updates pushOpts.Signers, pushOpts.SignPassphrase and SignSigstorePrivateKeyPassphrase based on cliOpts,
// and validates pushOpts.Sign* consistency.
// It may interactively prompt for a passphrase if one is required and wasnt provided otherwise;
// or it may interactively trigger an OIDC authentication, using standard input/output, or even open a web browser.
// Returns a cleanup callback on success, which must be called when done.
func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnlyOptions) (func(), error) {
// c/common/libimage.Image does allow creating both simple signing and sigstore signatures simultaneously,
// with independent passphrases, but that would make the CLI probably too confusing.
// For now, use the passphrase with either, but only one of them.
if cliOpts.signPassphraseFile != "" {
count := 0
if pushOpts.SignBy != "" {
count++
}
if cliOpts.signBySequoiaFingerprint != "" {
count++
}
if pushOpts.SignBySigstorePrivateKeyFile != "" {
count++
}
if count > 1 {
return nil, fmt.Errorf("only one of --sign-by, --sign-by-sq-fingerprint and --sign-by-sigstore-private-key can be used with --sign-passphrase-file")
}
}
var passphrase string
if cliOpts.signPassphraseFile != "" {
p, err := cli.ReadPassphraseFile(cliOpts.signPassphraseFile)
if err != nil {
return nil, err
}
passphrase = p
} else if pushOpts.SignBySigstorePrivateKeyFile != "" {
p := ssh.ReadPassphrase()
passphrase = string(p)
} // pushOpts.SignBy triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldnt prompt ourselves if no passphrase was explicitly provided.
// With signBySequoiaFingerprint, we dont prompt for a passphrase (for now??): We dont know whether the key requires a passphrase.
pushOpts.SignPassphrase = passphrase
pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase)
cleanup := signingCleanup{}
succeeded := false
defer func() {
if !succeeded {
cleanup.cleanup()
}
}()
if cliOpts.signBySigstoreParamFile != "" {
signer, err := sigstore.NewSignerFromParameterFile(cliOpts.signBySigstoreParamFile, &sigstore.Options{
PrivateKeyPassphrasePrompt: cli.ReadPassphraseFile,
Stdin: os.Stdin,
Stdout: os.Stdout,
})
if err != nil {
return nil, err
}
pushOpts.Signers = append(pushOpts.Signers, signer)
cleanup.signers = append(cleanup.signers, signer)
}
if cliOpts.signBySequoiaFingerprint != "" {
opts := []simplesequoia.Option{
simplesequoia.WithKeyFingerprint(cliOpts.signBySequoiaFingerprint),
}
if passphrase != "" {
opts = append(opts, simplesequoia.WithPassphrase(passphrase))
}
signer, err := simplesequoia.NewSigner(opts...)
if err != nil {
return nil, fmt.Errorf("error using --sign-by-sq-fingerprint: %w", err)
}
pushOpts.Signers = append(pushOpts.Signers, signer)
cleanup.signers = append(cleanup.signers, signer)
}
succeeded = true
return cleanup.cleanup, nil
}
// signingCleanup carries state for cleanup after PrepareSigning
type signingCleanup struct {
signers []*signer.Signer
}
func (c *signingCleanup) cleanup() {
for _, s := range c.signers {
s.Close()
}
}