From beb2d5ddf43825b231708a2574da0c86f6388ba4 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Thu, 20 Jan 2022 11:25:06 +0100 Subject: [PATCH 1/2] use github.com/proglottis/gpgme @mtrmac's fork was needed to allow for building on RHEL 7 which we do not target anymore. Moving to upstream allows for making use of more recent features and avoids diverging in the future. Signed-off-by: Valentin Rothberg --- go.mod | 2 +- go.sum | 4 ++-- signature/mechanism_gpgme.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 8393530c9..7fd9328b3 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,12 @@ require ( github.com/klauspost/compress v1.14.1 github.com/klauspost/pgzip v1.2.5 github.com/manifoldco/promptui v0.9.0 - github.com/mtrmac/gpgme v0.1.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 github.com/opencontainers/selinux v1.10.0 github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913 github.com/pkg/errors v0.9.1 + github.com/proglottis/gpgme v0.1.1 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 github.com/sylabs/sif/v2 v2.3.1 diff --git a/go.sum b/go.sum index 5d5fae3c0..8cf02c1f4 100644 --- a/go.sum +++ b/go.sum @@ -670,8 +670,6 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/mtrmac/gpgme v0.1.2 h1:dNOmvYmsrakgW7LcgiprD0yfRuQQe8/C8F6Z+zogO3s= -github.com/mtrmac/gpgme v0.1.2/go.mod h1:GYYHnGSuS7HK3zVS2n3y73y0okK/BeKzwnn5jgiVFNI= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -748,6 +746,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/proglottis/gpgme v0.1.1 h1:72xI0pt/hy7pqsRxk32KExITkXp+RZErRizsA+up/lQ= +github.com/proglottis/gpgme v0.1.1/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= diff --git a/signature/mechanism_gpgme.go b/signature/mechanism_gpgme.go index 6ae74d430..5df205003 100644 --- a/signature/mechanism_gpgme.go +++ b/signature/mechanism_gpgme.go @@ -9,7 +9,7 @@ import ( "io/ioutil" "os" - "github.com/mtrmac/gpgme" + "github.com/proglottis/gpgme" ) // A GPG/OpenPGP signing mechanism, implemented using gpgme. From 277310915e23ddda4d09a6a06d2755a61f3a5b0c Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Thu, 20 Jan 2022 11:53:32 +0100 Subject: [PATCH 2/2] GPGME: support passphrase for prompt-less signing To support signing images via gpgme without user prompt, allow for providing a passphrase via the copy options. Add a new *WithOptions API to the `signature` package and extend its interface. To prevent breaking the API, extend the signature API with an internal type as has already been done for other types and interfaces. Signed-off-by: Valentin Rothberg --- copy/copy.go | 5 +-- copy/sign.go | 4 +-- copy/sign_test.go | 6 ++-- pkg/cli/passphrase.go | 36 +++++++++++++++++++ signature/docker.go | 30 ++++++++++++++-- signature/docker_test.go | 61 ++++++++++++++++++++++++++++++++ signature/fixtures/pubring.gpg | Bin 661 -> 1478 bytes signature/fixtures/secring.gpg | Bin 1325 -> 2809 bytes signature/fixtures/trustdb.gpg | Bin 1360 -> 1440 bytes signature/fixtures_info_test.go | 4 +++ signature/mechanism.go | 11 ++++-- signature/mechanism_gpgme.go | 35 +++++++++++++++--- signature/mechanism_openpgp.go | 12 +++++-- signature/mechanism_test.go | 2 +- signature/signature.go | 10 +++++- signature/signature_test.go | 6 ++-- 16 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 pkg/cli/passphrase.go diff --git a/copy/copy.go b/copy/copy.go index 383215182..512e643b9 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -124,6 +124,7 @@ type ImageListSelection int type Options struct { RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature. SignBy string // If non-empty, asks for a signature to be added during the copy, and specifies a key ID, as accepted by signature.NewGPGSigningMechanism().SignDockerManifest(), + SignPassphrase string // Passphare to use when signing with the key ID from `SignBy`. ReportWriter io.Writer SourceCtx *types.SystemContext DestinationCtx *types.SystemContext @@ -569,7 +570,7 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur // Sign the manifest list. if options.SignBy != "" { - newSig, err := c.createSignature(manifestList, options.SignBy) + newSig, err := c.createSignature(manifestList, options.SignBy, options.SignPassphrase) if err != nil { return nil, err } @@ -791,7 +792,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli } if options.SignBy != "" { - newSig, err := c.createSignature(manifestBytes, options.SignBy) + newSig, err := c.createSignature(manifestBytes, options.SignBy, options.SignPassphrase) if err != nil { return nil, "", "", err } diff --git a/copy/sign.go b/copy/sign.go index 61612a4d3..21a3facd7 100644 --- a/copy/sign.go +++ b/copy/sign.go @@ -7,7 +7,7 @@ import ( ) // createSignature creates a new signature of manifest using keyIdentity. -func (c *copier) createSignature(manifest []byte, keyIdentity string) ([]byte, error) { +func (c *copier) createSignature(manifest []byte, keyIdentity string, passphrase string) ([]byte, error) { mech, err := signature.NewGPGSigningMechanism() if err != nil { return nil, errors.Wrap(err, "initializing GPG") @@ -23,7 +23,7 @@ func (c *copier) createSignature(manifest []byte, keyIdentity string) ([]byte, e } c.Printf("Signing manifest\n") - newSig, err := signature.SignDockerManifest(manifest, dockerReference.String(), mech, keyIdentity) + newSig, err := signature.SignDockerManifestWithOptions(manifest, dockerReference.String(), mech, keyIdentity, &signature.SignOptions{Passphrase: passphrase}) if err != nil { return nil, errors.Wrap(err, "creating signature") } diff --git a/copy/sign_test.go b/copy/sign_test.go index c61637663..271fe802c 100644 --- a/copy/sign_test.go +++ b/copy/sign_test.go @@ -50,7 +50,7 @@ func TestCreateSignature(t *testing.T) { dest: dirDest, reportWriter: ioutil.Discard, } - _, err = c.createSignature(manifestBlob, testKeyFingerprint) + _, err = c.createSignature(manifestBlob, testKeyFingerprint, "") assert.Error(t, err) // Set up a docker: reference @@ -66,14 +66,14 @@ func TestCreateSignature(t *testing.T) { } // Signing with an unknown key fails - _, err = c.createSignature(manifestBlob, "this key does not exist") + _, err = c.createSignature(manifestBlob, "this key does not exist", "") assert.Error(t, err) // Success mech, err = signature.NewGPGSigningMechanism() require.NoError(t, err) defer mech.Close() - sig, err := c.createSignature(manifestBlob, testKeyFingerprint) + sig, err := c.createSignature(manifestBlob, testKeyFingerprint, "") require.NoError(t, err) verified, err := signature.VerifyDockerManifestSignature(sig, manifestBlob, "docker.io/library/busybox:latest", mech, testKeyFingerprint) require.NoError(t, err) diff --git a/pkg/cli/passphrase.go b/pkg/cli/passphrase.go new file mode 100644 index 000000000..c46650cdc --- /dev/null +++ b/pkg/cli/passphrase.go @@ -0,0 +1,36 @@ +package cli + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +// ReadPassphraseFile returns the first line of the specified path. +// For convenience, an empty string is returned if the path is empty. +func ReadPassphraseFile(path string) (string, error) { + if path == "" { + return "", nil + } + + logrus.Debugf("Reading user-specified passphrase for signing from %s", path) + + ppf, err := os.Open(path) + if err != nil { + return "", err + } + defer ppf.Close() + + // Read the *first* line in the passphrase file, just as gpg(1) does. + buf, err := bufio.NewReader(ppf).ReadBytes('\n') + if err != nil && !errors.Is(err, io.EOF) { + return "", fmt.Errorf("reading passphrase file: %w", err) + } + + return strings.TrimSuffix(string(buf), "\n"), nil +} diff --git a/signature/docker.go b/signature/docker.go index 07fdd42a9..8e9ce0dd2 100644 --- a/signature/docker.go +++ b/signature/docker.go @@ -3,22 +3,46 @@ package signature import ( + "errors" "fmt" + "strings" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/manifest" "github.com/opencontainers/go-digest" ) +// SignOptions includes optional parameters for signing container images. +type SignOptions struct { + // Passphare to use when signing with the key identity. + Passphrase string +} + // SignDockerManifest returns a signature for manifest as the specified dockerReference, -// using mech and keyIdentity. -func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) { +// using mech and keyIdentity, and the specified options. +func SignDockerManifestWithOptions(m []byte, dockerReference string, mech SigningMechanism, keyIdentity string, options *SignOptions) ([]byte, error) { manifestDigest, err := manifest.Digest(m) if err != nil { return nil, err } sig := newUntrustedSignature(manifestDigest, dockerReference) - return sig.sign(mech, keyIdentity) + + var passphrase string + if options != nil { + passphrase = options.Passphrase + // The gpgme implementation can’t use passphrase with \n; reject it here for consistent behavior. + if strings.Contains(passphrase, "\n") { + return nil, errors.New("invalid passphrase: must not contain a line break") + } + } + + return sig.sign(mech, keyIdentity, passphrase) +} + +// SignDockerManifest returns a signature for manifest as the specified dockerReference, +// using mech and keyIdentity. +func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) { + return SignDockerManifestWithOptions(m, dockerReference, mech, keyIdentity, nil) } // VerifyDockerManifestSignature checks that unverifiedSignature uses expectedKeyIdentity to sign unverifiedManifest as expectedDockerReference, diff --git a/signature/docker_test.go b/signature/docker_test.go index a4baa21d2..f26bf01cd 100644 --- a/signature/docker_test.go +++ b/signature/docker_test.go @@ -2,12 +2,22 @@ package signature import ( "io/ioutil" + "os" + "os/exec" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Kill the running gpg-agent to drop unlocked keys. This allows for testing handling of invalid passphrases. +func killGPGAgent(t *testing.T) { + cmd := exec.Command("gpgconf", "--kill", "gpg-agent") + cmd.Env = append(os.Environ(), "GNUPGHOME="+testGPGHomeDirectory) + err := cmd.Run() + assert.NoError(t, err) +} + func TestSignDockerManifest(t *testing.T) { mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) require.NoError(t, err) @@ -44,6 +54,57 @@ func TestSignDockerManifest(t *testing.T) { assert.Error(t, err) } +func TestSignDockerManifestWithPassphrase(t *testing.T) { + killGPGAgent(t) + + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + manifest, err := ioutil.ReadFile("fixtures/image.manifest.json") + require.NoError(t, err) + + // Invalid passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: TestPassphrase + "\n"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid passphrase") + + // Wrong passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: "wrong"}) + require.Error(t, err) + + // No passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, nil) + require.Error(t, err) + + // Successful signing + signature, err := SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: TestPassphrase}) + require.NoError(t, err) + + verified, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase) + assert.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, verified.DockerReference) + assert.Equal(t, TestImageManifestDigest, verified.DockerManifestDigest) + + // Error computing Docker manifest + invalidManifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = SignDockerManifest(invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase) + assert.Error(t, err) + + // Error creating blob to sign + _, err = SignDockerManifest(manifest, "", mech, TestKeyFingerprintWithPassphrase) + assert.Error(t, err) + + // Error signing + _, err = SignDockerManifest(manifest, TestImageSignatureReference, mech, "this fingerprint doesn't exist") + assert.Error(t, err) +} + func TestVerifyDockerManifestSignature(t *testing.T) { mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) require.NoError(t, err) diff --git a/signature/fixtures/pubring.gpg b/signature/fixtures/pubring.gpg index 2d922b42d8820a545a3326e3d6c9268ea68f1743..9e7712b2986b1b21dbad06c26ebe7cb6bb396a0e 100644 GIT binary patch delta 866 zcmbQrdW?HQzrh9`28Q&4bVdd+*dkP%onMfeuTYX&T#}iWu8^Ht301)aQ897xZgw^X zW}p^^&54YLjP+2?j9!++_ut$YSs2#cPz+@`w(2>PY-sBnzDIurqBnO*Et=nweMwmT z*rJ8s{$33I5M9f9AT=q}xiIa5nIYGH=B5Ls?_XpX&MKR?`mWK%e}CJ4rrw^p?#!OD zxsOey{5ki9?cwCUv-$4jqbtl;?iT+n`FPK}e+RA@>jr1k?<>_1I$55yuz)c_jQL4H zoM9z{AR_}Kn!ilLGfOfQ0uqah3o?oli&GUef*~Fa0eVzZ!KNH2?N9)h)=SMz%*?Sv z^?Ju87GXvX1{*O&mdCHl1aF8c%8N+WyKUWY_8GTRXt5hB6DS0jq?uW{IXT#wn3b3q z*+jWGIk=fv#F&^FndI1+#2Xm6I0axD#QB*2SI+yidaq5{4A#2m<`>EZe~9P2`a5Ao z8{6VwZY7qu%Wr=uGB9aQkuVR+5#J>%u=1J1&eWTC_v}0Rf+x4)SWZJxeS_-~(Mt_# z7ca^r886#CP2|MoDPQxIj4%GvKIgom-gRxtUGpq$H{Xr^Ep3vkzD!)it(NiRn0w&7 zGt=V!Nz25|*Z_^d9q zn6gGILs6|eM4zL+!eWMQGvD(mFXbgRC%A9;H#_m}#MwohwwEsli$AVfydiVBSYOA^ zy&383|8hDGF-*T9dAvoFv(;izz@JGkruIi!>KRUrHcD=YnJ41TE>#?Kr&=5wfgRge zB!HoCARTFn%Nv&I?lmH={+}X z4`oLGh}K)Z&uNymW=^)XItbcQZ3FFmG;TG-L!Z0A?Z${{R30 diff --git a/signature/fixtures/secring.gpg b/signature/fixtures/secring.gpg index 36cf0f7db274dd9b56ba91c194d5350734e08428..1c274998cdaed4c5222e5c6aa97cf000cf5623bf 100644 GIT binary patch delta 1516 zcmajeYdq5n0LJm{Z(~MsA44w3CD#sOA*?W!BW08j8%-lj=8}~ER@B@QxhAWW+)^BI z3TdS%5hJx?Br>(6)0oRYYxdAuez)4dPd z-i2qWfS`qMV|)*%gj8?rP%S#`GJWj;b5tGAXtHY5th||%vujvxK|J}rdQIlN)q`>WawSI~LA48{4Xy3I*?Z3rE$0Sk2jn7(AF?XK)QNK?+%y<@jOGJ&kOhe)Ny zk>iO}lHNxA5lS41qUT4VCYb7NP5zOwalf(^DTWvs?ZOy>sX!5st?K%)DSna?M@?T> zRTJ0J*gqq;-Jgns1ONI0w4~s2vIrSK$^d{$t083(asW&n0HvV71{nZFgCJ!Ue=bl8 zQr}aU-|FvgO)7<_@i2qQO3SF|PiyCE&PZRw%Nf8zhUb>`A%JP22F5oU)uy9ZJA=JP zdc*DLd@?H^bF4d>7EklIt~Nw7X0x@!Y;Uv|t3DkrT%;J-vL)s(+-g9NhC^ITlsV2D zZ1PDzqgl6*Unyryp6>SC%j_!-k!Wd$lvV)JKaat=Q1f(e`RxIRH-WU|76(gw_Q}Ft zPQbw%)M*icsFc|R?kT)!M%FjZ_S=Lwwx!hav;wd2qpn6X!Lw0PPUPm7$ICho593i& z$FDU;R;!<5+`CUcEF+0#JOUvl9L=6|Q`w9ym3zbmvqgD<4p!DhK{nyEV5X|444UdY zmWujyjIQ|ev}(uvXRFr4Q)W#PpMBjkE#lAvFIVf6y!a7UzCNg8Ej$Z4CeGwwe&-J^ z#7LD}f)isz2JgPe(@aDc$iT6P5~Ez+oGTYz?fq5|lzpIt##(RS zY_>}?c;jk@bc|*gnBR$@qBjnRq;g|NT{A$Q+40tqlb3v625%B z-D;`qsr96&DuVTxjbXIHG=31*`yb-+|5g0?EevuACn@h;7i*FMvV5Gp=#`WN=TKkM zBM7908{Xz{&Ku{@N0C9xA@d3+4L9r*u}FUx+zK{Ztcpr;T}66Zt|P+x9ZEC%oo;s` zUFa{SL*Lz}()&y`(jBqIFj35eZEK!xiKlpNwzwSZfIF})z2nVFH5k%@sJ@%^`H6Ai^Ts()iQ;4J-=)x0VcY diff --git a/signature/fixtures_info_test.go b/signature/fixtures_info_test.go index a44a6b736..482d4bbc2 100644 --- a/signature/fixtures_info_test.go +++ b/signature/fixtures_info_test.go @@ -11,4 +11,8 @@ const ( TestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8" // TestKeyShortID is the short ID of the private key in this directory. TestKeyShortID = "DB72F2188BB46CC8" + // TestKeyFingerprintWithPassphrase is the fingerprint of the private key with passphrase in this directory. + TestKeyFingerprintWithPassphrase = "E3EB7611D815211F141946B5B0CDE60B42557346" + // TestPassphrase is the passphrase for TestKeyFingerprintWithPassphrase. + TestPassphrase = "WithPassphrase123" ) diff --git a/signature/mechanism.go b/signature/mechanism.go index ee3442cdf..9a32a4364 100644 --- a/signature/mechanism.go +++ b/signature/mechanism.go @@ -18,8 +18,6 @@ import ( // SigningMechanism abstracts a way to sign binary blobs and verify their signatures. // Each mechanism should eventually be closed by calling Close(). -// FIXME: Eventually expand on keyIdentity (namespace them between mechanisms to -// eliminate ambiguities, support CA signatures and perhaps other key properties) type SigningMechanism interface { // Close removes resources associated with the mechanism, if any. Close() error @@ -38,6 +36,15 @@ type SigningMechanism interface { UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) } +// signingMechanismWithPassphrase is an internal extension of SigningMechanism. +type signingMechanismWithPassphrase interface { + SigningMechanism + + // Sign creates a (non-detached) signature of input using keyIdentity and passphrase. + // Fails with a SigningNotSupportedError if the mechanism does not support signing. + SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) +} + // SigningNotSupportedError is returned when trying to sign using a mechanism which does not support that. type SigningNotSupportedError string diff --git a/signature/mechanism_gpgme.go b/signature/mechanism_gpgme.go index 5df205003..c166fb32d 100644 --- a/signature/mechanism_gpgme.go +++ b/signature/mechanism_gpgme.go @@ -5,6 +5,7 @@ package signature import ( "bytes" + "errors" "fmt" "io/ioutil" "os" @@ -20,7 +21,7 @@ type gpgmeSigningMechanism struct { // newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty. // The caller must call .Close() on the returned SigningMechanism. -func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) { +func newGPGSigningMechanismInDirectory(optionalDir string) (signingMechanismWithPassphrase, error) { ctx, err := newGPGMEContext(optionalDir) if err != nil { return nil, err @@ -35,7 +36,7 @@ func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, er // recognizes _only_ public keys from the supplied blob, and returns the identities // of these keys. // The caller must call .Close() on the returned SigningMechanism. -func newEphemeralGPGSigningMechanism(blob []byte) (SigningMechanism, []string, error) { +func newEphemeralGPGSigningMechanism(blob []byte) (signingMechanismWithPassphrase, []string, error) { dir, err := ioutil.TempDir("", "containers-ephemeral-gpg-") if err != nil { return nil, nil, err @@ -117,9 +118,9 @@ func (m *gpgmeSigningMechanism) SupportsSigning() error { return nil } -// Sign creates a (non-detached) signature of input using keyIdentity. +// 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 *gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) { +func (m *gpgmeSigningMechanism) SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) { key, err := m.ctx.GetKey(keyIdentity, true) if err != nil { return nil, err @@ -133,12 +134,38 @@ func (m *gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, if err != nil { return nil, err } + + if passphrase != "" { + // Callback to write the passphrase to the specified file descriptor. + callback := func(uidHint string, prevWasBad bool, gpgmeFD *os.File) error { + if prevWasBad { + return errors.New("bad passphrase") + } + _, err := gpgmeFD.WriteString(passphrase + "\n") + return err + } + if err := m.ctx.SetCallback(callback); err != nil { + return nil, fmt.Errorf("setting gpgme passphrase callback: %w", err) + } + + // Loopback mode will use the callback instead of prompting the user. + if err := m.ctx.SetPinEntryMode(gpgme.PinEntryLoopback); err != nil { + return nil, fmt.Errorf("setting gpgme pinentry mode: %w", err) + } + } + if err = m.ctx.Sign([]*gpgme.Key{key}, inputData, sigData, gpgme.SigModeNormal); err != nil { return nil, err } return sigBuffer.Bytes(), nil } +// Sign creates a (non-detached) signature of input using keyIdentity. +// Fails with a SigningNotSupportedError if the mechanism does not support signing. +func (m *gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) { + return m.SignWithPassphrase(input, keyIdentity, "") +} + // Verify parses unverifiedSignature and returns the content and the signer's identity func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) { signedBuffer := bytes.Buffer{} diff --git a/signature/mechanism_openpgp.go b/signature/mechanism_openpgp.go index 2f5ebb171..7a31425f1 100644 --- a/signature/mechanism_openpgp.go +++ b/signature/mechanism_openpgp.go @@ -30,7 +30,7 @@ type openpgpSigningMechanism struct { // newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty. // The caller must call .Close() on the returned SigningMechanism. -func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) { +func newGPGSigningMechanismInDirectory(optionalDir string) (signingMechanismWithPassphrase, error) { m := &openpgpSigningMechanism{ keyring: openpgp.EntityList{}, } @@ -61,7 +61,7 @@ func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, er // recognizes _only_ public keys from the supplied blob, and returns the identities // of these keys. // The caller must call .Close() on the returned SigningMechanism. -func newEphemeralGPGSigningMechanism(blob []byte) (SigningMechanism, []string, error) { +func newEphemeralGPGSigningMechanism(blob []byte) (signingMechanismWithPassphrase, []string, error) { m := &openpgpSigningMechanism{ keyring: openpgp.EntityList{}, } @@ -110,10 +110,16 @@ func (m *openpgpSigningMechanism) SupportsSigning() error { // Sign creates a (non-detached) signature of input using keyIdentity. // Fails with a SigningNotSupportedError if the mechanism does not support signing. -func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) { +func (m *openpgpSigningMechanism) SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) { return nil, SigningNotSupportedError("signing is not supported in github.com/containers/image built with the containers_image_openpgp build tag") } +// Sign creates a (non-detached) signature of input using keyIdentity. +// Fails with a SigningNotSupportedError if the mechanism does not support signing. +func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) { + return m.SignWithPassphrase(input, keyIdentity, "") +} + // Verify parses unverifiedSignature and returns the content and the signer's identity func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) { md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.keyring, nil, nil) diff --git a/signature/mechanism_test.go b/signature/mechanism_test.go index 5cc9d0747..a503223c2 100644 --- a/signature/mechanism_test.go +++ b/signature/mechanism_test.go @@ -150,7 +150,7 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) { mech, keyIdentities, err = NewEphemeralGPGSigningMechanism(bytes.Join([][]byte{keyBlob, keyBlob}, nil)) require.NoError(t, err) defer mech.Close() - assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprint}, keyIdentities) + assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprintWithPassphrase, TestKeyFingerprint, TestKeyFingerprintWithPassphrase}, keyIdentities) // Invalid input: This is, sadly, accepted anyway by GPG, just returns no keys. // For openpgpSigningMechanism we can detect this and fail. diff --git a/signature/signature.go b/signature/signature.go index 09f4f85e0..05bf8229e 100644 --- a/signature/signature.go +++ b/signature/signature.go @@ -190,12 +190,20 @@ func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error { // of the system just because it is a private key — actually the presence of a private key // on the system increases the likelihood of an a successful attack on that private key // on that particular system.) -func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string) ([]byte, error) { +func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string, passphrase string) ([]byte, error) { json, err := json.Marshal(s) if err != nil { return nil, err } + if newMech, ok := mech.(signingMechanismWithPassphrase); ok { + return newMech.SignWithPassphrase(json, keyIdentity, passphrase) + } + + if passphrase != "" { + return nil, errors.New("signing mechanism does not support passphrases") + } + return mech.Sign(json, keyIdentity) } diff --git a/signature/signature_test.go b/signature/signature_test.go index abfdb4bf5..08991532c 100644 --- a/signature/signature_test.go +++ b/signature/signature_test.go @@ -226,7 +226,7 @@ func TestSign(t *testing.T) { sig := newUntrustedSignature("digest!@#", "reference#@!") // Successful signing - signature, err := sig.sign(mech, TestKeyFingerprint) + signature, err := sig.sign(mech, TestKeyFingerprint, "") require.NoError(t, err) verified, err := verifyAndExtractSignature(mech, signature, signatureAcceptanceRules{ @@ -255,11 +255,11 @@ func TestSign(t *testing.T) { assert.Equal(t, sig.UntrustedDockerReference, verified.DockerReference) // Error creating blob to sign - _, err = untrustedSignature{}.sign(mech, TestKeyFingerprint) + _, err = untrustedSignature{}.sign(mech, TestKeyFingerprint, "") assert.Error(t, err) // Error signing - _, err = sig.sign(mech, "this fingerprint doesn't exist") + _, err = sig.sign(mech, "this fingerprint doesn't exist", "") assert.Error(t, err) }