diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 9236b4ac..b89d9381 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -26,6 +26,7 @@ import ( "go.podman.io/image/v5/pkg/cli/sigstore" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/signature/signer" + "go.podman.io/image/v5/signature/simplesequoia" "go.podman.io/image/v5/storage" "go.podman.io/image/v5/transports/alltransports" "go.podman.io/image/v5/types" @@ -329,6 +330,7 @@ func (opts *imageDestOptions) warnAboutIneffectiveOptions(destTransport types.Im type sharedCopyOptions struct { removeSignatures bool // Do not copy signatures from the source image signByFingerprint string // Sign the image using a GPG key with the specified fingerprint + signBySequoiaFingerprint string // Sign the image using a Sequoia-PGP key with the specified fingerprint signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file signBySigstorePrivateKey string // Sign the image using a sigstore private key signPassphraseFile string // Path pointing to a passphrase file when signing @@ -342,6 +344,7 @@ func sharedCopyFlags() (pflag.FlagSet, *sharedCopyOptions) { fs := pflag.FlagSet{} fs.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from source") fs.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`") + fs.StringVar(&opts.signBySequoiaFingerprint, "sign-by-sq-fingerprint", "", "Sign the image using a Sequoia-PGP key with the specified `FINGERPRINT`") fs.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`") fs.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`") fs.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "Read a passphrase for signing an image from `PATH`") @@ -365,8 +368,20 @@ func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, fun // c/image/copy.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 opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" { - return nil, nil, fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file") + if opts.signPassphraseFile != "" { + count := 0 + if opts.signByFingerprint != "" { + count++ + } + if opts.signBySequoiaFingerprint != "" { + count++ + } + if opts.signBySigstorePrivateKey != "" { + count++ + } + if count > 1 { + return nil, 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 opts.signPassphraseFile != "" { @@ -382,6 +397,7 @@ func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, fun } passphrase = p } // opts.signByFingerprint 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 opts.signBySequoiaFingerprint, we don’t prompt for a passphrase (for now??): We don’t know whether the key requires a passphrase. var passphraseBytes []byte if passphrase != "" { passphraseBytes = []byte(passphrase) @@ -412,6 +428,19 @@ func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, fun } signers = append(signers, signer) } + if opts.signBySequoiaFingerprint != "" { + sqOpts := []simplesequoia.Option{ + simplesequoia.WithKeyFingerprint(opts.signBySequoiaFingerprint), + } + if passphrase != "" { + sqOpts = append(sqOpts, simplesequoia.WithPassphrase(passphrase)) + } + signer, err := simplesequoia.NewSigner(sqOpts...) + if err != nil { + return nil, nil, fmt.Errorf("Error using --sign-by-sq-fingerprint: %w", err) + } + signers = append(signers, signer) + } succeeded = true return ©.Options{ diff --git a/cmd/skopeo/utils_nosequoia_test.go b/cmd/skopeo/utils_nosequoia_test.go new file mode 100644 index 00000000..e3683a45 --- /dev/null +++ b/cmd/skopeo/utils_nosequoia_test.go @@ -0,0 +1,5 @@ +//go:build !containers_image_sequoia + +package main + +const buildWithSequoia = false diff --git a/cmd/skopeo/utils_sequoia_test.go b/cmd/skopeo/utils_sequoia_test.go new file mode 100644 index 00000000..d077bf77 --- /dev/null +++ b/cmd/skopeo/utils_sequoia_test.go @@ -0,0 +1,5 @@ +//go:build containers_image_sequoia + +package main + +const buildWithSequoia = true diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 01790c12..4a6acf80 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "os" + "slices" "testing" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -378,6 +379,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) { // Set most flags to non-default values // This should also test --sign-by-sigstore and --sign-by-sigstore-private-key; we would have // to create test keys for that. + // This does not test --sign-by-sq-fingerprint, because that needs to be conditional based on buildWithSequoia. opts = fakeSharedCopyOptions(t, []string{ "--remove-signatures", "--sign-by", "gpgFingerprint", @@ -395,12 +397,13 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) { ForceManifestMIMEType: imgspecv1.MediaTypeImageManifest, }, res) - // --sign-passphrase-file + --sign-by work + // --sign-passphrase-file: passphraseFile, err := os.CreateTemp("", "passphrase") // Eventually we could refer to a passphrase fixture instead require.NoError(t, err) defer os.Remove(passphraseFile.Name()) _, err = passphraseFile.WriteString("test-passphrase") require.NoError(t, err) + // --sign-passphrase-file + --sign-by work opts = fakeSharedCopyOptions(t, []string{ "--sign-by", "gpgFingerprint", "--sign-passphrase-file", passphraseFile.Name(), @@ -414,14 +417,42 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) { SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"), ReportWriter: &someStdout, }, res) - // --sign-passphrase-file + --sign-by-sigstore-private-key should be tested here. + // If Sequoia is supported, --sign-passphrase-file + --sign-by-sq-fingerprint work + if buildWithSequoia { + opts = fakeSharedCopyOptions(t, []string{ + "--sign-by-sq-fingerprint", "sqFingerprint", + "--sign-passphrase-file", passphraseFile.Name(), + }) + res, cleanup, err = opts.copyOptions(&someStdout) + require.NoError(t, err) + defer cleanup() + assert.NotNil(t, res.Signers) // Contains a Sequoia signer + res.Signers = nil // To allow the comparison below + assert.Equal(t, ©.Options{ + SignPassphrase: "test-passphrase", + SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"), + ReportWriter: &someStdout, + }, res) + } // Invalid --format opts = fakeSharedCopyOptions(t, []string{"--format", "invalid"}) _, _, err = opts.copyOptions(&someStdout) assert.Error(t, err) - // More --sign-passphrase-file, --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here. + // More --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here. + // --sign-passphrase-file + more than one key option + for _, opts := range [][]string{ + {"--sign-by", "gpgFingerprint", "--sign-by-sq-fingerprint", "sqFingerprint"}, + {"--sign-by", "gpgFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey"}, + {"--sign-by-sq-fingerprint", "sqFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey"}, + } { + opts := fakeSharedCopyOptions(t, slices.Concat(opts, []string{ + "--sign-passphrase-file", passphraseFile.Name(), + })) + _, _, err = opts.copyOptions(&someStdout) + assert.Error(t, err) + } // --sign-passphrase-file not found opts = fakeSharedCopyOptions(t, []string{ diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index cde7004c..1e824c7e 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -107,9 +107,14 @@ See containers-sigstore-signing-params.yaml(5) for details about the file format Add a sigstore signature using a private key at _path_ for an image name corresponding to _destination-image_ +**--sign-by-sq-fingerprint** _fingerprint_ + +Add a “simple signing” signature using a Sequoia-PGP key with the specified _fingerprint_. + **--sign-passphrase-file** _path_ -The passphare to use when signing with `--sign-by` or `--sign-by-sigstore-private-key`. Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable. +The passphrase to use when signing with `--sign-by`, `--sign-by-sigstore-private-key` or `--sign-by-sq-fingerprint`. +Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable. **--sign-identity** _reference_ diff --git a/docs/skopeo-sync.1.md b/docs/skopeo-sync.1.md index 8addfa21..1b218396 100644 --- a/docs/skopeo-sync.1.md +++ b/docs/skopeo-sync.1.md @@ -103,9 +103,14 @@ See containers-sigstore-signing-params.yaml(5) for details about the file format Add a sigstore signature using a private key at _path_ for an image name corresponding to _destination-image_ +**--sign-by-sq-fingerprint** _fingerprint_ + +Add a “simple signing” signature using a Sequoia-PGP key with the specified _fingerprint_. + **--sign-passphrase-file** _path_ -The passphare to use when signing with `--sign-by` or `--sign-by-sigstore-private-key`. Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable. +The passphrase to use when signing with `--sign-by`, `--sign-by-sigstore-private-key` or `--sign-by-sq-fingerprint`. +Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable. **--src-creds** _username[:password]_ for accessing the source registry. diff --git a/integration/copy_test.go b/integration/copy_test.go index 1f57b6f2..7a5139ae 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/suite" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/signature" + "go.podman.io/image/v5/signature/simplesequoia" "go.podman.io/image/v5/types" ) @@ -106,7 +107,9 @@ func (s *copySuite) TearDownSuite() { // and returns a path to a policy, which will be automatically removed when the test completes. func (s *copySuite) policyFixture(extraSubstitutions map[string]string) string { t := s.T() - edits := map[string]string{"@keydir@": s.gpgHome} + fixtureDir, err := filepath.Abs("fixtures") + require.NoError(t, err) + edits := map[string]string{"@keydir@": s.gpgHome, "@fixturedir@": fixtureDir} maps.Copy(edits, extraSubstitutions) policyPath := fileFromFixture(t, "fixtures/policy.json", edits) return policyPath @@ -849,6 +852,39 @@ func (s *copySuite) TestCopyDirSignatures() { "--policy", policy, "copy", topDirDest+"/restricted/badidentity", topDirDest+"/dest") } +func (s *copySuite) TestCopySequoiaSignatures() { + t := s.T() + signer, err := simplesequoia.NewSigner(simplesequoia.WithSequoiaHome(testSequoiaHome), simplesequoia.WithKeyFingerprint(testSequoiaKeyFingerprint)) + if err != nil { + t.Skipf("Sequoia not supported: %v", err) + } + signer.Close() + + const ourRegistry = "docker://" + v2DockerRegistryURL + "/" + + dirDest := "dir:" + t.TempDir() + + policy := s.policyFixture(nil) + registriesDir := t.TempDir() + registriesFile := fileFromFixture(t, "fixtures/registries.yaml", + map[string]string{"@lookaside@": t.TempDir(), "@split-staging@": "/var/empty", "@split-read@": "file://var/empty"}) + err = os.Symlink(registriesFile, filepath.Join(registriesDir, "registries.yaml")) + require.NoError(t, err) + + // Sign the images + absSequoiaHome, err := filepath.Abs(testSequoiaHome) + require.NoError(t, err) + t.Setenv("SEQUOIA_HOME", absSequoiaHome) + assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--dest-tls-verify=false", "--sign-by-sq-fingerprint", testSequoiaKeyFingerprint, + testFQIN+":1.26", ourRegistry+"sequoia-no-passphrase") + assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--dest-tls-verify=false", "--sign-by-sq-fingerprint", testSequoiaKeyFingerprintWithPassphrase, + "--sign-passphrase-file", filepath.Join(absSequoiaHome, "with-passphrase.passphrase"), + testFQIN+":1.26.1", ourRegistry+"sequoia-with-passphrase") + // Verify that we can pull them + assertSkopeoSucceeds(t, "", "--policy", policy, "copy", "--src-tls-verify=false", ourRegistry+"sequoia-no-passphrase", dirDest) + assertSkopeoSucceeds(t, "", "--policy", policy, "copy", "--src-tls-verify=false", ourRegistry+"sequoia-with-passphrase", dirDest) +} + // Compression during copy func (s *copySuite) TestCopyCompression() { t := s.T() diff --git a/integration/fixtures/.gitignore b/integration/fixtures/.gitignore new file mode 100644 index 00000000..5dc6c4dd --- /dev/null +++ b/integration/fixtures/.gitignore @@ -0,0 +1 @@ +/data/pgp.cert.d/_sequoia* diff --git a/integration/fixtures/data/keystore/keystore.cookie b/integration/fixtures/data/keystore/keystore.cookie new file mode 100644 index 00000000..e69de29b diff --git a/integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp b/integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp new file mode 100644 index 00000000..86462c6b Binary files /dev/null and b/integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp differ diff --git a/integration/fixtures/data/keystore/softkeys/50DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp b/integration/fixtures/data/keystore/softkeys/50DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp new file mode 100644 index 00000000..4c6ad86f Binary files /dev/null and b/integration/fixtures/data/keystore/softkeys/50DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp differ diff --git a/integration/fixtures/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae b/integration/fixtures/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae new file mode 100644 index 00000000..eb6dd1f4 Binary files /dev/null and b/integration/fixtures/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae differ diff --git a/integration/fixtures/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 b/integration/fixtures/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 new file mode 100644 index 00000000..8dae8b8d Binary files /dev/null and b/integration/fixtures/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 differ diff --git a/integration/fixtures/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 b/integration/fixtures/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 new file mode 100644 index 00000000..b9fee9bb Binary files /dev/null and b/integration/fixtures/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 differ diff --git a/integration/fixtures/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 b/integration/fixtures/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 new file mode 100644 index 00000000..6a58a268 Binary files /dev/null and b/integration/fixtures/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 differ diff --git a/integration/fixtures/data/pgp.cert.d/trust-root b/integration/fixtures/data/pgp.cert.d/trust-root new file mode 100644 index 00000000..addf38a5 Binary files /dev/null and b/integration/fixtures/data/pgp.cert.d/trust-root differ diff --git a/integration/fixtures/data/pgp.cert.d/writelock b/integration/fixtures/data/pgp.cert.d/writelock new file mode 100644 index 00000000..e69de29b diff --git a/integration/fixtures/no-passphrase.pub b/integration/fixtures/no-passphrase.pub new file mode 100644 index 00000000..394d47ec --- /dev/null +++ b/integration/fixtures/no-passphrase.pub @@ -0,0 +1,38 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEaGwFVhYJKwYBBAHaRw8BAQdAZzfnqEAgvE3RoCtPWEOc3Xp8oMURR0qjq+Ru +PHJrc6TCwAsEHxYKAH0FgmhsBVYDCwkHCRD2+Qi2+kiiKUcUAAAAAAAeACBzYWx0 +QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcEjRQtILaFnIhczxeUkcfW0KMHEZ30 +wTdJ1v1iHB7NKQMVCggCmwECHgkWIQRQ3eiY305IdVyMK3r2+Qi2+kiiKQAA86gA +/1ZkXWPHUxh3nQu/EL72ZeP9k/SLWkEuNKs6dJrmRud9AQCHbWwSUwKyt12EFVt/ +QvMFSQ95brUxsWLHgFMPpNfWAc0aU2tvcGVvIFNlcXVvaWEgdGVzdGluZyBrZXnC +wA4EExYKAIAFgmhsBVYDCwkHCRD2+Qi2+kiiKUcUAAAAAAAeACBzYWx0QG5vdGF0 +aW9ucy5zZXF1b2lhLXBncC5vcmctF7xuY06GUyedOGjd2iNKwab85gV64zEAGKgi +ExHRxgMVCggCmQECmwECHgkWIQRQ3eiY305IdVyMK3r2+Qi2+kiiKQAA3SEBAMe1 +y6rWaPjDpkeiDthLV1Umr6NsXVBv/IJTcP9RM4quAQCwmlsdQMddCsc+K3Y5KH88 +saIG0/MRZaPJdsd8vRGUCs4zBGhsBVYWCSsGAQQB2kcPAQEHQLN8yt/21QDMzcB4 +2bzFRg1LpkFZWECjkb2ty7Iju/aOwsC/BBgWCgExBYJobAVWCRD2+Qi2+kiiKUcU +AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmce9QEurrtI24ys +vXssO/40rI5rlsNokEEFr7CVwVgWvAKbAr6gBBkWCgBvBYJobAVWCRB63Ra9Qdgp +tkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcBWCJsdUfj +oYpld4qcYBqjxsyScwpID2vkNlYMLmS+IhYhBKyZqvZ6WI3zgaapXHrdFr1B2Cm2 +AAAEZwEA/UhpNN1XElYx6Xq+JMKlXywoIgButkQy1+H2EcRBeHsBAM7lq8BXvRKz +bDjRlgxiIAYl77p7ihVQ5NYcuZcAlH0CFiEEUN3omN9OSHVcjCt69vkItvpIoikA +AJcwAP9D4spfb28k16w2cemrWAtAE1WUgV8V+OEpE7+gpV+17gEA+0Kzf7jBHgd3 +pBAWwttuRd8OHlZZzKs3f26z28I6mgLOMwRobAVWFgkrBgEEAdpHDwEBB0DPyS14 +jQk1mSWNmuYR4P9M5zOfU2mkhwaqx1l3OWTZD8LAvwQYFgoBMQWCaGwFVgkQ9vkI +tvpIoilHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn+wfK +FmPmtrsi0sY5zIq9KFmbrQyhXz/VZIw6K8D1zdECmyC+oAQZFgoAbwWCaGwFVgkQ +bwujLUxU69BHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn +xF3KXB4+dN9suOhCD2XkYlAWUJ4GVBVV2wAmdQAueyEWIQTv1sMw2eUTIMQmb7Zv +C6MtTFTr0AAA/LYA/iBkRh6dGbp76VzuuHVNUNgTqvXgz9FjizZGJKnVZctXAPwL +TlHxcH6XX96AuiCy9QAMUpm8ZvMu8TAgjgOrlFPKCBYhBFDd6JjfTkh1XIwrevb5 +CLb6SKIpAAA0rQD9HWbBeSoshjH6/k5ntZjOfIAha4/TLlBrMq2w+t4LWD0A/2q5 +DEbYh6PwMidDxXteyHWf4Qnr0vH8vip9d+WHbDYEzjgEaGwFVhIKKwYBBAGXVQEF +AQEHQLxXHw9STOAhb2PLEjrl3uQDwpaXIdigg67vId0jSstVAwEIB8LAAAQYFgoA +cgWCaGwFVgkQ9vkItvpIoilHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p +YS1wZ3Aub3Jn8bvuQCv3uEYJtK6h5y5e4AY9lJtVXx3brexR5bmFCwcCmwwWIQRQ +3eiY305IdVyMK3r2+Qi2+kiiKQAAEzkA/Az97rdlp3hf97S6a5AxU8pTry4gKI63 +lwKtBAT+uF/pAP9lAziQRlNEa1sX6qCXrQqeA/aQ0nj9gRJ1Wvi1PMxWBA== +=7jmE +-----END PGP PUBLIC KEY BLOCK----- diff --git a/integration/fixtures/policy.json b/integration/fixtures/policy.json index eb225649..52506054 100644 --- a/integration/fixtures/policy.json +++ b/integration/fixtures/policy.json @@ -13,6 +13,20 @@ "keyPath": "@keydir@/personal-pubkey.gpg" } ], + "localhost:5555/sequoia-no-passphrase": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "@fixturedir@/no-passphrase.pub" + } + ], + "localhost:5555/sequoia-with-passphrase": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "@fixturedir@/with-passphrase.pub" + } + ], "localhost:5000/myns/extension": [ { "type": "signedBy", diff --git a/integration/fixtures/with-passphrase.passphrase b/integration/fixtures/with-passphrase.passphrase new file mode 100644 index 00000000..94ccc942 --- /dev/null +++ b/integration/fixtures/with-passphrase.passphrase @@ -0,0 +1 @@ +WithPassphrase123 diff --git a/integration/fixtures/with-passphrase.pub b/integration/fixtures/with-passphrase.pub new file mode 100644 index 00000000..7d266b95 --- /dev/null +++ b/integration/fixtures/with-passphrase.pub @@ -0,0 +1,39 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +Dii+5qsdk0HFcgA= +=YS7U +-----END PGP PUBLIC KEY BLOCK----- diff --git a/integration/fixtures_info_test.go b/integration/fixtures_info_test.go index d1048168..2a7cb016 100644 --- a/integration/fixtures_info_test.go +++ b/integration/fixtures_info_test.go @@ -3,4 +3,12 @@ package main const ( // TestImageManifestDigest is the Docker manifest digest of "fixtures/image.manifest.json" TestImageManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55" + + testSequoiaHome = "./fixtures" + // testSequoiaKeyFingerprint is a fingerprint of a test key in testSequoiaHome, generated using + // > sq --home $(pwd)/signature/simplesequoia/testdata key generate --name 'Skopeo Sequoia testing key' --own-key --expiration=never + testSequoiaKeyFingerprint = "50DDE898DF4E48755C8C2B7AF6F908B6FA48A229" + // testSequoiaKeyFingerprintWithPassphrase is a fingerprint of a test key in testSequoiaHome, generated using + // > sq --home $(pwd)/signature/simplesequoia/testdata key generate --name 'Skopeo Sequoia testing key with passphrase' --own-key --expiration=never + testSequoiaKeyFingerprintWithPassphrase = "1F5825285B785E1DB13BF36D2D11A19ABA41C6AE" ) diff --git a/vendor/go.podman.io/image/v5/signature/simplesequoia/mechanism.go b/vendor/go.podman.io/image/v5/signature/simplesequoia/mechanism.go new file mode 100644 index 00000000..9ec71fa7 --- /dev/null +++ b/vendor/go.podman.io/image/v5/signature/simplesequoia/mechanism.go @@ -0,0 +1,52 @@ +//go:build containers_image_sequoia + +package simplesequoia + +// This implements a signature.signingMechanismWithPassphrase that only supports signing. +// +// FIXME: Consider restructuring the simple signing signature creation code path +// not to require this indirection and all those unimplemented methods. + +import ( + "go.podman.io/image/v5/signature/internal/sequoia" +) + +// A GPG/OpenPGP signing mechanism, implemented using Sequoia. +type sequoiaSigningOnlyMechanism struct { + inner *sequoia.SigningMechanism +} + +func (m *sequoiaSigningOnlyMechanism) Close() error { + panic("Should never be called") +} + +// SupportsSigning returns nil if the mechanism supports signing, or a SigningNotSupportedError. +func (m *sequoiaSigningOnlyMechanism) SupportsSigning() error { + panic("Should never be called") +} + +// Sign creates a (non-detached) signature of input using keyIdentity and passphrase. +// Fails with a SigningNotSupportedError if the mechanism does not support signing. +func (m *sequoiaSigningOnlyMechanism) SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) { + return m.inner.SignWithPassphrase(input, keyIdentity, passphrase) +} + +// Sign creates a (non-detached) signature of input using keyIdentity. +// Fails with a SigningNotSupportedError if the mechanism does not support signing. +func (m *sequoiaSigningOnlyMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) { + panic("Should never be called") +} + +// Verify parses unverifiedSignature and returns the content and the signer's identity +func (m *sequoiaSigningOnlyMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) { + panic("Should never be called") +} + +// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION, +// along with a short identifier of the key used for signing. +// WARNING: The short key identifier (which corresponds to "Key ID" for OpenPGP keys) +// is NOT the same as a "key identity" used in other calls to this interface, and +// the values may have no recognizable relationship if the public key is not available. +func (m *sequoiaSigningOnlyMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) { + panic("Should never be called") +} diff --git a/vendor/go.podman.io/image/v5/signature/simplesequoia/options.go b/vendor/go.podman.io/image/v5/signature/simplesequoia/options.go new file mode 100644 index 00000000..9e3448d7 --- /dev/null +++ b/vendor/go.podman.io/image/v5/signature/simplesequoia/options.go @@ -0,0 +1,37 @@ +package simplesequoia + +import ( + "errors" + "strings" +) + +type Option func(*simpleSequoiaSigner) error + +// WithSequoiaHome returns an Option for NewSigner, specifying a Sequoia home directory to use. +func WithSequoiaHome(sequoiaHome string) Option { + return func(s *simpleSequoiaSigner) error { + s.sequoiaHome = sequoiaHome + return nil + } +} + +// WithKeyFingerprint returns an Option for NewSigner, specifying a key to sign with, using the provided Sequoia-PGP key fingerprint. +func WithKeyFingerprint(keyFingerprint string) Option { + return func(s *simpleSequoiaSigner) error { + s.keyFingerprint = keyFingerprint + return nil + } +} + +// WithPassphrase returns an Option for NewSigner, specifying a passphrase for the private key. +func WithPassphrase(passphrase string) Option { + return func(s *simpleSequoiaSigner) error { + // The gpgme implementation can’t use passphrase with \n; reject it here for consistent behavior. + // FIXME: We don’t need it in this API at all, but the "\n" check exists in the current call stack. That should go away. + if strings.Contains(passphrase, "\n") { + return errors.New("invalid passphrase: must not contain a line break") + } + s.passphrase = passphrase + return nil + } +} diff --git a/vendor/go.podman.io/image/v5/signature/simplesequoia/signer.go b/vendor/go.podman.io/image/v5/signature/simplesequoia/signer.go new file mode 100644 index 00000000..f4f9b870 --- /dev/null +++ b/vendor/go.podman.io/image/v5/signature/simplesequoia/signer.go @@ -0,0 +1,88 @@ +//go:build containers_image_sequoia + +package simplesequoia + +import ( + "context" + "errors" + "fmt" + + "go.podman.io/image/v5/docker/reference" + internalSig "go.podman.io/image/v5/internal/signature" + internalSigner "go.podman.io/image/v5/internal/signer" + "go.podman.io/image/v5/signature" + "go.podman.io/image/v5/signature/internal/sequoia" + "go.podman.io/image/v5/signature/signer" +) + +// simpleSequoiaSigner is a signer.SignerImplementation implementation for simple signing signatures using Sequoia. +type simpleSequoiaSigner struct { + mech *sequoia.SigningMechanism + sequoiaHome string // "" if using the system’s default + keyFingerprint string + passphrase string // "" if not provided. +} + +// NewSigner returns a signature.Signer which creates “simple signing” signatures using the user’s default +// Sequoia PGP configuration. +// +// The set of options must identify a key to sign with, probably using a WithKeyFingerprint. +// +// The caller must call Close() on the returned Signer. +func NewSigner(opts ...Option) (*signer.Signer, error) { + s := simpleSequoiaSigner{} + for _, o := range opts { + if err := o(&s); err != nil { + return nil, err + } + } + if s.keyFingerprint == "" { + return nil, errors.New("no key identity provided for simple signing") + } + + if err := sequoia.Init(); err != nil { + return nil, err // Coverage: This is impractical to test in-process, with the static go_sequoia_dlhandle. + } + mech, err := sequoia.NewMechanismFromDirectory(s.sequoiaHome) + if err != nil { + return nil, fmt.Errorf("initializing Sequoia: %w", err) + } + s.mech = mech + succeeded := false + defer func() { + if !succeeded { + s.mech.Close() // Coverage: This is currently unreachable. + } + }() + + // Ideally, we should look up (and unlock?) the key at this point already. FIXME: is that possible? Anyway, low-priority. + + succeeded = true + return internalSigner.NewSigner(&s), nil +} + +// ProgressMessage returns a human-readable sentence that makes sense to write before starting to create a single signature. +func (s *simpleSequoiaSigner) ProgressMessage() string { + return "Signing image using Sequoia-PGP simple signing" +} + +// SignImageManifest creates a new signature for manifest m as dockerReference. +func (s *simpleSequoiaSigner) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (internalSig.Signature, error) { + if reference.IsNameOnly(dockerReference) { + return nil, fmt.Errorf("reference %s can’t be signed, it has neither a tag nor a digest", dockerReference.String()) + } + wrapped := sequoiaSigningOnlyMechanism{ + inner: s.mech, + } + simpleSig, err := signature.SignDockerManifestWithOptions(m, dockerReference.String(), &wrapped, s.keyFingerprint, &signature.SignOptions{ + Passphrase: s.passphrase, + }) + if err != nil { + return nil, err + } + return internalSig.SimpleSigningFromBlob(simpleSig), nil +} + +func (s *simpleSequoiaSigner) Close() error { + return s.mech.Close() +} diff --git a/vendor/go.podman.io/image/v5/signature/simplesequoia/signer_stub.go b/vendor/go.podman.io/image/v5/signature/simplesequoia/signer_stub.go new file mode 100644 index 00000000..0abb6c05 --- /dev/null +++ b/vendor/go.podman.io/image/v5/signature/simplesequoia/signer_stub.go @@ -0,0 +1,28 @@ +//go:build !containers_image_sequoia + +package simplesequoia + +import ( + "errors" + + "go.podman.io/image/v5/signature/signer" +) + +// simpleSequoiaSigner is a signer.SignerImplementation implementation for simple signing signatures using Sequoia. +type simpleSequoiaSigner struct { + // This is not really used, we just keep the struct fields so that the With… Option functions can be compiled. + + sequoiaHome string // "" if using the system's default + keyFingerprint string + passphrase string // "" if not provided. +} + +// NewSigner returns a signature.Signer which creates "simple signing" signatures using the user's default +// Sequoia PGP configuration. +// +// The set of options must identify a key to sign with, probably using a WithKeyFingerprint. +// +// The caller must call Close() on the returned Signer. +func NewSigner(opts ...Option) (*signer.Signer, error) { + return nil, errors.New("Sequoia-PGP support is not enabled in this build") +} diff --git a/vendor/modules.txt b/vendor/modules.txt index fa71afbc..1b843cbd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -431,6 +431,7 @@ go.podman.io/image/v5/signature/sigstore go.podman.io/image/v5/signature/sigstore/fulcio go.podman.io/image/v5/signature/sigstore/internal go.podman.io/image/v5/signature/sigstore/rekor +go.podman.io/image/v5/signature/simplesequoia go.podman.io/image/v5/signature/simplesigning go.podman.io/image/v5/storage go.podman.io/image/v5/tarball