Add --sign-by-sq-fingerprint to push operations

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č <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač
2025-07-11 17:54:50 +02:00
parent 2f005b67f4
commit 9e2850d0a8
25 changed files with 365 additions and 16 deletions

View File

@@ -12,13 +12,15 @@ import (
"go.podman.io/image/v5/pkg/cli" "go.podman.io/image/v5/pkg/cli"
"go.podman.io/image/v5/pkg/cli/sigstore" "go.podman.io/image/v5/pkg/cli/sigstore"
"go.podman.io/image/v5/signature/signer" "go.podman.io/image/v5/signature/signer"
"go.podman.io/image/v5/signature/simplesequoia"
) )
// SigningCLIOnlyOptions contains signing-related CLI options. // SigningCLIOnlyOptions contains signing-related CLI options.
// Some other options are defined in entities.ImagePushOptions. // Some other options are defined in entities.ImagePushOptions.
type SigningCLIOnlyOptions struct { type SigningCLIOnlyOptions struct {
signPassphraseFile string signPassphraseFile string
signBySigstoreParamFile string signBySequoiaFingerprint string
signBySigstoreParamFile string
} }
func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, pushOpts *entities.ImagePushOptions) { 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") flags.StringVar(&pushOpts.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key")
_ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone) _ = 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" signBySigstoreFlagName := "sign-by-sigstore"
flags.StringVar(&cliOpts.signBySigstoreParamFile, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`") flags.StringVar(&cliOpts.signBySigstoreParamFile, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault) _ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault)
@@ -42,6 +48,7 @@ func DefineSigningFlags(cmd *cobra.Command, cliOpts *SigningCLIOnlyOptions, push
if registry.IsRemote() { if registry.IsRemote() {
_ = flags.MarkHidden(signByFlagName) _ = flags.MarkHidden(signByFlagName)
_ = flags.MarkHidden(signBySequoiaFingerprintFlagName)
_ = flags.MarkHidden(signBySigstoreFlagName) _ = flags.MarkHidden(signBySigstoreFlagName)
_ = flags.MarkHidden(signBySigstorePrivateKeyFlagName) _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
_ = flags.MarkHidden(signPassphraseFileFlagName) _ = 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, // 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. // with independent passphrases, but that would make the CLI probably too confusing.
// For now, use the passphrase with either, but only one of them. // For now, use the passphrase with either, but only one of them.
if cliOpts.signPassphraseFile != "" && pushOpts.SignBy != "" && pushOpts.SignBySigstorePrivateKeyFile != "" { if cliOpts.signPassphraseFile != "" {
return nil, fmt.Errorf("only one of --sign-by and sign-by-sigstore-private-key can be used with --sign-passphrase-file") 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 var passphrase string
@@ -72,9 +91,16 @@ func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnly
p := ssh.ReadPassphrase() p := ssh.ReadPassphrase()
passphrase = string(p) passphrase = string(p)
} // pushOpts.SignBy triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldnt prompt ourselves if no passphrase was explicitly provided. } // pushOpts.SignBy triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldnt prompt ourselves if no passphrase was explicitly provided.
// With signBySequoiaFingerprint, we dont prompt for a passphrase (for now??): We dont know whether the key requires a passphrase.
pushOpts.SignPassphrase = passphrase pushOpts.SignPassphrase = passphrase
pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase) pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase)
cleanup := signingCleanup{} cleanup := signingCleanup{}
succeeded := false
defer func() {
if !succeeded {
cleanup.cleanup()
}
}()
if cliOpts.signBySigstoreParamFile != "" { if cliOpts.signBySigstoreParamFile != "" {
signer, err := sigstore.NewSignerFromParameterFile(cliOpts.signBySigstoreParamFile, &sigstore.Options{ signer, err := sigstore.NewSignerFromParameterFile(cliOpts.signBySigstoreParamFile, &sigstore.Options{
PrivateKeyPassphrasePrompt: cli.ReadPassphraseFile, PrivateKeyPassphrasePrompt: cli.ReadPassphraseFile,
@@ -87,6 +113,21 @@ func PrepareSigning(pushOpts *entities.ImagePushOptions, cliOpts *SigningCLIOnly
pushOpts.Signers = append(pushOpts.Signers, signer) pushOpts.Signers = append(pushOpts.Signers, signer)
cleanup.signers = append(cleanup.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 return cleanup.cleanup, nil
} }

View File

@@ -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)

View File

@@ -4,4 +4,4 @@
####> are applicable to all of those. ####> are applicable to all of those.
#### **--sign-passphrase-file**=*path* #### **--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.

View File

@@ -38,11 +38,12 @@ Add a “simple signing” signature at the destination using the specified key.
@@option sign-by-sigstore @@option sign-by-sigstore
#### **--sign-by-sigstore-private-key**=*path* #### **--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) 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 sign-passphrase-file
@@option tls-verify @@option tls-verify

View File

@@ -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) 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 sign-passphrase-file
@@option tls-verify @@option tls-verify

View File

@@ -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) 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 sign-passphrase-file
@@option tls-verify @@option tls-verify

View File

@@ -1309,8 +1309,8 @@ func (p *PodmanTestIntegration) removeNetwork(name string) {
// generatePolicyFile generates a signature verification policy file. // generatePolicyFile generates a signature verification policy file.
// it returns the policy file path. // it returns the policy file path.
func generatePolicyFile(tempDir string, port int) string { func generatePolicyFile(tempDir string, port int, sequoiaKeyPath string) string {
keyPath := filepath.Join(tempDir, "key.gpg") gpgKeyPath := filepath.Join(tempDir, "key.gpg")
policyPath := filepath.Join(tempDir, "policy.json") policyPath := filepath.Join(tempDir, "policy.json")
conf := fmt.Sprintf(` conf := fmt.Sprintf(`
{ {
@@ -1339,11 +1339,18 @@ func generatePolicyFile(tempDir string, port int) string {
"type": "sigstoreSigned", "type": "sigstoreSigned",
"keyPath": "testdata/sigstore-key.pub" "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) writeConf([]byte(conf), policyPath)
return policyPath return policyPath
} }

View File

@@ -3,9 +3,9 @@
package integration package integration
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -13,9 +13,13 @@ import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec" . "github.com/onsi/gomega/gexec"
"go.podman.io/image/v5/signature/simplesequoia"
"go.podman.io/storage/pkg/archive" "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() { var _ = Describe("Podman push", func() {
BeforeEach(func() { BeforeEach(func() {
@@ -235,22 +239,26 @@ var _ = Describe("Podman push", func() {
Expect(push2).Should(ExitCleanly()) Expect(push2).Should(ExitCleanly())
if !IsRemote() { // Remote does not support signing 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 doesnt // Ideally, this should set SystemContext.RegistriesDirPath, but Podman currently doesnt
// expose that as an option. So, for now, modify /etc/directly, and skip testing sigstore if // expose that as an option. So, for now, modify /etc/directly, and skip testing sigstore if
// we dont have permission to do so. // we dont 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" systemRegistriesDAddition := "/etc/containers/registries.d/podman-test-only-temporary-addition.yaml"
cmd := exec.Command("cp", "testdata/sigstore-registries.d-fragment.yaml", systemRegistriesDAddition) registriesDFragment, err := os.ReadFile("testdata/sigstore-registries.d-fragment.yaml")
output, err := cmd.CombinedOutput() Expect(err).ToNot(HaveOccurred())
registriesDFragment = bytes.ReplaceAll(registriesDFragment, []byte("@lookasideDir@"), []byte(lookasideDir))
err = os.WriteFile(systemRegistriesDAddition, registriesDFragment, 0644)
if err != nil { if err != nil {
GinkgoWriter.Printf("Skipping sigstore tests because /etc/containers/registries.d isnt writable: %s\n", string(output)) GinkgoWriter.Printf("Skipping sigstore tests because /etc/containers/registries.d isnt writable: %s\n", err)
} else { } else {
By("pushing and pulling with --sign-by-sigstore-private-key")
defer func() { defer func() {
err := os.Remove(systemRegistriesDAddition) err := os.Remove(systemRegistriesDAddition)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}() }()
// Generate a signature verification policy file // Generate a signature verification policy file
policyPath := generatePolicyFile(podmanTest.TempDir, 5003) policyPath := generatePolicyFile(podmanTest.TempDir, 5003, "testdata/sequoia-key.pub")
defer os.Remove(policyPath) defer os.Remove(policyPath)
// Verify that the policy rejects unsigned images // 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 = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5003/sigstore-signed-params"})
pull.WaitWithDefaultTimeout() pull.WaitWithDefaultTimeout()
Expect(pull).Should(ExitCleanly()) 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())
}
} }
} }
}) })

View File

@@ -189,7 +189,7 @@ default-docker:
if !IsRemote() { if !IsRemote() {
// Generate a signature verification policy file // Generate a signature verification policy file
policyPath := generatePolicyFile(podmanTest.TempDir, port) policyPath := generatePolicyFile(podmanTest.TempDir, port, "testdata/sequoia-key.pub")
defer os.Remove(policyPath) defer os.Remove(policyPath)
session = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, pushedImage}) session = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, pushedImage})

View File

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

38
test/e2e/testdata/sequoia-key.pub vendored Normal file
View File

@@ -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-----

View File

@@ -3,3 +3,5 @@ docker:
use-sigstore-attachments: true use-sigstore-attachments: true
localhost:5003/sigstore-signed-params: localhost:5003/sigstore-signed-params:
use-sigstore-attachments: true use-sigstore-attachments: true
localhost:5003/simple-sq-signed:
lookaside: file://@lookasideDir@

View File

@@ -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")
}

View File

@@ -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 cant use passphrase with \n; reject it here for consistent behavior.
// FIXME: We dont 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
}
}

View File

@@ -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 systems default
keyFingerprint string
passphrase string // "" if not provided.
}
// NewSigner returns a signature.Signer which creates “simple signing” signatures using the users 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 cant 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()
}

View File

@@ -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")
}

1
vendor/modules.txt vendored
View File

@@ -875,6 +875,7 @@ go.podman.io/image/v5/signature/sigstore
go.podman.io/image/v5/signature/sigstore/fulcio go.podman.io/image/v5/signature/sigstore/fulcio
go.podman.io/image/v5/signature/sigstore/internal go.podman.io/image/v5/signature/sigstore/internal
go.podman.io/image/v5/signature/sigstore/rekor 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/signature/simplesigning
go.podman.io/image/v5/storage go.podman.io/image/v5/storage
go.podman.io/image/v5/tarball go.podman.io/image/v5/tarball