Skip to content

Commit 8cab5d1

Browse files
committed
image: add adm verify-image-signature command
1 parent dc82c39 commit 8cab5d1

File tree

3 files changed

+417
-0
lines changed

3 files changed

+417
-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"
@@ -94,6 +95,7 @@ func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout
9495
migratestorage.NewCmdMigrateAPIStorage("storage", fullName+" "+migrate.MigrateRecommendedName+" storage", f, in, out, errout),
9596
),
9697
top.NewCommandTop(top.TopRecommendedName, fullName+" "+top.TopRecommendedName, f, out, errout),
98+
image.NewCmdVerifyImageSignature("verify-image-signature", fullName, f, out, errout),
9799
},
98100
},
99101
{

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+
}

0 commit comments

Comments
 (0)