aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiloslav Trmač <mitr@redhat.com>2022-07-29 00:08:40 +0200
committerMiloslav Trmač <mitr@redhat.com>2022-07-30 17:26:08 +0200
commitd462da676cf0e97420d42ea64d72f69cab675922 (patch)
tree2dd71b580ec8f39b0672ea26f5b4ac2cdced8c8a
parent7075e2e1d5cc669e698070081a74093bdcbf115e (diff)
downloadpodman-d462da676cf0e97420d42ea64d72f69cab675922.tar.gz
podman-d462da676cf0e97420d42ea64d72f69cab675922.tar.bz2
podman-d462da676cf0e97420d42ea64d72f69cab675922.zip
Add support for creating sigstore signatures, and providing passphrases
- Allow creating sigstore signatures via --sign-by-sigstore-private-key . Like existing --sign-by, it does not work remote (in this case because we would have to copy the private key to the server). - Allow passing a passphrase (which is mandatory for sigstore private keys) via --sign-passphrase-file; if it is not provided, prompt interactively. - Also, use that passphrase for --sign-by as well, allowing non-interactive GPG use. (But --sign-passphrase-file can only be used with _one of_ --sign-by and --sign-by-sigstore-private-key.) Note that unlike the existing code, (podman build) does not yet implement sigstore (I'm not sure why it needs to, it seems not to push images?) because Buildah does not expose the feature yet. Also, (podman image sign) was not extended to support sigstore. The test for this follows existing (podman image sign) tests and doesn't work rootless; that could be improved by exposing a registries.d override option. The test for push is getting large; I didn't want to start yet another registry container, but that would be an alternative. In the future, Ginkgo's Ordered/BeforeAll would allow starting a registry once and using it for two tests. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
-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 bdc3d9513..6606b2cd0 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 9dc401f60..2a1beb611 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