Skip to content

Commit 2d18ea0

Browse files
committed
image: add oc adm verify-image-signature command
1 parent c4e8026 commit 2d18ea0

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed

pkg/cmd/admin/admin.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/openshift/origin/pkg/cmd/admin/cert"
1313
diagnostics "github.com/openshift/origin/pkg/cmd/admin/diagnostics"
1414
"github.com/openshift/origin/pkg/cmd/admin/groups"
15+
"github.com/openshift/origin/pkg/cmd/admin/image"
1516
"github.com/openshift/origin/pkg/cmd/admin/migrate"
1617
migrateimages "github.com/openshift/origin/pkg/cmd/admin/migrate/images"
1718
migratestorage "github.com/openshift/origin/pkg/cmd/admin/migrate/storage"
@@ -93,6 +94,7 @@ func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout
9394
migratestorage.NewCmdMigrateAPIStorage("storage", fullName+" "+migrate.MigrateRecommendedName+" storage", f, in, out, errout),
9495
),
9596
top.NewCommandTop(top.TopRecommendedName, fullName+" "+top.TopRecommendedName, f, out, errout),
97+
image.NewCmdVerifyImageSignature("verify-image-signature", fullName, f, out, errout),
9698
},
9799
},
98100
{

pkg/cmd/admin/image/openpgp.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package image
2+
3+
// TODO: Remove this wrapper when the 'containers/image' is suitable to pull as godep.
4+
5+
import (
6+
"bytes"
7+
"errors"
8+
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"path"
12+
"strings"
13+
"time"
14+
15+
"golang.org/x/crypto/openpgp"
16+
)
17+
18+
type SigningMechanism interface {
19+
Close() error
20+
// SupportsSigning returns nil if the mechanism supports signing, or a SigningNotSupportedError.
21+
SupportsSigning() error
22+
// Sign creates a (non-detached) signature of input using keyIdentity.
23+
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
24+
Sign(input []byte, keyIdentity string) ([]byte, error)
25+
// Verify parses unverifiedSignature and returns the content and the signer's identity
26+
Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error)
27+
}
28+
29+
// A GPG/OpenPGP signing mechanism, implemented using x/crypto/openpgp.
30+
type openpgpSigningMechanism struct {
31+
keyring openpgp.EntityList
32+
}
33+
34+
// SigningNotSupportedError is returned when trying to sign using a mechanism which does not support that.
35+
type SigningNotSupportedError string
36+
37+
func (err SigningNotSupportedError) Error() string {
38+
return string(err)
39+
}
40+
41+
// InvalidSignatureError is returned when parsing an invalid signature.
42+
type InvalidSignatureError struct {
43+
msg string
44+
}
45+
46+
func (err InvalidSignatureError) Error() string {
47+
return err.msg
48+
}
49+
50+
// newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty.
51+
// The caller must call .Close() on the returned SigningMechanism.
52+
func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) {
53+
m := &openpgpSigningMechanism{
54+
keyring: openpgp.EntityList{},
55+
}
56+
57+
homeDir := os.Getenv("HOME")
58+
gpgHome := optionalDir
59+
if gpgHome == "" {
60+
gpgHome = os.Getenv("GNUPGHOME")
61+
if gpgHome == "" {
62+
gpgHome = path.Join(homeDir, ".gnupg")
63+
}
64+
}
65+
66+
pubring, err := ioutil.ReadFile(path.Join(gpgHome, "pubring.gpg"))
67+
if err != nil {
68+
if !os.IsNotExist(err) {
69+
return nil, err
70+
}
71+
} else {
72+
_, err := m.importKeysFromBytes(pubring)
73+
if err != nil {
74+
return nil, err
75+
}
76+
}
77+
return m, nil
78+
}
79+
80+
// newEphemeralGPGSigningMechanism returns a new GPG/OpenPGP signing mechanism which
81+
// recognizes _only_ public keys from the supplied blob, and returns the identities
82+
// of these keys.
83+
// The caller must call .Close() on the returned SigningMechanism.
84+
func newEphemeralGPGSigningMechanism(blob []byte) (SigningMechanism, []string, error) {
85+
m := &openpgpSigningMechanism{
86+
keyring: openpgp.EntityList{},
87+
}
88+
keyIdentities, err := m.importKeysFromBytes(blob)
89+
if err != nil {
90+
return nil, nil, err
91+
}
92+
return m, keyIdentities, nil
93+
}
94+
95+
func (m *openpgpSigningMechanism) Close() error {
96+
return nil
97+
}
98+
99+
// importKeysFromBytes imports public keys from the supplied blob and returns their identities.
100+
// The blob is assumed to have an appropriate format (the caller is expected to know which one).
101+
func (m *openpgpSigningMechanism) importKeysFromBytes(blob []byte) ([]string, error) {
102+
keyring, err := openpgp.ReadKeyRing(bytes.NewReader(blob))
103+
if err != nil {
104+
k, e2 := openpgp.ReadArmoredKeyRing(bytes.NewReader(blob))
105+
if e2 != nil {
106+
return nil, err // The original error -- FIXME: is this better?
107+
}
108+
keyring = k
109+
}
110+
111+
keyIdentities := []string{}
112+
for _, entity := range keyring {
113+
if entity.PrimaryKey == nil {
114+
continue
115+
}
116+
// Uppercase the fingerprint to be compatible with gpgme
117+
keyIdentities = append(keyIdentities, strings.ToUpper(fmt.Sprintf("%x", entity.PrimaryKey.Fingerprint)))
118+
m.keyring = append(m.keyring, entity)
119+
}
120+
return keyIdentities, nil
121+
}
122+
123+
// SupportsSigning returns nil if the mechanism supports signing, or a SigningNotSupportedError.
124+
func (m *openpgpSigningMechanism) SupportsSigning() error {
125+
return SigningNotSupportedError("signing is not supported in github.com/containers/image built with the containers_image_openpgp build tag")
126+
}
127+
128+
// Sign creates a (non-detached) signature of input using keyIdentity.
129+
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
130+
func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) {
131+
return nil, SigningNotSupportedError("signing is not supported in github.com/containers/image built with the containers_image_openpgp build tag")
132+
}
133+
134+
// Verify parses unverifiedSignature and returns the content and the signer's identity
135+
func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
136+
md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.keyring, nil, nil)
137+
if err != nil {
138+
return nil, "", err
139+
}
140+
if !md.IsSigned {
141+
return nil, "", errors.New("not signed")
142+
}
143+
content, err := ioutil.ReadAll(md.UnverifiedBody)
144+
if err != nil {
145+
return nil, "", err
146+
}
147+
if md.SignatureError != nil {
148+
return nil, "", fmt.Errorf("signature error: %v", md.SignatureError)
149+
}
150+
if md.SignedBy == nil {
151+
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Invalid GPG signature: %#v", md.Signature)}
152+
}
153+
if md.Signature.SigLifetimeSecs != nil {
154+
expiry := md.Signature.CreationTime.Add(time.Duration(*md.Signature.SigLifetimeSecs) * time.Second)
155+
if time.Now().After(expiry) {
156+
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Signature expired on %s", expiry)}
157+
}
158+
}
159+
160+
// Uppercase the fingerprint to be compatible with gpgme
161+
return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil
162+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/ioutil"
7+
"os"
8+
9+
"github.com/openshift/origin/pkg/client"
10+
"github.com/openshift/origin/pkg/cmd/templates"
11+
"github.com/openshift/origin/pkg/cmd/util/clientcmd"
12+
imageapi "github.com/openshift/origin/pkg/image/api"
13+
"github.com/spf13/cobra"
14+
kapi "k8s.io/kubernetes/pkg/api"
15+
"k8s.io/kubernetes/pkg/api/unversioned"
16+
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
17+
)
18+
19+
var (
20+
verifyImageSignatureLongDesc = templates.LongDesc(`
21+
Verifies the imported image signature using the local public key.
22+
23+
This command verifies if the signature attached to an image is trusted by
24+
using the provided public GPG key.
25+
By default this command will not record the signature condition back to the Image object but only
26+
print the verification status to the console.
27+
28+
To record a new condition, you have to pass the "--confirm" flag.
29+
`)
30+
31+
verifyImageSignatureExample = templates.Examples(`
32+
# Verify the image signature using the public key and record the status as a condition to image
33+
%[1]s sha256:c841e9b64e4579bd56c794bdd7c36e1c257110fd2404bebbb8b613e4935228c4 --public-key=production.gpg --confirm
34+
`)
35+
)
36+
37+
type VerifyImageSignatureOptions struct {
38+
InputImage string
39+
PublicKeyFilename string
40+
PublicKey []byte
41+
Confirm bool
42+
Remove bool
43+
CurrentUser string
44+
45+
Client client.Interface
46+
Out io.Writer
47+
ErrOut io.Writer
48+
}
49+
50+
func NewCmdVerifyImageSignature(name, fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
51+
opts := &VerifyImageSignatureOptions{ErrOut: errOut, Out: out}
52+
cmd := &cobra.Command{
53+
Use: fmt.Sprintf("%s IMAGE [--confirm]", name),
54+
Short: "Verifies the given IMAGE signature with local public key",
55+
Long: verifyImageSignatureLongDesc,
56+
Example: fmt.Sprintf(verifyImageSignatureExample, name),
57+
Run: func(cmd *cobra.Command, args []string) {
58+
kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
59+
if opts.Remove {
60+
kcmdutil.CheckErr(opts.removeImageSignature())
61+
} else {
62+
kcmdutil.CheckErr(opts.Run())
63+
}
64+
},
65+
}
66+
67+
cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "If true, the result of the verification will be recorded to an image object.")
68+
cmd.Flags().BoolVar(&opts.Remove, "remove", opts.Remove, "If set, the current signature verification will be removed from the image.")
69+
cmd.Flags().StringVar(&opts.PublicKeyFilename, "public-key", opts.PublicKeyFilename, "A path to a public GPG key to be used for verification.")
70+
return cmd
71+
}
72+
73+
func (o *VerifyImageSignatureOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
74+
if len(args) != 1 {
75+
return kcmdutil.UsageError(cmd, "exactly one image must be specified")
76+
}
77+
o.InputImage = args[0]
78+
var err error
79+
80+
// If --public-key is provided only this key will be used for verification and the
81+
// .gnupg/pubring.gpg will be ignored.
82+
if len(o.PublicKeyFilename) > 0 {
83+
o.PublicKey, err = ioutil.ReadFile(o.PublicKeyFilename)
84+
if err != nil {
85+
return err
86+
}
87+
}
88+
if o.Client, _, err = f.Clients(); err != nil {
89+
return err
90+
}
91+
if o.Confirm && !o.Remove {
92+
if me, err := o.Client.Users().Get("~"); err != nil {
93+
return err
94+
} else {
95+
o.CurrentUser = me.Name
96+
}
97+
}
98+
99+
return nil
100+
}
101+
102+
// verifySignature verifies the image signature and return the identity when the signature
103+
// is valid.
104+
// TODO: This should be calling the 'containers/image' library in future.
105+
func (o *VerifyImageSignatureOptions) verifySignature(signature []byte) (string, error) {
106+
var (
107+
mechanism SigningMechanism
108+
err error
109+
)
110+
// If public key is specified, use JUST that key for verification. Otherwise use all
111+
// keys in local GPG public keyring.
112+
if len(o.PublicKeyFilename) == 0 {
113+
mechanism, err = newGPGSigningMechanismInDirectory("")
114+
} else {
115+
mechanism, _, err = newEphemeralGPGSigningMechanism(o.PublicKey)
116+
}
117+
if err != nil {
118+
return "", err
119+
}
120+
defer mechanism.Close()
121+
_, identity, err := mechanism.Verify(signature)
122+
if err != nil {
123+
return "", err
124+
}
125+
return string(identity), nil
126+
}
127+
128+
// removeImageSignature removes the current image signature from the Image object by
129+
// erasing all signature fields that were previously set (when image signature was
130+
// previously verified).
131+
func (o *VerifyImageSignatureOptions) removeImageSignature() error {
132+
img, err := o.Client.Images().Get(o.InputImage)
133+
if err != nil {
134+
return err
135+
}
136+
if len(img.Signatures) == 0 {
137+
return fmt.Errorf("%s does not have signature", img.Name)
138+
}
139+
if !o.Confirm {
140+
fmt.Fprintf(o.Out, "(add --confirm to record signature verification status to server\n")
141+
return nil
142+
}
143+
for i := range img.Signatures {
144+
newConditions := []imageapi.SignatureCondition{}
145+
img.Signatures[i].Conditions = newConditions
146+
img.Signatures[i].IssuedBy = nil
147+
}
148+
_, err = o.Client.Images().Update(img)
149+
return err
150+
}
151+
152+
func (o *VerifyImageSignatureOptions) Run() error {
153+
img, err := o.Client.Images().Get(o.InputImage)
154+
if err != nil {
155+
return err
156+
}
157+
if len(img.Signatures) == 0 {
158+
return fmt.Errorf("%s does not have signature", img.Name)
159+
}
160+
161+
for i, s := range img.Signatures {
162+
// Do the actual verification of the image signature
163+
signedBy, signatureErr := o.verifySignature(s.Content)
164+
165+
if signatureErr != nil {
166+
if o.Confirm {
167+
fmt.Fprintf(o.ErrOut, "%s: %v\n", o.InputImage, signatureErr)
168+
} else {
169+
return fmt.Errorf("%s: %v", o.InputImage, signatureErr)
170+
}
171+
} else {
172+
fmt.Fprintf(o.Out, "%s signature is verified (signed by key: %q)\n", o.InputImage, signedBy)
173+
}
174+
175+
if signatureErr != nil {
176+
// If an error occured during signature verification, remove the verified fields
177+
// (if present).
178+
fmt.Fprintf(o.ErrOut, "%s signature cannot be verified: %v\n", o.InputImage, signatureErr)
179+
if err := o.removeImageSignature(); err == nil {
180+
os.Exit(1)
181+
} else {
182+
return err
183+
}
184+
}
185+
186+
if o.Confirm {
187+
now := unversioned.Now()
188+
newConditions := []imageapi.SignatureCondition{
189+
{
190+
Type: imageapi.SignatureTrusted,
191+
Status: kapi.ConditionTrue,
192+
LastProbeTime: now,
193+
LastTransitionTime: now,
194+
Reason: "verified manually",
195+
Message: fmt.Sprintf("verified by user %s", o.CurrentUser),
196+
},
197+
// FIXME: This condition is required to be set for validation.
198+
{
199+
Type: imageapi.SignatureForImage,
200+
Status: kapi.ConditionTrue,
201+
LastProbeTime: now,
202+
LastTransitionTime: now,
203+
},
204+
}
205+
img.Signatures[i].Conditions = newConditions
206+
img.Signatures[i].IssuedBy = &imageapi.SignatureIssuer{}
207+
// TODO: This should not be just a key id but a human-readable identity.
208+
img.Signatures[i].IssuedBy.CommonName = signedBy
209+
// Record updated information back to the server
210+
if _, err := o.Client.Images().Update(img); err != nil {
211+
return err
212+
}
213+
} else {
214+
fmt.Fprintf(o.Out, "(add --confirm to record signature verification status to server\n")
215+
return nil
216+
}
217+
218+
}
219+
return nil
220+
}

0 commit comments

Comments
 (0)