0
Fork 0
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:
Andreea Lupu 2023-05-24 19:46:16 +03:00 committed by GitHub
parent 6e6ffe800c
commit 970997f3a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2053 additions and 31 deletions

View file

@ -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
View file

@ -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

View file

@ -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")
})
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)
})
}

View file

@ -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

View file

@ -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)
})
})
})
}

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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()

View file

@ -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) {

View file

@ -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)
})
}

View 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
}

View 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
}

View 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
}
}

View 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, &notationSig)
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)
})
}

View file

@ -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,

View file

@ -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}

View file

@ -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 {