/
image.go
258 lines (207 loc) · 7.86 KB
/
image.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
package main
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"strings"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/theupdateframework/notary"
"github.com/cnabio/signy/pkg/intoto"
"github.com/cnabio/signy/pkg/tuf"
)
func buildImageCommands() *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Short: "Image commands",
Long: "Commands for working with images.",
}
cmd.AddCommand(buildImagePullCommand())
cmd.AddCommand(buildImagePushCommand())
return cmd
}
func buildImagePushCommand() *cobra.Command {
push := pushCmd{}
cmd := &cobra.Command{
Use: "push [target reference]",
Short: "Pushes an image to a registry and trust data to TUF",
Long: "Pushes an image to a registry and gets it's digest. After it's pushed, it pushes the digest to TUF alongside it's in-toto metadata",
RunE: func(cmd *cobra.Command, args []string) error {
return push.run()
},
}
//need to set this to automatically check for env variables for viper.get
viper.AutomaticEnv()
cmd.Flags().StringVarP(&push.pushImage, "image", "i", "", "container image to push (must be built on your local system)")
cmd.Flags().StringVarP(&push.layout, "layout", "", "intoto/root.layout", "Path to the in-toto root layout file")
cmd.Flags().StringVarP(&push.linkDir, "links", "", "intoto/", "Path to the in-toto links directory")
cmd.Flags().StringVarP(&push.layoutKey, "layout-key", "", "intoto/root.pub", "Path to the in-toto root layout public keys")
cmd.Flags().StringVarP(&push.registryUser, "registryUser", "", viper.GetString("PUSH_REGISTRY_USER"), "docker registry user, also uses the PUSH_REGISTRY_USER environment variable")
cmd.Flags().StringVarP(&push.registryCredentials, "registryCredentials", "", viper.GetString("PUSH_REGISTRY_CREDENTIALS"), "docker registry credentials (api key or password), uses the PUSH_REGISTRY_CREDENTIALS environment variable")
return cmd
}
func buildImagePullCommand() *cobra.Command {
pull := pullCmd{}
cmd := &cobra.Command{
Use: "pull [target reference]",
Short: "Pulls an image from a registry and trust data from TUF and verifies it",
Long: "Pulls an image from a registry. After it's pulled, it compares it's digest with what was stored in TUF and then verifies its in-toto metadata",
RunE: func(cmd *cobra.Command, args []string) error {
return pull.run()
},
}
cmd.Flags().StringVarP(&pull.pullImage, "image", "i", "", "container image to pull")
//TODO: Add --verifyOnOS flag and verificationImage
return cmd
}
type pullCmd struct {
pullImage string
}
type pushCmd struct {
pushImage string
layout string
// TODO: figure out a way to pass layout root key to TUF (not in the custom object)
layoutKey string
linkDir string
registryCredentials string
registryUser string
}
func (v *pullCmd) run() error {
if v.pullImage == "" {
return fmt.Errorf("Must specify an image for pull")
}
ctx := context.Background()
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("Couldn't initialize dockerClient")
}
//pull the image from the repository
log.Infof("Pulling image %v from registry", v.pullImage)
_, err = cli.ImagePull(ctx, v.pullImage, types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("Couldnt pull image %v", err)
}
//there has to be a better way do do this, we inspect the image we just pulled, that image has a few digests (for example, if an image was tagged multiple times)
imageDigests, _, err := cli.ImageInspectWithRaw(ctx, v.pullImage)
if err != nil {
return err
}
pulledSHA := ""
for _, element := range imageDigests.RepoDigests {
//remove the tag, since we have only digest now (image@sha256:)
parts := strings.Split(v.pullImage, ":")
if strings.Contains(element, parts[0]) {
//remove the image:@sha256, return only the actual sha
pulledSHA = strings.Split(element, ":")[1]
}
}
log.Infof("Successfully pulled image %v", v.pullImage)
//pull the data from notary
target, trustedSHA, err := tuf.GetTargetAndSHA(v.pullImage, trustServer, tlscacert, trustDir, timeout)
if err != nil {
return err
}
if pulledSHA == trustedSHA {
log.Infof("Pulled SHA matches TUF SHA: SHA256: %v matches %v", pulledSHA, trustedSHA)
} else {
return fmt.Errorf("Pulled image digest doesn't match TUF SHA! Pulled SHA: %v doesn't match TUF SHA: %v ", pulledSHA, trustedSHA)
}
if target.Custom == nil {
return fmt.Errorf("Error: TUF server doesn't have the custom field filled with in-toto metadata")
}
/*
TODO: Allow other verifications like `Signy verify` does, also fail better when RuleVerificationError happen
//return intoto.VerifyInContainer(target, []byte(v.pullImage), v.verificationImage, logLevel)
*/
return intoto.VerifyOnOS(target, []byte(v.pullImage))
}
func (v *pushCmd) run() error {
if v.pushImage == "" {
return fmt.Errorf("Must specify an image for push")
}
if v.layout == "" || v.linkDir == "" || v.layoutKey == "" {
return fmt.Errorf("Required in-toto metadata not found")
}
if intoto.ValidateFromPath(v.layout) != nil {
return fmt.Errorf("validation for in-toto metadata failed")
}
//set up our docker client
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
//setup auth to docker repo
authConfig := types.AuthConfig{
Username: v.registryUser,
Password: v.registryCredentials,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
ctx := context.Background()
log.Infof("Pushing image %v to registry", v.pushImage)
//push the image
resp, err := cli.ImagePush(ctx, v.pushImage, types.ImagePushOptions{RegistryAuth: authStr})
defer resp.Close()
if err != nil {
return fmt.Errorf("cannot push image to repository: %v", err)
}
//for debugging, or else you cant see if wrong pw
//TODO: How to see this info all the time? if you consume it, it's no longer usable in the future
//io.Copy(os.Stdout, resp)
//get the result of push, this is weird because it requires getting the aux. value of the response
pushResult, err := parseDockerDaemonJSONMessages(resp)
if err != nil {
return err
}
log.Infof("Image successfully pushed: {tag, sha, size} %v", pushResult)
log.Infof("Adding In-Toto layout and links metadata to TUF")
//get the json message we'll be adding to the custom field
custom, err := intoto.GetMetadataRawMessage(v.layout, v.linkDir, v.layoutKey)
if err != nil {
return fmt.Errorf("cannot get metadata message: %v", err)
}
//Sign and publish and get a target back
target, err := tuf.SignAndPublishWithImagePushResult(trustDir, trustServer, v.pushImage, pushResult, tlscacert, "", timeout, &custom)
if err != nil {
return fmt.Errorf("cannot sign and publish trust data: %v", err)
}
log.Infof("Pushed trust data for %v: %v ", v.pushImage, hex.EncodeToString(target.Hashes[notary.SHA256]))
return nil
}
//the docker daemon responds with a lot of messages. we're only interested in the response with the aux field, which contains the digest
func parseDockerDaemonJSONMessages(r io.Reader) (types.PushResult, error) {
var result types.PushResult
decoder := json.NewDecoder(r)
for {
var jsonMessage jsonmessage.JSONMessage
if err := decoder.Decode(&jsonMessage); err != nil {
if err == io.EOF {
break
}
return result, err
}
if err := jsonMessage.Error; err != nil {
return result, err
}
if jsonMessage.Aux != nil {
var r types.PushResult
if err := json.Unmarshal(*jsonMessage.Aux, &r); err != nil {
logrus.Warnf("Failed to unmarshal aux message. Cause: %s", err)
} else {
result = r
}
}
}
return result, nil
}