aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/common/sign.go36
-rw-r--r--cmd/podman/images/push.go19
-rw-r--r--cmd/podman/manifest/push.go19
-rw-r--r--docs/source/markdown/podman-manifest-push.1.md10
-rw-r--r--docs/source/markdown/podman-push.1.md10
-rw-r--r--pkg/domain/entities/images.go10
-rw-r--r--pkg/domain/infra/abi/images.go3
-rw-r--r--pkg/domain/infra/abi/manifest.go3
-rw-r--r--test/e2e/push_test.go40
-rw-r--r--test/e2e/sign/policy.json6
-rw-r--r--test/e2e/testdata/sigstore-key.key11
-rw-r--r--test/e2e/testdata/sigstore-key.key.pass1
-rw-r--r--test/e2e/testdata/sigstore-key.pub4
-rw-r--r--test/e2e/testdata/sigstore-registries.d-fragment.yaml3
-rw-r--r--vendor/github.com/containers/image/v5/pkg/cli/passphrase.go36
-rw-r--r--vendor/modules.txt1
16 files changed, 206 insertions, 6 deletions
diff --git a/cmd/podman/common/sign.go b/cmd/podman/common/sign.go
new file mode 100644
index 000000000..e8a90ed57
--- /dev/null
+++ b/cmd/podman/common/sign.go
@@ -0,0 +1,36 @@
+package common
+
+import (
+ "fmt"
+
+ "github.com/containers/image/v5/pkg/cli"
+ "github.com/containers/podman/v4/pkg/domain/entities"
+ "github.com/containers/podman/v4/pkg/terminal"
+)
+
+// PrepareSigningPassphrase updates pushOpts.SignPassphrase and SignSigstorePrivateKeyPassphrase based on a --sign-passphrase-file value signPassphraseFile,
+// and validates pushOpts.Sign* consistency.
+// It may interactively prompt for a passphrase if one is required and wasn’t provided otherwise.
+func PrepareSigningPassphrase(pushOpts *entities.ImagePushOptions, signPassphraseFile string) 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 signPassphraseFile != "" && pushOpts.SignBy != "" && pushOpts.SignBySigstorePrivateKeyFile != "" {
+ return fmt.Errorf("only one of --sign-by and sign-by-sigstore-private-key can be used with --sign-passphrase-file")
+ }
+
+ var passphrase string
+ if signPassphraseFile != "" {
+ p, err := cli.ReadPassphraseFile(signPassphraseFile)
+ if err != nil {
+ return err
+ }
+ passphrase = p
+ } else if pushOpts.SignBySigstorePrivateKeyFile != "" {
+ p := terminal.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.
+ pushOpts.SignPassphrase = passphrase
+ pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase)
+ return nil
+}
diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go
index 764936426..1734900de 100644
--- a/cmd/podman/images/push.go
+++ b/cmd/podman/images/push.go
@@ -17,8 +17,9 @@ import (
// CLI-only fields into the API types.
type pushOptionsWrapper struct {
entities.ImagePushOptions
- TLSVerifyCLI bool // CLI only
- CredentialsCLI string
+ TLSVerifyCLI bool // CLI only
+ CredentialsCLI string
+ SignPassphraseFileCLI string
}
var (
@@ -106,6 +107,14 @@ func pushFlags(cmd *cobra.Command) {
flags.StringVar(&pushOptions.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key")
_ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone)
+ signBySigstorePrivateKeyFlagName := "sign-by-sigstore-private-key"
+ flags.StringVar(&pushOptions.SignBySigstorePrivateKeyFile, signBySigstorePrivateKeyFlagName, "", "Sign the image using a sigstore private key at `PATH`")
+ _ = cmd.RegisterFlagCompletionFunc(signBySigstorePrivateKeyFlagName, completion.AutocompleteDefault)
+
+ signPassphraseFileFlagName := "sign-passphrase-file"
+ flags.StringVar(&pushOptions.SignPassphraseFileCLI, signPassphraseFileFlagName, "", "Read a passphrase for signing an image from `PATH`")
+ _ = cmd.RegisterFlagCompletionFunc(signPassphraseFileFlagName, completion.AutocompleteDefault)
+
flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
compressionFormat := "compression-format"
@@ -118,6 +127,8 @@ func pushFlags(cmd *cobra.Command) {
_ = flags.MarkHidden("digestfile")
_ = flags.MarkHidden("quiet")
_ = flags.MarkHidden(signByFlagName)
+ _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
+ _ = flags.MarkHidden(signPassphraseFileFlagName)
}
if !registry.IsRemote() {
flags.StringVar(&pushOptions.SignaturePolicy, "signature-policy", "", "Path to a signature-policy file")
@@ -153,6 +164,10 @@ func imagePush(cmd *cobra.Command, args []string) error {
pushOptions.Password = creds.Password
}
+ if err := common.PrepareSigningPassphrase(&pushOptions.ImagePushOptions, pushOptions.SignPassphraseFileCLI); err != nil {
+ return err
+ }
+
// Let's do all the remaining Yoga in the API to prevent us from scattering
// logic across (too) many parts of the code.
return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptions.ImagePushOptions)
diff --git a/cmd/podman/manifest/push.go b/cmd/podman/manifest/push.go
index df394d275..6afd8ea4c 100644
--- a/cmd/podman/manifest/push.go
+++ b/cmd/podman/manifest/push.go
@@ -20,8 +20,9 @@ import (
type manifestPushOptsWrapper struct {
entities.ImagePushOptions
- TLSVerifyCLI bool // CLI only
- CredentialsCLI string
+ TLSVerifyCLI bool // CLI only
+ CredentialsCLI string
+ SignPassphraseFileCLI string
}
var (
@@ -72,6 +73,14 @@ func init() {
flags.StringVar(&manifestPushOpts.SignBy, signByFlagName, "", "sign the image using a GPG key with the specified `FINGERPRINT`")
_ = pushCmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone)
+ signBySigstorePrivateKeyFlagName := "sign-by-sigstore-private-key"
+ flags.StringVar(&manifestPushOpts.SignBySigstorePrivateKeyFile, signBySigstorePrivateKeyFlagName, "", "Sign the image using a sigstore private key at `PATH`")
+ _ = pushCmd.RegisterFlagCompletionFunc(signBySigstorePrivateKeyFlagName, completion.AutocompleteDefault)
+
+ signPassphraseFileFlagName := "sign-passphrase-file"
+ flags.StringVar(&manifestPushOpts.SignPassphraseFileCLI, signPassphraseFileFlagName, "", "Read a passphrase for signing an image from `PATH`")
+ _ = pushCmd.RegisterFlagCompletionFunc(signPassphraseFileFlagName, completion.AutocompleteDefault)
+
flags.BoolVar(&manifestPushOpts.TLSVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
flags.BoolVarP(&manifestPushOpts.Quiet, "quiet", "q", false, "don't output progress information when pushing lists")
flags.SetNormalizeFunc(utils.AliasFlags)
@@ -79,6 +88,8 @@ func init() {
if registry.IsRemote() {
_ = flags.MarkHidden("cert-dir")
_ = flags.MarkHidden(signByFlagName)
+ _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
+ _ = flags.MarkHidden(signPassphraseFileFlagName)
}
}
@@ -104,6 +115,10 @@ func push(cmd *cobra.Command, args []string) error {
manifestPushOpts.Password = creds.Password
}
+ if err := common.PrepareSigningPassphrase(&manifestPushOpts.ImagePushOptions, manifestPushOpts.SignPassphraseFileCLI); err != nil {
+ return err
+ }
+
// TLS verification in c/image is controlled via a `types.OptionalBool`
// which allows for distinguishing among set-true, set-false, unspecified
// which is important to implement a sane way of dealing with defaults of
diff --git a/docs/source/markdown/podman-manifest-push.1.md b/docs/source/markdown/podman-manifest-push.1.md
index 22e8cae9a..66cdb8324 100644
--- a/docs/source/markdown/podman-manifest-push.1.md
+++ b/docs/source/markdown/podman-manifest-push.1.md
@@ -60,7 +60,15 @@ Delete the manifest list or image index from local storage if pushing succeeds.
#### **--sign-by**=*fingerprint*
-Sign the pushed images using the GPG key that matches the specified fingerprint.
+Sign the pushed images with a “simple signing” signature using the specified key. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
+
+#### **--sign-by-sigstore-private-key**=*path*
+
+Sign the pushed images with a sigstore signature using a private key at the specified path. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
+
+#### **--sign-passphrase-file**=*path*
+
+If signing the image (using either **--sign-by** or **--sign-by-sigstore-private-key**), read the passphrase to use from the specified path.
#### **--tls-verify**
diff --git a/docs/source/markdown/podman-push.1.md b/docs/source/markdown/podman-push.1.md
index 3cda982ac..a69bdce0a 100644
--- a/docs/source/markdown/podman-push.1.md
+++ b/docs/source/markdown/podman-push.1.md
@@ -99,7 +99,15 @@ Discard any pre-existing signatures in the image.
#### **--sign-by**=*key*
-Add a signature at the destination using the specified key. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
+Add a “simple signing” signature at the destination using the specified key. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
+
+#### **--sign-by-sigstore-private-key**=*path*
+
+Add a sigstore signature at the destination using a private key at the specified path. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
+
+#### **--sign-passphrase-file**=*path*
+
+If signing the image (using either **--sign-by** or **--sign-by-sigstore-private-key**), read the passphrase to use from the specified path.
#### **--tls-verify**
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index b8b346005..dad2dc6cc 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -205,6 +205,16 @@ type ImagePushOptions struct {
// SignBy adds a signature at the destination using the specified key.
// Ignored for remote calls.
SignBy string
+ // SignPassphrase, if non-empty, specifies a passphrase to use when signing
+ // with the key ID from SignBy.
+ SignPassphrase string
+ // SignBySigstorePrivateKeyFile, if non-empty, asks for a signature to be added
+ // during the copy, using a sigstore private key file at the provided path.
+ // Ignored for remote calls.
+ SignBySigstorePrivateKeyFile string
+ // SignSigstorePrivateKeyPassphrase is the passphrase to use when signing with
+ // SignBySigstorePrivateKeyFile.
+ SignSigstorePrivateKeyPassphrase []byte
// SkipTLSVerify to skip HTTPS and certificate verification.
SkipTLSVerify types.OptionalBool
// Progress to get progress notifications
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index ff42b0367..94178a8e2 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -304,6 +304,9 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
pushOptions.ManifestMIMEType = manifestType
pushOptions.RemoveSignatures = options.RemoveSignatures
pushOptions.SignBy = options.SignBy
+ pushOptions.SignPassphrase = options.SignPassphrase
+ pushOptions.SignBySigstorePrivateKeyFile = options.SignBySigstorePrivateKeyFile
+ pushOptions.SignSigstorePrivateKeyPassphrase = options.SignSigstorePrivateKeyPassphrase
pushOptions.InsecureSkipTLSVerify = options.SkipTLSVerify
pushOptions.Writer = options.Writer
diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go
index d20744d76..60f727234 100644
--- a/pkg/domain/infra/abi/manifest.go
+++ b/pkg/domain/infra/abi/manifest.go
@@ -316,6 +316,9 @@ func (ir *ImageEngine) ManifestPush(ctx context.Context, name, destination strin
pushOptions.ManifestMIMEType = manifestType
pushOptions.RemoveSignatures = opts.RemoveSignatures
pushOptions.SignBy = opts.SignBy
+ pushOptions.SignPassphrase = opts.SignPassphrase
+ pushOptions.SignBySigstorePrivateKeyFile = opts.SignBySigstorePrivateKeyFile
+ pushOptions.SignSigstorePrivateKeyPassphrase = opts.SignSigstorePrivateKeyPassphrase
pushOptions.InsecureSkipTLSVerify = opts.SkipTLSVerify
if opts.All {
diff --git a/test/e2e/push_test.go b/test/e2e/push_test.go
index 0faa040f4..898d21d00 100644
--- a/test/e2e/push_test.go
+++ b/test/e2e/push_test.go
@@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
+ "os/exec"
"path/filepath"
"strings"
@@ -136,6 +137,45 @@ var _ = Describe("Podman push", func() {
Expect(fi.Name()).To(Equal("digestfile.txt"))
Expect(push2).Should(Exit(0))
}
+
+ if !IsRemote() { // Remote does not support signing
+ By("pushing and pulling with sigstore signatures")
+ // Ideally, this should set SystemContext.RegistriesDirPath, but Podman currently doesn’t
+ // expose that as an option. So, for now, modify /etc/directly, and skip testing sigstore if
+ // we don’t have permission to do so.
+ systemRegistriesDAddition := "/etc/containers/registries.d/podman-test-only-temporary-addition.yaml"
+ cmd := exec.Command("cp", "testdata/sigstore-registries.d-fragment.yaml", systemRegistriesDAddition)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Skipping sigstore tests because /etc/containers/registries.d isn’t writable: %s", string(output))
+ } else {
+ defer func() {
+ err := os.Remove(systemRegistriesDAddition)
+ Expect(err).ToNot(HaveOccurred())
+ }()
+
+ // Verify that the policy rejects unsigned images
+ push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/sigstore-signed"})
+ push.WaitWithDefaultTimeout()
+ Expect(push).Should(Exit(0))
+ Expect(len(push.ErrorToString())).To(Equal(0))
+
+ pull := podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", "sign/policy.json", "localhost:5000/sigstore-signed"})
+ pull.WaitWithDefaultTimeout()
+ Expect(pull).To(ExitWithError())
+ Expect(pull.ErrorToString()).To(ContainSubstring("A signature was required, but no signature exists"))
+
+ // Sign an image, and verify it is accepted.
+ push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", "--sign-by-sigstore-private-key", "testdata/sigstore-key.key", "--sign-passphrase-file", "testdata/sigstore-key.key.pass", ALPINE, "localhost:5000/sigstore-signed"})
+ push.WaitWithDefaultTimeout()
+ Expect(push).Should(Exit(0))
+ Expect(len(push.ErrorToString())).To(Equal(0))
+
+ pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", "sign/policy.json", "localhost:5000/sigstore-signed"})
+ pull.WaitWithDefaultTimeout()
+ Expect(pull).Should(Exit(0))
+ }
+ }
})
It("podman push to local registry with authorization", func() {
diff --git a/test/e2e/sign/policy.json b/test/e2e/sign/policy.json
index ab01137bf..812c14989 100644
--- a/test/e2e/sign/policy.json
+++ b/test/e2e/sign/policy.json
@@ -12,6 +12,12 @@
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg"
}
+ ],
+ "localhost:5000/sigstore-signed": [
+ {
+ "type": "sigstoreSigned",
+ "keyPath": "testdata/sigstore-key.pub"
+ }
]
}
}
diff --git a/test/e2e/testdata/sigstore-key.key b/test/e2e/testdata/sigstore-key.key
new file mode 100644
index 000000000..c4eed76a8
--- /dev/null
+++ b/test/e2e/testdata/sigstore-key.key
@@ -0,0 +1,11 @@
+-----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
+eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
+OCwicCI6MX0sInNhbHQiOiI2ckxVcEl1M1pTallrY3dua1pNVktuTHNDUjRENTJv
+Y3J5Wmh2anZ4L1VrPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
+Iiwibm9uY2UiOiJMTVpkeTNBL285NS9SektUZGR3RURhODJTVThVcDdlKyJ9LCJj
+aXBoZXJ0ZXh0IjoiNkkzUlRCc1IwRXpHZWs0SE5LazlVdlpyMEp6Y1Bxemw0ZkEr
+SitJdHlCc0RBSkcyNmhESnFLUDFuQkJTUE5XdHpJRzJUVzQ5Z2hObEJmQy9qYVNk
+eFo2QmhXYk9ldlY0MDB4WjVNZ1oyVHdGSnJxaE9HK0JMdmNvanVkc2tOUFpJTlpE
+LytFZVBIYTRlRVJPTWhnSWlTRC9BYTd3eitlc2trVjkrN216Y3N2RVRiTTJTZGd6
+L3daMUtqV3FlOUc2MWlXSTJPSm1rRlhxQWc9PSJ9
+-----END ENCRYPTED COSIGN PRIVATE KEY-----
diff --git a/test/e2e/testdata/sigstore-key.key.pass b/test/e2e/testdata/sigstore-key.key.pass
new file mode 100644
index 000000000..beb5c7687
--- /dev/null
+++ b/test/e2e/testdata/sigstore-key.key.pass
@@ -0,0 +1 @@
+sigstore pass
diff --git a/test/e2e/testdata/sigstore-key.pub b/test/e2e/testdata/sigstore-key.pub
new file mode 100644
index 000000000..1f470f72b
--- /dev/null
+++ b/test/e2e/testdata/sigstore-key.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX/AWbBiFPuAU5+ys+Ce8YFPhTr1a
+nM7A8h6NrQi6w8w8/4dJCzlGH4SN+P93nopATs6jDXs4Lpc2/tiA1SBmzA==
+-----END PUBLIC KEY-----
diff --git a/test/e2e/testdata/sigstore-registries.d-fragment.yaml b/test/e2e/testdata/sigstore-registries.d-fragment.yaml
new file mode 100644
index 000000000..d79f4c935
--- /dev/null
+++ b/test/e2e/testdata/sigstore-registries.d-fragment.yaml
@@ -0,0 +1,3 @@
+docker:
+ localhost:5000/sigstore-signed:
+ use-sigstore-attachments: true
diff --git a/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go b/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go
new file mode 100644
index 000000000..c46650cdc
--- /dev/null
+++ b/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go
@@ -0,0 +1,36 @@
+package cli
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+)
+
+// ReadPassphraseFile returns the first line of the specified path.
+// For convenience, an empty string is returned if the path is empty.
+func ReadPassphraseFile(path string) (string, error) {
+ if path == "" {
+ return "", nil
+ }
+
+ logrus.Debugf("Reading user-specified passphrase for signing from %s", path)
+
+ ppf, err := os.Open(path)
+ if err != nil {
+ return "", err
+ }
+ defer ppf.Close()
+
+ // Read the *first* line in the passphrase file, just as gpg(1) does.
+ buf, err := bufio.NewReader(ppf).ReadBytes('\n')
+ if err != nil && !errors.Is(err, io.EOF) {
+ return "", fmt.Errorf("reading passphrase file: %w", err)
+ }
+
+ return strings.TrimSuffix(string(buf), "\n"), nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2330107ab..8d23648f0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -210,6 +210,7 @@ github.com/containers/image/v5/pkg/blobinfocache/boltdb
github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize
github.com/containers/image/v5/pkg/blobinfocache/memory
github.com/containers/image/v5/pkg/blobinfocache/none
+github.com/containers/image/v5/pkg/cli
github.com/containers/image/v5/pkg/compression
github.com/containers/image/v5/pkg/compression/internal
github.com/containers/image/v5/pkg/compression/types