From f59e8938db952f6bf454c9cdd520dcfbce64fcc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 8 Jul 2025 20:13:34 +0200 Subject: [PATCH] Add --sign-by-sq-fingerprint and an integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/skopeo/utils.go | 33 ++++++- cmd/skopeo/utils_nosequoia_test.go | 5 + cmd/skopeo/utils_sequoia_test.go | 5 + cmd/skopeo/utils_test.go | 37 +++++++- docs/skopeo-copy.1.md | 7 +- docs/skopeo-sync.1.md | 7 +- integration/copy_test.go | 38 +++++++- integration/fixtures/.gitignore | 1 + .../fixtures/data/keystore/keystore.cookie | 0 ...5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp | Bin 0 -> 2170 bytes ...DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp | Bin 0 -> 1970 bytes .../1f/5825285b785e1db13bf36d2d11a19aba41c6ae | Bin 0 -> 2037 bytes .../4d/8bcd544b7573eefaad18c278473e5f255d10b8 | Bin 0 -> 492 bytes .../50/dde898df4e48755c8c2b7af6f908b6fa48a229 | Bin 0 -> 2021 bytes .../68/de230c4a009f5ee5fbb27984642d0130b86046 | Bin 0 -> 696 bytes .../fixtures/data/pgp.cert.d/trust-root | Bin 0 -> 529 bytes .../fixtures/data/pgp.cert.d/writelock | 0 integration/fixtures/no-passphrase.pub | 38 ++++++++ integration/fixtures/policy.json | 14 +++ .../fixtures/with-passphrase.passphrase | 1 + integration/fixtures/with-passphrase.pub | 39 ++++++++ integration/fixtures_info_test.go | 8 ++ .../v5/signature/simplesequoia/mechanism.go | 52 +++++++++++ .../v5/signature/simplesequoia/options.go | 37 ++++++++ .../v5/signature/simplesequoia/signer.go | 88 ++++++++++++++++++ .../v5/signature/simplesequoia/signer_stub.go | 28 ++++++ vendor/modules.txt | 1 + 27 files changed, 431 insertions(+), 8 deletions(-) create mode 100644 cmd/skopeo/utils_nosequoia_test.go create mode 100644 cmd/skopeo/utils_sequoia_test.go create mode 100644 integration/fixtures/.gitignore create mode 100644 integration/fixtures/data/keystore/keystore.cookie create mode 100644 integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp create mode 100644 integration/fixtures/data/keystore/softkeys/50DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp create mode 100644 integration/fixtures/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae create mode 100644 integration/fixtures/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 create mode 100644 integration/fixtures/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 create mode 100644 integration/fixtures/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 create mode 100644 integration/fixtures/data/pgp.cert.d/trust-root create mode 100644 integration/fixtures/data/pgp.cert.d/writelock create mode 100644 integration/fixtures/no-passphrase.pub create mode 100644 integration/fixtures/with-passphrase.passphrase create mode 100644 integration/fixtures/with-passphrase.pub create mode 100644 vendor/go.podman.io/image/v5/signature/simplesequoia/mechanism.go create mode 100644 vendor/go.podman.io/image/v5/signature/simplesequoia/options.go create mode 100644 vendor/go.podman.io/image/v5/signature/simplesequoia/signer.go create mode 100644 vendor/go.podman.io/image/v5/signature/simplesequoia/signer_stub.go 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 0000000000000000000000000000000000000000..86462c6b64ac57df02c285e5da86fc8585d09eef GIT binary patch literal 2170 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eb6dd1f4cc25a75626a4f1350ad539ee06804630 GIT binary patch literal 2037 zcmbuAdpOg39LK-EnQUu`bevR}N->69N*CFja!ZIQoWf*nZf$LIY)SRR;?TvVQmHm7 zQ$(iFjhS#x$K=vQ$*n~p<&r!yc1Fkf<2>~oJ$?WGem>vV=ly-ZKUEfRYM641CemmY z1czQY%!MEod}c>wuSpyK_4(VSG7v*{vwEYBYRZK%bgO$?N^X?Yh()vEx|%2ut$cX$ zveIm%3Q`THo^kFDp(@W|KA1c@0K+7OvG99nED|f2wufoLB=3!&1(R@e3f+XpplsEy zYfg#WEx2jZ9iMAjTqqU}IDK3y&8?)>7|`AeOO<{?AzW=_ zC(UXOwk7bvk{(Q4-*Pru*IeiVBv{W-8l6nTcz$^xhDBzwg7;7`p=1swGMGih&`C@t zoys6F$zqWzTth*A=tsy!B)Xl8jWWficc1;q1s46bcHv`KzUA$H@?hj1%|X>E^0UzY zz+dhLV=5NLt)_1pWe#rPZW9MgCpH!2ZGuMeQB3M;(cLb4PcX5jXURLMaHy-jbg%Y_ zXwOWwS?cdROEnOf5`EK`S*|K#5ge|j0Q&6<5G)obapi2J8rxxmzy40Ol9{UMo^%kN zm4CTo;~n>#ypC}}{lLTcqg3~2E@t}emmlpCC^X!XC?Rf+V7~u+eSvtoJOJ0c!MMiV zg6*T5M|j!M&ZI!;{)|V$bQ5={(bRdF%^AuV@?!`b3o#eQxZch8vR`;VMw)&vyol6T zZDp4M-_Tv2bMfk2u?S3IF#g3rs>KH3AhVJH`gGn7K*{_y4i~%f!r7!SL7DpR zzPLP(PquM82Ds`BAb0ODb?t0ht3KDkD2tsALh4>H5t}bu+QdEdS)eCvgx7w}i4xA? z_TpQLjuRZyPi_l`duzaZimJTcB!?hGYgf`uR*nofOZTwz$rgL5Gk)4t= z7Iu_kQJ{(*U*vx;5As*F?S>`v%R1UL@(7B#>YOuKwcat}j>Pm?#&mfAJd7Z~cQLUJ zB7R6fcsf@mEKD7c_PMKwu>v*aa_V0=Z-Nz&hkAu+4W($GT5*YXd{no-DoskR&m*kG zs*j&YNb@7Ad^0^z=OIu!YdD{Rl(^~XynFikHBI5`I`sBF{ z#g%cWQ{AsxeO9bmTo=NNjlA~iIoTbNbIBeCzAD?}(`aU()d=~H?4{iSv5w<0(`A`kz+v+Mln}OWM-VTv+Uy) z)D!x2c>sKg5V+;*b?LfU;^>(pEeAbghwe%Jb;Td9w)*c{21Pht=MW(9HpFtY=&3c+ zFsWikva!COX1Sp+rwtd+MfMx3G*eLDl#Lq$CXCWZgdPqvc)npa+AFIJs^AYA51ea< zr}CWlw~y)2A;8rOn8Tc`b3+LI|N#p;Bu-wfE+sruJT% z7oY(S2 zhMXR)*j6*_bK=6d`GrE8x^dAFAop#52HU!i*6{P6RtAfn1ex6K+!s)wYEashqn%2q HWPrZ`MD0XQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8dae8b8d8bd6ffb3e6f1c662c2fbd869681a2173 GIT binary patch literal 492 zcmX?R%wki*utSVfn~jl$@s>M3BO|-Rn&RZ}HQTp&SNFcTZV|O&Q*w$`=S+jQc|z5{ zUYPI67dv!-k40XLi=lzF38a~+g@Kuylbutb!rd-jHCAAUy9fga$T27sC+3tm~P(OU2?Ymy3E$109C9R|6L zXIah)_~a)i<|u>|l@^yM1m)+K96BJ#A`J9mGlgE%3H-|uCRSKh(691w=JXFi>-aKX z`j;~>CS>V8U-whx5ZH?|8437u!*quKY1_K={olQn&ERJ~s5SqKWzXKrJJxreS140n uS?2A3XCouSf$OEGv|^UVPjdcdJz>f0BhoSxE`Bcj%Q0JBM?rb*TTTF$Qn%0m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b9fee9bb2484133d7f5d83134b3e3c053ecbd9dd GIT binary patch literal 2021 zcmbuAc{tR09LK-E@pFusV#p*LjiYj87Fk!tsCFnv7{@3~jtR}|jB;N~mb8kcWI{w_ z%N;p#Wu};Nh9^fJRIY5hV?81(%t~wj*r%S=)A#@H`}2K$-rx7<)4Uh9qX z3!sp~Nb&ipGV0UwSW!1=n;{Rj-EpR1oYli`c^rJnI;db_Ioc>3U(XFcKg zE$X<+nClmuI=3b#aeJTU2xBtdvUQL2F1u})YgZAF5*0zDLkKw}Rvxw*dVOug($wGS zl3K`mApmA?1HQe7vpbh09hiZ7Al@)f9bLZi_Tq74ymoxw+rW!ERSc4&B*`xfDh6;al&GD~3R6+6h?#*IATns1~5F9I^lIO`)Dy3^nIT z05_~2lPt<Y2sGc3U%lU*2MoG-q8NfQPnVNo?5OPsB4xr3BNSH2>`b z!=^JFGu2>~)T%4XkJy_MQy`F_b$NO~jVm-8lRL~=Q9`klw*M^KCT(LBX7SpL5Dqwv zU=8E!zZgjIXb*KrqxvLqqX0eW!^==NGPxMe@3jCAr2h>b5Nuv-xGFt-MtP zuVF>>QEnx=y4r4bZ1gf% z#!i^S(PQQ!uWYh&Wx_A_?VZ$3C)2yxtDr$QQ(IJYQ!lobG@ z@~~Fl%X!Qr-+Sd6*Jt!XeUFy#GvpoFgtI*x7-xgP|?3iAJ&ZZbW^SET7);QPRg-FN!*ES|9N8Q}^&C`wA_Q<)#NzTx3 zgkJsnpcr+`%?fLkDP${UHwbq51AM6bYG0>&A8HJYOX42wVpgup1F%#HR&X9fxr}W! zp;J!0YJUr>e#!i~7oA=17j=kAcb-vO9l#RG(t5_{M;2u`vIbW+2hcXDb*E!;#r`60!lt#$&Ci13F|vl=<@wG$j#L92x{-DiO74!*^_ve z7eWV;)9om-gGIkvkKd0$2_w?cpM{sy2TSN>UKRD%Aj-RYHMB%cQVXdBk5f*lVj+rP rc%_;$1^83}y+k7EFzv+F-?CUmqL~8oiQPfV=Op}(FI4Prvxol%@?bOI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a58a268c8f53a2ee18e5a387a2a420fdb94b5df GIT binary patch literal 696 zcmX?R%wki*utSVfn~jl$@s>M3BO|**^?8Z^(**;Vd35-mm%W%=(;+l7ajoGjeW~S+ z#}8lbZa8#+k40XLi=lzF38a~+g@KuylbutbB}JFfU`K+Ry9fga$T27sC+3tmF-TD8fie=9Cs4e|xS}Y&$FKj)yX7zE74-U^- zm(5@~E9#z_ms*rqlA5BBpQn(USyH5%o0xp)fFO%7(4Wl|`t$FrX!#9|le)QtZlAt; z&v4x~%?VlhYQG)5%R28@Fuy$x_UBAS0$$x6$jGq&3`5rJ84{nKU1V=EdDD4Srr$tr zpIpP^H?JN|e*HwFn2}*h=D|9BY3;+A^W}N&Dl{d2O}d^hcW|R(_QYe1PZB!V4jo`& z5diwB4B@BN|BRdh74CNNs<8q)h>g3e@5`pPPH*YhWW7<{`Cg6c+=mt)I_M0 y$&E9&eAD;9$efYk>XgI>9FFqZANu4>Hzuz>VfKI1;eDdBJ|tRuacL{0asU80_Yfrj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..addf38a5618ffc2f03e6a94c693942fc3ceaf5a2 GIT binary patch literal 529 zcmX>a!D3UwutSVfn~jl$@s>M3BO|-Rn&RZ}HQTp&SNFcTZV|O&Q*w$`=S+jQc|z5{ zUYPI67h_=fpM3dT`I$H3_k=!9o%7^i{pXJ%NnTGk-1a!b8~wn~EVoj4=Ai?8Eb?Mp z3=OPJARCxk7?`;^**OI&-0k93V+D4&i!gwI9D_n}Vor%eUVcepNoIatv0ia%VQGG5 zqHaNYfnI)5y6wlyMWKr#ILeI+_*-i|nz$cNcpzn1AY)>GNI3MVVhyt>7YEa9MkYB< zF+~>N?z17@rN!@lt(783_h8x+tp)$GCaJJ>tm0_XVUXK+mgTH~PkwS@jzUOL zX>o}{P=0>Np#y>}!ay%JQ|LvVz`q<}VufV|{VE@4PX7?JjxY12e>nqVLYD6Hbw5=O zfxS4Bk$^8ZOlSC?wyjIw|J_^J41VT=TJyhH_Uyg9V}0j&g)-%pW#0aGHZn3CxL$fn iD`si@B 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