diff --git a/.cirrus.yml b/.cirrus.yml index aa99e87dd4..c3bf0765d5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -21,8 +21,14 @@ env: SCRIPT_BASE: "./contrib/cirrus" # Google-cloud VM Images - IMAGE_SUFFIX: "c20250721t181111z-f42f41d13" + # If you are updating IMAGE_SUFFIX: We are currently using rawhide for + # the containers_image_sequoia tests because the rust-podman-sequoia + # package is not available in earlier releases; once we update to a future + # Fedora release (or if the package is backported), switch back from Rawhide + # to the latest Fedora release. + IMAGE_SUFFIX: "c20250910t092246z-f42f41d13" FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" + RAWHIDE_CACHE_IMAGE_NAME: "rawhide-${IMAGE_SUFFIX}" # Container FQIN's FEDORA_CONTAINER_FQIN: "quay.io/libpod/fedora_podman:${IMAGE_SUFFIX}" @@ -190,14 +196,21 @@ test_skopeo_task: # Required to be 200gig, do not modify - has i/o performance impact # according to gcloud CLI tool warning messages. disk: 200 - image_name: ${FEDORA_CACHE_IMAGE_NAME} + image_name: ${VM_IMAGE_NAME} matrix: - name: "Skopeo Test" # N/B: Name ref. by hack/get_fqin.sh env: BUILDTAGS: '' + VM_IMAGE_NAME: ${FEDORA_CACHE_IMAGE_NAME} - name: "Skopeo Test w/ opengpg" env: BUILDTAGS: *withopengpg + VM_IMAGE_NAME: ${FEDORA_CACHE_IMAGE_NAME} + - name: "Skopeo test w/ Sequoia (currently Rawhide)" + env: + BUILDTAGS: 'containers_image_sequoia' + # If you are removing the use of rawhide, also remove the VM_IMAGE_NAME condition from runner.sh . + VM_IMAGE_NAME: ${RAWHIDE_CACHE_IMAGE_NAME} setup_script: >- "${GOSRC}/${SCRIPT_BASE}/runner.sh" setup vendor_script: >- @@ -226,6 +239,7 @@ meta_task: # Space-separated list of images used by this repository state IMGNAMES: | ${FEDORA_CACHE_IMAGE_NAME} + ${RAWHIDE_CACHE_IMAGE_NAME} build-push-${IMAGE_SUFFIX} BUILDID: "${CIRRUS_BUILD_ID}" REPOREF: "${CIRRUS_REPO_NAME}" diff --git a/Makefile b/Makefile index 4fd12949e4..3c6aaf5768 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,8 @@ GOBIN := $(shell $(GO) env GOBIN) GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) +SEQUOIA_SONAME_DIR = + # N/B: This value is managed by Renovate, manual changes are # possible, as long as they don't disturb the formatting # (i.e. DO NOT ADD A 'v' prefix!) @@ -82,7 +84,7 @@ CONTAINER_GOSRC = /src/github.com/containers/skopeo CONTAINER_RUN ?= $(CONTAINER_CMD) --security-opt label=disable -v $(CURDIR):$(CONTAINER_GOSRC) -w $(CONTAINER_GOSRC) $(SKOPEO_CIDEV_CONTAINER_FQIN) EXTRA_LDFLAGS ?= -SKOPEO_LDFLAGS := -ldflags '$(EXTRA_LDFLAGS)' +SKOPEO_LDFLAGS := -ldflags '-X go.podman.io/image/v5/signature/internal/sequoia.sequoiaLibraryDir=$(SEQUOIA_SONAME_DIR) $(EXTRA_LDFLAGS)' MANPAGES_MD = $(wildcard docs/*.md) MANPAGES ?= $(MANPAGES_MD:%.md=%) @@ -251,7 +253,7 @@ validate-docs: bin/skopeo hack/xref-helpmsgs-manpages test-unit-local: - $(GO) test -tags "$(BUILDTAGS)" $$($(GO) list -tags "$(BUILDTAGS)" -e ./... | grep -v '^github\.com/containers/skopeo/\(integration\|vendor/.*\)$$') + $(GO) test $(SKOPEO_LDFLAGS) -tags "$(BUILDTAGS)" $$($(GO) list -tags "$(BUILDTAGS)" -e ./... | grep -v '^github\.com/containers/skopeo/\(integration\|vendor/.*\)$$') vendor: $(GO) mod tidy diff --git a/cmd/skopeo/signing_test.go b/cmd/skopeo/signing_test.go index 3cb676ddc9..e10b56679d 100644 --- a/cmd/skopeo/signing_test.go +++ b/cmd/skopeo/signing_test.go @@ -29,7 +29,8 @@ func assertTestFailed(t *testing.T, stdout string, err error, substring string) } func TestStandaloneSign(t *testing.T) { - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + t.Setenv("GNUPGHOME", "fixtures") + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { @@ -38,7 +39,6 @@ func TestStandaloneSign(t *testing.T) { manifestPath := "fixtures/image.manifest.json" dockerReference := "testing/manifest" - t.Setenv("GNUPGHOME", "fixtures") // Invalid command-line arguments for _, args := range [][]string{ @@ -87,9 +87,6 @@ func TestStandaloneSign(t *testing.T) { require.NoError(t, err) manifest, err := os.ReadFile(manifestPath) require.NoError(t, err) - mech, err = signature.NewGPGSigningMechanism() - require.NoError(t, err) - defer mech.Close() verified, err := signature.VerifyDockerManifestSignature(sig, manifest, dockerReference, mech, fixturesTestKeyFingerprint) require.NoError(t, err) assert.Equal(t, dockerReference, verified.DockerReference) diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 9236b4ac0c..b89d9381fe 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 0000000000..e3683a45c7 --- /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 0000000000..d077bf7734 --- /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 01790c1204..4a6acf8064 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/contrib/cirrus/runner.sh b/contrib/cirrus/runner.sh index e5ac3df509..3ec9f49c13 100755 --- a/contrib/cirrus/runner.sh +++ b/contrib/cirrus/runner.sh @@ -99,7 +99,7 @@ _run_vendor() { _run_build() { make bin/skopeo BUILDTAGS="$BUILDTAGS" - make install PREFIX=/usr/local + make install PREFIX=/usr/local BUILDTAGS="$BUILDTAGS" } _run_cross() { diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index cde7004c50..1e824c7ea2 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 8addfa2131..1b2183962b 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 77e7f3122f..7a5139ae7d 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 @@ -745,7 +748,7 @@ func (s *copySuite) TestCopyOCIRoundTrip() { // --sign-by and --policy copy, primarily using atomic: func (s *copySuite) TestCopySignatures() { t := s.T() - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that verification and policy enforcement works, using signatures from fixtures @@ -776,9 +779,10 @@ func (s *copySuite) TestCopySignatures() { // Verify that mis-signed images are rejected assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/personal:personal", "atomic:localhost:5006/myns/official:attack") assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/personal:attack") - assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|.* was not found).*", + // "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia. + assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/personal:attack", dirDest) - assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|.* was not found).*", + assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/official:attack", dirDest) // Verify that signed identity is verified. @@ -791,7 +795,8 @@ func (s *copySuite) TestCopySignatures() { // Verify that cosigning requirements are enforced assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/cosigned:cosigned") - assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|.* was not found).*", + // "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia. + assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5006/myns/cosigned:cosigned", dirDest) assertSkopeoSucceeds(t, "", "--tls-verify=false", "copy", "--sign-by", "personal@example.com", "atomic:localhost:5006/myns/official:official", "atomic:localhost:5006/myns/cosigned:cosigned") @@ -801,7 +806,7 @@ func (s *copySuite) TestCopySignatures() { // --policy copy for dir: sources func (s *copySuite) TestCopyDirSignatures() { t := s.T() - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that verification and policy enforcement works, using signatures from fixtures @@ -836,7 +841,8 @@ func (s *copySuite) TestCopyDirSignatures() { // Verify that correct images are accepted assertSkopeoSucceeds(t, "", "--policy", policy, "copy", topDirDest+"/restricted/official", topDirDest+"/dest") // ... and that mis-signed images are rejected. - assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|.* was not found).*", + // "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia. + assertSkopeoFails(t, ".*Source image rejected: (Invalid GPG signature|Missing key:).*", "--policy", policy, "copy", topDirDest+"/restricted/personal", topDirDest+"/dest") // Verify that the signed identity is verified. @@ -846,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() @@ -902,7 +941,7 @@ func findRegularFiles(t *testing.T, root string) []string { // --sign-by and policy use for docker: with lookaside func (s *copySuite) TestCopyDockerLookaside() { t := s.T() - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that verification and policy enforcement works, using signatures from fixtures @@ -971,7 +1010,7 @@ func (s *copySuite) TestCopyDockerLookaside() { // atomic: and docker: X-Registry-Supports-Signatures works and interoperates func (s *copySuite) TestCopyAtomicExtension() { t := s.T() - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that the reading/writing works using signatures from fixtures @@ -1031,7 +1070,7 @@ func (s *copySuite) TestCopyVerifyingMirroredSignatures() { t := s.T() const regPrefix = "docker://localhost:5006/myns/mirroring-" - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that verification and policy enforcement works, using signatures from fixtures diff --git a/integration/fixtures/.gitignore b/integration/fixtures/.gitignore new file mode 100644 index 0000000000..5dc6c4dd6d --- /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 0000000000..e69de29bb2 diff --git a/integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp b/integration/fixtures/data/keystore/softkeys/1F5825285B785E1DB13BF36D2D11A19ABA41C6AE.pgp new file mode 100644 index 0000000000..86462c6b64 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 0000000000..4c6ad86f0f 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 0000000000..eb6dd1f4cc 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 0000000000..8dae8b8d8b 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 0000000000..b9fee9bb24 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 0000000000..6a58a268c8 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 0000000000..addf38a561 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 0000000000..e69de29bb2 diff --git a/integration/fixtures/no-passphrase.pub b/integration/fixtures/no-passphrase.pub new file mode 100644 index 0000000000..394d47ec12 --- /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 eb225649e1..52506054a4 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 0000000000..94ccc942c0 --- /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 0000000000..7d266b9567 --- /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 d104816829..2a7cb01631 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/integration/signing_test.go b/integration/signing_test.go index 4a6a9a16f0..7678c6e65b 100644 --- a/integration/signing_test.go +++ b/integration/signing_test.go @@ -57,7 +57,7 @@ func (s *signingSuite) SetupSuite() { func (s *signingSuite) TestSignVerifySmoke() { t := s.T() - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) + mech, err := signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() if err := mech.SupportsSigning(); err != nil { // FIXME? Test that verification and policy enforcement works, using signatures from fixtures diff --git a/systemtest/050-signing.bats b/systemtest/050-signing.bats index 2f12cb9b26..88c6cfafaa 100644 --- a/systemtest/050-signing.bats +++ b/systemtest/050-signing.bats @@ -136,6 +136,8 @@ END_PUSH # Done pushing. Now try to fetch. From here on we use the --policy option. # The table below lists the paths to fetch, and the expected errors (or # none, if we expect them to pass). + # + # "Invalid GPG signature" is reported by the gpgme mechanism; "Missing key: $fingerprint" by Sequoia. while read path expected_error; do expected_rc= if [[ -n $expected_error ]]; then @@ -154,7 +156,7 @@ END_PUSH fi done <