Skip to content

Commit f5efeb9

Browse files
committed
Add --sign-by-sq-fingerprint and an integration test
Signed-off-by: Miloslav Trmač <[email protected]>
1 parent beccfb3 commit f5efeb9

27 files changed

+431
-8
lines changed

cmd/skopeo/utils.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"go.podman.io/image/v5/pkg/cli/sigstore"
2727
"go.podman.io/image/v5/pkg/compression"
2828
"go.podman.io/image/v5/signature/signer"
29+
"go.podman.io/image/v5/signature/simplesequoia"
2930
"go.podman.io/image/v5/storage"
3031
"go.podman.io/image/v5/transports/alltransports"
3132
"go.podman.io/image/v5/types"
@@ -329,6 +330,7 @@ func (opts *imageDestOptions) warnAboutIneffectiveOptions(destTransport types.Im
329330
type sharedCopyOptions struct {
330331
removeSignatures bool // Do not copy signatures from the source image
331332
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
333+
signBySequoiaFingerprint string // Sign the image using a Sequoia-PGP key with the specified fingerprint
332334
signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file
333335
signBySigstorePrivateKey string // Sign the image using a sigstore private key
334336
signPassphraseFile string // Path pointing to a passphrase file when signing
@@ -342,6 +344,7 @@ func sharedCopyFlags() (pflag.FlagSet, *sharedCopyOptions) {
342344
fs := pflag.FlagSet{}
343345
fs.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from source")
344346
fs.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`")
347+
fs.StringVar(&opts.signBySequoiaFingerprint, "sign-by-sq-fingerprint", "", "Sign the image using a Sequoia-PGP key with the specified `FINGERPRINT`")
345348
fs.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`")
346349
fs.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`")
347350
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
365368
// c/image/copy.Image does allow creating both simple signing and sigstore signatures simultaneously,
366369
// with independent passphrases, but that would make the CLI probably too confusing.
367370
// For now, use the passphrase with either, but only one of them.
368-
if opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" {
369-
return nil, nil, fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file")
371+
if opts.signPassphraseFile != "" {
372+
count := 0
373+
if opts.signByFingerprint != "" {
374+
count++
375+
}
376+
if opts.signBySequoiaFingerprint != "" {
377+
count++
378+
}
379+
if opts.signBySigstorePrivateKey != "" {
380+
count++
381+
}
382+
if count > 1 {
383+
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")
384+
}
370385
}
371386
var passphrase string
372387
if opts.signPassphraseFile != "" {
@@ -382,6 +397,7 @@ func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, fun
382397
}
383398
passphrase = p
384399
} // 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.
400+
// With opts.signBySequoiaFingerprint, we don’t prompt for a passphrase (for now??): We don’t know whether the key requires a passphrase.
385401
var passphraseBytes []byte
386402
if passphrase != "" {
387403
passphraseBytes = []byte(passphrase)
@@ -412,6 +428,19 @@ func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, fun
412428
}
413429
signers = append(signers, signer)
414430
}
431+
if opts.signBySequoiaFingerprint != "" {
432+
sqOpts := []simplesequoia.Option{
433+
simplesequoia.WithKeyFingerprint(opts.signBySequoiaFingerprint),
434+
}
435+
if passphrase != "" {
436+
sqOpts = append(sqOpts, simplesequoia.WithPassphrase(passphrase))
437+
}
438+
signer, err := simplesequoia.NewSigner(sqOpts...)
439+
if err != nil {
440+
return nil, nil, fmt.Errorf("Error using --sign-by-sq-fingerprint: %w", err)
441+
}
442+
signers = append(signers, signer)
443+
}
415444

416445
succeeded = true
417446
return &copy.Options{

cmd/skopeo/utils_nosequoia_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build !containers_image_sequoia
2+
3+
package main
4+
5+
const buildWithSequoia = false

cmd/skopeo/utils_sequoia_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//go:build containers_image_sequoia
2+
3+
package main
4+
5+
const buildWithSequoia = true

cmd/skopeo/utils_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"os"
7+
"slices"
78
"testing"
89

910
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -378,6 +379,7 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
378379
// Set most flags to non-default values
379380
// This should also test --sign-by-sigstore and --sign-by-sigstore-private-key; we would have
380381
// to create test keys for that.
382+
// This does not test --sign-by-sq-fingerprint, because that needs to be conditional based on buildWithSequoia.
381383
opts = fakeSharedCopyOptions(t, []string{
382384
"--remove-signatures",
383385
"--sign-by", "gpgFingerprint",
@@ -395,12 +397,13 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
395397
ForceManifestMIMEType: imgspecv1.MediaTypeImageManifest,
396398
}, res)
397399

398-
// --sign-passphrase-file + --sign-by work
400+
// --sign-passphrase-file:
399401
passphraseFile, err := os.CreateTemp("", "passphrase") // Eventually we could refer to a passphrase fixture instead
400402
require.NoError(t, err)
401403
defer os.Remove(passphraseFile.Name())
402404
_, err = passphraseFile.WriteString("test-passphrase")
403405
require.NoError(t, err)
406+
// --sign-passphrase-file + --sign-by work
404407
opts = fakeSharedCopyOptions(t, []string{
405408
"--sign-by", "gpgFingerprint",
406409
"--sign-passphrase-file", passphraseFile.Name(),
@@ -414,14 +417,42 @@ func TestSharedCopyOptionsCopyOptions(t *testing.T) {
414417
SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"),
415418
ReportWriter: &someStdout,
416419
}, res)
417-
// --sign-passphrase-file + --sign-by-sigstore-private-key should be tested here.
420+
// If Sequoia is supported, --sign-passphrase-file + --sign-by-sq-fingerprint work
421+
if buildWithSequoia {
422+
opts = fakeSharedCopyOptions(t, []string{
423+
"--sign-by-sq-fingerprint", "sqFingerprint",
424+
"--sign-passphrase-file", passphraseFile.Name(),
425+
})
426+
res, cleanup, err = opts.copyOptions(&someStdout)
427+
require.NoError(t, err)
428+
defer cleanup()
429+
assert.NotNil(t, res.Signers) // Contains a Sequoia signer
430+
res.Signers = nil // To allow the comparison below
431+
assert.Equal(t, &copy.Options{
432+
SignPassphrase: "test-passphrase",
433+
SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"),
434+
ReportWriter: &someStdout,
435+
}, res)
436+
}
418437

419438
// Invalid --format
420439
opts = fakeSharedCopyOptions(t, []string{"--format", "invalid"})
421440
_, _, err = opts.copyOptions(&someStdout)
422441
assert.Error(t, err)
423442

424-
// More --sign-passphrase-file, --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here.
443+
// More --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here.
444+
// --sign-passphrase-file + more than one key option
445+
for _, opts := range [][]string{
446+
{"--sign-by", "gpgFingerprint", "--sign-by-sq-fingerprint", "sqFingerprint"},
447+
{"--sign-by", "gpgFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey"},
448+
{"--sign-by-sq-fingerprint", "sqFingerprint", "--sign-by-sigstore-private-key", "sigstorePrivateKey"},
449+
} {
450+
opts := fakeSharedCopyOptions(t, slices.Concat(opts, []string{
451+
"--sign-passphrase-file", passphraseFile.Name(),
452+
}))
453+
_, _, err = opts.copyOptions(&someStdout)
454+
assert.Error(t, err)
455+
}
425456

426457
// --sign-passphrase-file not found
427458
opts = fakeSharedCopyOptions(t, []string{

docs/skopeo-copy.1.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,14 @@ See containers-sigstore-signing-params.yaml(5) for details about the file format
107107

108108
Add a sigstore signature using a private key at _path_ for an image name corresponding to _destination-image_
109109

110+
**--sign-by-sq-fingerprint** _fingerprint_
111+
112+
Add a “simple signing” signature using a Sequoia-PGP key with the specified _fingerprint_.
113+
110114
**--sign-passphrase-file** _path_
111115

112-
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.
116+
The passphrase to use when signing with `--sign-by`, `--sign-by-sigstore-private-key` or `--sign-by-sq-fingerprint`.
117+
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.
113118

114119
**--sign-identity** _reference_
115120

docs/skopeo-sync.1.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,14 @@ See containers-sigstore-signing-params.yaml(5) for details about the file format
103103

104104
Add a sigstore signature using a private key at _path_ for an image name corresponding to _destination-image_
105105

106+
**--sign-by-sq-fingerprint** _fingerprint_
107+
108+
Add a “simple signing” signature using a Sequoia-PGP key with the specified _fingerprint_.
109+
106110
**--sign-passphrase-file** _path_
107111

108-
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.
112+
The passphrase to use when signing with `--sign-by`, `--sign-by-sigstore-private-key` or `--sign-by-sq-fingerprint`.
113+
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.
109114

110115
**--src-creds** _username[:password]_ for accessing the source registry.
111116

integration/copy_test.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/stretchr/testify/suite"
2525
"go.podman.io/image/v5/manifest"
2626
"go.podman.io/image/v5/signature"
27+
"go.podman.io/image/v5/signature/simplesequoia"
2728
"go.podman.io/image/v5/types"
2829
)
2930

@@ -106,7 +107,9 @@ func (s *copySuite) TearDownSuite() {
106107
// and returns a path to a policy, which will be automatically removed when the test completes.
107108
func (s *copySuite) policyFixture(extraSubstitutions map[string]string) string {
108109
t := s.T()
109-
edits := map[string]string{"@keydir@": s.gpgHome}
110+
fixtureDir, err := filepath.Abs("fixtures")
111+
require.NoError(t, err)
112+
edits := map[string]string{"@keydir@": s.gpgHome, "@fixturedir@": fixtureDir}
110113
maps.Copy(edits, extraSubstitutions)
111114
policyPath := fileFromFixture(t, "fixtures/policy.json", edits)
112115
return policyPath
@@ -846,6 +849,39 @@ func (s *copySuite) TestCopyDirSignatures() {
846849
"--policy", policy, "copy", topDirDest+"/restricted/badidentity", topDirDest+"/dest")
847850
}
848851

852+
func (s *copySuite) TestCopySequoiaSignatures() {
853+
t := s.T()
854+
signer, err := simplesequoia.NewSigner(simplesequoia.WithSequoiaHome(testSequoiaHome), simplesequoia.WithKeyFingerprint(testSequoiaKeyFingerprint))
855+
if err != nil {
856+
t.Skipf("Sequoia not supported: %v", err)
857+
}
858+
signer.Close()
859+
860+
const ourRegistry = "docker://" + v2DockerRegistryURL + "/"
861+
862+
dirDest := "dir:" + t.TempDir()
863+
864+
policy := s.policyFixture(nil)
865+
registriesDir := t.TempDir()
866+
registriesFile := fileFromFixture(t, "fixtures/registries.yaml",
867+
map[string]string{"@lookaside@": t.TempDir(), "@split-staging@": "/var/empty", "@split-read@": "file://var/empty"})
868+
err = os.Symlink(registriesFile, filepath.Join(registriesDir, "registries.yaml"))
869+
require.NoError(t, err)
870+
871+
// Sign the images
872+
absSequoiaHome, err := filepath.Abs(testSequoiaHome)
873+
require.NoError(t, err)
874+
t.Setenv("SEQUOIA_HOME", absSequoiaHome)
875+
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--dest-tls-verify=false", "--sign-by-sq-fingerprint", testSequoiaKeyFingerprint,
876+
testFQIN+":1.26", ourRegistry+"sequoia-no-passphrase")
877+
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--dest-tls-verify=false", "--sign-by-sq-fingerprint", testSequoiaKeyFingerprintWithPassphrase,
878+
"--sign-passphrase-file", filepath.Join(absSequoiaHome, "with-passphrase.passphrase"),
879+
testFQIN+":1.26.1", ourRegistry+"sequoia-with-passphrase")
880+
// Verify that we can pull them
881+
assertSkopeoSucceeds(t, "", "--policy", policy, "copy", "--src-tls-verify=false", ourRegistry+"sequoia-no-passphrase", dirDest)
882+
assertSkopeoSucceeds(t, "", "--policy", policy, "copy", "--src-tls-verify=false", ourRegistry+"sequoia-with-passphrase", dirDest)
883+
}
884+
849885
// Compression during copy
850886
func (s *copySuite) TestCopyCompression() {
851887
t := s.T()

integration/fixtures/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/data/pgp.cert.d/_sequoia*

integration/fixtures/data/keystore/keystore.cookie

Whitespace-only changes.

0 commit comments

Comments
 (0)