From 9e2850d0a8343a7f92b9050b5444171bf25fbd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Fri, 11 Jul 2025 17:54:50 +0200 Subject: [PATCH] Add --sign-by-sq-fingerprint to push operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new feature that allows signing using Sequoia-backed keys. The existing options to sign using GPG-backed keys (and sigstore) remain unchanged, and continue to use the same backends as usual. Signed-off-by: Miloslav Trmač --- cmd/podman/common/sign.go | 49 +++++++++- .../options/sign-by-sq-fingerprint.md | 8 ++ .../markdown/options/sign-passphrase-file.md | 2 +- .../markdown/podman-artifact-push.1.md.in | 3 +- .../markdown/podman-manifest-push.1.md.in | 2 + docs/source/markdown/podman-push.1.md.in | 2 + test/e2e/common_test.go | 13 ++- test/e2e/push_test.go | 54 +++++++++-- test/e2e/save_test.go | 2 +- .../testdata/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 test/e2e/testdata/data/pgp.cert.d/trust-root | Bin 0 -> 529 bytes test/e2e/testdata/data/pgp.cert.d/writelock | 0 test/e2e/testdata/sequoia-key.pub | 38 ++++++++ .../sigstore-registries.d-fragment.yaml | 2 + .../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 + 25 files changed, 365 insertions(+), 16 deletions(-) create mode 100644 docs/source/markdown/options/sign-by-sq-fingerprint.md create mode 100644 test/e2e/testdata/data/keystore/keystore.cookie create mode 100644 test/e2e/testdata/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp create mode 100644 test/e2e/testdata/data/keystore/softkeys/50DDE898DF4E48755C8C2B7AF6F908B6FA48A229.pgp create mode 100644 test/e2e/testdata/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae create mode 100644 test/e2e/testdata/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 create mode 100644 test/e2e/testdata/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 create mode 100644 test/e2e/testdata/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 create mode 100644 test/e2e/testdata/data/pgp.cert.d/trust-root create mode 100644 test/e2e/testdata/data/pgp.cert.d/writelock create mode 100644 test/e2e/testdata/sequoia-key.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/podman/common/sign.go b/cmd/podman/common/sign.go index 57d1ed617c..b9551a866e 100644 --- a/cmd/podman/common/sign.go +++ b/cmd/podman/common/sign.go @@ -12,13 +12,15 @@ import ( "go.podman.io/image/v5/pkg/cli" "go.podman.io/image/v5/pkg/cli/sigstore" "go.podman.io/image/v5/signature/signer" + "go.podman.io/image/v5/signature/simplesequoia" ) // SigningCLIOnlyOptions contains signing-related CLI options. // Some other options are defined in entities.ImagePushOptions. type SigningCLIOnlyOptions struct { - signPassphraseFile string - signBySigstoreParamFile string + signPassphraseFile string + signBySequoiaFingerprint string + signBySigstoreParamFile string } func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, pushOpts *entities.ImagePushOptions) { @@ -28,6 +30,10 @@ func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, push flags.StringVar(&pushOpts.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key") _ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone) + signBySequoiaFingerprintFlagName := "sign-by-sq-fingerprint" + flags.StringVar(&cliOpts.signBySequoiaFingerprint, signBySequoiaFingerprintFlagName, "", "Sign the image using a Sequoia-PGP key with the specified `FINGERPRINT`") + _ = cmd.RegisterFlagCompletionFunc(signBySequoiaFingerprintFlagName, completion.AutocompleteNone) + signBySigstoreFlagName := "sign-by-sigstore" flags.StringVar(&cliOpts.signBySigstoreParamFile, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`") _ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault) @@ -42,6 +48,7 @@ func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, push if registry.IsRemote() { _ = flags.MarkHidden(signByFlagName) + _ = flags.MarkHidden(signBySequoiaFingerprintFlagName) _ = flags.MarkHidden(signBySigstoreFlagName) _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName) _ = flags.MarkHidden(signPassphraseFileFlagName) @@ -57,8 +64,20 @@ func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnly // 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 cliOpts.signPassphraseFile != "" && pushOpts.SignBy != "" && pushOpts.SignBySigstorePrivateKeyFile != "" { - return nil, fmt.Errorf("only one of --sign-by and sign-by-sigstore-private-key can be used with --sign-passphrase-file") + if cliOpts.signPassphraseFile != "" { + count := 0 + if pushOpts.SignBy != "" { + count++ + } + if cliOpts.signBySequoiaFingerprint != "" { + count++ + } + if pushOpts.SignBySigstorePrivateKeyFile != "" { + count++ + } + if count > 1 { + return 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 @@ -72,9 +91,16 @@ func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnly p := ssh.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. + // With signBySequoiaFingerprint, we don’t prompt for a passphrase (for now??): We don’t know whether the key requires a passphrase. pushOpts.SignPassphrase = passphrase pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase) cleanup := signingCleanup{} + succeeded := false + defer func() { + if !succeeded { + cleanup.cleanup() + } + }() if cliOpts.signBySigstoreParamFile != "" { signer, err := sigstore.NewSignerFromParameterFile(cliOpts.signBySigstoreParamFile, &sigstore.Options{ PrivateKeyPassphrasePrompt: cli.ReadPassphraseFile, @@ -87,6 +113,21 @@ func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnly pushOpts.Signers = append(pushOpts.Signers, signer) cleanup.signers = append(cleanup.signers, signer) } + if cliOpts.signBySequoiaFingerprint != "" { + opts := []simplesequoia.Option{ + simplesequoia.WithKeyFingerprint(cliOpts.signBySequoiaFingerprint), + } + if passphrase != "" { + opts = append(opts, simplesequoia.WithPassphrase(passphrase)) + } + signer, err := simplesequoia.NewSigner(opts...) + if err != nil { + return nil, fmt.Errorf("error using --sign-by-sq-fingerprint: %w", err) + } + pushOpts.Signers = append(pushOpts.Signers, signer) + cleanup.signers = append(cleanup.signers, signer) + } + succeeded = true return cleanup.cleanup, nil } diff --git a/docs/source/markdown/options/sign-by-sq-fingerprint.md b/docs/source/markdown/options/sign-by-sq-fingerprint.md new file mode 100644 index 0000000000..9f3ddf3788 --- /dev/null +++ b/docs/source/markdown/options/sign-by-sq-fingerprint.md @@ -0,0 +1,8 @@ +####> This option file is used in: +####> podman artifact push, manifest push, push +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--sign-by-sq-fingerprint**=*fingerprint* + +Add a “simple signing” signature using a Sequoia-PGP key with the specified fingerprint. +(This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) diff --git a/docs/source/markdown/options/sign-passphrase-file.md b/docs/source/markdown/options/sign-passphrase-file.md index f25233ac1d..d30393a28e 100644 --- a/docs/source/markdown/options/sign-passphrase-file.md +++ b/docs/source/markdown/options/sign-passphrase-file.md @@ -4,4 +4,4 @@ ####> are applicable to all of those. #### **--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. +If signing the image (using **--sign-by**, **sign-by-sq-fingerprint** or **--sign-by-sigstore-private-key**), read the passphrase to use from the specified path. diff --git a/docs/source/markdown/podman-artifact-push.1.md.in b/docs/source/markdown/podman-artifact-push.1.md.in index a1dc7568dd..2157911ce9 100644 --- a/docs/source/markdown/podman-artifact-push.1.md.in +++ b/docs/source/markdown/podman-artifact-push.1.md.in @@ -38,11 +38,12 @@ Add a “simple signing” signature at the destination using the specified key. @@option sign-by-sigstore - #### **--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) +@@option sign-by-sq-fingerprint + @@option sign-passphrase-file @@option tls-verify diff --git a/docs/source/markdown/podman-manifest-push.1.md.in b/docs/source/markdown/podman-manifest-push.1.md.in index 47ced3c1b6..a4b2ba118e 100644 --- a/docs/source/markdown/podman-manifest-push.1.md.in +++ b/docs/source/markdown/podman-manifest-push.1.md.in @@ -70,6 +70,8 @@ Sign the pushed images with a “simple signing” signature using the specified 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) +@@option sign-by-sq-fingerprint + @@option sign-passphrase-file @@option tls-verify diff --git a/docs/source/markdown/podman-push.1.md.in b/docs/source/markdown/podman-push.1.md.in index 093182f048..dbcf61fd01 100644 --- a/docs/source/markdown/podman-push.1.md.in +++ b/docs/source/markdown/podman-push.1.md.in @@ -98,6 +98,8 @@ Add a “simple signing” signature at the destination using the specified key. 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) +@@option sign-by-sq-fingerprint + @@option sign-passphrase-file @@option tls-verify diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 587b273284..e04c6b148e 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -1309,8 +1309,8 @@ func (p *PodmanTestIntegration) removeNetwork(name string) { // generatePolicyFile generates a signature verification policy file. // it returns the policy file path. -func generatePolicyFile(tempDir string, port int) string { - keyPath := filepath.Join(tempDir, "key.gpg") +func generatePolicyFile(tempDir string, port int, sequoiaKeyPath string) string { + gpgKeyPath := filepath.Join(tempDir, "key.gpg") policyPath := filepath.Join(tempDir, "policy.json") conf := fmt.Sprintf(` { @@ -1339,11 +1339,18 @@ func generatePolicyFile(tempDir string, port int) string { "type": "sigstoreSigned", "keyPath": "testdata/sigstore-key.pub" } + ], + "localhost:%[1]d/simple-sq-signed": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "%[3]s" + } ] } } } -`, port, keyPath) +`, port, gpgKeyPath, sequoiaKeyPath) writeConf([]byte(conf), policyPath) return policyPath } diff --git a/test/e2e/push_test.go b/test/e2e/push_test.go index d9e37cc068..01fdbf3969 100644 --- a/test/e2e/push_test.go +++ b/test/e2e/push_test.go @@ -3,9 +3,9 @@ package integration import ( + "bytes" "fmt" "os" - "os/exec" "path/filepath" "strings" @@ -13,9 +13,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" + "go.podman.io/image/v5/signature/simplesequoia" "go.podman.io/storage/pkg/archive" ) +// testSequoiaKeyFingerprint is a fingerprint of a test Sequoia key in testdata. +const testSequoiaKeyFingerprint = "50DDE898DF4E48755C8C2B7AF6F908B6FA48A229" + var _ = Describe("Podman push", func() { BeforeEach(func() { @@ -235,22 +239,26 @@ var _ = Describe("Podman push", func() { Expect(push2).Should(ExitCleanly()) if !IsRemote() { // Remote does not support signing - By("pushing and pulling with --sign-by-sigstore-private-key") // 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. + lookasideDir, err := filepath.Abs(filepath.Join(podmanTest.TempDir, "test-lookaside")) + Expect(err).ToNot(HaveOccurred()) 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() + registriesDFragment, err := os.ReadFile("testdata/sigstore-registries.d-fragment.yaml") + Expect(err).ToNot(HaveOccurred()) + registriesDFragment = bytes.ReplaceAll(registriesDFragment, []byte("@lookasideDir@"), []byte(lookasideDir)) + err = os.WriteFile(systemRegistriesDAddition, registriesDFragment, 0644) if err != nil { - GinkgoWriter.Printf("Skipping sigstore tests because /etc/containers/registries.d isn’t writable: %s\n", string(output)) + GinkgoWriter.Printf("Skipping sigstore tests because /etc/containers/registries.d isn’t writable: %s\n", err) } else { + By("pushing and pulling with --sign-by-sigstore-private-key") defer func() { err := os.Remove(systemRegistriesDAddition) Expect(err).ToNot(HaveOccurred()) }() // Generate a signature verification policy file - policyPath := generatePolicyFile(podmanTest.TempDir, 5003) + policyPath := generatePolicyFile(podmanTest.TempDir, 5003, "testdata/sequoia-key.pub") defer os.Remove(policyPath) // Verify that the policy rejects unsigned images @@ -289,6 +297,40 @@ var _ = Describe("Podman push", func() { pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5003/sigstore-signed-params"}) pull.WaitWithDefaultTimeout() Expect(pull).Should(ExitCleanly()) + + signer, err := simplesequoia.NewSigner( + simplesequoia.WithSequoiaHome("testdata"), + simplesequoia.WithKeyFingerprint(testSequoiaKeyFingerprint), + ) + if err != nil { + GinkgoWriter.Printf("Skipping Sequoia tests because simplesequoia.NewSigner failed: %s\n", err) + } else { + signer.Close() + + By("pushing and pulling with --sign-by-sq-fingerprint") + absSequoiaHome, err := filepath.Abs("testdata") + Expect(err).ToNot(HaveOccurred()) + defer os.Unsetenv("SEQUOIA_HOME") + os.Setenv("SEQUOIA_HOME", absSequoiaHome) + + // Verify that the policy rejects unsigned images + push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5003/simple-sq-signed"}) + push.WaitWithDefaultTimeout() + Expect(push).Should(ExitCleanly()) + + pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5003/simple-sq-signed"}) + pull.WaitWithDefaultTimeout() + Expect(pull).To(ExitWithError(125, "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-sq-fingerprint", testSequoiaKeyFingerprint, ALPINE, "localhost:5003/simple-sq-signed"}) + push.WaitWithDefaultTimeout() + Expect(push).Should(ExitCleanly()) + + pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5003/simple-sq-signed"}) + pull.WaitWithDefaultTimeout() + Expect(pull).Should(ExitCleanly()) + } } } }) diff --git a/test/e2e/save_test.go b/test/e2e/save_test.go index b4dbecc54c..0ad7101a60 100644 --- a/test/e2e/save_test.go +++ b/test/e2e/save_test.go @@ -189,7 +189,7 @@ default-docker: if !IsRemote() { // Generate a signature verification policy file - policyPath := generatePolicyFile(podmanTest.TempDir, port) + policyPath := generatePolicyFile(podmanTest.TempDir, port, "testdata/sequoia-key.pub") defer os.Remove(policyPath) session = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, pushedImage}) diff --git a/test/e2e/testdata/data/keystore/keystore.cookie b/test/e2e/testdata/data/keystore/keystore.cookie new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/e2e/testdata/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp b/test/e2e/testdata/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/test/e2e/testdata/data/pgp.cert.d/1f/5825285b785e1db13bf36d2d11a19aba41c6ae b/test/e2e/testdata/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/test/e2e/testdata/data/pgp.cert.d/4d/8bcd544b7573eefaad18c278473e5f255d10b8 b/test/e2e/testdata/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/test/e2e/testdata/data/pgp.cert.d/50/dde898df4e48755c8c2b7af6f908b6fa48a229 b/test/e2e/testdata/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/test/e2e/testdata/data/pgp.cert.d/68/de230c4a009f5ee5fbb27984642d0130b86046 b/test/e2e/testdata/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/test/e2e/testdata/data/pgp.cert.d/trust-root b/test/e2e/testdata/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