-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
sign.go
258 lines (233 loc) · 8.45 KB
/
sign.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 trust
import (
"context"
"fmt"
"io"
"path"
"sort"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
)
type signOptions struct {
local bool
imageName string
}
func newSignCommand(dockerCli command.Cli) *cobra.Command {
options := signOptions{}
cmd := &cobra.Command{
Use: "sign IMAGE:TAG",
Short: "Sign an image",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.imageName = args[0]
return runSignImage(dockerCli, options)
},
}
flags := cmd.Flags()
flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image")
return cmd
}
func runSignImage(cli command.Cli, options signOptions) error {
imageName := options.imageName
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), imageName)
if err != nil {
return err
}
if err := validateTag(imgRefAndAuth); err != nil {
return err
}
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if err = clearChangeList(notaryRepo); err != nil {
return err
}
defer clearChangeList(notaryRepo)
// get the latest repository metadata so we can figure out which roles to sign
if _, err = notaryRepo.ListTargets(); err != nil {
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
// before initializing a new repo, check that the image exists locally:
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
return err
}
userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username))
if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
fmt.Fprintf(cli.Out(), "Created signer: %s\n", imgRefAndAuth.AuthConfig().Username)
fmt.Fprintf(cli.Out(), "Finished initializing signed repository for %s\n", imageName)
default:
return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
}
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
target, err := createTarget(notaryRepo, imgRefAndAuth.Tag())
if err != nil || options.local {
switch err := err.(type) {
// If the error is nil then the local flag is set
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist, nil:
// Fail fast if the image doesn't exist locally
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
return err
}
fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
authConfig := command.ResolveAuthConfig(ctx, cli, imgRefAndAuth.RepoInfo().Index)
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePushOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
}
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), options)
default:
return err
}
}
return signAndPublishToTarget(cli.Out(), imgRefAndAuth, notaryRepo, target)
}
func signAndPublishToTarget(out io.Writer, imgRefAndAuth trust.ImageRefAndAuth, notaryRepo client.Repository, target client.Target) error {
tag := imgRefAndAuth.Tag()
fmt.Fprintf(out, "Signing and pushing trust metadata for %s\n", imgRefAndAuth.Name())
existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag)
if err != nil {
return err
}
err = image.AddTargetToAllSignableRoles(notaryRepo, &target)
if err == nil {
prettyPrintExistingSignatureInfo(out, existingSigInfo)
err = notaryRepo.Publish()
}
if err != nil {
return errors.Wrapf(err, "failed to sign %s:%s", imgRefAndAuth.RepoInfo().Name.Name(), tag)
}
fmt.Fprintf(out, "Successfully signed %s:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag)
return nil
}
func validateTag(imgRefAndAuth trust.ImageRefAndAuth) error {
tag := imgRefAndAuth.Tag()
if tag == "" {
if imgRefAndAuth.Digest() != "" {
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
}
return fmt.Errorf("No tag specified for %s", imgRefAndAuth.Name())
}
return nil
}
func checkLocalImageExistence(ctx context.Context, cli command.Cli, imageName string) error {
_, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
return err
}
func createTarget(notaryRepo client.Repository, tag string) (client.Target, error) {
target := &client.Target{}
var err error
if tag == "" {
return *target, fmt.Errorf("No tag specified")
}
target.Name = tag
target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag)
return *target, err
}
func getSignedManifestHashAndSize(notaryRepo client.Repository, tag string) (data.Hashes, int64, error) {
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
if err != nil {
return nil, 0, err
}
return getReleasedTargetHashAndSize(targets, tag)
}
func getReleasedTargetHashAndSize(targets []client.TargetSignedStruct, tag string) (data.Hashes, int64, error) {
for _, tgt := range targets {
if isReleasedTarget(tgt.Role.Name) {
return tgt.Target.Hashes, tgt.Target.Length, nil
}
}
return nil, 0, client.ErrNoSuchTarget(tag)
}
func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag string) (trustTagRow, error) {
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
if err != nil {
return trustTagRow{}, err
}
releasedTargetInfoList := matchReleasedSignatures(targets)
if len(releasedTargetInfoList) == 0 {
return trustTagRow{}, nil
}
return releasedTargetInfoList[0], nil
}
func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) {
sort.Strings(existingSigInfo.Signers)
joinedSigners := strings.Join(existingSigInfo.Signers, ", ")
fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners)
}
func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
if err != nil {
return err
}
rootKeyID := rootKey.ID()
// Initialize the notary repository with a remotely managed snapshot key
if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
return err
}
signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner)
if err != nil {
return err
}
if err := addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey}); err != nil {
return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSigner.String(), "targets/"))
}
return notaryRepo.Publish()
}
// generates an ECDSA key without a GUN for the specified role
func getOrGenerateNotaryKey(notaryRepo client.Repository, role data.RoleName) (data.PublicKey, error) {
// use the signer name in the PEM headers if this is a delegation key
if data.IsDelegation(role) {
role = data.RoleName(notaryRoleToSigner(role))
}
keys := notaryRepo.GetCryptoService().ListKeys(role)
var err error
var key data.PublicKey
// always select the first key by ID
if len(keys) > 0 {
sort.Strings(keys)
keyID := keys[0]
privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID)
if err != nil {
return nil, err
}
key = data.PublicKeyFromPrivate(privKey)
} else {
key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey)
if err != nil {
return nil, err
}
}
return key, nil
}
// stages changes to add a signer with the specified name and key(s). Adds to targets/<name> and targets/releases
func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) error {
// create targets/<username>
if err := notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys); err != nil {
return err
}
if err := notaryRepo.AddDelegationPaths(newSigner, []string{""}); err != nil {
return err
}
// create targets/releases
if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
return err
}
return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
}