From 5a3fac40dbc4a530d83c37cd3e8f7a695c2e7307 Mon Sep 17 00:00:00 2001 From: Andreea Lupu <58118008+Andreea-Lupu@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:03:58 +0300 Subject: [PATCH] feat: upload cosign public key and notation certificates to cloud (#1744) - using secrets manager for storing public keys and certificates - adding a default truststore for notation verification and upload all certificates to this default truststore - removig `truststoreName` query param from notation api for uploading certificates (cherry picked from commit eafcc1a213f9d46fcbb3c5347e49e33af360e2cf) Signed-off-by: Andreea-Lupu --- go.mod | 2 + go.sum | 10 + pkg/api/controller.go | 5 + pkg/api/routes.go | 2 +- pkg/extensions/README_imagetrust.md | 21 +- pkg/extensions/extension_image_trust.go | 66 +- .../extension_image_trust_disabled.go | 9 +- pkg/extensions/extension_image_trust_test.go | 116 ++- pkg/extensions/get_extensions.go | 4 + pkg/extensions/imagetrust/cosign.go | 242 ++++- pkg/extensions/imagetrust/image_trust.go | 117 ++- .../imagetrust/image_trust_disabled.go | 14 +- .../imagetrust/image_trust_disabled_test.go | 40 +- pkg/extensions/imagetrust/image_trust_test.go | 954 ++++++++++++++---- pkg/extensions/imagetrust/notation.go | 380 +++++-- pkg/meta/boltdb/boltdb.go | 33 +- pkg/meta/boltdb/boltdb_test.go | 12 + pkg/meta/dynamodb/dynamodb.go | 23 +- pkg/meta/dynamodb/dynamodb_test.go | 9 + pkg/meta/meta.go | 6 - pkg/meta/meta_test.go | 111 +- pkg/meta/types/types.go | 11 + pkg/meta/version/version_test.go | 3 +- pkg/test/mocks/repo_db_mock.go | 18 + swagger/docs.go | 6 - swagger/swagger.json | 6 - swagger/swagger.yaml | 4 - 27 files changed, 1661 insertions(+), 563 deletions(-) diff --git a/go.mod b/go.mod index 294764aa..ea1e0493 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,8 @@ require ( require ( github.com/aquasecurity/trivy v0.44.1 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.5 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 + github.com/aws/aws-secretsmanager-caching-go v1.1.2 github.com/containers/image/v5 v5.27.0 github.com/google/go-github/v52 v52.0.0 github.com/gorilla/handlers v1.5.1 diff --git a/go.sum b/go.sum index 191d2fc9..1684d835 100644 --- a/go.sum +++ b/go.sum @@ -406,6 +406,7 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.287/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.45.2 h1:hTong9YUklQKqzrGk3WnKABReb5R8GjbG4Y6dEQfjnk= github.com/aws/aws-sdk-go v1.45.2/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= @@ -464,6 +465,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EO github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= github.com/aws/aws-sdk-go-v2/service/kms v1.23.0 h1:NXYeZBNg35rDBhcus60DFkIP7q6RNSkarLx+37ERX1g= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 h1:H6ZipEknzu7RkJW3w2PP75zd8XOdR35AEY5D57YrJtA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3/go.mod h1:5W2cYXDPabUmwULErlC92ffLhtTuyv4ai+5HhdbhfNo= github.com/aws/aws-sdk-go-v2/service/sso v1.3.1/go.mod h1:J3A3RGUvuCZjvSuZEcOpHDnzZP/sKbhDWV2T1EOzFIM= github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 h1:oCvTFSDi67AX0pOX3PuPdGFewvLRU2zzFSrTsgURNo0= github.com/aws/aws-sdk-go-v2/service/sso v1.13.5/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= @@ -472,6 +475,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3 github.com/aws/aws-sdk-go-v2/service/sts v1.6.0/go.mod h1:q7o0j7d7HrJk/vr9uUt3BVRASvcU7gYZB9PUgPiByXg= github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= +github.com/aws/aws-secretsmanager-caching-go v1.1.2 h1:tY3pRhAkaohm75KFpGHoqjWrnRpznqrc8iX/wTLVpH0= +github.com/aws/aws-secretsmanager-caching-go v1.1.2/go.mod h1:s3Or+O0O8obPyDJz6875Rg1WApAbQ64L0WTBwYNnKLo= github.com/aws/smithy-go v1.6.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.11.0/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= @@ -1794,6 +1799,7 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1903,6 +1909,7 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2061,6 +2068,7 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2071,6 +2079,7 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2088,6 +2097,7 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/api/controller.go b/pkg/api/controller.go index c854384d..09ca9615 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -265,6 +265,11 @@ func (c *Controller) InitMetaDB(reloadCtx context.Context) error { return err } + err = ext.SetupExtensions(c.Config, driver, c.Log) //nolint:contextcheck + if err != nil { + return err + } + err = driver.PatchDB() if err != nil { return err diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 7a8efb35..c57020d0 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -195,7 +195,7 @@ func (rh *RouteHandler) SetupRoutes() { // Preconditions for enabling the actual extension routes are part of extensions themselves ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo, rh.c.Log) - ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.Log) + ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log) ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log) ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log) // last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer. diff --git a/pkg/extensions/README_imagetrust.md b/pkg/extensions/README_imagetrust.md index 03006593..9018b583 100644 --- a/pkg/extensions/README_imagetrust.md +++ b/pkg/extensions/README_imagetrust.md @@ -38,24 +38,23 @@ curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/cosign ``` As a result of this request, the uploaded file will be stored in `_cosign` directory -under the rootDir specified in the zot config. +under the rootDir specified in the zot config or in Secrets Manager. ### Uploading a Notation certificate Notation certificates are used to sign images with the `notation` tool. The user needs to specify the type of the truststore through the `truststoreType` -query parameter and its name through the `truststoreName` parameter. -`truststoreType` defaults to `ca`, while `truststoreName` is a mandatory parameter. +query parameter. +`truststoreType` defaults to `ca`. ***Example of request*** ```bash -curl --data-binary @certificate.crt -X POST "http://localhost:8080/v2/_zot/ext/notation?truststoreType=ca&truststoreName=upload-cert" +curl --data-binary @certificate.crt -X POST "http://localhost:8080/v2/_zot/ext/notation?truststoreType=ca" ``` -As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}` -directory under the rootDir specified in the zot config. -The `truststores` field found in `_notation/trustpolicy.json` file will be updated automatically as well. +As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/default` +directory under the rootDir specified in the zot config or in Secrets Manager. ## Verification and results @@ -118,7 +117,7 @@ The information above will be included in the ManifestSummary objects returned b ## Notes -- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir`. +- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir` in case of local filesystem or in Secrets Manager in case of cloud. - `_cosign` directory will contain the uploaded public keys @@ -136,11 +135,11 @@ The information above will be included in the ManifestSummary objects returned b └── truststore └── x509 └── $truststoreType - └── $truststoreName + └── default └── $certificate ``` - where `trustpolicy.json` file has this default content which can not be modified by the user and which is updated each time a new certificate is added to a new truststore: + where `trustpolicy.json` file has this default content which can not be modified by the user: ```json { @@ -152,7 +151,7 @@ The information above will be included in the ManifestSummary objects returned b "signatureVerification": { "level" : "strict" }, - "trustStores": [], + "trustStores": ["ca:default","signingAuthority:default"], "trustedIdentities": [ "*" ] diff --git a/pkg/extensions/extension_image_trust.go b/pkg/extensions/extension_image_trust.go index abf0f0c0..a5f7cf4e 100644 --- a/pkg/extensions/extension_image_trust.go +++ b/pkg/extensions/extension_image_trust.go @@ -21,16 +21,11 @@ import ( "zotregistry.io/zot/pkg/scheduler" ) -const ( - ConfigResource = "config" - SignaturesResource = "signatures" -) - func IsBuiltWithImageTrustExtension() bool { return true } -func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, log log.Logger) { +func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, metaDB mTypes.MetaDB, log log.Logger) { if !conf.IsImageTrustEnabled() || (!conf.IsCosignEnabled() && !conf.IsNotationEnabled()) { log.Info().Msg("skip enabling the image trust routes as the config prerequisites are not met") @@ -39,7 +34,8 @@ func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, log log.Logg log.Info().Msg("setting up image trust routes") - trust := ImageTrust{Conf: conf, Log: log} + imgTrustStore, _ := metaDB.ImageTrustStore().(*imagetrust.ImageTrustStore) + trust := ImageTrust{Conf: conf, ImageTrustStore: imgTrustStore, Log: log} allowedMethods := zcommon.AllowedMethods(http.MethodPost) if conf.IsNotationEnabled() { @@ -70,8 +66,9 @@ func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, log log.Logg } type ImageTrust struct { - Conf *config.Config - Log log.Logger + Conf *config.Config + ImageTrustStore *imagetrust.ImageTrustStore + Log log.Logger } // Cosign handler godoc @@ -93,7 +90,7 @@ func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWrite return } - err = imagetrust.UploadPublicKey(body) + err = imagetrust.UploadPublicKey(trust.ImageTrustStore.CosignStorage, body) if err != nil { if errors.Is(err, zerr.ErrInvalidPublicKeyContent) { response.WriteHeader(http.StatusBadRequest) @@ -115,7 +112,6 @@ func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWrite // @Accept octet-stream // @Produce json // @Param truststoreType query string false "truststore type" -// @Param truststoreName query string false "truststore name" // @Param requestBody body string true "Certificate content" // @Success 200 {string} string "ok" // @Failure 400 {string} string "bad request". @@ -123,26 +119,12 @@ func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWrite func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseWriter, request *http.Request) { var truststoreType string - if !zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreName"}) { - response.WriteHeader(http.StatusBadRequest) - - return - } - if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) { truststoreType = request.URL.Query().Get("truststoreType") } else { truststoreType = "ca" // default value of "truststoreType" query param } - truststoreName := request.URL.Query().Get("truststoreName") - - if truststoreType == "" || truststoreName == "" { - response.WriteHeader(http.StatusBadRequest) - - return - } - body, err := io.ReadAll(request.Body) if err != nil { trust.Log.Error().Err(err).Msg("image trust: couldn't read notation certificate body") @@ -151,10 +133,9 @@ func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseW return } - err = imagetrust.UploadCertificate(body, truststoreType, truststoreName) + err = imagetrust.UploadCertificate(trust.ImageTrustStore.NotationStorage, body, truststoreType) if err != nil { if errors.Is(err, zerr.ErrInvalidTruststoreType) || - errors.Is(err, zerr.ErrInvalidTruststoreName) || errors.Is(err, zerr.ErrInvalidCertificateContent) { response.WriteHeader(http.StatusBadRequest) } else { @@ -178,6 +159,35 @@ func EnableImageTrustVerification(conf *config.Config, taskScheduler *scheduler. generator := imagetrust.NewTaskGenerator(metaDB, log) numberOfHours := 2 - interval := time.Duration(numberOfHours) * time.Minute + interval := time.Duration(numberOfHours) * time.Hour taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority) } + +func SetupImageTrustExtension(conf *config.Config, metaDB mTypes.MetaDB, log log.Logger) error { + if !conf.IsImageTrustEnabled() { + return nil + } + + var imgTrustStore mTypes.ImageTrustStore + + var err error + + if conf.Storage.RemoteCache { + endpoint, _ := conf.Storage.CacheDriver["endpoint"].(string) + region, _ := conf.Storage.CacheDriver["region"].(string) + imgTrustStore, err = imagetrust.NewAWSImageTrustStore(region, endpoint) + + if err != nil { + return err + } + } else { + imgTrustStore, err = imagetrust.NewLocalImageTrustStore(conf.Storage.RootDirectory) + if err != nil { + return err + } + } + + metaDB.SetImageTrustStore(imgTrustStore) + + return nil +} diff --git a/pkg/extensions/extension_image_trust_disabled.go b/pkg/extensions/extension_image_trust_disabled.go index 0e123563..7f85a003 100644 --- a/pkg/extensions/extension_image_trust_disabled.go +++ b/pkg/extensions/extension_image_trust_disabled.go @@ -16,7 +16,7 @@ func IsBuiltWithImageTrustExtension() bool { return false } -func SetupImageTrustRoutes(config *config.Config, router *mux.Router, log log.Logger) { +func SetupImageTrustRoutes(config *config.Config, router *mux.Router, metaDB mTypes.MetaDB, log log.Logger) { log.Warn().Msg("skipping setting up image trust routes because given zot binary doesn't include this feature," + "please build a binary that does so") } @@ -27,3 +27,10 @@ func EnableImageTrustVerification(config *config.Config, taskScheduler *schedule log.Warn().Msg("skipping adding to the scheduler a generator for updating signatures validity because " + "given binary doesn't include this feature, please build a binary that does so") } + +func SetupImageTrustExtension(conf *config.Config, metaDB mTypes.MetaDB, log log.Logger) error { + log.Warn().Msg("skipping setting up image trust because given zot binary doesn't include this feature," + + "please build a binary that does so") + + return nil +} diff --git a/pkg/extensions/extension_image_trust_test.go b/pkg/extensions/extension_image_trust_test.go index e55adb8f..ed3f18b1 100644 --- a/pkg/extensions/extension_image_trust_test.go +++ b/pkg/extensions/extension_image_trust_test.go @@ -16,6 +16,7 @@ import ( "testing" "time" + guuid "github.com/gofrs/uuid" "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" @@ -64,7 +65,6 @@ func TestSignatureHandlers(t *testing.T) { Convey("Test error handling when Notation handler reads the request body", t, func() { request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, "baseURL", errReader(0)) query := request.URL.Query() - query.Add("truststoreName", "someName") request.URL.RawQuery = query.Encode() response := httptest.NewRecorder() @@ -111,7 +111,49 @@ func TestSignaturesAllowedMethodsHeader(t *testing.T) { }) } -func TestSignatureUploadAndVerification(t *testing.T) { +func TestSignatureUploadAndVerificationLocal(t *testing.T) { + Convey("test with local storage", t, func() { + var cacheDriverParams map[string]interface{} + + RunSignatureUploadAndVerificationTests(t, cacheDriverParams) + }) +} + +func TestSignatureUploadAndVerificationAWS(t *testing.T) { + skipIt(t) + + Convey("test with AWS", t, func() { + uuid, err := guuid.NewV4() + So(err, ShouldBeNil) + + cacheTablename := "BlobTable" + uuid.String() + repoMetaTablename := "RepoMetadataTable" + uuid.String() + manifestDataTablename := "ManifestDataTable" + uuid.String() + versionTablename := "Version" + uuid.String() + indexDataTablename := "IndexDataTable" + uuid.String() + userDataTablename := "UserDataTable" + uuid.String() + apiKeyTablename := "ApiKeyTable" + uuid.String() + + cacheDriverParams := map[string]interface{}{ + "name": "dynamoDB", + "endpoint": os.Getenv("DYNAMODBMOCK_ENDPOINT"), + "region": "us-east-2", + "cacheTablename": cacheTablename, + "repoMetaTablename": repoMetaTablename, + "manifestDataTablename": manifestDataTablename, + "indexDataTablename": indexDataTablename, + "userDataTablename": userDataTablename, + "apiKeyTablename": apiKeyTablename, + "versionTablename": versionTablename, + } + + t.Logf("using dynamo driver options: %v", cacheDriverParams) + + RunSignatureUploadAndVerificationTests(t, cacheDriverParams) + }) +} + +func RunSignatureUploadAndVerificationTests(t *testing.T, cacheDriverParams map[string]interface{}) { //nolint: thelper repo := "repo" tag := "0.0.1" certName := "test" @@ -128,12 +170,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { } }` - Convey("Verify cosign public key upload without search or notation being enabled", t, func() { + Convey("Verify cosign public key upload without search or notation being enabled", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Trust = &extconf.ImageTrustConfig{} conf.Extensions.Trust.Enable = &defaultValue @@ -246,12 +291,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) }) - Convey("Verify notation certificate upload without search or cosign being enabled", t, func() { + Convey("Verify notation certificate upload without search or cosign being enabled", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Trust = &extconf.ImageTrustConfig{} conf.Extensions.Trust.Enable = &defaultValue @@ -309,7 +357,6 @@ func TestSignatureUploadAndVerification(t *testing.T) { client := resty.New() resp, err := client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", certName). SetBody(certificateContent).Post(baseURL + constants.FullNotation) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) @@ -330,18 +377,6 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(found, ShouldBeTrue) resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetBody(certificateContent).Post(baseURL + constants.FullNotation) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - - resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", ""). - SetBody(certificateContent).Post(baseURL + constants.FullNotation) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - - resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", "test"). SetQueryParam("truststoreType", "signatureAuthority"). SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation) So(err, ShouldBeNil) @@ -360,12 +395,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) }) - Convey("Verify uploading notation certificates", t, func() { + Convey("Verify uploading notation certificates", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} conf.Extensions.Search.Enable = &defaultValue @@ -453,7 +491,6 @@ func TestSignatureUploadAndVerification(t *testing.T) { client := resty.New() resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", certName). SetBody(certificateContent).Post(baseURL + constants.FullNotation) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) @@ -507,18 +544,6 @@ func TestSignatureUploadAndVerification(t *testing.T) { ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US") resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetBody(certificateContent).Post(baseURL + constants.FullNotation) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - - resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", ""). - SetBody(certificateContent).Post(baseURL + constants.FullNotation) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - - resp, err = client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", "test"). SetQueryParam("truststoreType", "signatureAuthority"). SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation) So(err, ShouldBeNil) @@ -533,12 +558,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) }) - Convey("Verify uploading cosign public keys", t, func() { + Convey("Verify uploading cosign public keys", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} conf.Extensions.Search.Enable = &defaultValue @@ -698,7 +726,7 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) }) - Convey("Verify uploading cosign public keys with auth configured", t, func() { + Convey("Verify uploading cosign public keys with auth configured", func() { globalDir := t.TempDir() port := test.GetFreePort() testCreds := test.GetCredString("admin", "admin") + "\n" + test.GetCredString("test", "test") @@ -713,6 +741,9 @@ func TestSignatureUploadAndVerification(t *testing.T) { Actions: []string{}, }, } + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} conf.Extensions.Search.Enable = &defaultValue @@ -788,12 +819,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusOK) }) - Convey("Verify signatures are read from the disk and updated in the DB when zot starts", t, func() { + Convey("Verify signatures are read from the disk and updated in the DB when zot starts", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} conf.Extensions.Search.Enable = &defaultValue @@ -890,12 +924,15 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(imgSummary.Manifests[0].SignatureInfo[0].Author, ShouldEqual, "") }) - Convey("Verify failures when saving uploaded certificates and public keys", t, func() { + Convey("Verify failures when saving uploaded certificates and public keys", func() { globalDir := t.TempDir() port := test.GetFreePort() conf := config.New() conf.HTTP.Port = port + if cacheDriverParams != nil { + conf.Storage.CacheDriver = cacheDriverParams + } conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} conf.Extensions.Search.Enable = &defaultValue @@ -955,7 +992,6 @@ func TestSignatureUploadAndVerification(t *testing.T) { client := resty.New() resp, err := client.R().SetHeader("Content-type", "application/octet-stream"). - SetQueryParam("truststoreName", "test"). SetBody(certificateContent).Post(baseURL + constants.FullNotation) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) @@ -966,3 +1002,11 @@ func TestSignatureUploadAndVerification(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) } + +func skipIt(t *testing.T) { + t.Helper() + + if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" { + t.Skip("Skipping testing without AWS mock server") + } +} diff --git a/pkg/extensions/get_extensions.go b/pkg/extensions/get_extensions.go index 2bd8ea32..6a24f1b2 100644 --- a/pkg/extensions/get_extensions.go +++ b/pkg/extensions/get_extensions.go @@ -55,3 +55,7 @@ func EnableScheduledTasks(conf *config.Config, taskScheduler *scheduler.Schedule ) { EnableImageTrustVerification(conf, taskScheduler, metaDB, log) } + +func SetupExtensions(conf *config.Config, metaDB mTypes.MetaDB, log log.Logger) error { + return SetupImageTrustExtension(conf, metaDB, log) +} diff --git a/pkg/extensions/imagetrust/cosign.go b/pkg/extensions/imagetrust/cosign.go index be05955a..7764965d 100644 --- a/pkg/extensions/imagetrust/cosign.go +++ b/pkg/extensions/imagetrust/cosign.go @@ -13,10 +13,14 @@ import ( "os" "path" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/aws/aws-secretsmanager-caching-go/secretcache" 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/cryptoutils" + sigstoreSigs "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" zerr "zotregistry.io/zot/errors" @@ -24,116 +28,242 @@ import ( const cosignDirRelativePath = "_cosign" -var cosignDir = "" //nolint:gochecknoglobals +type PublicKeyLocalStorage struct { + cosignDir string +} -func InitCosignDir(rootDir string) error { +type PublicKeyAWSStorage struct { + secretsManagerClient *secretsmanager.Client + secretsManagerCache *secretcache.Cache +} + +type publicKeyStorage interface { + StorePublicKey(name godigest.Digest, publicKeyContent []byte) error + GetPublicKeyVerifier(name string) (sigstoreSigs.Verifier, []byte, error) + GetPublicKeys() ([]string, error) +} + +func NewPublicKeyLocalStorage(rootDir string) (*PublicKeyLocalStorage, error) { dir := path.Join(rootDir, cosignDirRelativePath) _, err := os.Stat(dir) if os.IsNotExist(err) { err = os.MkdirAll(dir, defaultDirPerms) if err != nil { - return err + return nil, err } } - if err == nil { - cosignDir = dir + if err != nil { + return nil, err } - return err + return &PublicKeyLocalStorage{ + cosignDir: dir, + }, nil } -func GetCosignDirPath() (string, error) { - if cosignDir != "" { - return cosignDir, nil +func NewPublicKeyAWSStorage( + secretsManagerClient *secretsmanager.Client, secretsManagerCache *secretcache.Cache, +) *PublicKeyAWSStorage { + return &PublicKeyAWSStorage{ + secretsManagerClient: secretsManagerClient, + secretsManagerCache: secretsManagerCache, + } +} + +func (local *PublicKeyLocalStorage) GetCosignDirPath() (string, error) { + if local.cosignDir != "" { + return local.cosignDir, nil } return "", zerr.ErrSignConfigDirNotSet } func VerifyCosignSignature( - repo string, digest godigest.Digest, signatureKey string, layerContent []byte, + cosignStorage publicKeyStorage, repo string, digest godigest.Digest, signatureKey string, layerContent []byte, ) (string, bool, error) { - cosignDir, err := GetCosignDirPath() + publicKeys, err := cosignStorage.GetPublicKeys() if err != nil { return "", false, err } - files, err := os.ReadDir(cosignDir) - if err != nil { - return "", false, err - } + for _, publicKey := range publicKeys { + // cosign verify the image + pubKeyVerifier, pubKeyContent, err := cosignStorage.GetPublicKeyVerifier(publicKey) + if err != nil { + continue + } - for _, file := range files { - if !file.IsDir() { - // cosign verify the image - ctx := context.Background() - keyRef := path.Join(cosignDir, file.Name()) - hashAlgorithm := crypto.SHA256 + pkcs11Key, ok := pubKeyVerifier.(*pkcs11key.Key) + if ok { + defer pkcs11Key.Close() + } - pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) - if err != nil { - continue - } + verifier := pubKeyVerifier - pkcs11Key, ok := pubKey.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } + b64sig := signatureKey - verifier := pubKey + signature, err := base64.StdEncoding.DecodeString(b64sig) + if err != nil { + continue + } - b64sig := signatureKey + compressed := io.NopCloser(bytes.NewReader(layerContent)) - signature, err := base64.StdEncoding.DecodeString(b64sig) - if err != nil { - continue - } + payload, err := io.ReadAll(compressed) + if err != nil { + continue + } - compressed := io.NopCloser(bytes.NewReader(layerContent)) + err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), + options.WithContext(context.Background())) - 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 - } + if err == nil { + return string(pubKeyContent), true, nil } } return "", false, nil } -func UploadPublicKey(publicKeyContent []byte) error { +func (local *PublicKeyLocalStorage) GetPublicKeyVerifier(fileName string) (sigstoreSigs.Verifier, []byte, error) { + cosignDir, err := local.GetCosignDirPath() + if err != nil { + return nil, []byte{}, err + } + + ctx := context.Background() + keyRef := path.Join(cosignDir, fileName) + hashAlgorithm := crypto.SHA256 + + pubKeyContent, err := os.ReadFile(keyRef) + if err != nil { + return nil, nil, err + } + + pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) + if err != nil { + return nil, nil, err + } + + return pubKey, pubKeyContent, nil +} + +func (cloud *PublicKeyAWSStorage) GetPublicKeyVerifier(secretName string) (sigstoreSigs.Verifier, []byte, error) { + hashAlgorithm := crypto.SHA256 + + // get key + raw, err := cloud.secretsManagerCache.GetSecretString(secretName) + if err != nil { + return nil, nil, err + } + + rawDecoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, nil, err + } + + // PEM encoded file. + key, err := cryptoutils.UnmarshalPEMToPublicKey(rawDecoded) + if err != nil { + return nil, nil, err + } + + pubKey, err := sigstoreSigs.LoadVerifier(key, hashAlgorithm) + if err != nil { + return nil, nil, err + } + + return pubKey, rawDecoded, nil +} + +func (local *PublicKeyLocalStorage) GetPublicKeys() ([]string, error) { + cosignDir, err := local.GetCosignDirPath() + if err != nil { + return []string{}, err + } + + files, err := os.ReadDir(cosignDir) + if err != nil { + return []string{}, err + } + + publicKeys := []string{} + for _, file := range files { + publicKeys = append(publicKeys, file.Name()) + } + + return publicKeys, nil +} + +func (cloud *PublicKeyAWSStorage) GetPublicKeys() ([]string, error) { + ctx := context.Background() + listSecretsInput := secretsmanager.ListSecretsInput{ + Filters: []types.Filter{ + { + Key: types.FilterNameStringTypeDescription, + Values: []string{"cosign public key"}, + }, + }, + } + + secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput) + if err != nil { + return []string{}, err + } + + publicKeys := []string{} + + for _, secret := range secrets.SecretList { + publicKeys = append(publicKeys, *(secret.Name)) + } + + return publicKeys, nil +} + +func UploadPublicKey(cosignStorage publicKeyStorage, publicKeyContent []byte) error { // validate public key if ok, err := validatePublicKey(publicKeyContent); !ok { return err } + name := godigest.FromBytes(publicKeyContent) + + return cosignStorage.StorePublicKey(name, publicKeyContent) +} + +func (local *PublicKeyLocalStorage) StorePublicKey(name godigest.Digest, publicKeyContent []byte) error { // add public key to "{rootDir}/_cosign/{name.pub}" - configDir, err := GetCosignDirPath() + cosignDir, err := local.GetCosignDirPath() if err != nil { return err } - name := godigest.FromBytes(publicKeyContent) - // store public key - publicKeyPath := path.Join(configDir, name.String()) + publicKeyPath := path.Join(cosignDir, name.String()) return os.WriteFile(publicKeyPath, publicKeyContent, defaultFilePerms) } +func (cloud *PublicKeyAWSStorage) StorePublicKey(name godigest.Digest, publicKeyContent []byte) error { + n := name.Encoded() + description := "cosign public key" + secret := base64.StdEncoding.EncodeToString(publicKeyContent) + secretInputParam := &secretsmanager.CreateSecretInput{ + Name: &n, + Description: &description, + SecretString: &secret, + } + + _, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) + if err != nil { + return err + } + + return nil +} + func validatePublicKey(publicKeyContent []byte) (bool, error) { _, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent) if err != nil { diff --git a/pkg/extensions/imagetrust/image_trust.go b/pkg/extensions/imagetrust/image_trust.go index 42c1466c..b64b17d4 100644 --- a/pkg/extensions/imagetrust/image_trust.go +++ b/pkg/extensions/imagetrust/image_trust.go @@ -8,6 +8,14 @@ import ( "encoding/json" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + aws1 "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + smanager "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-secretsmanager-caching-go/secretcache" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -23,18 +31,103 @@ const ( defaultFilePerms = 0o644 ) -func InitCosignAndNotationDirs(rootDir string) error { - err := InitCosignDir(rootDir) - if err != nil { - return err - } - - err = InitNotationDir(rootDir) - - return err +type ImageTrustStore struct { + CosignStorage publicKeyStorage + NotationStorage certificateStorage } -func VerifySignature( +func NewLocalImageTrustStore(rootDir string) (*ImageTrustStore, error) { + publicKeyStorage, err := NewPublicKeyLocalStorage(rootDir) + if err != nil { + return nil, err + } + + certStorage, err := NewCertificateLocalStorage(rootDir) + if err != nil { + return nil, err + } + + return &ImageTrustStore{ + CosignStorage: publicKeyStorage, + NotationStorage: certStorage, + }, nil +} + +func NewAWSImageTrustStore(region, endpoint string) (*ImageTrustStore, error) { + secretsManagerClient, err := GetSecretsManagerClient(region, endpoint) + if err != nil { + return nil, err + } + + secretsManagerCache := GetSecretsManagerRetrieval(region, endpoint) + + publicKeyStorage := NewPublicKeyAWSStorage(secretsManagerClient, secretsManagerCache) + + certStorage, err := NewCertificateAWSStorage(secretsManagerClient, secretsManagerCache) + if err != nil { + return nil, err + } + + return &ImageTrustStore{ + CosignStorage: publicKeyStorage, + NotationStorage: certStorage, + }, nil +} + +func GetSecretsManagerClient(region, endpoint string) (*secretsmanager.Client, error) { + customResolver := aws.EndpointResolverWithOptionsFunc( + func(service, region string, options ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{ + PartitionID: "aws", + URL: endpoint, + SigningRegion: region, + }, nil + }) + + // Using the SDK's default configuration, loading additional config + // and credentials values from the environment variables, shared + // credentials, and shared configuration files + cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), + config.WithEndpointResolverWithOptions(customResolver)) + if err != nil { + return nil, err + } + + return secretsmanager.NewFromConfig(cfg), nil +} + +func GetSecretsManagerRetrieval(region, endpoint string) *secretcache.Cache { + endpointFunc := func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { + return endpoints.ResolvedEndpoint{ + PartitionID: "aws", + URL: endpoint, + SigningRegion: region, + }, nil + } + customResolver := endpoints.ResolverFunc(endpointFunc) + + cfg := aws1.NewConfig().WithRegion(region).WithEndpointResolver(customResolver) + + newSession := session.Must(session.NewSession()) + + client := smanager.New(newSession, cfg) + // Create a custom CacheConfig struct + config := secretcache.CacheConfig{ + MaxCacheSize: secretcache.DefaultMaxCacheSize, + VersionStage: secretcache.DefaultVersionStage, + CacheItemTTL: secretcache.DefaultCacheItemTTL, + } + + // Instantiate the cache + cache, _ := secretcache.New( + func(c *secretcache.Cache) { c.CacheConfig = config }, + func(c *secretcache.Cache) { c.Client = client }, + ) + + return cache +} + +func (imgTrustStore *ImageTrustStore) VerifySignature( signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte, repo string, ) (string, time.Time, bool, error) { @@ -55,11 +148,11 @@ func VerifySignature( switch signatureType { case zcommon.CosignSignature: - author, isValid, err := VerifyCosignSignature(repo, manifestDigest, sigKey, rawSignature) + author, isValid, err := VerifyCosignSignature(imgTrustStore.CosignStorage, repo, manifestDigest, sigKey, rawSignature) return author, time.Time{}, isValid, err case zcommon.NotationSignature: - return VerifyNotationSignature(desc, manifestDigest.String(), rawSignature, sigKey) + return VerifyNotationSignature(imgTrustStore.NotationStorage, desc, manifestDigest.String(), rawSignature, sigKey) default: return "", time.Time{}, false, zerr.ErrInvalidSignatureType } diff --git a/pkg/extensions/imagetrust/image_trust_disabled.go b/pkg/extensions/imagetrust/image_trust_disabled.go index 6a5b9e36..dda917a3 100644 --- a/pkg/extensions/imagetrust/image_trust_disabled.go +++ b/pkg/extensions/imagetrust/image_trust_disabled.go @@ -9,19 +9,17 @@ import ( godigest "github.com/opencontainers/go-digest" ) -func InitCosignAndNotationDirs(rootDir string) error { - return nil +func NewLocalImageTrustStore(dir string) (*imageTrustDisabled, error) { + return &imageTrustDisabled{}, nil } -func InitCosignDir(rootDir string) error { - return nil +func NewAWSImageTrustStore(region, endpoint string) (*imageTrustDisabled, error) { + return &imageTrustDisabled{}, nil } -func InitNotationDir(rootDir string) error { - return nil -} +type imageTrustDisabled struct{} -func VerifySignature( +func (imgTrustStore *imageTrustDisabled) VerifySignature( signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte, repo string, ) (string, time.Time, bool, error) { diff --git a/pkg/extensions/imagetrust/image_trust_disabled_test.go b/pkg/extensions/imagetrust/image_trust_disabled_test.go index 9f365004..f2b632e3 100644 --- a/pkg/extensions/imagetrust/image_trust_disabled_test.go +++ b/pkg/extensions/imagetrust/image_trust_disabled_test.go @@ -3,6 +3,7 @@ package imagetrust_test import ( + "encoding/json" "os" "path" "testing" @@ -10,35 +11,56 @@ import ( . "github.com/smartystreets/goconvey/convey" "zotregistry.io/zot/pkg/extensions/imagetrust" + "zotregistry.io/zot/pkg/test" ) func TestImageTrust(t *testing.T) { Convey("binary doesn't include imagetrust", t, func() { rootDir := t.TempDir() - err := imagetrust.InitCosignDir(rootDir) - So(err, ShouldBeNil) - cosignDir := path.Join(rootDir, "_cosign") - _, err = os.Stat(cosignDir) + _, err := os.Stat(cosignDir) So(os.IsNotExist(err), ShouldBeTrue) - err = imagetrust.InitNotationDir(rootDir) - So(err, ShouldBeNil) - notationDir := path.Join(rootDir, "_notation") _, err = os.Stat(notationDir) So(os.IsNotExist(err), ShouldBeTrue) - err = imagetrust.InitCosignAndNotationDirs(rootDir) + repo := "repo" + + image, err := test.GetRandomImage() //nolint:staticcheck + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest := image.Digest() + + localImgTrustStore, err := imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldBeNil) + + author, expTime, ok, err := localImgTrustStore.VerifySignature("cosign", + []byte(""), "", manifestDigest, manifestContent, repo, + ) + So(author, ShouldBeEmpty) + So(expTime, ShouldBeZeroValue) + So(ok, ShouldBeFalse) So(err, ShouldBeNil) _, err = os.Stat(cosignDir) So(os.IsNotExist(err), ShouldBeTrue) + _, err = os.Stat(notationDir) So(os.IsNotExist(err), ShouldBeTrue) - author, expTime, ok, err := imagetrust.VerifySignature("", []byte{}, "", "", []byte{}, "") + cloudImgTrustStore, err := imagetrust.NewAWSImageTrustStore("region", + "endpoint", + ) + So(err, ShouldBeNil) + + author, expTime, ok, err = cloudImgTrustStore.VerifySignature("cosign", + []byte(""), "", manifestDigest, manifestContent, repo, + ) So(author, ShouldBeEmpty) So(expTime, ShouldBeZeroValue) So(ok, ShouldBeFalse) diff --git a/pkg/extensions/imagetrust/image_trust_test.go b/pkg/extensions/imagetrust/image_trust_test.go index 4916f9ef..9b31f385 100644 --- a/pkg/extensions/imagetrust/image_trust_test.go +++ b/pkg/extensions/imagetrust/image_trust_test.go @@ -5,26 +5,35 @@ package imagetrust_test import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" + "io" + "net/http" "os" "path" "testing" "time" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + guuid "github.com/gofrs/uuid" "github.com/notaryproject/notation-go" + notreg "github.com/notaryproject/notation-go/registry" "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" + "gopkg.in/resty.v1" zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/api/constants" zcommon "zotregistry.io/zot/pkg/common" + extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/test" ) @@ -37,16 +46,17 @@ func TestInitCosignAndNotationDirs(t *testing.T) { err := os.Chmod(dir, 0o000) So(err, ShouldBeNil) - err = imagetrust.InitCosignAndNotationDirs(dir) + _, err = imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldNotBeNil) err = os.Chmod(dir, 0o500) So(err, ShouldBeNil) - err = imagetrust.InitCosignAndNotationDirs(dir) + _, err = imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldNotBeNil) - cosignDir, err := imagetrust.GetCosignDirPath() + pubKeyStorage := &imagetrust.PublicKeyLocalStorage{} + cosignDir, err := pubKeyStorage.GetCosignDirPath() So(cosignDir, ShouldBeEmpty) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) @@ -57,22 +67,23 @@ func TestInitCosignAndNotationDirs(t *testing.T) { err := os.Chmod(dir, 0o000) So(err, ShouldBeNil) - err = imagetrust.InitCosignAndNotationDirs(dir) + _, err = imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldNotBeNil) - err = imagetrust.InitNotationDir(dir) + _, err = imagetrust.NewCertificateLocalStorage(dir) So(err, ShouldNotBeNil) err = os.Chmod(dir, 0o500) So(err, ShouldBeNil) - err = imagetrust.InitCosignAndNotationDirs(dir) + _, err = imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldNotBeNil) - err = imagetrust.InitNotationDir(dir) + _, err = imagetrust.NewCertificateLocalStorage(dir) So(err, ShouldNotBeNil) - notationDir, err := imagetrust.GetNotationDirPath() + certStorage := &imagetrust.CertificateLocalStorage{} + notationDir, err := certStorage.GetNotationDirPath() So(notationDir, ShouldBeEmpty) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) @@ -94,7 +105,8 @@ func TestInitCosignAndNotationDirs(t *testing.T) { So(err, ShouldBeNil) So(certificateContent, ShouldNotBeNil) - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") + certStorgae := &imagetrust.CertificateLocalStorage{} + err = imagetrust.UploadCertificate(certStorgae, certificateContent, "ca") So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) }) @@ -118,7 +130,8 @@ func TestInitCosignAndNotationDirs(t *testing.T) { So(err, ShouldBeNil) So(publicKeyContent, ShouldNotBeNil) - err = imagetrust.UploadPublicKey(publicKeyContent) + pubKeyStorage := &imagetrust.PublicKeyLocalStorage{} + err = imagetrust.UploadPublicKey(pubKeyStorage, publicKeyContent) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) }) @@ -128,7 +141,8 @@ func TestVerifySignatures(t *testing.T) { Convey("wrong manifest content", t, func() { manifestContent := []byte("wrong json") - _, _, _, err := imagetrust.VerifySignature("", []byte(""), "", "", manifestContent, "repo") + imgTrustStore := &imagetrust.ImageTrustStore{} + _, _, _, err := imgTrustStore.VerifySignature("", []byte(""), "", "", manifestContent, "repo") So(err, ShouldNotBeNil) }) @@ -139,7 +153,8 @@ func TestVerifySignatures(t *testing.T) { manifestContent, err := json.Marshal(image.Manifest) So(err, ShouldBeNil) - _, _, _, err = imagetrust.VerifySignature("", []byte(""), "", "", manifestContent, "repo") + imgTrustStore := &imagetrust.ImageTrustStore{} + _, _, _, err = imgTrustStore.VerifySignature("", []byte(""), "", "", manifestContent, "repo") So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrBadManifestDigest) }) @@ -153,14 +168,15 @@ func TestVerifySignatures(t *testing.T) { manifestDigest := image.Digest() - _, _, _, err = imagetrust.VerifySignature("wrongType", []byte(""), "", manifestDigest, manifestContent, "repo") + imgTrustStore := &imagetrust.ImageTrustStore{} + _, _, _, err = imgTrustStore.VerifySignature("wrongType", []byte(""), "", manifestDigest, manifestContent, "repo") So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrInvalidSignatureType) }) Convey("verify cosign signature", t, func() { - repo := "repo" - tag := "test" + repo := "repo" //nolint:goconst + tag := "test" //nolint:goconst image, err := test.GetRandomImage() //nolint:staticcheck So(err, ShouldBeNil) @@ -170,7 +186,11 @@ func TestVerifySignatures(t *testing.T) { manifestDigest := image.Digest() Convey("cosignDir is not set", func() { - _, _, _, err = imagetrust.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + CosignStorage: &imagetrust.PublicKeyLocalStorage{}, + } + + _, _, _, err = imgTrustStore.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) }) @@ -178,31 +198,40 @@ func TestVerifySignatures(t *testing.T) { Convey("cosignDir does not have read permissions", func() { dir := t.TempDir() - err := imagetrust.InitCosignDir(dir) + pubKeyStorage, err := imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldBeNil) - cosignDir, err := imagetrust.GetCosignDirPath() + cosignDir, err := pubKeyStorage.GetCosignDirPath() So(err, ShouldBeNil) err = os.Chmod(cosignDir, 0o300) So(err, ShouldBeNil) - _, _, _, err = imagetrust.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + CosignStorage: pubKeyStorage, + } + + _, _, _, err = imgTrustStore.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) So(err, ShouldNotBeNil) }) Convey("no valid public key", func() { dir := t.TempDir() - err := imagetrust.InitCosignDir(dir) + pubKeyStorage, err := imagetrust.NewPublicKeyLocalStorage(dir) So(err, ShouldBeNil) - cosignDir, err := imagetrust.GetCosignDirPath() + cosignDir, err := pubKeyStorage.GetCosignDirPath() So(err, ShouldBeNil) err = test.WriteFileWithPermission(path.Join(cosignDir, "file"), []byte("not a public key"), 0o600, false) So(err, ShouldBeNil) - _, _, isTrusted, err := imagetrust.VerifySignature("cosign", []byte(""), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + CosignStorage: pubKeyStorage, + } + + _, _, isTrusted, err := imgTrustStore.VerifySignature("cosign", []byte(""), "", manifestDigest, + manifestContent, repo) So(err, ShouldBeNil) So(isTrusted, ShouldBeFalse) }) @@ -225,10 +254,10 @@ func TestVerifySignatures(t *testing.T) { err := test.UploadImage(image, baseURL, repo, tag) So(err, ShouldBeNil) - err = imagetrust.InitCosignDir(rootDir) + pubKeyStorage, err := imagetrust.NewPublicKeyLocalStorage(rootDir) So(err, ShouldBeNil) - cosignDir, err := imagetrust.GetCosignDirPath() + cosignDir, err := pubKeyStorage.GetCosignDirPath() So(err, ShouldBeNil) cwd, err := os.Getwd() @@ -284,8 +313,12 @@ func TestVerifySignatures(t *testing.T) { } } + imgTrustStore := &imagetrust.ImageTrustStore{ + CosignStorage: pubKeyStorage, + } + // signature is trusted - author, _, isTrusted, err := imagetrust.VerifySignature("cosign", rawSignature, sigKey, manifestDigest, + author, _, isTrusted, err := imgTrustStore.VerifySignature("cosign", rawSignature, sigKey, manifestDigest, manifestContent, repo) So(err, ShouldBeNil) So(isTrusted, ShouldBeTrue) @@ -294,8 +327,8 @@ func TestVerifySignatures(t *testing.T) { }) Convey("verify notation signature", t, func() { - repo := "repo" - tag := "test" + repo := "repo" //nolint:goconst + tag := "test" //nolint:goconst image, err := test.GetRandomImage() //nolint:staticcheck So(err, ShouldBeNil) @@ -305,7 +338,12 @@ func TestVerifySignatures(t *testing.T) { manifestDigest := image.Digest() Convey("notationDir is not set", func() { - _, _, _, err = imagetrust.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: &imagetrust.CertificateLocalStorage{}, + } + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) }) @@ -313,10 +351,15 @@ func TestVerifySignatures(t *testing.T) { Convey("no signature provided", func() { dir := t.TempDir() - err := imagetrust.InitNotationDir(dir) + certStorage, err := imagetrust.NewCertificateLocalStorage(dir) So(err, ShouldBeNil) - _, _, isTrusted, err := imagetrust.VerifySignature("notation", []byte(""), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: certStorage, + } + + _, _, isTrusted, err := imgTrustStore.VerifySignature("notation", []byte(""), "", manifestDigest, + manifestContent, repo) So(err, ShouldNotBeNil) So(isTrusted, ShouldBeFalse) }) @@ -324,32 +367,41 @@ func TestVerifySignatures(t *testing.T) { Convey("trustpolicy.json does not exist", func() { dir := t.TempDir() - err := imagetrust.InitNotationDir(dir) + certStorage, err := imagetrust.NewCertificateLocalStorage(dir) So(err, ShouldBeNil) - notationDir, _ := imagetrust.GetNotationDirPath() + notationDir, _ := certStorage.GetNotationDirPath() err = os.Remove(path.Join(notationDir, "trustpolicy.json")) So(err, ShouldBeNil) - _, _, _, err = imagetrust.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: certStorage, + } + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) So(err, ShouldNotBeNil) }) Convey("trustpolicy.json has invalid content", func() { dir := t.TempDir() - err := imagetrust.InitNotationDir(dir) + certStorage, err := imagetrust.NewCertificateLocalStorage(dir) So(err, ShouldBeNil) - notationDir, err := imagetrust.GetNotationDirPath() + notationDir, err := certStorage.GetNotationDirPath() So(err, ShouldBeNil) err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"), 0o600, true) So(err, ShouldBeNil) - _, _, _, err = imagetrust.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: certStorage, + } + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) So(err, ShouldNotBeNil) }) @@ -372,10 +424,10 @@ func TestVerifySignatures(t *testing.T) { err := test.UploadImage(image, baseURL, repo, tag) So(err, ShouldBeNil) - err = imagetrust.InitNotationDir(rootDir) + certStorage, err := imagetrust.NewCertificateLocalStorage(rootDir) So(err, ShouldBeNil) - notationDir, err := imagetrust.GetNotationDirPath() + notationDir, err := certStorage.GetNotationDirPath() So(err, ShouldBeNil) test.NotationPathLock.Lock() @@ -447,8 +499,12 @@ func TestVerifySignatures(t *testing.T) { } } + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: certStorage, + } + // signature is trusted - author, _, isTrusted, err := imagetrust.VerifySignature("notation", rawSignature, sigKey, manifestDigest, + author, _, isTrusted, err := imgTrustStore.VerifySignature("notation", rawSignature, sigKey, manifestDigest, manifestContent, repo) So(err, ShouldBeNil) So(isTrusted, ShouldBeTrue) @@ -458,7 +514,7 @@ func TestVerifySignatures(t *testing.T) { So(err, ShouldBeNil) // signature is not trusted - author, _, isTrusted, err = imagetrust.VerifySignature("notation", rawSignature, sigKey, manifestDigest, + author, _, isTrusted, err = imgTrustStore.VerifySignature("notation", rawSignature, sigKey, manifestDigest, manifestContent, repo) So(err, ShouldNotBeNil) So(isTrusted, ShouldBeFalse) @@ -492,101 +548,7 @@ func TestCheckExpiryErr(t *testing.T) { }) } -func TestUploadPublicKey(t *testing.T) { - Convey("public key - invalid content", t, func() { - err := imagetrust.UploadPublicKey([]byte("wrong content")) - So(err, ShouldNotBeNil) - }) - - Convey("upload public key successfully", t, func() { - rootDir := t.TempDir() - - cwd, err := os.Getwd() - So(err, ShouldBeNil) - - _ = os.Chdir(rootDir) - - // generate a keypair - os.Setenv("COSIGN_PASSWORD", "") - err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil) - So(err, ShouldBeNil) - - _ = os.Chdir(cwd) - - publicKeyContent, err := os.ReadFile(path.Join(rootDir, "cosign.pub")) - So(err, ShouldBeNil) - So(publicKeyContent, ShouldNotBeNil) - - err = imagetrust.InitCosignDir(rootDir) - So(err, ShouldBeNil) - - err = imagetrust.UploadPublicKey(publicKeyContent) - So(err, ShouldBeNil) - }) -} - -func TestUploadCertificate(t *testing.T) { - Convey("invalid truststore type", t, func() { - err := imagetrust.UploadCertificate([]byte("certificate content"), "wrongType", "store") - So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrInvalidTruststoreType) - }) - - Convey("invalid truststore name", t, func() { - err := imagetrust.UploadCertificate([]byte("certificate content"), "ca", "*store?") - So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrInvalidTruststoreName) - }) - - Convey("invalid certificate content", t, func() { - err := imagetrust.UploadCertificate([]byte("invalid content"), "ca", "store") - So(err, ShouldNotBeNil) - - content := `-----BEGIN CERTIFICATE----- ------END CERTIFICATE----- - ` - - err = imagetrust.UploadCertificate([]byte(content), "ca", "store") - So(err, ShouldNotBeNil) - - content = `` - - err = imagetrust.UploadCertificate([]byte(content), "ca", "store") - So(err, ShouldNotBeNil) - }) - - Convey("truststore dir can not be created", t, func() { - rootDir := t.TempDir() - - test.NotationPathLock.Lock() - defer test.NotationPathLock.Unlock() - - test.LoadNotationPath(rootDir) - - // generate a keypair - err := test.GenerateNotationCerts(rootDir, "notation-upload-test") - So(err, ShouldBeNil) - - certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "notation-upload-test.crt")) - So(err, ShouldBeNil) - So(certificateContent, ShouldNotBeNil) - - err = imagetrust.InitNotationDir(rootDir) - So(err, ShouldBeNil) - - notationDir, err := imagetrust.GetNotationDirPath() - So(err, ShouldBeNil) - - err = os.Chmod(notationDir, 0o100) - So(err, ShouldBeNil) - - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") - So(err, ShouldNotBeNil) - - err = os.Chmod(notationDir, 0o777) - So(err, ShouldBeNil) - }) - +func TestLocalTrustStoreUploadErr(t *testing.T) { Convey("certificate can't be stored", t, func() { rootDir := t.TempDir() @@ -603,110 +565,698 @@ func TestUploadCertificate(t *testing.T) { So(err, ShouldBeNil) So(certificateContent, ShouldNotBeNil) - err = imagetrust.InitNotationDir(rootDir) + certStorage, err := imagetrust.NewCertificateLocalStorage(rootDir) So(err, ShouldBeNil) - notationDir, err := imagetrust.GetNotationDirPath() + notationDir, err := certStorage.GetNotationDirPath() So(err, ShouldBeNil) - err = os.MkdirAll(path.Join(notationDir, "truststore/x509/ca/notation-upload-test"), 0o777) + err = os.Chmod(path.Join(notationDir, "truststore/x509/ca/default"), 0o100) So(err, ShouldBeNil) - err = os.Chmod(path.Join(notationDir, "truststore/x509/ca/notation-upload-test"), 0o100) + err = imagetrust.UploadCertificate(certStorage, certificateContent, "ca") + So(err, ShouldNotBeNil) + }) +} + +func TestLocalTrustStore(t *testing.T) { + Convey("NewLocalImageTrustStore error", t, func() { + rootDir := t.TempDir() + err := os.Chmod(rootDir, 0o000) So(err, ShouldBeNil) - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") + _, err = imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldNotBeNil) + + err = os.Chmod(rootDir, 0o700) + So(err, ShouldBeNil) + + notationDir := path.Join(rootDir, "_notation") + + err = os.MkdirAll(notationDir, 0o000) + So(err, ShouldBeNil) + + _, err = imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldNotBeNil) + + err = os.Chmod(notationDir, 0o700) + So(err, ShouldBeNil) + + err = os.MkdirAll(path.Join(notationDir, "truststore"), 0o500) + So(err, ShouldBeNil) + + _, err = imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldNotBeNil) + + err = os.Chmod(path.Join(notationDir, "truststore"), 0o700) + So(err, ShouldBeNil) + + err = os.MkdirAll(path.Join(notationDir, "truststore/x509/ca/default"), 0o700) + So(err, ShouldBeNil) + + err = os.Chmod(path.Join(notationDir, "truststore/x509/ca"), 0o000) + So(err, ShouldBeNil) + + _, err = imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldNotBeNil) + + err = os.Chmod(path.Join(notationDir, "truststore/x509/ca"), 0o700) + So(err, ShouldBeNil) + }) + + Convey("InitTrustpolicy error", t, func() { + notationStorage := &imagetrust.CertificateLocalStorage{} + err := notationStorage.InitTrustpolicy([]byte{}) So(err, ShouldNotBeNil) }) - Convey("trustpolicy - invalid content", t, func() { - rootDir := t.TempDir() - - test.NotationPathLock.Lock() - defer test.NotationPathLock.Unlock() - - test.LoadNotationPath(rootDir) - - // generate a keypair - err := test.GenerateNotationCerts(rootDir, "notation-upload-test") - So(err, ShouldBeNil) - - certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "notation-upload-test.crt")) - So(err, ShouldBeNil) - So(certificateContent, ShouldNotBeNil) - - err = imagetrust.InitNotationDir(rootDir) - So(err, ShouldBeNil) - - notationDir, err := imagetrust.GetNotationDirPath() - So(err, ShouldBeNil) - - err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"), - 0o600, true) - So(err, ShouldBeNil) - - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") + Convey("GetVerifier error", t, func() { + notationStorage := &imagetrust.CertificateLocalStorage{} + _, err := notationStorage.GetVerifier(&trustpolicy.Document{}) So(err, ShouldNotBeNil) }) - Convey("trustpolicy - truststore already exists", t, func() { + Convey("GetPublicKeyVerifier errors", t, func() { + cosignStorage := &imagetrust.PublicKeyLocalStorage{} + _, _, err := cosignStorage.GetPublicKeyVerifier("") + So(err, ShouldNotBeNil) + rootDir := t.TempDir() - test.NotationPathLock.Lock() - defer test.NotationPathLock.Unlock() + cosignStorage, err = imagetrust.NewPublicKeyLocalStorage(rootDir) + So(err, ShouldBeNil) - test.LoadNotationPath(rootDir) + _, _, err = cosignStorage.GetPublicKeyVerifier("inexistentfile") + So(err, ShouldNotBeNil) + }) + + Convey("test with local storage", t, func() { + rootDir := t.TempDir() + + imageTrustStore, err := imagetrust.NewLocalImageTrustStore(rootDir) + So(err, ShouldBeNil) + + var dbDriverParams map[string]interface{} + + RunUploadTests(t, *imageTrustStore) + RunVerificationTests(t, dbDriverParams) + }) +} + +func TestAWSTrustStore(t *testing.T) { + skipIt(t) + + Convey("NewAWSImageTrustStore error", t, func() { + _, err := imagetrust.NewAWSImageTrustStore("us-east-2", "wrong;endpoint") + So(err, ShouldNotBeNil) + }) + + Convey("GetCertificates errors", t, func() { + smanager, err := imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache := imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + notationStorage, err := imagetrust.NewCertificateAWSStorage(smanager, smCache) + So(err, ShouldBeNil) + + _, err = notationStorage.GetCertificates(context.Background(), "wrongType", "") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrInvalidTruststoreType) + + _, err = notationStorage.GetCertificates(context.Background(), "ca", "invalid;name") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrInvalidTruststoreName) + + name := "ca/test/digest" + description := "notation certificate" + content := "invalid certificate content" + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &name, + Description: &description, + SecretString: &content, + }) + So(err, ShouldBeNil) + + _, err = notationStorage.GetCertificates(context.Background(), "ca", "test") + So(err, ShouldNotBeNil) + + newName := "ca/newtest/digest" + newSecret := base64.StdEncoding.EncodeToString([]byte(content)) + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &newName, + Description: &description, + SecretString: &newSecret, + }) + So(err, ShouldBeNil) + + _, err = notationStorage.GetCertificates(context.Background(), "ca", "newtest") + So(err, ShouldNotBeNil) + }) + + Convey("GetPublicKeyVerifier errors", t, func() { + smanager, err := imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache := imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + cosignStorage := imagetrust.NewPublicKeyAWSStorage(smanager, smCache) + + _, _, err = cosignStorage.GetPublicKeyVerifier("badsecret") + So(err, ShouldNotBeNil) + + secretName := "digest" + description := "cosign public key" + secret := "invalid public key content" + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &secretName, + Description: &description, + SecretString: &secret, + }) + So(err, ShouldBeNil) + + _, _, err = cosignStorage.GetPublicKeyVerifier(secretName) + So(err, ShouldNotBeNil) + + secretName = "newdigest" + + newSecret := base64.StdEncoding.EncodeToString([]byte(secret)) + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &secretName, + Description: &description, + SecretString: &newSecret, + }) + So(err, ShouldBeNil) + + _, _, err = cosignStorage.GetPublicKeyVerifier(secretName) + So(err, ShouldNotBeNil) + }) + + Convey("VerifySignature - trustpolicy.json does not exist", t, func() { + repo := "repo" + image := test.CreateRandomImage() + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest := image.Digest() + + smanager, err := imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache := imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + notationStorage, err := imagetrust.NewCertificateAWSStorage(smanager, smCache) + So(err, ShouldBeNil) + + secretName := "trustpolicy" + force := true + + _, err = smanager.DeleteSecret(context.Background(), + &secretsmanager.DeleteSecretInput{ + SecretId: &secretName, + ForceDeleteWithoutRecovery: &force, + }) + So(err, ShouldBeNil) + + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: notationStorage, + } + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) + So(err, ShouldNotBeNil) + }) + + Convey("VerifySignature - trustpolicy.json has invalid content", t, func() { + repo := "repo" + image := test.CreateRandomImage() + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest := image.Digest() + + smanager, err := imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache := imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + notationStorage, err := imagetrust.NewCertificateAWSStorage(smanager, smCache) + So(err, ShouldBeNil) + + imgTrustStore := &imagetrust.ImageTrustStore{ + NotationStorage: notationStorage, + } + + secretName := "trustpolicy" + force := true + + _, err = smanager.DeleteSecret(context.Background(), + &secretsmanager.DeleteSecretInput{ + SecretId: &secretName, + ForceDeleteWithoutRecovery: &force, + }) + So(err, ShouldBeNil) + + description := "notation trustpolicy file" + secret := "invalid content" + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &secretName, + Description: &description, + SecretString: &secret, + }) + So(err, ShouldBeNil) + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) + So(err, ShouldNotBeNil) + + smanager, err = imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache = imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + notationStorage, err = imagetrust.NewCertificateAWSStorage(smanager, smCache) + So(err, ShouldBeNil) + + imgTrustStore = &imagetrust.ImageTrustStore{ + NotationStorage: notationStorage, + } + + _, err = smanager.DeleteSecret(context.Background(), + &secretsmanager.DeleteSecretInput{ + SecretId: &secretName, + ForceDeleteWithoutRecovery: &force, + }) + So(err, ShouldBeNil) + + newSecret := base64.StdEncoding.EncodeToString([]byte(secret)) + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &secretName, + Description: &description, + SecretString: &newSecret, + }) + So(err, ShouldBeNil) + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) + So(err, ShouldNotBeNil) + + smanager, err = imagetrust.GetSecretsManagerClient("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + So(err, ShouldBeNil) + + smCache = imagetrust.GetSecretsManagerRetrieval("us-east-2", os.Getenv("DYNAMODBMOCK_ENDPOINT")) + + notationStorage, err = imagetrust.NewCertificateAWSStorage(smanager, smCache) + So(err, ShouldBeNil) + + imgTrustStore = &imagetrust.ImageTrustStore{ + NotationStorage: notationStorage, + } + + _, err = smanager.DeleteSecret(context.Background(), + &secretsmanager.DeleteSecretInput{ + SecretId: &secretName, + ForceDeleteWithoutRecovery: &force, + }) + So(err, ShouldBeNil) + + newSecret = base64.StdEncoding.EncodeToString([]byte(`{"Version": {"bad": "input"}}`)) + + _, err = smanager.CreateSecret(context.Background(), + &secretsmanager.CreateSecretInput{ + Name: &secretName, + Description: &description, + SecretString: &newSecret, + }) + So(err, ShouldBeNil) + + _, _, _, err = imgTrustStore.VerifySignature("notation", []byte("signature"), "", manifestDigest, + manifestContent, repo) + So(err, ShouldNotBeNil) + }) + + Convey("test with AWS storage", t, func() { + uuid, err := guuid.NewV4() + if err != nil { + panic(err) + } + + repoMetaTablename := "RepoMetadataTable" + uuid.String() + manifestDataTablename := "ManifestDataTable" + uuid.String() + versionTablename := "Version" + uuid.String() + indexDataTablename := "IndexDataTable" + uuid.String() + userDataTablename := "UserDataTable" + uuid.String() + apiKeyTablename := "ApiKeyTable" + uuid.String() + + dynamoDBDriverParams := map[string]interface{}{ + "name": "dynamoDB", + "endpoint": os.Getenv("DYNAMODBMOCK_ENDPOINT"), + "region": "us-east-2", + "repometatablename": repoMetaTablename, + "manifestdatatablename": manifestDataTablename, + "indexdatatablename": indexDataTablename, + "userdatatablename": userDataTablename, + "apikeytablename": apiKeyTablename, + "versiontablename": versionTablename, + } + + t.Logf("using dynamo driver options: %v", dynamoDBDriverParams) + + imageTrustStore, err := imagetrust.NewAWSImageTrustStore( + "us-east-2", + os.Getenv("DYNAMODBMOCK_ENDPOINT"), + ) + So(err, ShouldBeNil) + + RunUploadTests(t, *imageTrustStore) + RunVerificationTests(t, dynamoDBDriverParams) + }) +} + +func RunUploadTests(t *testing.T, imageTrustStore imagetrust.ImageTrustStore) { //nolint: thelper + cosignStorage := imageTrustStore.CosignStorage + notationStorage := imageTrustStore.NotationStorage + + Convey("public key - invalid content", func() { + err := imagetrust.UploadPublicKey(cosignStorage, []byte("wrong content")) + So(err, ShouldNotBeNil) + }) + + Convey("upload public key successfully", func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + + keyDir := t.TempDir() + _ = os.Chdir(keyDir) // generate a keypair - err := test.GenerateNotationCerts(rootDir, "notation-upload-test") + os.Setenv("COSIGN_PASSWORD", "") + err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil) So(err, ShouldBeNil) - certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "notation-upload-test.crt")) + _ = os.Chdir(cwd) + + publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub")) So(err, ShouldBeNil) - So(certificateContent, ShouldNotBeNil) + So(publicKeyContent, ShouldNotBeNil) - err = imagetrust.InitNotationDir(rootDir) - So(err, ShouldBeNil) - - notationDir, err := imagetrust.GetNotationDirPath() - So(err, ShouldBeNil) - - trustpolicyDoc, err := imagetrust.LoadTrustPolicyDocument(notationDir) - So(err, ShouldBeNil) - - trustpolicyDoc.TrustPolicies[0].TrustStores = append(trustpolicyDoc.TrustPolicies[0].TrustStores, - "ca:notation-upload-test") - - trustpolicyDocContent, err := json.Marshal(trustpolicyDoc) - So(err, ShouldBeNil) - - err = os.WriteFile(path.Join(notationDir, "trustpolicy.json"), trustpolicyDocContent, 0o400) - So(err, ShouldBeNil) - - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") + err = imagetrust.UploadPublicKey(cosignStorage, publicKeyContent) So(err, ShouldBeNil) }) - Convey("upload certificate successfully", t, func() { - rootDir := t.TempDir() + Convey("invalid truststore type", func() { + err := imagetrust.UploadCertificate(notationStorage, + []byte("certificate content"), "wrongType", + ) + So(err, ShouldNotBeNil) + So(err, ShouldEqual, zerr.ErrInvalidTruststoreType) + }) + + Convey("invalid certificate content", func() { + content := "invalid certificate content" + + err := imagetrust.UploadCertificate(notationStorage, + []byte(content), "ca", + ) + So(err, ShouldNotBeNil) + + content = `-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + ` + + err = imagetrust.UploadCertificate(notationStorage, + []byte(content), "ca", + ) + So(err, ShouldNotBeNil) + + content = `` + + err = imagetrust.UploadCertificate(notationStorage, + []byte(content), "ca", + ) + So(err, ShouldNotBeNil) + }) + + Convey("upload certificate successfully", func() { + certDir := t.TempDir() test.NotationPathLock.Lock() defer test.NotationPathLock.Unlock() - test.LoadNotationPath(rootDir) + test.LoadNotationPath(certDir) // generate a keypair - err := test.GenerateNotationCerts(rootDir, "notation-upload-test") + err := test.GenerateNotationCerts(certDir, "notation-upload-test") So(err, ShouldBeNil) - certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "notation-upload-test.crt")) + certificateContent, err := os.ReadFile(path.Join(certDir, "notation/localkeys", "notation-upload-test.crt")) So(err, ShouldBeNil) So(certificateContent, ShouldNotBeNil) - err = imagetrust.InitNotationDir(rootDir) - So(err, ShouldBeNil) - - err = imagetrust.UploadCertificate(certificateContent, "ca", "notation-upload-test") + err = imagetrust.UploadCertificate(notationStorage, certificateContent, "ca") So(err, ShouldBeNil) }) } + +func RunVerificationTests(t *testing.T, dbDriverParams map[string]interface{}) { //nolint: thelper + Convey("verify signatures are trusted", func() { + defaultValue := true + rootDir := t.TempDir() + logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") + So(err, ShouldBeNil) + logPath := logFile.Name() + defer os.Remove(logPath) + + writers := io.MultiWriter(os.Stdout, logFile) + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + + if dbDriverParams != nil { + conf.Storage.RemoteCache = true + + conf.Storage.CacheDriver = dbDriverParams + } + conf.Extensions = &extconf.ExtensionConfig{} + conf.Extensions.Trust = &extconf.ImageTrustConfig{} + conf.Extensions.Trust.Enable = &defaultValue + conf.Extensions.Trust.Cosign = defaultValue + conf.Extensions.Trust.Notation = defaultValue + + ctlr := api.NewController(conf) + ctlr.Log.Logger = ctlr.Log.Output(writers) + ctlr.Config.Storage.RootDirectory = rootDir + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + repo := "repo" //nolint:goconst + tag := "test" //nolint:goconst + + Convey("verify cosign signature is trusted", func() { + image, err := test.GetRandomImage() //nolint:staticcheck + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest := image.Digest() + + err = test.UploadImage(image, baseURL, repo, tag) + So(err, ShouldBeNil) + + cwd, err := os.Getwd() + So(err, ShouldBeNil) + + keyDir := t.TempDir() + _ = os.Chdir(keyDir) + + // 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(keyDir, "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) + + 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[zcommon.CosignSigKey] + + rawSignature, err = ctlr.StoreController.DefaultStore.GetBlobContent(repo, cosignSig.Layers[0].Digest) + So(err, ShouldBeNil) + } + } + + publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub")) + So(err, ShouldBeNil) + So(publicKeyContent, ShouldNotBeNil) + + // upload the public key + client := resty.New() + resp, err := client.R().SetHeader("Content-type", "application/octet-stream"). + SetBody(publicKeyContent).Post(baseURL + constants.FullCosign) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + imageTrustStore := ctlr.MetaDB.ImageTrustStore() + + // signature is trusted + author, _, isTrusted, err := imageTrustStore.VerifySignature("cosign", rawSignature, sigKey, manifestDigest, + manifestContent, repo) + So(err, ShouldBeNil) + So(isTrusted, ShouldBeTrue) + So(author, ShouldNotBeEmpty) + }) + + Convey("verify notation signature is trusted", func() { + image, err := test.GetRandomImage() //nolint:staticcheck + So(err, ShouldBeNil) + + manifestContent, err := json.Marshal(image.Manifest) + So(err, ShouldBeNil) + + manifestDigest := image.Digest() + + err = test.UploadImage(image, baseURL, repo, tag) + So(err, ShouldBeNil) + + notationDir := t.TempDir() + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(notationDir) + + uuid, err := guuid.NewV4() + So(err, ShouldBeNil) + + certName := fmt.Sprintf("notation-sign-test-%s", uuid) + + // generate a keypair + err = test.GenerateNotationCerts(notationDir, certName) + So(err, ShouldBeNil) + + // sign the image + imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) + + err = test.SignWithNotation(certName, imageURL, notationDir) + 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 { + blobContent, err := ctlr.StoreController.DefaultStore.GetBlobContent(repo, manifest.Digest) + So(err, ShouldBeNil) + + var notationSig ispec.Manifest + + err = json.Unmarshal(blobContent, ¬ationSig) + So(err, ShouldBeNil) + + t.Logf("Processing manifest %v", notationSig) + if notationSig.Config.MediaType != notreg.ArtifactTypeNotation || + notationSig.Subject.Digest != manifestDigest { + continue + } + + sigKey = notationSig.Layers[0].MediaType + + rawSignature, err = ctlr.StoreController.DefaultStore.GetBlobContent(repo, notationSig.Layers[0].Digest) + So(err, ShouldBeNil) + + t.Logf("Identified notation signature manifest %v", notationSig) + + break + } + + So(sigKey, ShouldNotBeEmpty) + + certificateContent, err := os.ReadFile( + path.Join(notationDir, + fmt.Sprintf("notation/truststore/x509/ca/%s", certName), + fmt.Sprintf("%s.crt", certName), + ), + ) + So(err, ShouldBeNil) + So(certificateContent, ShouldNotBeNil) + + client := resty.New() + resp, err := client.R().SetHeader("Content-type", "application/octet-stream"). + SetBody(certificateContent).Post(baseURL + constants.FullNotation) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + imageTrustStore := ctlr.MetaDB.ImageTrustStore() + + // signature is trusted + author, _, isTrusted, err := imageTrustStore.VerifySignature("notation", rawSignature, sigKey, manifestDigest, + manifestContent, repo) + So(err, ShouldBeNil) + So(isTrusted, ShouldBeTrue) + So(author, ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US") + }) + }) +} + +func skipIt(t *testing.T) { + t.Helper() + + if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" { + t.Skip("Skipping testing without AWS mock server") + } +} diff --git a/pkg/extensions/imagetrust/notation.go b/pkg/extensions/imagetrust/notation.go index 113fb18c..c3d0d821 100644 --- a/pkg/extensions/imagetrust/notation.go +++ b/pkg/extensions/imagetrust/notation.go @@ -4,8 +4,10 @@ package imagetrust import ( + "bytes" "context" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "errors" @@ -14,9 +16,12 @@ import ( "path" "path/filepath" "regexp" - "sync" + "strings" "time" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/aws/aws-secretsmanager-caching-go/secretcache" _ "github.com/notaryproject/notation-core-go/signature/jws" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/dir" @@ -30,36 +35,94 @@ import ( zerr "zotregistry.io/zot/errors" ) -const notationDirRelativePath = "_notation" - -var ( - notationDir = "" //nolint:gochecknoglobals - TrustpolicyLock = new(sync.Mutex) //nolint: gochecknoglobals +const ( + notationDirRelativePath = "_notation" + truststoreName = "default" ) -func InitNotationDir(rootDir string) error { +type CertificateLocalStorage struct { + notationDir string +} + +type CertificateAWSStorage struct { + secretsManagerClient *secretsmanager.Client + secretsManagerCache *secretcache.Cache +} + +type certificateStorage interface { + LoadTrustPolicyDocument() (*trustpolicy.Document, error) + StoreCertificate(certificateContent []byte, truststoreType string) error + GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) + InitTrustpolicy(trustpolicy []byte) error +} + +func NewCertificateLocalStorage(rootDir string) (*CertificateLocalStorage, error) { dir := path.Join(rootDir, notationDirRelativePath) _, err := os.Stat(dir) if os.IsNotExist(err) { err = os.MkdirAll(dir, defaultDirPerms) if err != nil { - return err + return nil, err } } - if err == nil { - notationDir = dir + if err != nil { + return nil, err + } - if _, err := LoadTrustPolicyDocument(notationDir); os.IsNotExist(err) { - return InitTrustpolicyFile(notationDir) + certStorage := &CertificateLocalStorage{ + notationDir: dir, + } + + if err := InitTrustpolicyFile(certStorage); err != nil { + return nil, err + } + + for _, truststoreType := range truststore.Types { + defaultTruststore := path.Join(dir, "truststore", "x509", string(truststoreType), truststoreName) + + _, err = os.Stat(defaultTruststore) + if os.IsNotExist(err) { + err = os.MkdirAll(defaultTruststore, defaultDirPerms) + if err != nil { + return nil, err + } + } + + if err != nil { + return nil, err } } - return err + return certStorage, nil } -func InitTrustpolicyFile(configDir string) error { +func NewCertificateAWSStorage( + secretsManagerClient *secretsmanager.Client, secretsManagerCache *secretcache.Cache, +) (*CertificateAWSStorage, error) { + certStorage := &CertificateAWSStorage{ + secretsManagerClient: secretsManagerClient, + secretsManagerCache: secretsManagerCache, + } + + err := InitTrustpolicyFile(certStorage) + if err != nil { + return nil, err + } + + return certStorage, nil +} + +func InitTrustpolicyFile(notationStorage certificateStorage) error { + truststores := []string{} + + for _, truststoreType := range truststore.Types { + truststores = append(truststores, fmt.Sprintf("\"%s:%s\"", string(truststoreType), truststoreName)) + } + + defaultTruststores := strings.Join(truststores, ",") + // according to https://github.com/notaryproject/notation/blob/main/specs/commandline/verify.md // the value of signatureVerification.level field from trustpolicy.json file // could be one of these values: `strict`, `permissive`, `audit` or `skip` @@ -68,40 +131,138 @@ func InitTrustpolicyFile(configDir string) error { // a certificate that verifies a signature, but that certificate has expired, then the // signature is not trusted; if this field were set to `permissive` then the // signature would be trusted) - trustPolicy := ` - { - "version": "1.0", - "trustPolicies": [ - { - "name": "default-config", - "registryScopes": [ "*" ], - "signatureVerification": { - "level" : "strict" - }, - "trustStores": [], - "trustedIdentities": [ - "*" - ] - } - ] - }` + trustPolicy := `{ + "version": "1.0", + "trustPolicies": [ + { + "name": "default-config", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [` + defaultTruststores + `], + "trustedIdentities": [ + "*" + ] + } + ] +}` - TrustpolicyLock.Lock() - defer TrustpolicyLock.Unlock() - - return os.WriteFile(path.Join(configDir, dir.PathTrustPolicy), []byte(trustPolicy), defaultDirPerms) + return notationStorage.InitTrustpolicy([]byte(trustPolicy)) } -func GetNotationDirPath() (string, error) { - if notationDir != "" { - return notationDir, nil +func (local *CertificateLocalStorage) InitTrustpolicy(trustpolicy []byte) error { + notationDir, err := local.GetNotationDirPath() + if err != nil { + return err + } + + return os.WriteFile(path.Join(notationDir, dir.PathTrustPolicy), trustpolicy, defaultDirPerms) +} + +func (cloud *CertificateAWSStorage) InitTrustpolicy(trustpolicy []byte) error { + name := "trustpolicy" + description := "notation trustpolicy file" + secret := base64.StdEncoding.EncodeToString(trustpolicy) + secretInputParam := &secretsmanager.CreateSecretInput{ + Name: &name, + Description: &description, + SecretString: &secret, + } + + _, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) + if err != nil && strings.Contains(err.Error(), "the secret trustpolicy already exists.") { + force := true + + deleteSecretParam := &secretsmanager.DeleteSecretInput{ + SecretId: &name, + ForceDeleteWithoutRecovery: &force, + } + + _, err = cloud.secretsManagerClient.DeleteSecret(context.Background(), deleteSecretParam) + + if err != nil { + return err + } + + _, err = cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) + + return err + } + + return err +} + +func (local *CertificateLocalStorage) GetNotationDirPath() (string, error) { + if local.notationDir != "" { + return local.notationDir, nil } return "", zerr.ErrSignConfigDirNotSet } +func (cloud *CertificateAWSStorage) GetCertificates( + ctx context.Context, storeType truststore.Type, namedStore string, +) ([]*x509.Certificate, error) { + certificates := []*x509.Certificate{} + + if !validateTruststoreType(string(storeType)) { + return []*x509.Certificate{}, zerr.ErrInvalidTruststoreType + } + + if !validateTruststoreName(namedStore) { + return []*x509.Certificate{}, zerr.ErrInvalidTruststoreName + } + + listSecretsInput := secretsmanager.ListSecretsInput{ + Filters: []types.Filter{ + { + Key: types.FilterNameStringTypeName, + Values: []string{path.Join(string(storeType), namedStore)}, + }, + }, + } + + secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput) + if err != nil { + return []*x509.Certificate{}, err + } + + for _, secret := range secrets.SecretList { + // get key + raw, err := cloud.secretsManagerCache.GetSecretString(*(secret.Name)) + if err != nil { + return []*x509.Certificate{}, err + } + + rawDecoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return []*x509.Certificate{}, err + } + + certs, _, err := parseAndValidateCertificateContent(rawDecoded) + if err != nil { + return []*x509.Certificate{}, err + } + + err = truststore.ValidateCertificates(certs) + if err != nil { + return []*x509.Certificate{}, err + } + + certificates = append(certificates, certs...) + } + + return certificates, nil +} + // Equivalent function for trustpolicy.LoadDocument() but using a specific SysFS not the one returned by ConfigFS(). -func LoadTrustPolicyDocument(notationDir string) (*trustpolicy.Document, error) { +func (local *CertificateLocalStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) { + notationDir, err := local.GetNotationDirPath() + if err != nil { + return nil, err + } + jsonFile, err := dir.NewSysFS(notationDir).Open(dir.PathTrustPolicy) if err != nil { return nil, err @@ -119,33 +280,66 @@ func LoadTrustPolicyDocument(notationDir string) (*trustpolicy.Document, error) return policyDocument, nil } +func (cloud *CertificateAWSStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) { + policyDocument := &trustpolicy.Document{} + + raw, err := cloud.secretsManagerCache.GetSecretString("trustpolicy") + if err != nil { + return nil, err + } + + rawDecoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + + err = json.Compact(&buf, rawDecoded) + if err != nil { + return nil, err + } + + err = json.Unmarshal(buf.Bytes(), 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 - } - +func NewFromConfig(notationStorage certificateStorage) (notation.Verifier, error) { // Load trust policy. - TrustpolicyLock.Lock() - defer TrustpolicyLock.Unlock() - - policyDocument, err := LoadTrustPolicyDocument(notationDir) + policyDocument, err := notationStorage.LoadTrustPolicyDocument() if err != nil { return nil, err } - // Load trust store. + return notationStorage.GetVerifier(policyDocument) +} + +func (local *CertificateLocalStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) { + notationDir, err := local.GetNotationDirPath() + if err != nil { + return nil, err + } x509TrustStore := truststore.NewX509TrustStore(dir.NewSysFS(notationDir)) - return verifier.New(policyDocument, x509TrustStore, + return verifier.New(policyDoc, x509TrustStore, plugin.NewCLIManager(dir.NewSysFS(path.Join(notationDir, dir.PathPlugins)))) } +func (cloud *CertificateAWSStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) { + return verifier.New(policyDoc, cloud, + plugin.NewCLIManager(dir.NewSysFS(path.Join(dir.PathPlugins)))) +} + func VerifyNotationSignature( - artifactDescriptor ispec.Descriptor, artifactReference string, rawSignature []byte, signatureMediaType string, + notationStorage certificateStorage, artifactDescriptor ispec.Descriptor, artifactReference string, + rawSignature []byte, signatureMediaType string, ) (string, time.Time, bool, error) { var ( date time.Time @@ -161,7 +355,7 @@ func VerifyNotationSignature( } // Initialize verifier. - verifier, err := NewFromConfig() + verifier, err := NewFromConfig(notationStorage) if err != nil { return author, date, false, err } @@ -219,24 +413,30 @@ func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter t return false } -func UploadCertificate(certificateContent []byte, truststoreType, truststoreName string) error { +func UploadCertificate( + notationStorage certificateStorage, certificateContent []byte, truststoreType string, +) error { // validate truststore type if !validateTruststoreType(truststoreType) { return zerr.ErrInvalidTruststoreType } - // validate truststore name - if !validateTruststoreName(truststoreName) { - return zerr.ErrInvalidTruststoreName - } - // validate certificate - if ok, err := validateCertificate(certificateContent); !ok { + if _, ok, err := parseAndValidateCertificateContent(certificateContent); !ok { return err } - // add certificate to "{rootDir}/_notation/truststore/x509/{type}/{name}/{name.crt}" - configDir, err := GetNotationDirPath() + // store certificate + err := notationStorage.StoreCertificate(certificateContent, truststoreType) + + return err +} + +func (local *CertificateLocalStorage) StoreCertificate( + certificateContent []byte, truststoreType string, +) error { + // add certificate to "{rootDir}/_notation/truststore/x509/{type}/default/{name.crt}" + configDir, err := local.GetNotationDirPath() if err != nil { return err } @@ -250,36 +450,24 @@ func UploadCertificate(certificateContent []byte, truststoreType, truststoreName return err } - err = os.WriteFile(truststorePath, certificateContent, defaultFilePerms) - if err != nil { - return err + return os.WriteFile(truststorePath, certificateContent, defaultFilePerms) +} + +func (cloud *CertificateAWSStorage) StoreCertificate( + certificateContent []byte, truststoreType string, +) error { + name := path.Join(truststoreType, truststoreName, godigest.FromBytes(certificateContent).Encoded()) + description := "notation certificate" + secret := base64.StdEncoding.EncodeToString(certificateContent) + secretInputParam := &secretsmanager.CreateSecretInput{ + Name: &name, + Description: &description, + SecretString: &secret, } - // add certificate to "trustpolicy.json" - TrustpolicyLock.Lock() - defer TrustpolicyLock.Unlock() + _, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) - trustpolicyDoc, err := LoadTrustPolicyDocument(configDir) - if err != nil { - return err - } - - truststoreToAppend := fmt.Sprintf("%s:%s", truststoreType, truststoreName) - - for _, t := range trustpolicyDoc.TrustPolicies[0].TrustStores { - if t == truststoreToAppend { - return nil - } - } - - trustpolicyDoc.TrustPolicies[0].TrustStores = append(trustpolicyDoc.TrustPolicies[0].TrustStores, truststoreToAppend) - - trustpolicyDocContent, err := json.Marshal(trustpolicyDoc) - if err != nil { - return err - } - - return os.WriteFile(path.Join(configDir, dir.PathTrustPolicy), trustpolicyDocContent, defaultFilePerms) + return err } func validateTruststoreType(truststoreType string) bool { @@ -293,11 +481,15 @@ func validateTruststoreType(truststoreType string) bool { } func validateTruststoreName(truststoreName string) bool { + if strings.Contains(truststoreName, "..") { + return false + } + return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(truststoreName) } // implementation from https://github.com/notaryproject/notation-core-go/blob/main/x509/cert.go#L20 -func validateCertificate(certificateContent []byte) (bool, error) { +func parseAndValidateCertificateContent(certificateContent []byte) ([]*x509.Certificate, bool, error) { var certs []*x509.Certificate block, rest := pem.Decode(certificateContent) @@ -305,7 +497,7 @@ func validateCertificate(certificateContent []byte) (bool, error) { // data may be in DER format derCerts, err := x509.ParseCertificates(certificateContent) if err != nil { - return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) + return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) } certs = append(certs, derCerts...) @@ -314,7 +506,7 @@ func validateCertificate(certificateContent []byte) (bool, error) { for block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) + return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) } certs = append(certs, cert) block, rest = pem.Decode(rest) @@ -322,9 +514,9 @@ func validateCertificate(certificateContent []byte) (bool, error) { } if len(certs) == 0 { - return false, fmt.Errorf("%w: no valid certificates found in payload", + return []*x509.Certificate{}, false, fmt.Errorf("%w: no valid certificates found in payload", zerr.ErrInvalidCertificateContent) } - return true, nil + return certs, true, nil } diff --git a/pkg/meta/boltdb/boltdb.go b/pkg/meta/boltdb/boltdb.go index 83b1043c..1b23c0aa 100644 --- a/pkg/meta/boltdb/boltdb.go +++ b/pkg/meta/boltdb/boltdb.go @@ -15,7 +15,6 @@ import ( zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" zcommon "zotregistry.io/zot/pkg/common" - "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/common" mTypes "zotregistry.io/zot/pkg/meta/types" @@ -24,9 +23,10 @@ import ( ) type BoltDB struct { - DB *bbolt.DB - Patches []func(DB *bbolt.DB) error - Log log.Logger + DB *bbolt.DB + Patches []func(DB *bbolt.DB) error + imgTrustStore mTypes.ImageTrustStore + Log log.Logger } func New(boltDB *bbolt.DB, log log.Logger) (*BoltDB, error) { @@ -73,12 +73,21 @@ func New(boltDB *bbolt.DB, log log.Logger) (*BoltDB, error) { } return &BoltDB{ - DB: boltDB, - Patches: version.GetBoltDBPatches(), - Log: log, + DB: boltDB, + Patches: version.GetBoltDBPatches(), + imgTrustStore: nil, + Log: log, }, nil } +func (bdw *BoltDB) ImageTrustStore() mTypes.ImageTrustStore { + return bdw.imgTrustStore +} + +func (bdw *BoltDB) SetImageTrustStore(imgTrustStore mTypes.ImageTrustStore) { + bdw.imgTrustStore = imgTrustStore +} + func (bdw *BoltDB) SetManifestData(manifestDigest godigest.Digest, manifestData mTypes.ManifestData) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(ManifestDataBucket)) @@ -722,6 +731,12 @@ func (bdw *BoltDB) IncrementImageDownloads(repo string, reference string) error func (bdw *BoltDB) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { err := bdw.DB.Update(func(transaction *bbolt.Tx) error { + imgTrustStore := bdw.ImageTrustStore() + + if imgTrustStore == nil { + return nil + } + // get ManifestData of signed manifest manifestBuck := transaction.Bucket([]byte(ManifestDataBucket)) mdBlob := manifestBuck.Get([]byte(manifestDigest)) @@ -779,8 +794,8 @@ func (bdw *BoltDB) UpdateSignaturesValidity(repo string, manifestDigest godigest layersInfo := []mTypes.LayerInfo{} for _, layerInfo := range sigInfo.LayersInfo { - author, date, isTrusted, _ := imagetrust.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey, - manifestDigest, blob, repo) + author, date, isTrusted, _ := imgTrustStore.VerifySignature(sigType, layerInfo.LayerContent, + layerInfo.SignatureKey, manifestDigest, blob, repo) if isTrusted { layerInfo.Signer = author diff --git a/pkg/meta/boltdb/boltdb_test.go b/pkg/meta/boltdb/boltdb_test.go index d0125b09..c624d95a 100644 --- a/pkg/meta/boltdb/boltdb_test.go +++ b/pkg/meta/boltdb/boltdb_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "math" "testing" + "time" "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -22,6 +23,15 @@ import ( "zotregistry.io/zot/pkg/test" ) +type imgTrustStore struct{} + +func (its imgTrustStore) VerifySignature( + signatureType string, rawSignature []byte, sigKey string, manifestDigest digest.Digest, manifestContent []byte, + repo string, +) (string, time.Time, bool, error) { + return "", time.Time{}, false, nil +} + func TestWrapperErrors(t *testing.T) { Convey("Errors", t, func() { tmpDir := t.TempDir() @@ -35,6 +45,8 @@ func TestWrapperErrors(t *testing.T) { So(boltdbWrapper, ShouldNotBeNil) So(err, ShouldBeNil) + boltdbWrapper.SetImageTrustStore(imgTrustStore{}) + repoMeta := mTypes.RepoMetadata{ Tags: map[string]mTypes.Descriptor{}, Signatures: map[string]mTypes.ManifestSignatures{}, diff --git a/pkg/meta/dynamodb/dynamodb.go b/pkg/meta/dynamodb/dynamodb.go index 8dac9eb8..48d2c5ae 100644 --- a/pkg/meta/dynamodb/dynamodb.go +++ b/pkg/meta/dynamodb/dynamodb.go @@ -18,7 +18,6 @@ import ( zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" zcommon "zotregistry.io/zot/pkg/common" - "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/common" mTypes "zotregistry.io/zot/pkg/meta/types" @@ -37,10 +36,13 @@ type DynamoDB struct { UserDataTablename string VersionTablename string Patches []func(client *dynamodb.Client, tableNames map[string]string) error + imgTrustStore mTypes.ImageTrustStore Log log.Logger } -func New(client *dynamodb.Client, params DBDriverParameters, log log.Logger) (*DynamoDB, error) { +func New( + client *dynamodb.Client, params DBDriverParameters, log log.Logger, +) (*DynamoDB, error) { dynamoWrapper := DynamoDB{ Client: client, RepoMetaTablename: params.RepoMetaTablename, @@ -50,6 +52,7 @@ func New(client *dynamodb.Client, params DBDriverParameters, log log.Logger) (*D UserDataTablename: params.UserDataTablename, APIKeyTablename: params.APIKeyTablename, Patches: version.GetDynamoDBPatches(), + imgTrustStore: nil, Log: log, } @@ -87,6 +90,14 @@ func New(client *dynamodb.Client, params DBDriverParameters, log log.Logger) (*D return &dynamoWrapper, nil } +func (dwr *DynamoDB) ImageTrustStore() mTypes.ImageTrustStore { + return dwr.imgTrustStore +} + +func (dwr *DynamoDB) SetImageTrustStore(imgTrustStore mTypes.ImageTrustStore) { + dwr.imgTrustStore = imgTrustStore +} + func (dwr *DynamoDB) SetManifestData(manifestDigest godigest.Digest, manifestData mTypes.ManifestData) error { mdAttributeValue, err := attributevalue.Marshal(manifestData) if err != nil { @@ -625,6 +636,12 @@ func (dwr *DynamoDB) IncrementImageDownloads(repo string, reference string) erro } func (dwr *DynamoDB) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { + imgTrustStore := dwr.ImageTrustStore() + + if imgTrustStore == nil { + return nil + } + // get ManifestData of signed manifest var blob []byte @@ -659,7 +676,7 @@ func (dwr *DynamoDB) UpdateSignaturesValidity(repo string, manifestDigest godige layersInfo := []mTypes.LayerInfo{} for _, layerInfo := range sigInfo.LayersInfo { - author, date, isTrusted, _ := imagetrust.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey, + author, date, isTrusted, _ := imgTrustStore.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey, manifestDigest, blob, repo) if isTrusted { diff --git a/pkg/meta/dynamodb/dynamodb_test.go b/pkg/meta/dynamodb/dynamodb_test.go index e547ca3e..2fc39867 100644 --- a/pkg/meta/dynamodb/dynamodb_test.go +++ b/pkg/meta/dynamodb/dynamodb_test.go @@ -17,6 +17,7 @@ import ( "github.com/rs/zerolog" . "github.com/smartystreets/goconvey/convey" + "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/log" mdynamodb "zotregistry.io/zot/pkg/meta/dynamodb" mTypes "zotregistry.io/zot/pkg/meta/types" @@ -164,9 +165,14 @@ func TestWrapperErrors(t *testing.T) { client, err := mdynamodb.GetDynamoClient(params) //nolint:contextcheck So(err, ShouldBeNil) + imgTrustStore, err := imagetrust.NewAWSImageTrustStore(params.Region, params.Endpoint) + So(err, ShouldBeNil) + dynamoWrapper, err := mdynamodb.New(client, params, log) //nolint:contextcheck So(err, ShouldBeNil) + dynamoWrapper.SetImageTrustStore(imgTrustStore) + So(dynamoWrapper.ResetManifestDataTable(), ShouldBeNil) //nolint:contextcheck So(dynamoWrapper.ResetRepoMetaTable(), ShouldBeNil) //nolint:contextcheck @@ -697,6 +703,9 @@ func TestWrapperErrors(t *testing.T) { err = dynamoWrapper.UpdateSignaturesValidity("repo", "dig") So(err, ShouldNotBeNil) + + err = dynamoWrapper.UpdateSignaturesValidity("repo", digest.FromString("dig")) + So(err, ShouldBeNil) }) Convey("UpdateSignaturesValidity GetRepoMeta error", func() { diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 30ca29ec..7d47c78e 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -6,7 +6,6 @@ import ( "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/config" - "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/boltdb" mdynamodb "zotregistry.io/zot/pkg/meta/dynamodb" @@ -33,11 +32,6 @@ func New(storageConfig config.StorageConfig, log log.Logger) (mTypes.MetaDB, err return nil, err } - err = imagetrust.InitCosignAndNotationDirs(params.RootDir) - if err != nil { - return nil, err - } - return Create("boltdb", driver, params, log) //nolint:contextcheck } diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 4e76c9ca..433f6ea7 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -6,6 +6,7 @@ package meta_test import ( "context" "encoding/json" + "fmt" "math/rand" "os" "path" @@ -22,7 +23,6 @@ import ( ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/extensions/imagetrust" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta" @@ -43,9 +43,12 @@ const ( func TestBoltDB(t *testing.T) { Convey("BoltDB creation", t, func() { - boltDBParams := boltdb.DBParameters{} + boltDBParams := boltdb.DBParameters{RootDir: t.TempDir()} + repoDBPath := path.Join(boltDBParams.RootDir, "repo.db") + boltDriver, err := boltdb.GetBoltDriver(boltDBParams) So(err, ShouldBeNil) + defer os.Remove(repoDBPath) log := log.NewLogger("debug", "") @@ -53,27 +56,36 @@ func TestBoltDB(t *testing.T) { So(metaDB, ShouldNotBeNil) So(err, ShouldBeNil) - err = os.Chmod("repo.db", 0o200) + err = os.Chmod(repoDBPath, 0o200) So(err, ShouldBeNil) _, err = boltdb.GetBoltDriver(boltDBParams) So(err, ShouldNotBeNil) - err = os.Chmod("repo.db", 0o600) + err = os.Chmod(repoDBPath, 0o600) So(err, ShouldBeNil) - - defer os.Remove("repo.db") }) Convey("BoltDB Wrapper", t, func() { - boltDBParams := boltdb.DBParameters{} + boltDBParams := boltdb.DBParameters{RootDir: t.TempDir()} boltDriver, err := boltdb.GetBoltDriver(boltDBParams) So(err, ShouldBeNil) log := log.NewLogger("debug", "") + imgTrustStore, err := imagetrust.NewLocalImageTrustStore(boltDBParams.RootDir) + So(err, ShouldBeNil) + boltdbWrapper, err := boltdb.New(boltDriver, log) - defer os.Remove("repo.db") + + boltdbWrapper.SetImageTrustStore(imgTrustStore) + + defer func() { + os.Remove(path.Join(boltDBParams.RootDir, "repo.db")) + os.RemoveAll(path.Join(boltDBParams.RootDir, "_cosign")) + os.RemoveAll(path.Join(boltDBParams.RootDir, "_notation")) + }() + So(boltdbWrapper, ShouldNotBeNil) So(err, ShouldBeNil) @@ -108,6 +120,8 @@ func TestDynamoDBWrapper(t *testing.T) { Region: "us-east-2", } + t.Logf("using dynamo driver options: %v", dynamoDBDriverParams) + dynamoClient, err := mdynamodb.GetDynamoClient(dynamoDBDriverParams) So(err, ShouldBeNil) @@ -117,6 +131,11 @@ func TestDynamoDBWrapper(t *testing.T) { So(dynamoDriver, ShouldNotBeNil) So(err, ShouldBeNil) + imgTrustStore, err := imagetrust.NewAWSImageTrustStore(dynamoDBDriverParams.Region, dynamoDBDriverParams.Endpoint) + So(err, ShouldBeNil) + + dynamoDriver.SetImageTrustStore(imgTrustStore) + resetDynamoDBTables := func() error { err := dynamoDriver.ResetRepoMetaTable() if err != nil { @@ -1300,7 +1319,6 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func So(repoData.Signatures[string(manifestDigest1)]["cosign"][0].LayersInfo[0].Date, ShouldBeZeroValue) }) - Convey("trusted signature", func() { _, _, manifest, _ := test.GetRandomImageComponents(10) //nolint:staticcheck manifestContent, _ := json.Marshal(manifest) @@ -1326,7 +1344,10 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func } tdir := t.TempDir() - keyName := "notation-sign-test" + uuid, err := guuid.NewV4() + So(err, ShouldBeNil) + + keyName := fmt.Sprintf("notation-sign-test-%s", uuid) test.NotationPathLock.Lock() defer test.NotationPathLock.Unlock() @@ -1376,44 +1397,18 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func }) So(err, ShouldBeNil) - err = imagetrust.InitNotationDir(tdir) + certificateContent, err := os.ReadFile(path.Join( + tdir, + "notation/localkeys", + fmt.Sprintf("%s.crt", keyName), + )) So(err, ShouldBeNil) + So(certificateContent, ShouldNotBeNil) - trustpolicyPath := path.Join(tdir, "_notation/trustpolicy.json") + imgTrustStore, ok := metaDB.ImageTrustStore().(*imagetrust.ImageTrustStore) + So(ok, ShouldBeTrue) - 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")) + err = imagetrust.UploadCertificate(imgTrustStore.NotationStorage, certificateContent, "ca") So(err, ShouldBeNil) err = metaDB.UpdateSignaturesValidity(repo, manifestDigest) //nolint:contextcheck @@ -1421,6 +1416,7 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func repoData, err := metaDB.GetRepoMeta(repo) So(err, ShouldBeNil) + So(repoData.Signatures[string(manifestDigest)]["notation"][0].LayersInfo[0].Signer, ShouldNotBeEmpty) So(repoData.Signatures[string(manifestDigest)]["notation"][0].LayersInfo[0].Date, @@ -2693,31 +2689,6 @@ 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) - - newMetaDB, err := meta.New(storageConfig, log) - So(newMetaDB, ShouldBeNil) - So(err, ShouldNotBeNil) - - err = os.Chmod(rootDir, 0o777) - So(err, ShouldBeNil) - }) -} - func skipDynamo(t *testing.T) { t.Helper() diff --git a/pkg/meta/types/types.go b/pkg/meta/types/types.go index 51f3aa4a..cb9a74aa 100644 --- a/pkg/meta/types/types.go +++ b/pkg/meta/types/types.go @@ -121,6 +121,10 @@ type MetaDB interface { //nolint:interfacebloat []RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, error) PatchDB() error + + ImageTrustStore() ImageTrustStore + + SetImageTrustStore(imgTrustStore ImageTrustStore) } type UserDB interface { //nolint:interfacebloat @@ -160,6 +164,13 @@ type UserDB interface { //nolint:interfacebloat DeleteUserAPIKey(ctx context.Context, id string) error } +type ImageTrustStore interface { + VerifySignature( + signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte, + repo string, + ) (string, time.Time, bool, error) +} + type ManifestMetadata struct { ManifestBlob []byte ConfigBlob []byte diff --git a/pkg/meta/version/version_test.go b/pkg/meta/version/version_test.go index 1021296a..4a478779 100644 --- a/pkg/meta/version/version_test.go +++ b/pkg/meta/version/version_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "os" + "path" "testing" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" @@ -31,7 +32,7 @@ func TestVersioningBoltDB(t *testing.T) { log := log.NewLogger("debug", "") boltdbWrapper, err := boltdb.New(boltDriver, log) - defer os.Remove("repo.db") + defer os.Remove(path.Join(boltDBParams.RootDir, "repo.db")) So(boltdbWrapper, ShouldNotBeNil) So(err, ShouldBeNil) diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index fd33b582..4f05e035 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -106,6 +106,24 @@ type MetaDBMock struct { DeleteUserAPIKeyFn func(ctx context.Context, id string) error PatchDBFn func() error + + ImageTrustStoreFn func() mTypes.ImageTrustStore + + SetImageTrustStoreFn func(mTypes.ImageTrustStore) +} + +func (sdm MetaDBMock) ImageTrustStore() mTypes.ImageTrustStore { + if sdm.ImageTrustStoreFn != nil { + return sdm.ImageTrustStoreFn() + } + + return nil +} + +func (sdm MetaDBMock) SetImageTrustStore(imgTrustStore mTypes.ImageTrustStore) { + if sdm.SetImageTrustStoreFn != nil { + sdm.SetImageTrustStoreFn(imgTrustStore) + } } func (sdm MetaDBMock) SetRepoDescription(repo, description string) error { diff --git a/swagger/docs.go b/swagger/docs.go index bef36946..6c325a88 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -388,12 +388,6 @@ const docTemplate = `{ "name": "truststoreType", "in": "query" }, - { - "type": "string", - "description": "truststore name", - "name": "truststoreName", - "in": "query" - }, { "description": "Certificate content", "name": "requestBody", diff --git a/swagger/swagger.json b/swagger/swagger.json index f77cfb36..f3f02786 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -379,12 +379,6 @@ "name": "truststoreType", "in": "query" }, - { - "type": "string", - "description": "truststore name", - "name": "truststoreName", - "in": "query" - }, { "description": "Certificate content", "name": "requestBody", diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index c5c3c6ad..cbe46dc1 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -483,10 +483,6 @@ paths: in: query name: truststoreType type: string - - description: truststore name - in: query - name: truststoreName - type: string - description: Certificate content in: body name: requestBody