mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
feat(graphql & repodb): add info about signature validity (#1344)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
parent
6e6ffe800c
commit
970997f3a8
24 changed files with 2053 additions and 31 deletions
|
@ -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")
|
||||
)
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
113
pkg/meta/signatures/cosign.go
Normal file
113
pkg/meta/signatures/cosign.go
Normal file
|
@ -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
|
||||
}
|
167
pkg/meta/signatures/notation.go
Normal file
167
pkg/meta/signatures/notation.go
Normal file
|
@ -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
|
||||
}
|
59
pkg/meta/signatures/signatures.go
Normal file
59
pkg/meta/signatures/signatures.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
439
pkg/meta/signatures/signatures_test.go
Normal file
439
pkg/meta/signatures/signatures_test.go
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue