Skip to content

Commit

Permalink
Add support for creating sigstore signatures, and providing passphrases
Browse files Browse the repository at this point in the history
- 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>
  • Loading branch information
mtrmac committed Jul 30, 2022
1 parent 7075e2e commit d462da6
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 6 deletions.
36 changes: 36 additions & 0 deletions cmd/podman/common/sign.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 17 additions & 2 deletions cmd/podman/images/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions cmd/podman/manifest/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -72,13 +73,23 @@ 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)

if registry.IsRemote() {
_ = flags.MarkHidden("cert-dir")
_ = flags.MarkHidden(signByFlagName)
_ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
_ = flags.MarkHidden(signPassphraseFileFlagName)
}
}

Expand All @@ -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
Expand Down
10 changes: 9 additions & 1 deletion docs/source/markdown/podman-manifest-push.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
10 changes: 9 additions & 1 deletion docs/source/markdown/podman-push.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
10 changes: 10 additions & 0 deletions pkg/domain/entities/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/domain/infra/abi/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions pkg/domain/infra/abi/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"

Expand Down Expand Up @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/sign/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg"
}
],
"localhost:5000/sigstore-signed": [
{
"type": "sigstoreSigned",
"keyPath": "testdata/sigstore-key.pub"
}
]
}
}
Expand Down
11 changes: 11 additions & 0 deletions test/e2e/testdata/sigstore-key.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
OCwicCI6MX0sInNhbHQiOiI2ckxVcEl1M1pTallrY3dua1pNVktuTHNDUjRENTJv
Y3J5Wmh2anZ4L1VrPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiJMTVpkeTNBL285NS9SektUZGR3RURhODJTVThVcDdlKyJ9LCJj
aXBoZXJ0ZXh0IjoiNkkzUlRCc1IwRXpHZWs0SE5LazlVdlpyMEp6Y1Bxemw0ZkEr
SitJdHlCc0RBSkcyNmhESnFLUDFuQkJTUE5XdHpJRzJUVzQ5Z2hObEJmQy9qYVNk
eFo2QmhXYk9ldlY0MDB4WjVNZ1oyVHdGSnJxaE9HK0JMdmNvanVkc2tOUFpJTlpE
LytFZVBIYTRlRVJPTWhnSWlTRC9BYTd3eitlc2trVjkrN216Y3N2RVRiTTJTZGd6
L3daMUtqV3FlOUc2MWlXSTJPSm1rRlhxQWc9PSJ9
-----END ENCRYPTED COSIGN PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/e2e/testdata/sigstore-key.key.pass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sigstore pass
4 changes: 4 additions & 0 deletions test/e2e/testdata/sigstore-key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX/AWbBiFPuAU5+ys+Ce8YFPhTr1a
nM7A8h6NrQi6w8w8/4dJCzlGH4SN+P93nopATs6jDXs4Lpc2/tiA1SBmzA==
-----END PUBLIC KEY-----
3 changes: 3 additions & 0 deletions test/e2e/testdata/sigstore-registries.d-fragment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
docker:
localhost:5000/sigstore-signed:
use-sigstore-attachments: true
36 changes: 36 additions & 0 deletions vendor/github.com/containers/image/v5/pkg/cli/passphrase.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d462da6

Please sign in to comment.