forked from notaryproject/notation-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
notation.go
327 lines (278 loc) · 12.3 KB
/
notation.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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// Package notation provides signer and verifier for notation Sign
// and Verification.
package notation
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
orasRegistry "oras.land/oras-go/v2/registry"
)
const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256"
var errDoneVerification = errors.New("done verification")
// SignOptions contains parameters for Signer.Sign.
type SignOptions struct {
// ArtifactReference sets the reference of the artifact that needs to be signed.
ArtifactReference string
// SignatureMediaType is the envelope type of the signature.
// Currently both `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string
// ExpiryDuration identifies the expiry duration of the resulted signature. Zero value
// represents no expiry duration.
ExpiryDuration time.Duration
// PluginConfig sets or overrides the plugin configuration.
PluginConfig map[string]string
// SigningAgent sets the signing agent name
SigningAgent string
}
// Signer is a generic interface for signing an artifact.
// The interface allows signing with local or remote keys,
// and packing in various signature formats.
type Signer interface {
// Sign signs the artifact described by its descriptor,
// and returns the signature and SignerInfo.
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error)
}
// Sign signs the artifact in the remote registry and push the signature to the
// remote.
// The descriptor of the sign content is returned upon sucessful signing.
func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (ocispec.Descriptor, error) {
// Input validation for expiry duration
if opts.ExpiryDuration < 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
}
if opts.ExpiryDuration%time.Second != 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
}
logger := log.GetLogger(ctx)
artifactRef := opts.ArtifactReference
ref, err := orasRegistry.ParseReference(artifactRef)
if err != nil {
return ocispec.Descriptor{}, err
}
if ref.Reference == "" {
return ocispec.Descriptor{}, errors.New("reference is missing digest or tag")
}
targetDesc, err := repo.Resolve(ctx, artifactRef)
if err != nil {
return ocispec.Descriptor{}, err
}
if ref.ValidateReferenceAsDigest() != nil {
// artifactRef is not a digest reference
logger.Warnf("Always sign the artifact using digest(`@sha256:...`) rather than a tag(`:%s`) because tags are mutable and a tag reference can point to a different artifact than the one signed", ref.Reference)
logger.Infof("Resolved artifact tag `%s` to digest `%s` before signing", ref.Reference, targetDesc.Digest.String())
}
sig, signerInfo, err := signer.Sign(ctx, targetDesc, opts)
if err != nil {
return ocispec.Descriptor{}, err
}
logger.Debug("Generating annotation")
annotations, err := generateAnnotations(signerInfo)
if err != nil {
return ocispec.Descriptor{}, err
}
logger.Debugf("Generated annotations: %+v", annotations)
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, opts.SignatureMediaType)
_, _, err = repo.PushSignature(ctx, opts.SignatureMediaType, sig, targetDesc, annotations)
if err != nil {
logger.Error("Failed to push the signature")
return ocispec.Descriptor{}, err
}
return targetDesc, nil
}
// ValidationResult encapsulates the verification result (passed or failed)
// for a verification type, including the desired verification action as
// specified in the trust policy
type ValidationResult struct {
// Type of verification that is performed
Type trustpolicy.ValidationType
// Action is the intended action for the given verification type as defined
// in the trust policy
Action trustpolicy.ValidationAction
// Error is set if there are any errors during the verification process
Error error
}
// VerificationOutcome encapsulates a signature blob's descriptor, its content,
// the verification level and results for each verification type that was
// performed.
type VerificationOutcome struct {
// RawSignature is the signature envelope blob
RawSignature []byte
// EnvelopeContent contains the details of the digital signature and
// associated metadata
EnvelopeContent *signature.EnvelopeContent
// VerificationLevel describes what verification level was used for
// performing signature verification
VerificationLevel *trustpolicy.VerificationLevel
// VerificationResults contains the verifications performed on the signature
// and their results
VerificationResults []*ValidationResult
// Error that caused the verification to fail (if it fails)
Error error
}
// VerifyOptions contains parameters for Verifier.Verify.
type VerifyOptions struct {
// ArtifactReference is the reference of the artifact that is been
// verified against to.
ArtifactReference string
// SignatureMediaType is the envelope type of the signature.
// Currently both `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string
// PluginConfig is a map of plugin configs.
PluginConfig map[string]string
}
// Verifier is a generic interface for verifying an artifact.
type Verifier interface {
// Verify verifies the signature blob `signature` against the target OCI
// artifact with manifest descriptor `desc`, and returns the outcome upon
// successful verification.
// If nil signature is present and the verification level is not 'skip',
// an error will be returned.
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifyOptions) (*VerificationOutcome, error)
}
// RemoteVerifyOptions contains parameters for notation.Verify.
type RemoteVerifyOptions struct {
// ArtifactReference is the reference of the artifact that is been
// verified against to.
ArtifactReference string
// PluginConfig is a map of plugin configs.
PluginConfig map[string]string
// MaxSignatureAttempts is the maximum number of signature envelopes that
// will be processed for verification. If set to less than or equals
// to zero, an error will be returned.
MaxSignatureAttempts int
}
type skipVerifier interface {
// SkipVerify validates whether the verification level is skip.
SkipVerify(ctx context.Context, artifactRef string) (bool, *trustpolicy.VerificationLevel, error)
}
// Verify performs signature verification on each of the notation supported
// verification types (like integrity, authenticity, etc.) and return the
// successful signature verification outcome.
// For more details on signature verification, see
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, remoteOpts RemoteVerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) {
logger := log.GetLogger(ctx)
// opts to be passed in verifier.Verify()
opts := VerifyOptions{
ArtifactReference: remoteOpts.ArtifactReference,
PluginConfig: remoteOpts.PluginConfig,
}
if skipChecker, ok := verifier.(skipVerifier); ok {
logger.Info("Checking whether signature verification should be skipped or not")
skip, verificationLevel, err := skipChecker.SkipVerify(ctx, opts.ArtifactReference)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
if skip {
logger.Infoln("Verification skipped for", remoteOpts.ArtifactReference)
return ocispec.Descriptor{}, []*VerificationOutcome{{VerificationLevel: verificationLevel}}, nil
}
logger.Info("Check over. Trust policy is not configured to skip signature verification")
}
// check MaxSignatureAttempts
if remoteOpts.MaxSignatureAttempts <= 0 {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", remoteOpts.MaxSignatureAttempts)}
}
// get artifact descriptor
artifactRef := remoteOpts.ArtifactReference
ref, err := orasRegistry.ParseReference(artifactRef)
if err != nil {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()}
}
if ref.Reference == "" {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: "reference is missing digest or tag"}
}
artifactDescriptor, err := repo.Resolve(ctx, artifactRef)
if err != nil {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()}
}
if ref.ValidateReferenceAsDigest() != nil {
// artifactRef is not a digest reference
logger.Infof("Resolved artifact tag `%s` to digest `%s` before verification", ref.Reference, artifactDescriptor.Digest.String())
logger.Warn("The resolved digest may not point to the same signed artifact, since tags are mutable")
}
var verificationOutcomes []*VerificationOutcome
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", remoteOpts.MaxSignatureAttempts)}
numOfSignatureProcessed := 0
// get signature manifests
logger.Debug("Fetching signature manifests using referrers API")
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
// process signatures
for _, sigManifestDesc := range signatureManifests {
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
break
}
numOfSignatureProcessed++
logger.Infof("Processing signature with manifest mediaType: %v and digest: %v", sigManifestDesc.MediaType, sigManifestDesc.Digest)
// get signature envelope
sigBlob, sigDesc, err := repo.FetchSignatureBlob(ctx, sigManifestDesc)
if err != nil {
return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %v", sigManifestDesc.Digest, artifactRef, err.Error())}
}
// using signature media type fetched from registry
opts.SignatureMediaType = sigDesc.MediaType
// verify each signature
outcome, err := verifier.Verify(ctx, artifactDescriptor, sigBlob, opts)
if err != nil {
logger.Infof("Signature %v failed verification with error: %v", sigManifestDesc.Digest, err)
if outcome == nil {
logger.Error("Got nil outcome. Expecting non-nil outcome on verification failure")
return err
}
continue
}
// at this point, the signature is verified successfully. Add
// it to the verificationOutcomes.
verificationOutcomes = append(verificationOutcomes, outcome)
logger.Debugf("Signature verification succeeded for artifact %v with signature digest %v", artifactDescriptor.Digest, sigManifestDesc.Digest)
// early break on success
return errDoneVerification
}
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
return errExceededMaxVerificationLimit
}
return nil
})
if err != nil && !errors.Is(err, errDoneVerification) {
if errors.Is(err, errExceededMaxVerificationLimit) {
return ocispec.Descriptor{}, verificationOutcomes, err
}
return ocispec.Descriptor{}, nil, err
}
// If there's no signature associated with the reference
if numOfSignatureProcessed == 0 {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)}
}
// Verification Failed
if len(verificationOutcomes) == 0 {
logger.Debugf("Signature verification failed for all the signatures associated with artifact %v", artifactDescriptor.Digest)
return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{}
}
// Verification Succeeded
return artifactDescriptor, verificationOutcomes, nil
}
func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, error) {
var thumbprints []string
for _, cert := range signerInfo.CertificateChain {
checkSum := sha256.Sum256(cert.Raw)
thumbprints = append(thumbprints, hex.EncodeToString(checkSum[:]))
}
val, err := json.Marshal(thumbprints)
if err != nil {
return nil, err
}
return map[string]string{
annotationX509ChainThumbprint: string(val),
}, nil
}