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 <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea Lupu 2023-09-08 10:03:58 +03:00 committed by GitHub
parent 6115eed4ec
commit 5a3fac40db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1661 additions and 563 deletions

2
go.mod
View File

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

10
go.sum
View File

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

View File

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

View File

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

View File

@ -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": [
"*"
]

View File

@ -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() {
@ -71,6 +67,7 @@ func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, log log.Logg
type ImageTrust struct {
Conf *config.Config
ImageTrustStore *imagetrust.ImageTrustStore
Log log.Logger
}
@ -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
}

View File

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

View File

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

View File

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

View File

@ -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,65 +28,79 @@ 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 _, file := range files {
if !file.IsDir() {
for _, publicKey := range publicKeys {
// cosign verify the image
ctx := context.Background()
keyRef := path.Join(cosignDir, file.Name())
hashAlgorithm := crypto.SHA256
pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm)
pubKeyVerifier, pubKeyContent, err := cosignStorage.GetPublicKeyVerifier(publicKey)
if err != nil {
continue
}
pkcs11Key, ok := pubKey.(*pkcs11key.Key)
pkcs11Key, ok := pubKeyVerifier.(*pkcs11key.Key)
if ok {
defer pkcs11Key.Close()
}
verifier := pubKey
verifier := pubKeyVerifier
b64sig := signatureKey
@ -98,42 +116,154 @@ func VerifyCosignSignature(
continue
}
err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx))
err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload),
options.WithContext(context.Background()))
if err == nil {
publicKey, err := os.ReadFile(keyRef)
if err != nil {
continue
}
return string(publicKey), true, 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 {

View File

@ -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)
type ImageTrustStore struct {
CosignStorage publicKeyStorage
NotationStorage certificateStorage
}
func NewLocalImageTrustStore(rootDir string) (*ImageTrustStore, error) {
publicKeyStorage, err := NewPublicKeyLocalStorage(rootDir)
if err != nil {
return err
return nil, err
}
err = InitNotationDir(rootDir)
return err
certStorage, err := NewCertificateLocalStorage(rootDir)
if err != nil {
return nil, err
}
func VerifySignature(
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
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}
return err
if err != nil {
return nil, err
}
}
func InitTrustpolicyFile(configDir string) error {
return certStorage, nil
}
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,8 +131,7 @@ 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 := `
{
trustPolicy := `{
"version": "1.0",
"trustPolicies": [
{
@ -78,7 +140,7 @@ func InitTrustpolicyFile(configDir string) error {
"signatureVerification": {
"level" : "strict"
},
"trustStores": [],
"trustStores": [` + defaultTruststores + `],
"trustedIdentities": [
"*"
]
@ -86,22 +148,121 @@ func InitTrustpolicyFile(configDir string) error {
]
}`
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,38 +450,26 @@ func UploadCertificate(certificateContent []byte, truststoreType, truststoreName
return err
}
err = os.WriteFile(truststorePath, certificateContent, defaultFilePerms)
if err != nil {
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,
}
_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
return err
}
// add certificate to "trustpolicy.json"
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
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)
}
func validateTruststoreType(truststoreType string) bool {
for _, t := range truststore.Types {
if string(t) == truststoreType {
@ -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
}

View File

@ -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"
@ -26,6 +25,7 @@ import (
type BoltDB struct {
DB *bbolt.DB
Patches []func(DB *bbolt.DB) error
imgTrustStore mTypes.ImageTrustStore
Log log.Logger
}
@ -75,10 +75,19 @@ func New(boltDB *bbolt.DB, log log.Logger) (*BoltDB, error) {
return &BoltDB{
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -388,12 +388,6 @@ const docTemplate = `{
"name": "truststoreType",
"in": "query"
},
{
"type": "string",
"description": "truststore name",
"name": "truststoreName",
"in": "query"
},
{
"description": "Certificate content",
"name": "requestBody",

View File

@ -379,12 +379,6 @@
"name": "truststoreType",
"in": "query"
},
{
"type": "string",
"description": "truststore name",
"name": "truststoreName",
"in": "query"
},
{
"description": "Certificate content",
"name": "requestBody",

View File

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