diff --git a/errors/errors.go b/errors/errors.go index 572e5e42..c4cac6e4 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -89,4 +89,7 @@ var ( ErrUserDataNotAllowed = errors.New("repodb: user data operations are not allowed") ErrCouldNotPersistData = errors.New("repodb: could not persist to db") ErrDedupeRebuild = errors.New("dedupe: couldn't rebuild dedupe index") + ErrSignConfigDirNotSet = errors.New("signatures: signature config dir not set") + ErrBadManifestDigest = errors.New("signatures: bad manifest digest") + ErrInvalidSignatureType = errors.New("signatures: invalid signature type") ) diff --git a/go.mod b/go.mod index fbee19f2..87fcfaf9 100644 --- a/go.mod +++ b/go.mod @@ -384,7 +384,7 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/fulcio v1.2.0 // indirect github.com/sigstore/rekor v1.1.1 // indirect - github.com/sigstore/sigstore v1.6.3 // indirect + github.com/sigstore/sigstore v1.6.3 github.com/sirupsen/logrus v1.9.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartystreets/assertions v1.13.1 // indirect diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index 316ac889..f3d83b83 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -385,3 +385,43 @@ func TestLabels(t *testing.T) { So(vendor, ShouldEqual, "zot-vendor") }) } + +func TestGetSignaturesInfo(t *testing.T) { + Convey("Test get signatures info - cosign", t, func() { + indexDigest := godigest.FromString("123") + repoMeta := repodb.RepoMetadata{ + Signatures: map[string]repodb.ManifestSignatures{string(indexDigest): {"cosign": []repodb.SignatureInfo{{ + LayersInfo: []repodb.LayerInfo{{LayerContent: []byte{}, LayerDigest: "", SignatureKey: "", Signer: "author"}}, + }}}}, + } + + signaturesSummary := convert.GetSignaturesInfo(true, repoMeta, indexDigest) + So(signaturesSummary, ShouldNotBeEmpty) + So(*signaturesSummary[0].Author, ShouldEqual, "author") + So(*signaturesSummary[0].IsTrusted, ShouldEqual, true) + So(*signaturesSummary[0].Tool, ShouldEqual, "cosign") + }) + + Convey("Test get signatures info - notation", t, func() { + indexDigest := godigest.FromString("123") + repoMeta := repodb.RepoMetadata{ + Signatures: map[string]repodb.ManifestSignatures{string(indexDigest): {"notation": []repodb.SignatureInfo{{ + LayersInfo: []repodb.LayerInfo{ + { + LayerContent: []byte{}, + LayerDigest: "", + SignatureKey: "", + Signer: "author", + Date: time.Now().AddDate(0, 0, -1), + }, + }, + }}}}, + } + + signaturesSummary := convert.GetSignaturesInfo(true, repoMeta, indexDigest) + So(signaturesSummary, ShouldNotBeEmpty) + So(*signaturesSummary[0].Author, ShouldEqual, "author") + So(*signaturesSummary[0].IsTrusted, ShouldEqual, false) + So(*signaturesSummary[0].Tool, ShouldEqual, "notation") + }) +} diff --git a/pkg/extensions/search/convert/repodb.go b/pkg/extensions/search/convert/repodb.go index 80fd74cf..9ed1847c 100644 --- a/pkg/extensions/search/convert/repodb.go +++ b/pkg/extensions/search/convert/repodb.go @@ -243,6 +243,8 @@ func ImageIndex2ImageSummary(ctx context.Context, repo, tag string, indexDigest annotations := GetAnnotations(indexContent.Annotations, map[string]string{}) + signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, indexDigest) + indexSummary := gql_generated.ImageSummary{ RepoName: &repo, Tag: &tag, @@ -251,6 +253,7 @@ func ImageIndex2ImageSummary(ctx context.Context, repo, tag string, indexDigest Manifests: manifestSummaries, LastUpdated: &indexLastUpdated, IsSigned: &isSigned, + SignatureInfo: signaturesInfo, Size: &indexSize, DownloadCount: &totalDownloadCount, Description: &annotations.Description, @@ -354,6 +357,8 @@ func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest go } } + signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, digest) + imageSummary := gql_generated.ImageSummary{ RepoName: &repoName, Tag: &tag, @@ -366,6 +371,7 @@ func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest go LastUpdated: &imageLastUpdated, Size: &imageSize, IsSigned: &isSigned, + SignatureInfo: signaturesInfo, Platform: &platform, DownloadCount: &downloadCount, Layers: getLayersSummaries(manifestContent), @@ -380,6 +386,7 @@ func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest go }, LastUpdated: &imageLastUpdated, IsSigned: &isSigned, + SignatureInfo: signaturesInfo, Size: &imageSize, DownloadCount: &downloadCount, Description: &annotations.Description, @@ -511,6 +518,8 @@ func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descri } } + signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, digest) + manifestSummary := gql_generated.ManifestSummary{ Digest: &manifestDigestStr, ConfigDigest: &configDigest, @@ -521,6 +530,7 @@ func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descri Layers: getLayersSummaries(manifestContent), History: historyEntries, IsSigned: &isSigned, + SignatureInfo: signaturesInfo, Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ MaxSeverity: &imageCveSummary.MaxSeverity, Count: &imageCveSummary.Count, @@ -744,3 +754,44 @@ func GetPreloadString(prefix, name string) string { return name } + +func GetSignaturesInfo(isSigned bool, repoMeta repodb.RepoMetadata, indexDigest godigest.Digest, +) []*gql_generated.SignatureSummary { + signaturesInfo := []*gql_generated.SignatureSummary{} + + if !isSigned { + return signaturesInfo + } + + for sigType, signatures := range repoMeta.Signatures[indexDigest.String()] { + for _, sig := range signatures { + for _, layer := range sig.LayersInfo { + var ( + isTrusted bool + author string + tool string + ) + + if layer.Signer != "" { + author = layer.Signer + + if !layer.Date.IsZero() && time.Now().After(layer.Date) { + isTrusted = false + } else { + isTrusted = true + } + } else { + isTrusted = false + author = "" + } + + tool = sigType + + signaturesInfo = append(signaturesInfo, + &gql_generated.SignatureSummary{Tool: &tool, IsTrusted: &isTrusted, Author: &author}) + } + } + } + + return signaturesInfo +} diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index be90de0a..8b241ae3 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -91,6 +91,7 @@ type ComplexityRoot struct { MediaType func(childComplexity int) int Referrers func(childComplexity int) int RepoName func(childComplexity int) int + SignatureInfo func(childComplexity int) int Size func(childComplexity int) int Source func(childComplexity int) int Tag func(childComplexity int) int @@ -125,6 +126,7 @@ type ComplexityRoot struct { Layers func(childComplexity int) int Platform func(childComplexity int) int Referrers func(childComplexity int) int + SignatureInfo func(childComplexity int) int Size func(childComplexity int) int Vulnerabilities func(childComplexity int) int } @@ -197,6 +199,12 @@ type ComplexityRoot struct { StarCount func(childComplexity int) int Vendors func(childComplexity int) int } + + SignatureSummary struct { + Author func(childComplexity int) int + IsTrusted func(childComplexity int) int + Tool func(childComplexity int) int + } } type QueryResolver interface { @@ -455,6 +463,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.RepoName(childComplexity), true + case "ImageSummary.SignatureInfo": + if e.complexity.ImageSummary.SignatureInfo == nil { + break + } + + return e.complexity.ImageSummary.SignatureInfo(childComplexity), true + case "ImageSummary.Size": if e.complexity.ImageSummary.Size == nil { break @@ -609,6 +624,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ManifestSummary.Referrers(childComplexity), true + case "ManifestSummary.SignatureInfo": + if e.complexity.ManifestSummary.SignatureInfo == nil { + break + } + + return e.complexity.ManifestSummary.SignatureInfo(childComplexity), true + case "ManifestSummary.Size": if e.complexity.ManifestSummary.Size == nil { break @@ -987,6 +1009,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RepoSummary.Vendors(childComplexity), true + case "SignatureSummary.Author": + if e.complexity.SignatureSummary.Author == nil { + break + } + + return e.complexity.SignatureSummary.Author(childComplexity), true + + case "SignatureSummary.IsTrusted": + if e.complexity.SignatureSummary.IsTrusted == nil { + break + } + + return e.complexity.SignatureSummary.IsTrusted(childComplexity), true + + case "SignatureSummary.Tool": + if e.complexity.SignatureSummary.Tool == nil { + break + } + + return e.complexity.SignatureSummary.Tool(childComplexity), true + } return 0, false } @@ -1200,6 +1243,10 @@ type ImageSummary { """ IsSigned: Boolean """ + Info about signature validity + """ + SignatureInfo: [SignatureSummary] + """ License(s) under which contained software is distributed as an SPDX License Expression """ Licenses: String # The value of the annotation if present, 'unknown' otherwise). @@ -1262,6 +1309,10 @@ type ManifestSummary { """ IsSigned: Boolean """ + Info about signature validity + """ + SignatureInfo: [SignatureSummary] + """ OS and architecture supported by this image """ Platform: Platform @@ -1466,6 +1517,24 @@ type Platform { Arch: String } +""" +Contains details about the signature +""" +type SignatureSummary { + """ + Tool is the tool used for signing image + """ + Tool: String + """ + True if the signature is trusted, false otherwise + """ + IsTrusted: Boolean + """ + Author is the author of the signature + """ + Author: String +} + """ All sort criteria usable with pagination, some of these criteria applies only to certain queries. For example sort by severity is available for CVEs but not @@ -2698,6 +2767,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C return ec.fieldContext_ImageSummary_Description(ctx, field) case "IsSigned": return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) case "Licenses": return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": @@ -3248,6 +3319,8 @@ func (ec *executionContext) fieldContext_ImageSummary_Manifests(ctx context.Cont return ec.fieldContext_ManifestSummary_Size(ctx, field) case "IsSigned": return ec.fieldContext_ManifestSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ManifestSummary_SignatureInfo(ctx, field) case "Platform": return ec.fieldContext_ManifestSummary_Platform(ctx, field) case "DownloadCount": @@ -3474,6 +3547,55 @@ func (ec *executionContext) fieldContext_ImageSummary_IsSigned(ctx context.Conte return fc, nil } +func (ec *executionContext) _ImageSummary_SignatureInfo(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.SignatureInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*SignatureSummary) + fc.Result = res + return ec.marshalOSignatureSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSignatureSummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_SignatureInfo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Tool": + return ec.fieldContext_SignatureSummary_Tool(ctx, field) + case "IsTrusted": + return ec.fieldContext_SignatureSummary_IsTrusted(ctx, field) + case "Author": + return ec.fieldContext_SignatureSummary_Author(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SignatureSummary", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ImageSummary_Licenses(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageSummary_Licenses(ctx, field) if err != nil { @@ -4330,6 +4452,55 @@ func (ec *executionContext) fieldContext_ManifestSummary_IsSigned(ctx context.Co return fc, nil } +func (ec *executionContext) _ManifestSummary_SignatureInfo(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_SignatureInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.SignatureInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*SignatureSummary) + fc.Result = res + return ec.marshalOSignatureSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSignatureSummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_SignatureInfo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Tool": + return ec.fieldContext_SignatureSummary_Tool(ctx, field) + case "IsTrusted": + return ec.fieldContext_SignatureSummary_IsTrusted(ctx, field) + case "Author": + return ec.fieldContext_SignatureSummary_Author(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SignatureSummary", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ManifestSummary_Platform(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ManifestSummary_Platform(ctx, field) if err != nil { @@ -4970,6 +5141,8 @@ func (ec *executionContext) fieldContext_PaginatedImagesResult_Results(ctx conte return ec.fieldContext_ImageSummary_Description(ctx, field) case "IsSigned": return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) case "Licenses": return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": @@ -5865,6 +6038,8 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field return ec.fieldContext_ImageSummary_Description(ctx, field) case "IsSigned": return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) case "Licenses": return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": @@ -6489,6 +6664,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi return ec.fieldContext_ImageSummary_Description(ctx, field) case "IsSigned": return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) case "Licenses": return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": @@ -6844,6 +7021,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con return ec.fieldContext_ImageSummary_Description(ctx, field) case "IsSigned": return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "SignatureInfo": + return ec.fieldContext_ImageSummary_SignatureInfo(ctx, field) case "Licenses": return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": @@ -7033,6 +7212,129 @@ func (ec *executionContext) fieldContext_RepoSummary_IsStarred(ctx context.Conte return fc, nil } +func (ec *executionContext) _SignatureSummary_Tool(ctx context.Context, field graphql.CollectedField, obj *SignatureSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SignatureSummary_Tool(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tool, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SignatureSummary_Tool(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SignatureSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SignatureSummary_IsTrusted(ctx context.Context, field graphql.CollectedField, obj *SignatureSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SignatureSummary_IsTrusted(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsTrusted, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SignatureSummary_IsTrusted(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SignatureSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SignatureSummary_Author(ctx context.Context, field graphql.CollectedField, obj *SignatureSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SignatureSummary_Author(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Author, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SignatureSummary_Author(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SignatureSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_name(ctx, field) if err != nil { @@ -9157,6 +9459,10 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_IsSigned(ctx, field, obj) + case "SignatureInfo": + + out.Values[i] = ec._ImageSummary_SignatureInfo(ctx, field, obj) + case "Licenses": out.Values[i] = ec._ImageSummary_Licenses(ctx, field, obj) @@ -9321,6 +9627,10 @@ func (ec *executionContext) _ManifestSummary(ctx context.Context, sel ast.Select out.Values[i] = ec._ManifestSummary_IsSigned(ctx, field, obj) + case "SignatureInfo": + + out.Values[i] = ec._ManifestSummary_SignatureInfo(ctx, field, obj) + case "Platform": out.Values[i] = ec._ManifestSummary_Platform(ctx, field, obj) @@ -10019,6 +10329,39 @@ func (ec *executionContext) _RepoSummary(ctx context.Context, sel ast.SelectionS return out } +var signatureSummaryImplementors = []string{"SignatureSummary"} + +func (ec *executionContext) _SignatureSummary(ctx context.Context, sel ast.SelectionSet, obj *SignatureSummary) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, signatureSummaryImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SignatureSummary") + case "Tool": + + out.Values[i] = ec._SignatureSummary_Tool(ctx, field, obj) + + case "IsTrusted": + + out.Values[i] = ec._SignatureSummary_IsTrusted(ctx, field, obj) + + case "Author": + + out.Values[i] = ec._SignatureSummary_Author(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -11411,6 +11754,54 @@ func (ec *executionContext) marshalORepoSummary2ᚖzotregistryᚗioᚋzotᚋpkg return ec._RepoSummary(ctx, sel, v) } +func (ec *executionContext) marshalOSignatureSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSignatureSummary(ctx context.Context, sel ast.SelectionSet, v []*SignatureSummary) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOSignatureSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSignatureSummary(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOSignatureSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSignatureSummary(ctx context.Context, sel ast.SelectionSet, v *SignatureSummary) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._SignatureSummary(ctx, sel, v) +} + func (ec *executionContext) unmarshalOSortCriteria2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐSortCriteria(ctx context.Context, v interface{}) (*SortCriteria, error) { if v == nil { return nil, nil diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 2ee26739..0d8a0309 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -111,6 +111,8 @@ type ImageSummary struct { Description *string `json:"Description,omitempty"` // True if the image has a signature associated with it, false otherwise IsSigned *bool `json:"IsSigned,omitempty"` + // Info about signature validity + SignatureInfo []*SignatureSummary `json:"SignatureInfo,omitempty"` // License(s) under which contained software is distributed as an SPDX License Expression Licenses *string `json:"Licenses,omitempty"` // Labels associated with this image @@ -168,6 +170,8 @@ type ManifestSummary struct { Size *string `json:"Size,omitempty"` // True if the manifest has a signature associated with it, false otherwise IsSigned *bool `json:"IsSigned,omitempty"` + // Info about signature validity + SignatureInfo []*SignatureSummary `json:"SignatureInfo,omitempty"` // OS and architecture supported by this image Platform *Platform `json:"Platform,omitempty"` // Total numer of image manifest downloads from this repository @@ -291,6 +295,16 @@ type RepoSummary struct { IsStarred *bool `json:"IsStarred,omitempty"` } +// Contains details about the signature +type SignatureSummary struct { + // Tool is the tool used for signing image + Tool *string `json:"Tool,omitempty"` + // True if the signature is trusted, false otherwise + IsTrusted *bool `json:"IsTrusted,omitempty"` + // Author is the author of the signature + Author *string `json:"Author,omitempty"` +} + // All sort criteria usable with pagination, some of these criteria applies only // to certain queries. For example sort by severity is available for CVEs but not // for repositories diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index e6cd3936..690305c6 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -156,6 +156,10 @@ type ImageSummary { """ IsSigned: Boolean """ + Info about signature validity + """ + SignatureInfo: [SignatureSummary] + """ License(s) under which contained software is distributed as an SPDX License Expression """ Licenses: String # The value of the annotation if present, 'unknown' otherwise). @@ -218,6 +222,10 @@ type ManifestSummary { """ IsSigned: Boolean """ + Info about signature validity + """ + SignatureInfo: [SignatureSummary] + """ OS and architecture supported by this image """ Platform: Platform @@ -422,6 +430,24 @@ type Platform { Arch: String } +""" +Contains details about the signature +""" +type SignatureSummary { + """ + Tool is the tool used for signing image + """ + Tool: String + """ + True if the signature is trusted, false otherwise + """ + IsTrusted: Boolean + """ + Author is the author of the signature + """ + Author: String +} + """ All sort criteria usable with pagination, some of these criteria applies only to certain queries. For example sort by severity is available for CVEs but not diff --git a/pkg/extensions/sync/signatures.go b/pkg/extensions/sync/signatures.go index f6e22c7b..ca9400ff 100644 --- a/pkg/extensions/sync/signatures.go +++ b/pkg/extensions/sync/signatures.go @@ -22,6 +22,7 @@ import ( syncconf "zotregistry.io/zot/pkg/extensions/config/sync" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/repodb" + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/storage" ) @@ -194,7 +195,7 @@ func (sig *signaturesCopier) syncCosignSignature(localRepo, remoteRepo, digestSt Msg("trying to sync cosign signature for repo digest") err := sig.repoDB.AddManifestSignature(localRepo, godigest.Digest(digestStr), repodb.SignatureMetadata{ - SignatureType: repodb.CosignType, + SignatureType: signatures.CosignSignature, SignatureDigest: signatureDigest.String(), }) if err != nil { @@ -274,7 +275,7 @@ func (sig *signaturesCopier) syncORASRefs(localRepo, remoteRepo, digestStr strin Msg("trying to sync oras artifact for digest") err := sig.repoDB.AddManifestSignature(localRepo, godigest.Digest(digestStr), repodb.SignatureMetadata{ - SignatureType: repodb.NotationType, + SignatureType: signatures.NotationSignature, SignatureDigest: signatureDigest.String(), }) if err != nil { diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index 0669e078..adbfb715 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -41,7 +41,7 @@ import ( syncconf "zotregistry.io/zot/pkg/extensions/config/sync" "zotregistry.io/zot/pkg/extensions/sync" logger "zotregistry.io/zot/pkg/log" - "zotregistry.io/zot/pkg/meta/repodb" + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage/local" "zotregistry.io/zot/pkg/test" @@ -3722,10 +3722,10 @@ func TestSyncedSignaturesRepoDB(t *testing.T) { So(repoMeta.Signatures, ShouldContainKey, signedImageDigest.String()) imageSignatures := repoMeta.Signatures[signedImageDigest.String()] - So(imageSignatures, ShouldContainKey, repodb.CosignType) - So(len(imageSignatures[repodb.CosignType]), ShouldEqual, 1) - So(imageSignatures, ShouldContainKey, repodb.NotationType) - So(len(imageSignatures[repodb.NotationType]), ShouldEqual, 1) + So(imageSignatures, ShouldContainKey, signatures.CosignSignature) + So(len(imageSignatures[signatures.CosignSignature]), ShouldEqual, 1) + So(imageSignatures, ShouldContainKey, signatures.NotationSignature) + So(len(imageSignatures[signatures.NotationSignature]), ShouldEqual, 1) }) } diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go index eb646299..b1ce4224 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go @@ -17,6 +17,7 @@ import ( "zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/common" "zotregistry.io/zot/pkg/meta/repodb" + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/meta/version" localCtx "zotregistry.io/zot/pkg/requestcontext" ) @@ -726,6 +727,102 @@ func (bdw *DBWrapper) IncrementImageDownloads(repo string, reference string) err return err } +func (bdw *DBWrapper) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { + err := bdw.DB.Update(func(transaction *bbolt.Tx) error { + // get ManifestData of signed manifest + manifestBuck := transaction.Bucket([]byte(bolt.ManifestDataBucket)) + mdBlob := manifestBuck.Get([]byte(manifestDigest)) + + var blob []byte + + if len(mdBlob) != 0 { + var manifestData repodb.ManifestData + + err := json.Unmarshal(mdBlob, &manifestData) + if err != nil { + return fmt.Errorf("repodb: %w error while unmashaling manifest meta for digest %s", err, manifestDigest) + } + + blob = manifestData.ManifestBlob + } else { + var indexData repodb.IndexData + + indexBuck := transaction.Bucket([]byte(bolt.IndexDataBucket)) + idBlob := indexBuck.Get([]byte(manifestDigest)) + + if len(idBlob) == 0 { + // manifest meta not found, updating signatures with details about validity and author will not be performed + return nil + } + + err := json.Unmarshal(idBlob, &indexData) + if err != nil { + return fmt.Errorf("repodb: %w error while unmashaling index meta for digest %s", err, manifestDigest) + } + + blob = indexData.IndexBlob + } + + // update signatures with details about validity and author + repoBuck := transaction.Bucket([]byte(bolt.RepoMetadataBucket)) + + repoMetaBlob := repoBuck.Get([]byte(repo)) + if repoMetaBlob == nil { + return zerr.ErrRepoMetaNotFound + } + + var repoMeta repodb.RepoMetadata + + err := json.Unmarshal(repoMetaBlob, &repoMeta) + if err != nil { + return err + } + + manifestSignatures := repodb.ManifestSignatures{} + for sigType, sigs := range repoMeta.Signatures[manifestDigest.String()] { + signaturesInfo := []repodb.SignatureInfo{} + + for _, sigInfo := range sigs { + layersInfo := []repodb.LayerInfo{} + + for _, layerInfo := range sigInfo.LayersInfo { + author, date, isTrusted, _ := signatures.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey, + manifestDigest, blob, repo) + + if isTrusted { + layerInfo.Signer = author + } + + if !date.IsZero() { + layerInfo.Signer = author + layerInfo.Date = date + } + + layersInfo = append(layersInfo, layerInfo) + } + + signaturesInfo = append(signaturesInfo, repodb.SignatureInfo{ + SignatureManifestDigest: sigInfo.SignatureManifestDigest, + LayersInfo: layersInfo, + }) + } + + manifestSignatures[sigType] = signaturesInfo + } + + repoMeta.Signatures[manifestDigest.String()] = manifestSignatures + + repoMetaBlob, err = json.Marshal(repoMeta) + if err != nil { + return err + } + + return repoBuck.Put([]byte(repo), repoMetaBlob) + }) + + return err +} + func (bdw *DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, sygMeta repodb.SignatureMetadata, ) error { @@ -780,10 +877,17 @@ func (bdw *DBWrapper) AddManifestSignature(repo string, signedManifestDigest god signatureSlice := manifestSignatures[sygMeta.SignatureType] if !common.SignatureAlreadyExists(signatureSlice, sygMeta) { - signatureSlice = append(signatureSlice, repodb.SignatureInfo{ - SignatureManifestDigest: sygMeta.SignatureDigest, - LayersInfo: sygMeta.LayersInfo, - }) + if sygMeta.SignatureType == signatures.NotationSignature { + signatureSlice = append(signatureSlice, repodb.SignatureInfo{ + SignatureManifestDigest: sygMeta.SignatureDigest, + LayersInfo: sygMeta.LayersInfo, + }) + } else if sygMeta.SignatureType == signatures.CosignSignature { + signatureSlice = []repodb.SignatureInfo{{ + SignatureManifestDigest: sygMeta.SignatureDigest, + LayersInfo: sygMeta.LayersInfo, + }} + } } manifestSignatures[sygMeta.SignatureType] = signatureSlice diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go index d8ed248f..d109fe5f 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go @@ -3,8 +3,15 @@ package bolt_test import ( "context" "encoding/json" + "errors" + "os" + "path" "testing" + "time" + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/signer" "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -14,6 +21,7 @@ import ( "zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/repodb" boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" + "zotregistry.io/zot/pkg/meta/signatures" localCtx "zotregistry.io/zot/pkg/requestcontext" "zotregistry.io/zot/pkg/test" ) @@ -308,6 +316,20 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) + err = boltdbWrapper.AddManifestSignature("repo1", digest.FromString("dig"), + repodb.SignatureMetadata{ + SignatureType: "cosign", + SignatureDigest: "digest2", + }) + So(err, ShouldBeNil) + + repoData, err := boltdbWrapper.GetRepoMeta("repo1") + So(err, ShouldBeNil) + So(len(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature]), + ShouldEqual, 1) + So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].SignatureManifestDigest, + ShouldEqual, "digest2") + err = boltdbWrapper.AddManifestSignature("repo1", digest.FromString("dig"), repodb.SignatureMetadata{ SignatureType: "notation", @@ -930,6 +952,231 @@ func TestWrapperErrors(t *testing.T) { _, err := boltdbWrapper.GetUserRepoMeta(ctx, "repo") So(err, ShouldNotBeNil) }) + + Convey("UpdateSignaturesValidity", func() { + Convey("manifestMeta of signed manifest not found", func() { + err := boltdbWrapper.UpdateSignaturesValidity("repo", digest.FromString("dig")) + So(err, ShouldBeNil) + }) + + Convey("repoMeta of signed manifest not found", func() { + // repo Meta not found + err := boltdbWrapper.SetManifestData(digest.FromString("dig"), repodb.ManifestData{ + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo", digest.FromString("dig")) + So(err, ShouldNotBeNil) + }) + + Convey("manifest - bad content", func() { + err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { + dataBuck := tx.Bucket([]byte(bolt.ManifestDataBucket)) + + return dataBuck.Put([]byte("digest1"), []byte("wrong json")) + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo1", "digest1") + So(err, ShouldNotBeNil) + }) + + Convey("index - bad content", func() { + err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { + dataBuck := tx.Bucket([]byte(bolt.IndexDataBucket)) + + return dataBuck.Put([]byte("digest1"), []byte("wrong json")) + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo1", "digest1") + So(err, ShouldNotBeNil) + }) + + Convey("repo - bad content", func() { + // repo Meta not found + err := boltdbWrapper.SetManifestData(digest.FromString("dig"), repodb.ManifestData{ + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { + repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket)) + + return repoBuck.Put([]byte("repo1"), []byte("wrong json")) + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig")) + So(err, ShouldNotBeNil) + }) + + Convey("VerifySignature -> untrusted signature", func() { + err := boltdbWrapper.SetManifestData(digest.FromString("dig"), repodb.ManifestData{ + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { + repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket)) + + return repoBuck.Put([]byte("repo1"), repoMetaBlob) + }) + So(err, ShouldBeNil) + + layerInfo := repodb.LayerInfo{LayerDigest: "", LayerContent: []byte{}, SignatureKey: ""} + + err = boltdbWrapper.AddManifestSignature("repo1", digest.FromString("dig"), + repodb.SignatureMetadata{ + SignatureType: signatures.CosignSignature, + SignatureDigest: string(digest.FromString("signature digest")), + LayersInfo: []repodb.LayerInfo{layerInfo}, + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig")) + So(err, ShouldBeNil) + + repoData, err := boltdbWrapper.GetRepoMeta("repo1") + So(err, ShouldBeNil) + So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Signer, + ShouldBeEmpty) + So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Date, + ShouldBeZeroValue) + }) + + Convey("VerifySignature -> trusted signature", func() { + _, _, manifest, _ := test.GetRandomImageComponents(10) + manifestContent, _ := json.Marshal(manifest) + manifestDigest := digest.FromBytes(manifestContent) + + err := boltdbWrapper.SetManifestData(manifestDigest, repodb.ManifestData{ + ManifestBlob: manifestContent, + ConfigBlob: []byte("configContent"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { + repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket)) + + return repoBuck.Put([]byte("repo"), repoMetaBlob) + }) + So(err, ShouldBeNil) + + mediaType := jws.MediaTypeEnvelope + + signOpts := notation.SignerSignOptions{ + SignatureMediaType: mediaType, + PluginConfig: map[string]string{}, + ExpiryDuration: 24 * time.Hour, + } + + tdir := t.TempDir() + keyName := "notation-sign-test" + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, keyName) + So(err, ShouldBeNil) + + // getSigner + var newSigner notation.Signer + + // ResolveKey + signingKeys, err := test.LoadNotationSigningkeys(tdir) + So(err, ShouldBeNil) + + idx := test.Index(signingKeys.Keys, keyName) + So(idx, ShouldBeGreaterThanOrEqualTo, 0) + + key := signingKeys.Keys[idx] + + if key.X509KeyPair != nil { + newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath) + So(err, ShouldBeNil) + } + + descToSign := ispec.Descriptor{ + MediaType: manifest.MediaType, + Digest: manifestDigest, + Size: int64(len(manifestContent)), + } + sig, _, err := newSigner.Sign(ctx, descToSign, signOpts) + So(err, ShouldBeNil) + + layerInfo := repodb.LayerInfo{ + LayerDigest: string(digest.FromBytes(sig)), + LayerContent: sig, SignatureKey: mediaType, + } + + err = boltdbWrapper.AddManifestSignature("repo", manifestDigest, + repodb.SignatureMetadata{ + SignatureType: signatures.NotationSignature, + SignatureDigest: string(digest.FromString("signature digest")), + LayersInfo: []repodb.LayerInfo{layerInfo}, + }) + So(err, ShouldBeNil) + + err = signatures.InitNotationDir(tdir) + So(err, ShouldBeNil) + + trustpolicyPath := path.Join(tdir, "_notation/trustpolicy.json") + + if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) { + trustPolicy := ` + { + "version": "1.0", + "trustPolicies": [ + { + "name": "notation-sign-test", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": ["ca:notation-sign-test"], + "trustedIdentities": [ + "*" + ] + } + ] + }` + + file, err := os.Create(trustpolicyPath) + So(err, ShouldBeNil) + + defer file.Close() + + _, err = file.WriteString(trustPolicy) + So(err, ShouldBeNil) + } + + truststore := "_notation/truststore/x509/ca/notation-sign-test" + truststoreSrc := "notation/truststore/x509/ca/notation-sign-test" + err = os.MkdirAll(path.Join(tdir, truststore), 0o755) + So(err, ShouldBeNil) + + err = test.CopyFile(path.Join(tdir, truststoreSrc, "notation-sign-test.crt"), + path.Join(tdir, truststore, "notation-sign-test.crt")) + So(err, ShouldBeNil) + + err = boltdbWrapper.UpdateSignaturesValidity("repo", manifestDigest) //nolint:contextcheck + So(err, ShouldBeNil) + + repoData, err := boltdbWrapper.GetRepoMeta("repo") + So(err, ShouldBeNil) + So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Signer, + ShouldNotBeEmpty) + So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Date, + ShouldNotBeZeroValue) + }) + }) }) } diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go index 7cce82e7..1f305a4d 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go @@ -21,6 +21,7 @@ import ( "zotregistry.io/zot/pkg/meta/common" "zotregistry.io/zot/pkg/meta/dynamo" "zotregistry.io/zot/pkg/meta/repodb" //nolint:go-staticcheck + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/meta/version" localCtx "zotregistry.io/zot/pkg/requestcontext" ) @@ -616,6 +617,10 @@ func (dwr *DBWrapper) IncrementImageDownloads(repo string, reference string) err return dwr.SetRepoMeta(repo, repoMeta) } +func (dwr *DBWrapper) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { + return nil +} + func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, sygMeta repodb.SignatureMetadata, ) error { @@ -656,10 +661,17 @@ func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest god signatureSlice := manifestSignatures[sygMeta.SignatureType] if !common.SignatureAlreadyExists(signatureSlice, sygMeta) { - signatureSlice = append(signatureSlice, repodb.SignatureInfo{ - SignatureManifestDigest: sygMeta.SignatureDigest, - LayersInfo: sygMeta.LayersInfo, - }) + if sygMeta.SignatureType == signatures.NotationSignature { + signatureSlice = append(signatureSlice, repodb.SignatureInfo{ + SignatureManifestDigest: sygMeta.SignatureDigest, + LayersInfo: sygMeta.LayersInfo, + }) + } else if sygMeta.SignatureType == signatures.CosignSignature { + signatureSlice = []repodb.SignatureInfo{{ + SignatureManifestDigest: sygMeta.SignatureDigest, + LayersInfo: sygMeta.LayersInfo, + }} + } } manifestSignatures[sygMeta.SignatureType] = signatureSlice diff --git a/pkg/meta/repodb/repodb.go b/pkg/meta/repodb/repodb.go index aeccb32f..a08d42c8 100644 --- a/pkg/meta/repodb/repodb.go +++ b/pkg/meta/repodb/repodb.go @@ -7,13 +7,6 @@ import ( godigest "github.com/opencontainers/go-digest" ) -const ( - SignaturesDirPath = "/tmp/zot/signatures" - SigKey = "dev.cosignproject.cosign/signature" - NotationType = "notation" - CosignType = "cosign" -) - // Used to model changes to an object after a call to the DB. type ToggleState int @@ -97,6 +90,9 @@ type RepoDB interface { //nolint:interfacebloat // DeleteSignature delets signature metadata to a given manifest from the database DeleteSignature(repo string, signedManifestDigest godigest.Digest, sm SignatureMetadata) error + // UpdateSignaturesValidity checks and updates signatures validity of a given manifest + UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error + // SearchRepos searches for repos given a search string SearchRepos(ctx context.Context, searchText string, filter Filter, requestedPage PageInput) ( []RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, PageInfo, error) @@ -183,6 +179,7 @@ type LayerInfo struct { LayerContent []byte SignatureKey string Signer string + Date time.Time } type SignatureInfo struct { diff --git a/pkg/meta/repodb/repodbfactory/repodb_factory.go b/pkg/meta/repodb/repodbfactory/repodb_factory.go index f91efbcb..b4fd878f 100644 --- a/pkg/meta/repodb/repodbfactory/repodb_factory.go +++ b/pkg/meta/repodb/repodbfactory/repodb_factory.go @@ -12,6 +12,7 @@ import ( "zotregistry.io/zot/pkg/meta/repodb" boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" dynamodb_wrapper "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper" + "zotregistry.io/zot/pkg/meta/signatures" ) func New(storageConfig config.StorageConfig, log log.Logger) (repodb.RepoDB, error) { @@ -34,6 +35,11 @@ func New(storageConfig config.StorageConfig, log log.Logger) (repodb.RepoDB, err return nil, err } + err = signatures.InitCosignAndNotationDirs(params.RootDir) + if err != nil { + return nil, err + } + return Create("boltdb", driver, params, log) //nolint:contextcheck } diff --git a/pkg/meta/repodb/repodbfactory/repodb_factory_test.go b/pkg/meta/repodb/repodbfactory/repodb_factory_test.go index 484ae3de..b3930207 100644 --- a/pkg/meta/repodb/repodbfactory/repodb_factory_test.go +++ b/pkg/meta/repodb/repodbfactory/repodb_factory_test.go @@ -2,11 +2,13 @@ package repodbfactory_test import ( "os" + "path" "testing" "github.com/aws/aws-sdk-go-v2/service/dynamodb" . "github.com/smartystreets/goconvey/convey" + "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/dynamo" @@ -73,6 +75,31 @@ func TestCreateBoltDB(t *testing.T) { }) } +func TestNew(t *testing.T) { + Convey("InitCosignAndNotationDirs fails", t, func() { + rootDir := t.TempDir() + + var storageConfig config.StorageConfig + + storageConfig.RootDirectory = rootDir + storageConfig.RemoteCache = false + log := log.NewLogger("debug", "") + + _, err := os.Create(path.Join(rootDir, "repo.db")) + So(err, ShouldBeNil) + + err = os.Chmod(rootDir, 0o555) + So(err, ShouldBeNil) + + newRepodb, err := repodbfactory.New(storageConfig, log) + So(newRepodb, ShouldBeNil) + So(err, ShouldNotBeNil) + + err = os.Chmod(rootDir, 0o777) + So(err, ShouldBeNil) + }) +} + func skipDynamo(t *testing.T) { t.Helper() diff --git a/pkg/meta/repodb/storage_parsing.go b/pkg/meta/repodb/storage_parsing.go index dd9b706c..4c8893f7 100644 --- a/pkg/meta/repodb/storage_parsing.go +++ b/pkg/meta/repodb/storage_parsing.go @@ -11,6 +11,7 @@ import ( zerr "zotregistry.io/zot/errors" zcommon "zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/storage" ) @@ -105,15 +106,30 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll } if isSignature { - err := repoDB.AddManifestSignature(repo, signedManifestDigest, + layers, err := GetSignatureLayersInfo(repo, tag, manifest.Digest.String(), signatureType, manifestBlob, + imageStore, log) + if err != nil { + return err + } + + err = repoDB.AddManifestSignature(repo, signedManifestDigest, SignatureMetadata{ SignatureType: signatureType, SignatureDigest: digest.String(), + LayersInfo: layers, }) if err != nil { log.Error().Err(err).Str("repository", repo).Str("tag", tag). Str("manifestDigest", signedManifestDigest.String()). - Msg("load-repo: failed set signature meta for signed image manifest digest") + Msg("load-repo: failed set signature meta for signed image") + + return err + } + + err = repoDB.UpdateSignaturesValidity(repo, signedManifestDigest) + if err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", tag).Str("digest", signedManifestDigest.String()).Msg( + "load-repo: failed verify signatures validity for signed image") return err } @@ -199,6 +215,98 @@ func isManifestMetaPresent(repo string, manifest ispec.Descriptor, repoDB RepoDB return true, nil } +func GetSignatureLayersInfo( + repo, tag, manifestDigest, signatureType string, manifestBlob []byte, imageStore storage.ImageStore, log log.Logger, +) ([]LayerInfo, error) { + switch signatureType { + case signatures.CosignSignature: + return getCosignSignatureLayersInfo(repo, tag, manifestDigest, manifestBlob, imageStore, log) + case signatures.NotationSignature: + return getNotationSignatureLayersInfo(repo, manifestDigest, manifestBlob, imageStore, log) + default: + return []LayerInfo{}, nil + } +} + +func getCosignSignatureLayersInfo( + repo, tag, manifestDigest string, manifestBlob []byte, imageStore storage.ImageStore, log log.Logger, +) ([]LayerInfo, error) { + layers := []LayerInfo{} + + var manifestContent ispec.Manifest + if err := json.Unmarshal(manifestBlob, &manifestContent); err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", tag).Str("digest", manifestDigest).Msg( + "load-repo: unable to marshal blob index") + + return layers, err + } + + for _, layer := range manifestContent.Layers { + layerContent, err := imageStore.GetBlobContent(repo, layer.Digest) + if err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", tag).Str("layerDigest", layer.Digest.String()).Msg( + "load-repo: unable to get cosign signature layer content") + + return layers, err + } + + layerSigKey, ok := layer.Annotations[signatures.CosignSigKey] + if !ok { + log.Error().Err(err).Str("repository", repo).Str("reference", tag).Str("layerDigest", layer.Digest.String()).Msg( + "load-repo: unable to get specific annotation of cosign signature") + } + + layers = append(layers, LayerInfo{ + LayerDigest: layer.Digest.String(), + LayerContent: layerContent, + SignatureKey: layerSigKey, + }) + } + + return layers, nil +} + +func getNotationSignatureLayersInfo( + repo, manifestDigest string, manifestBlob []byte, imageStore storage.ImageStore, log log.Logger, +) ([]LayerInfo, error) { + layers := []LayerInfo{} + + var manifestContent ispec.Manifest + if err := json.Unmarshal(manifestBlob, &manifestContent); err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", manifestDigest).Msg( + "load-repo: unable to marshal blob index") + + return layers, err + } + + if len(manifestContent.Layers) != 1 { + log.Error().Err(zerr.ErrBadManifest).Str("repository", repo).Str("reference", manifestDigest). + Msg("load-repo: notation signature manifest requires exactly one layer but it does not") + + return layers, zerr.ErrBadManifest + } + + layer := manifestContent.Layers[0].Digest + + layerContent, err := imageStore.GetBlobContent(repo, layer) + if err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", manifestDigest).Str("layerDigest", layer.String()).Msg( + "load-repo: unable to get notation signature blob content") + + return layers, err + } + + layerSigKey := manifestContent.Layers[0].MediaType + + layers = append(layers, LayerInfo{ + LayerDigest: layer.String(), + LayerContent: layerContent, + SignatureKey: layerSigKey, + }) + + return layers, nil +} + // NewManifestMeta takes raw data about an image and createa a new ManifestMetadate object. func NewManifestData(repoName string, manifestBlob []byte, imageStore storage.ImageStore, ) (ManifestData, error) { diff --git a/pkg/meta/repodb/storage_parsing_test.go b/pkg/meta/repodb/storage_parsing_test.go index 3e5426df..26be67a8 100644 --- a/pkg/meta/repodb/storage_parsing_test.go +++ b/pkg/meta/repodb/storage_parsing_test.go @@ -22,6 +22,7 @@ import ( "zotregistry.io/zot/pkg/meta/repodb" bolt_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" dynamo_wrapper "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper" + "zotregistry.io/zot/pkg/meta/signatures" "zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage/local" "zotregistry.io/zot/pkg/test" @@ -242,6 +243,7 @@ func TestParseStorageErrors(t *testing.T) { Digest: "123", }, ArtifactType: "application/vnd.cncf.notary.signature", + Layers: []ispec.Descriptor{{MediaType: ispec.MediaTypeImageLayer}}, } manifestBlob, err := json.Marshal(manifestContent) @@ -259,6 +261,100 @@ func TestParseStorageErrors(t *testing.T) { err = repodb.ParseRepo("repo", repoDB, storeController, log) So(err, ShouldNotBeNil) + + repoDB.AddManifestSignatureFn = func(repo string, signedManifestDigest godigest.Digest, + sm repodb.SignatureMetadata, + ) error { + return nil + } + + repoDB.UpdateSignaturesValidityFn = func(repo string, signedManifestDigest godigest.Digest, + ) error { + return ErrTestError + } + + err = repodb.ParseRepo("repo", repoDB, storeController, log) + So(err, ShouldNotBeNil) + }) + + Convey("GetSignatureLayersInfo errors", func() { + // get notation signature layers info + badNotationManifestContent := ispec.Manifest{ + Subject: &ispec.Descriptor{ + Digest: "123", + }, + ArtifactType: "application/vnd.cncf.notary.signature", + } + + badNotationManifestBlob, err := json.Marshal(badNotationManifestContent) + So(err, ShouldBeNil) + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return badNotationManifestBlob, "", "", nil + } + + // wrong number of layers of notation manifest + err = repodb.ParseRepo("repo", repoDB, storeController, log) + So(err, ShouldNotBeNil) + + notationManifestContent := ispec.Manifest{ + Subject: &ispec.Descriptor{ + Digest: "123", + }, + ArtifactType: "application/vnd.cncf.notary.signature", + Layers: []ispec.Descriptor{{MediaType: ispec.MediaTypeImageLayer}}, + } + + notationManifestBlob, err := json.Marshal(notationManifestContent) + So(err, ShouldBeNil) + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return notationManifestBlob, "", "", nil + } + + imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { + return []byte{}, ErrTestError + } + + // unable to get layer content + err = repodb.ParseRepo("repo", repoDB, storeController, log) + So(err, ShouldNotBeNil) + + _, _, cosignManifestContent, _ := test.GetRandomImageComponents(10) + _, _, signedManifest, _ := test.GetRandomImageComponents(10) + signatureTag, err := test.GetCosignSignatureTagForManifest(signedManifest) + So(err, ShouldBeNil) + + cosignManifestContent.Annotations = map[string]string{ispec.AnnotationRefName: signatureTag} + + cosignManifestBlob, err := json.Marshal(cosignManifestContent) + So(err, ShouldBeNil) + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return cosignManifestBlob, "", "", nil + } + + indexContent := ispec.Index{ + Manifests: []ispec.Descriptor{ + { + Digest: godigest.FromString("cosignSig"), + MediaType: ispec.MediaTypeImageManifest, + Annotations: map[string]string{ + ispec.AnnotationRefName: signatureTag, + }, + }, + }, + } + indexBlob, err := json.Marshal(indexContent) + So(err, ShouldBeNil) + + imageStore.GetIndexContentFn = func(repo string) ([]byte, error) { + return indexBlob, nil + } + + // unable to get layer content + err = repodb.ParseRepo("repo", repoDB, storeController, log) + So(err, ShouldNotBeNil) }) }) }) @@ -534,3 +630,22 @@ func skipIt(t *testing.T) { t.Skip("Skipping testing without AWS S3 mock server") } } + +func TestGetSignatureLayersInfo(t *testing.T) { + Convey("wrong signature type", t, func() { + layers, err := repodb.GetSignatureLayersInfo("repo", "tag", "123", "wrong signature type", []byte{}, + nil, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(layers, ShouldBeEmpty) + }) + + Convey("error while unmarshaling manifest content", t, func() { + _, err := repodb.GetSignatureLayersInfo("repo", "tag", "123", signatures.CosignSignature, []byte("bad manifest"), + nil, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + + _, err = repodb.GetSignatureLayersInfo("repo", "tag", "123", signatures.NotationSignature, []byte("bad manifest"), + nil, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + }) +} diff --git a/pkg/meta/signatures/cosign.go b/pkg/meta/signatures/cosign.go new file mode 100644 index 00000000..953d3e0f --- /dev/null +++ b/pkg/meta/signatures/cosign.go @@ -0,0 +1,113 @@ +package signatures + +import ( + "bytes" + "context" + "crypto" + "encoding/base64" + "io" + "os" + "path" + + godigest "github.com/opencontainers/go-digest" + "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" + sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + + zerr "zotregistry.io/zot/errors" +) + +const ( + CosignSigKey = "dev.cosignproject.cosign/signature" + cosignDirRelativePath = "_cosign" +) + +var cosignDir = "" //nolint:gochecknoglobals + +func InitCosignDir(rootDir string) error { + dir := path.Join(rootDir, cosignDirRelativePath) + + _, err := os.Stat(dir) + if os.IsNotExist(err) { + err = os.MkdirAll(dir, defaultDirPerms) + if err != nil { + return err + } + } + + if err == nil { + cosignDir = dir + } + + return err +} + +func GetCosignDirPath() (string, error) { + if cosignDir != "" { + return cosignDir, nil + } + + return "", zerr.ErrSignConfigDirNotSet +} + +func VerifyCosignSignature( + repo string, digest godigest.Digest, signatureKey string, layerContent []byte, +) (string, bool, error) { + cosignDir, err := GetCosignDirPath() + if err != nil { + return "", false, err + } + + files, err := os.ReadDir(cosignDir) + if err != nil { + return "", false, err + } + + for _, file := range files { + if !file.IsDir() { + // cosign verify the image + ctx := context.Background() + keyRef := path.Join(cosignDir, file.Name()) + hashAlgorithm := crypto.SHA256 + + pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) + if err != nil { + continue + } + + pkcs11Key, ok := pubKey.(*pkcs11key.Key) + if ok { + defer pkcs11Key.Close() + } + + verifier := pubKey + + b64sig := signatureKey + + signature, err := base64.StdEncoding.DecodeString(b64sig) + if err != nil { + continue + } + + compressed := io.NopCloser(bytes.NewReader(layerContent)) + + payload, err := io.ReadAll(compressed) + if err != nil { + continue + } + + err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)) + + if err == nil { + publicKey, err := os.ReadFile(keyRef) + if err != nil { + continue + } + + return string(publicKey), true, nil + } + } + } + + return "", false, nil +} diff --git a/pkg/meta/signatures/notation.go b/pkg/meta/signatures/notation.go new file mode 100644 index 00000000..d4fa32ed --- /dev/null +++ b/pkg/meta/signatures/notation.go @@ -0,0 +1,167 @@ +package signatures + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path" + "time" + + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/verifier" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verifier/truststore" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + + zerr "zotregistry.io/zot/errors" +) + +const notationDirRelativePath = "_notation" + +var notationDir = "" //nolint:gochecknoglobals + +func InitNotationDir(rootDir string) error { + dir := path.Join(rootDir, notationDirRelativePath) + + _, err := os.Stat(dir) + if os.IsNotExist(err) { + err = os.MkdirAll(dir, defaultDirPerms) + if err != nil { + return err + } + } + + if err == nil { + notationDir = dir + } + + return err +} + +func GetNotationDirPath() (string, error) { + if notationDir != "" { + return notationDir, nil + } + + return "", zerr.ErrSignConfigDirNotSet +} + +// Equivalent function for trustpolicy.LoadDocument() but using a specific SysFS not the one returned by ConfigFS(). +func LoadTrustPolicyDocument(notationDir string) (*trustpolicy.Document, error) { + jsonFile, err := dir.NewSysFS(notationDir).Open(dir.PathTrustPolicy) + if err != nil { + return nil, err + } + + defer jsonFile.Close() + + policyDocument := &trustpolicy.Document{} + + err = json.NewDecoder(jsonFile).Decode(policyDocument) + if err != nil { + return nil, err + } + + return policyDocument, nil +} + +// NewFromConfig returns a verifier based on local file system. +// Equivalent function for verifier.NewFromConfig() +// but using LoadTrustPolicyDocumnt() function instead of trustpolicy.LoadDocument() function. +func NewFromConfig() (notation.Verifier, error) { + notationDir, err := GetNotationDirPath() + if err != nil { + return nil, err + } + + // Load trust policy. + policyDocument, err := LoadTrustPolicyDocument(notationDir) + if err != nil { + return nil, err + } + + // Load trust store. + x509TrustStore := truststore.NewX509TrustStore(dir.NewSysFS(notationDir)) + + return verifier.New(policyDocument, x509TrustStore, + plugin.NewCLIManager(dir.NewSysFS(path.Join(notationDir, dir.PathPlugins)))) +} + +func VerifyNotationSignature( + artifactDescriptor ispec.Descriptor, artifactReference string, rawSignature []byte, signatureMediaType string, +) (string, time.Time, bool, error) { + var ( + date time.Time + author string + ) + + // If there's no signature associated with the reference. + if len(rawSignature) == 0 { + return author, date, false, notation.ErrorSignatureRetrievalFailed{ + Msg: fmt.Sprintf("no signature associated with %q is provided, make sure the image was signed successfully", + artifactReference), + } + } + + // Initialize verifier. + verifier, err := NewFromConfig() + if err != nil { + return author, date, false, err + } + + ctx := context.Background() + + // Set VerifyOptions. + opts := notation.VerifierVerifyOptions{ + // ArtifactReference is important to validate registry scope format + // If "registryScopes" field from trustpolicy.json file is not wildcard then "domain:80/repo@" should not be hardcoded + ArtifactReference: "domain:80/repo@" + artifactReference, + SignatureMediaType: signatureMediaType, + PluginConfig: map[string]string{}, + } + + // Verify the notation signature which should be associated with the artifactDescriptor. + outcome, err := verifier.Verify(ctx, artifactDescriptor, rawSignature, opts) + if outcome.EnvelopeContent != nil { + author = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].Subject.String() + + if outcome.VerificationLevel == trustpolicy.LevelStrict && (err == nil || + CheckExpiryErr(outcome.VerificationResults, outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter, err)) { + expiry := outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry + if !expiry.IsZero() && expiry.Before(outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter) { + date = outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry + } else { + date = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter + } + } + } + + if err != nil { + return author, date, false, err + } + + // Verification Succeeded. + return author, date, true, nil +} + +func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter time.Time, err error) bool { + for _, result := range verificationResults { + if result.Type == trustpolicy.TypeExpiry { + if errors.Is(err, result.Error) { + return true + } + } else if result.Type == trustpolicy.TypeAuthenticTimestamp { + if errors.Is(err, result.Error) && time.Now().After(notAfter) { + return true + } else { + return false + } + } + } + + return false +} diff --git a/pkg/meta/signatures/signatures.go b/pkg/meta/signatures/signatures.go new file mode 100644 index 00000000..5b266302 --- /dev/null +++ b/pkg/meta/signatures/signatures.go @@ -0,0 +1,59 @@ +package signatures + +import ( + "encoding/json" + "time" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + + zerr "zotregistry.io/zot/errors" +) + +const ( + CosignSignature = "cosign" + NotationSignature = "notation" + defaultDirPerms = 0o700 +) + +func InitCosignAndNotationDirs(rootDir string) error { + err := InitCosignDir(rootDir) + if err != nil { + return err + } + + err = InitNotationDir(rootDir) + + return err +} + +func VerifySignature( + signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte, + repo string, +) (string, time.Time, bool, error) { + var manifest ispec.Manifest + if err := json.Unmarshal(manifestContent, &manifest); err != nil { + return "", time.Time{}, false, err + } + + desc := ispec.Descriptor{ + MediaType: manifest.MediaType, + Digest: manifestDigest, + Size: int64(len(manifestContent)), + } + + if manifestDigest.String() == "" { + return "", time.Time{}, false, zerr.ErrBadManifestDigest + } + + switch signatureType { + case CosignSignature: + author, isValid, err := VerifyCosignSignature(repo, manifestDigest, sigKey, rawSignature) + + return author, time.Time{}, isValid, err + case NotationSignature: + return VerifyNotationSignature(desc, manifestDigest.String(), rawSignature, sigKey) + default: + return "", time.Time{}, false, zerr.ErrInvalidSignatureType + } +} diff --git a/pkg/meta/signatures/signatures_test.go b/pkg/meta/signatures/signatures_test.go new file mode 100644 index 00000000..27905f3d --- /dev/null +++ b/pkg/meta/signatures/signatures_test.go @@ -0,0 +1,439 @@ +package signatures_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path" + "testing" + "time" + + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + . "github.com/smartystreets/goconvey/convey" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/api" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/meta/signatures" + "zotregistry.io/zot/pkg/test" +) + +var errExpiryError = errors.New("expiry err") + +func TestInitCosignAndNotationDirs(t *testing.T) { + Convey("InitCosignDir error", t, func() { + dir := t.TempDir() + err := os.Chmod(dir, 0o000) + So(err, ShouldBeNil) + + err = signatures.InitCosignAndNotationDirs(dir) + So(err, ShouldNotBeNil) + + err = os.Chmod(dir, 0o500) + So(err, ShouldBeNil) + + err = signatures.InitCosignAndNotationDirs(dir) + So(err, ShouldNotBeNil) + + cosignDir, err := signatures.GetCosignDirPath() + So(cosignDir, ShouldBeEmpty) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) + }) + + Convey("InitNotationDir error", t, func() { + dir := t.TempDir() + err := os.Chmod(dir, 0o000) + So(err, ShouldBeNil) + + err = signatures.InitCosignAndNotationDirs(dir) + So(err, ShouldNotBeNil) + + err = os.Chmod(dir, 0o500) + So(err, ShouldBeNil) + + err = signatures.InitCosignAndNotationDirs(dir) + So(err, ShouldNotBeNil) + + err = signatures.InitNotationDir(dir) + So(err, ShouldNotBeNil) + + notationDir, err := signatures.GetNotationDirPath() + So(notationDir, ShouldBeEmpty) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) + }) +} + +func TestVerifySignatures(t *testing.T) { + Convey("wrong manifest content", t, func() { + manifestContent := []byte("wrong json") + + _, _, _, err := signatures.VerifySignature("", []byte(""), "", "", manifestContent, "repo") + So(err, ShouldNotBeNil) + }) + + Convey("empty manifest digest", t, func() { + image, err := test.GetRandomImage("image") + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + _, _, _, err = signatures.VerifySignature("", []byte(""), "", "", manifestContent, "repo") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrBadManifestDigest) + }) + + Convey("wrong signature type", t, func() { + image, err := test.GetRandomImage("image") + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest, err := image.Digest() + So(err, ShouldBeNil) + + _, _, _, err = signatures.VerifySignature("wrongType", []byte(""), "", manifestDigest, manifestContent, "repo") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrInvalidSignatureType) + }) + + Convey("verify cosign signature", t, func() { + repo := "repo" + tag := "test" + image, err := test.GetRandomImage(tag) + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest, err := image.Digest() + So(err, ShouldBeNil) + + Convey("cosignDir is not set", func() { + _, _, _, err = signatures.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) + }) + + Convey("cosignDir does not have read permissions", func() { + dir := t.TempDir() + + err := signatures.InitCosignDir(dir) + So(err, ShouldBeNil) + + cosignDir, err := signatures.GetCosignDirPath() + So(err, ShouldBeNil) + err = os.Chmod(cosignDir, 0o300) + So(err, ShouldBeNil) + + _, _, _, err = signatures.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + So(err, ShouldNotBeNil) + }) + + Convey("no valid public key", func() { + dir := t.TempDir() + + err := signatures.InitCosignDir(dir) + So(err, ShouldBeNil) + + cosignDir, err := signatures.GetCosignDirPath() + So(err, ShouldBeNil) + + err = test.WriteFileWithPermission(path.Join(cosignDir, "file"), []byte("not a public key"), 0o600, false) + So(err, ShouldBeNil) + + _, _, isTrusted, err := signatures.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + So(err, ShouldBeNil) + So(isTrusted, ShouldBeFalse) + }) + + Convey("signature is trusted", func() { + rootDir := t.TempDir() + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + err := test.UploadImage(image, baseURL, repo) + So(err, ShouldBeNil) + + err = signatures.InitCosignDir(rootDir) + So(err, ShouldBeNil) + + cosignDir, err := signatures.GetCosignDirPath() + So(err, ShouldBeNil) + + cwd, err := os.Getwd() + So(err, ShouldBeNil) + + _ = os.Chdir(cosignDir) + + // generate a keypair + os.Setenv("COSIGN_PASSWORD", "") + err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil) + So(err, ShouldBeNil) + + _ = os.Chdir(cwd) + + // sign the image + err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute}, + options.KeyOpts{KeyRef: path.Join(cosignDir, "cosign.key"), PassFunc: generate.GetPass}, + options.SignOptions{ + Registry: options.RegistryOptions{AllowInsecure: true}, + AnnotationOptions: options.AnnotationOptions{Annotations: []string{fmt.Sprintf("tag=%s", tag)}}, + Upload: true, + }, + []string{fmt.Sprintf("localhost:%s/%s@%s", port, repo, manifestDigest.String())}) + So(err, ShouldBeNil) + + err = os.Remove(path.Join(cosignDir, "cosign.key")) + So(err, ShouldBeNil) + + indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo) + So(err, ShouldBeNil) + + var index ispec.Index + err = json.Unmarshal(indexContent, &index) + So(err, ShouldBeNil) + + var rawSignature []byte + var sigKey string + + for _, manifest := range index.Manifests { + if manifest.Digest != manifestDigest { + blobContent, err := ctlr.StoreController.DefaultStore.GetBlobContent(repo, manifest.Digest) + So(err, ShouldBeNil) + + var cosignSig ispec.Manifest + + err = json.Unmarshal(blobContent, &cosignSig) + So(err, ShouldBeNil) + + sigKey = cosignSig.Layers[0].Annotations[signatures.CosignSigKey] + + rawSignature, err = ctlr.StoreController.DefaultStore.GetBlobContent(repo, cosignSig.Layers[0].Digest) + So(err, ShouldBeNil) + } + } + + // signature is trusted + author, _, isTrusted, err := signatures.VerifySignature("cosign", rawSignature, sigKey, manifestDigest, + manifestContent, repo) + So(err, ShouldBeNil) + So(isTrusted, ShouldBeTrue) + So(author, ShouldNotBeEmpty) + }) + }) + + Convey("verify notation signature", t, func() { + repo := "repo" + tag := "test" + image, err := test.GetRandomImage(tag) + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest, err := image.Digest() + So(err, ShouldBeNil) + + Convey("notationDir is not set", func() { + _, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) + }) + + Convey("no signature provided", func() { + dir := t.TempDir() + + err := signatures.InitNotationDir(dir) + So(err, ShouldBeNil) + + _, _, isTrusted, err := signatures.VerifySignature("notation", []byte(""), "", manifestDigest, manifestContent, repo) + So(err, ShouldNotBeNil) + So(isTrusted, ShouldBeFalse) + }) + + Convey("trustpolicy.json does not exist", func() { + dir := t.TempDir() + + err := signatures.InitNotationDir(dir) + So(err, ShouldBeNil) + + _, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) + So(err, ShouldNotBeNil) + }) + + Convey("trustpolicy.json has invalid content", func() { + dir := t.TempDir() + + err := signatures.InitNotationDir(dir) + So(err, ShouldBeNil) + + notationDir, err := signatures.GetNotationDirPath() + So(err, ShouldBeNil) + + err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"), + 0o600, false) + So(err, ShouldBeNil) + + _, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, + repo) + So(err, ShouldNotBeNil) + }) + + Convey("signature is trusted", func() { + rootDir := t.TempDir() + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + err := test.UploadImage(image, baseURL, repo) + So(err, ShouldBeNil) + + err = signatures.InitNotationDir(rootDir) + So(err, ShouldBeNil) + + notationDir, err := signatures.GetNotationDirPath() + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(notationDir) + + // generate a keypair + err = test.GenerateNotationCerts(notationDir, "notation-sign-test") + So(err, ShouldBeNil) + + // sign the image + image := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) + + err = test.SignWithNotation("notation-sign-test", image, notationDir) + So(err, ShouldBeNil) + + err = test.CopyFiles(path.Join(notationDir, "notation", "truststore"), path.Join(notationDir, "truststore")) + So(err, ShouldBeNil) + + err = os.RemoveAll(path.Join(notationDir, "notation")) + So(err, ShouldBeNil) + + trustPolicy := ` + { + "version": "1.0", + "trustPolicies": [ + { + "name": "notation-sign-test", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": ["ca:notation-sign-test"], + "trustedIdentities": [ + "*" + ] + } + ] + }` + + err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte(trustPolicy), 0o600, false) + So(err, ShouldBeNil) + + indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo) + So(err, ShouldBeNil) + + var index ispec.Index + err = json.Unmarshal(indexContent, &index) + So(err, ShouldBeNil) + + var rawSignature []byte + var sigKey string + + for _, manifest := range index.Manifests { + if manifest.Digest != manifestDigest { + blobContent, err := ctlr.StoreController.DefaultStore.GetBlobContent(repo, manifest.Digest) + So(err, ShouldBeNil) + + var notationSig ispec.Manifest + + err = json.Unmarshal(blobContent, ¬ationSig) + So(err, ShouldBeNil) + + sigKey = notationSig.Layers[0].MediaType + + rawSignature, err = ctlr.StoreController.DefaultStore.GetBlobContent(repo, notationSig.Layers[0].Digest) + So(err, ShouldBeNil) + } + } + + // signature is trusted + author, _, isTrusted, err := signatures.VerifySignature("notation", rawSignature, sigKey, manifestDigest, + manifestContent, repo) + So(err, ShouldBeNil) + So(isTrusted, ShouldBeTrue) + So(author, ShouldNotBeEmpty) + + err = os.Truncate(path.Join(notationDir, "truststore/x509/ca/notation-sign-test/notation-sign-test.crt"), 0) + So(err, ShouldBeNil) + + // signature is not trusted + author, _, isTrusted, err = signatures.VerifySignature("notation", rawSignature, sigKey, manifestDigest, + manifestContent, repo) + So(err, ShouldNotBeNil) + So(isTrusted, ShouldBeFalse) + So(author, ShouldNotBeEmpty) + }) + }) +} + +func TestCheckExpiryErr(t *testing.T) { + Convey("no expiry err", t, func() { + isExpiryErr := signatures.CheckExpiryErr([]*notation.ValidationResult{{Error: nil, Type: "wrongtype"}}, time.Now(), + nil) + So(isExpiryErr, ShouldBeFalse) + + isExpiryErr = signatures.CheckExpiryErr([]*notation.ValidationResult{{ + Error: nil, Type: trustpolicy.TypeAuthenticTimestamp, + }}, time.Now(), errExpiryError) + So(isExpiryErr, ShouldBeFalse) + }) + + Convey("expiry err", t, func() { + isExpiryErr := signatures.CheckExpiryErr([]*notation.ValidationResult{ + {Error: errExpiryError, Type: trustpolicy.TypeExpiry}, + }, time.Now(), errExpiryError) + So(isExpiryErr, ShouldBeTrue) + + isExpiryErr = signatures.CheckExpiryErr([]*notation.ValidationResult{ + {Error: errExpiryError, Type: trustpolicy.TypeAuthenticTimestamp}, + }, time.Now().AddDate(0, 0, -1), errExpiryError) + So(isExpiryErr, ShouldBeTrue) + }) +} diff --git a/pkg/meta/update.go b/pkg/meta/update.go index 81a513f3..be537940 100644 --- a/pkg/meta/update.go +++ b/pkg/meta/update.go @@ -34,13 +34,28 @@ func OnUpdateManifest(repo, reference, mediaType string, digest godigest.Digest, metadataSuccessfullySet := true if isSignature { - err = repoDB.AddManifestSignature(repo, signedManifestDigest, repodb.SignatureMetadata{ - SignatureType: signatureType, - SignatureDigest: digest.String(), - }) - if err != nil { - log.Error().Err(err).Msg("repodb: error while putting repo meta") + layersInfo, errGetLayers := repodb.GetSignatureLayersInfo(repo, reference, digest.String(), signatureType, body, + imgStore, log) + if errGetLayers != nil { metadataSuccessfullySet = false + err = errGetLayers + } else { + err = repoDB.AddManifestSignature(repo, signedManifestDigest, repodb.SignatureMetadata{ + SignatureType: signatureType, + SignatureDigest: digest.String(), + LayersInfo: layersInfo, + }) + if err != nil { + log.Error().Err(err).Msg("repodb: error while putting repo meta") + metadataSuccessfullySet = false + } else { + err = repoDB.UpdateSignaturesValidity(repo, signedManifestDigest) + if err != nil { + log.Error().Err(err).Str("repository", repo).Str("reference", reference).Str("digest", + signedManifestDigest.String()).Msg("repodb: failed verify signatures validity for signed image") + metadataSuccessfullySet = false + } + } } } else { err := repodb.SetImageMetaFromInput(repo, reference, mediaType, digest, body, diff --git a/pkg/meta/update_test.go b/pkg/meta/update_test.go index 96b8fb1f..688065b8 100644 --- a/pkg/meta/update_test.go +++ b/pkg/meta/update_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + notreg "github.com/notaryproject/notation-go/registry" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -92,6 +93,82 @@ func TestOnUpdateManifest(t *testing.T) { func TestUpdateErrors(t *testing.T) { Convey("Update operations", t, func() { + Convey("On UpdateManifest", func() { + imageStore := mocks.MockedImageStore{} + storeController := storage.StoreController{DefaultStore: &imageStore} + repoDB := mocks.RepoDBMock{} + log := log.NewLogger("debug", "") + + Convey("CheckIsImageSignature errors", func() { + badManifestBlob := []byte("bad") + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "", "", zerr.ErrManifestNotFound + } + + imageStore.DeleteImageManifestFn = func(repo, reference string, detectCollision bool) error { + return nil + } + + err := meta.OnUpdateManifest("repo", "tag1", "digest", "media", badManifestBlob, + storeController, repoDB, log) + So(err, ShouldNotBeNil) + }) + + Convey("GetSignatureLayersInfo errors", func() { + // get notation signature layers info + badNotationManifestContent := ispec.Manifest{ + Subject: &ispec.Descriptor{ + Digest: "123", + }, + Config: ispec.Descriptor{MediaType: notreg.ArtifactTypeNotation}, + } + + badNotationManifestBlob, err := json.Marshal(badNotationManifestContent) + So(err, ShouldBeNil) + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return badNotationManifestBlob, "", "", nil + } + + err = meta.OnUpdateManifest("repo", "tag1", "", "digest", badNotationManifestBlob, + storeController, repoDB, log) + So(err, ShouldNotBeNil) + }) + + Convey("UpdateSignaturesValidity", func() { + notationManifestContent := ispec.Manifest{ + Subject: &ispec.Descriptor{ + Digest: "123", + }, + Config: ispec.Descriptor{MediaType: notreg.ArtifactTypeNotation}, + Layers: []ispec.Descriptor{{ + MediaType: ispec.MediaTypeImageLayer, + Digest: godigest.FromString("blob digest"), + }}, + } + + notationManifestBlob, err := json.Marshal(notationManifestContent) + So(err, ShouldBeNil) + + imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return notationManifestBlob, "", "", nil + } + + imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { + return []byte{}, nil + } + + repoDB.UpdateSignaturesValidityFn = func(repo string, manifestDigest godigest.Digest) error { + return ErrTestError + } + + err = meta.OnUpdateManifest("repo", "tag1", "", "digest", notationManifestBlob, + storeController, repoDB, log) + So(err, ShouldNotBeNil) + }) + }) + Convey("On DeleteManifest", func() { imageStore := mocks.MockedImageStore{} storeController := storage.StoreController{DefaultStore: &imageStore} diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index a5761140..f5930841 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -55,6 +55,8 @@ type RepoDBMock struct { IncrementImageDownloadsFn func(repo string, reference string) error + UpdateSignaturesValidityFn func(repo string, manifestDigest godigest.Digest) error + AddManifestSignatureFn func(repo string, signedManifestDigest godigest.Digest, sm repodb.SignatureMetadata) error DeleteSignatureFn func(repo string, signedManifestDigest godigest.Digest, sm repodb.SignatureMetadata) error @@ -219,6 +221,14 @@ func (sdm RepoDBMock) IncrementImageDownloads(repo string, reference string) err return nil } +func (sdm RepoDBMock) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { + if sdm.UpdateSignaturesValidityFn != nil { + return sdm.UpdateSignaturesValidityFn(repo, manifestDigest) + } + + return nil +} + func (sdm RepoDBMock) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, sm repodb.SignatureMetadata, ) error {