mirror of
https://github.com/containers/podman.git
synced 2025-11-29 17:48:05 +08:00
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>
144 lines
5.7 KiB
Go
144 lines
5.7 KiB
Go
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 wasn’t 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 shouldn’t prompt ourselves if no passphrase was explicitly provided.
|
||
// With signBySequoiaFingerprint, we don’t prompt for a passphrase (for now??): We don’t 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()
|
||
}
|
||
}
|