0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat: upload certificates and public keys for verifying signatures (#1485)

In order to verify signatures, users could upload their certificates and public keys using these routes:
	-> for public keys:
		/v2/_zot/ext/mgmt?resource=signatures&tool=cosign
	-> for certificates:
		/v2/_zot/ext/mgmt?resource=signatures&tool=notation&truststoreType=ca&truststoreName=name
Then the public keys will be stored under $rootdir/_cosign and the certificates will be stored under
$rootdir/_notation/truststore/x509/$truststoreType/$truststoreName.
Also, for notation case, the "truststores" field of $rootir/_notation/trustpolicy.json file will be
updated with a new entry "$truststoreType:$truststoreName".
Also based on the uploaded files, the information about the signatures validity will be updated
periodically.

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea Lupu 2023-07-06 14:57:59 +03:00 committed by GitHub
parent 49e4d93f42
commit 41b05c60dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1575 additions and 193 deletions

View file

@ -97,4 +97,7 @@ var (
ErrSyncImageNotSigned = errors.New("sync: image is not signed") ErrSyncImageNotSigned = errors.New("sync: image is not signed")
ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config") ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config")
ErrCallerInfo = errors.New("runtime: failed to get info regarding the current runtime") ErrCallerInfo = errors.New("runtime: failed to get info regarding the current runtime")
ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type")
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
) )

View file

@ -361,6 +361,12 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
c.SyncOnDemand = syncOnDemand c.SyncOnDemand = syncOnDemand
} }
if c.Config.Extensions != nil {
if c.Config.Extensions.Mgmt != nil && *c.Config.Extensions.Mgmt.Enable {
ext.EnablePeriodicSignaturesVerification(c.Config, taskScheduler, c.RepoDB, c.Log) //nolint: contextcheck
}
}
} }
type SyncOnDemand interface { type SyncOnDemand interface {

View file

@ -4,15 +4,27 @@
package extensions package extensions
import ( import (
"context"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/meta/signatures"
"zotregistry.io/zot/pkg/scheduler"
)
const (
ConfigResource = "config"
SignaturesResource = "signatures"
) )
type HTPasswd struct { type HTPasswd struct {
@ -70,23 +82,38 @@ type mgmt struct {
log log.Logger log log.Logger
} }
// mgmtHandler godoc
// @Summary Get current server configuration
// @Description Get current server configuration
// @Router /v2/_zot/ext/mgmt [get]
// @Accept json
// @Produce json
// @Success 200 {object} extensions.StrippedConfig
// @Failure 500 {string} string "internal server error".
func (mgmt *mgmt) handler() http.Handler { func (mgmt *mgmt) handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sanitizedConfig := mgmt.config.Sanitize() var resource string
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
if err != nil { if queryHasParams(r.URL.Query(), []string{"resource"}) {
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response") resource = r.URL.Query().Get("resource")
w.WriteHeader(http.StatusInternalServerError) } else {
resource = ConfigResource // default value of "resource" query param
}
switch resource {
case ConfigResource:
if r.Method == http.MethodGet {
mgmt.HandleGetConfig(w, r)
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
case SignaturesResource:
if r.Method == http.MethodPost {
HandleCertificatesAndPublicKeysUploads(w, r) //nolint: contextcheck
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
default:
w.WriteHeader(http.StatusBadRequest)
return
} }
_, _ = w.Write(buf)
}) })
} }
@ -96,7 +123,7 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
mgmt := mgmt{config: config, log: log} mgmt := mgmt{config: config, log: log}
allowedMethods := zcommon.AllowedMethods(http.MethodGet) allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter() mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
mgmtRouter.Use(zcommon.ACHeadersHandler(allowedMethods...)) mgmtRouter.Use(zcommon.ACHeadersHandler(allowedMethods...))
@ -104,3 +131,194 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler()) mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler())
} }
} }
// mgmtHandler godoc
// @Summary Get current server configuration
// @Description Get current server configuration
// @Router /v2/_zot/ext/mgmt [get]
// @Accept json
// @Produce json
// @Param resource query string false "specify resource" Enums(config)
// @Success 200 {object} extensions.StrippedConfig
// @Failure 500 {string} string "internal server error".
func (mgmt *mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
sanitizedConfig := mgmt.config.Sanitize()
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
if err != nil {
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
w.WriteHeader(http.StatusInternalServerError)
}
_, _ = w.Write(buf)
}
// mgmtHandler godoc
// @Summary Upload certificates and public keys for verifying signatures
// @Description Upload certificates and public keys for verifying signatures
// @Router /v2/_zot/ext/mgmt [post]
// @Accept octet-stream
// @Produce json
// @Param resource query string true "specify resource" Enums(signatures)
// @Param tool query string true "specify signing tool" Enums(cosign, notation)
// @Param truststoreType query string false "truststore type"
// @Param truststoreName query string false "truststore name"
// @Param requestBody body string true "Public key or Certificate content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request".
// @Failure 500 {string} string "internal server error".
func HandleCertificatesAndPublicKeysUploads(response http.ResponseWriter, request *http.Request) {
if !queryHasParams(request.URL.Query(), []string{"tool"}) {
response.WriteHeader(http.StatusBadRequest)
return
}
body, err := io.ReadAll(request.Body)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
tool := request.URL.Query().Get("tool")
switch tool {
case signatures.CosignSignature:
err := signatures.UploadPublicKey(body)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
case signatures.NotationSignature:
var truststoreType string
if !queryHasParams(request.URL.Query(), []string{"truststoreName"}) {
response.WriteHeader(http.StatusBadRequest)
return
}
if 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
}
err = signatures.UploadCertificate(body, truststoreType, truststoreName)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
default:
response.WriteHeader(http.StatusBadRequest)
return
}
response.WriteHeader(http.StatusOK)
}
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
repoDB repodb.RepoDB, log log.Logger,
) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
ctx := context.Background()
repos, err := repoDB.GetMultipleRepoMeta(ctx, func(repoMeta repodb.RepoMetadata) bool {
return true
}, repodb.PageInput{})
if err != nil {
return
}
generator := &taskGeneratorSigValidity{
repos: repos,
repoDB: repoDB,
repoIndex: -1,
log: log,
}
numberOfHours := 2
interval := time.Duration(numberOfHours) * time.Minute
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
}
}
type taskGeneratorSigValidity struct {
repos []repodb.RepoMetadata
repoDB repodb.RepoDB
repoIndex int
done bool
log log.Logger
}
func (gen *taskGeneratorSigValidity) Next() (scheduler.Task, error) {
gen.repoIndex++
if gen.repoIndex >= len(gen.repos) {
gen.done = true
return nil, nil
}
return NewValidityTask(gen.repoDB, gen.repos[gen.repoIndex], gen.log), nil
}
func (gen *taskGeneratorSigValidity) IsDone() bool {
return gen.done
}
func (gen *taskGeneratorSigValidity) Reset() {
gen.done = false
gen.repoIndex = -1
ctx := context.Background()
repos, err := gen.repoDB.GetMultipleRepoMeta(ctx, func(repoMeta repodb.RepoMetadata) bool {
return true
}, repodb.PageInput{})
if err != nil {
return
}
gen.repos = repos
}
type validityTask struct {
repoDB repodb.RepoDB
repo repodb.RepoMetadata
log log.Logger
}
func NewValidityTask(repoDB repodb.RepoDB, repo repodb.RepoMetadata, log log.Logger) *validityTask {
return &validityTask{repoDB, repo, log}
}
func (validityT *validityTask) DoWork() error {
validityT.log.Info().Msg("updating signatures validity")
for signedManifest, sigs := range validityT.repo.Signatures {
if len(sigs[signatures.CosignSignature]) != 0 || len(sigs[signatures.NotationSignature]) != 0 {
err := validityT.repoDB.UpdateSignaturesValidity(validityT.repo.Name, digest.Digest(signedManifest))
if err != nil {
validityT.log.Info().Msg("error while verifying signatures")
return err
}
}
}
validityT.log.Info().Msg("verifying signatures successfully completed")
return nil
}

View file

@ -8,6 +8,8 @@ import (
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/scheduler"
) )
func IsBuiltWithMGMTExtension() bool { func IsBuiltWithMGMTExtension() bool {
@ -18,3 +20,10 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," + log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," +
"please build a binary that does so") "please build a binary that does so")
} }
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
repoDB repodb.RepoDB, log log.Logger,
) {
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")
}

View file

@ -0,0 +1,54 @@
//go:build !mgmt
package extensions_test
import (
"os"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
func TestMgmtExtension(t *testing.T) {
Convey("periodic signature verification is skipped when binary doesn't include mgmt", t, func() {
conf := config.New()
port := test.GetFreePort()
globalDir := t.TempDir()
defaultValue := true
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name())
conf.HTTP.Port = port
conf.Storage.RootDirectory = globalDir
conf.Storage.Commit = true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Log.Level = "warn"
conf.Log.Output = logFile.Name()
ctlr := api.NewController(conf)
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"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")
})
}

View file

@ -9,7 +9,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"testing" "testing"
"time"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
@ -20,6 +22,10 @@ import (
"zotregistry.io/zot/pkg/extensions" "zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
syncconf "zotregistry.io/zot/pkg/extensions/config/sync" syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
@ -518,6 +524,158 @@ func TestMgmtExtension(t *testing.T) {
data, _ := os.ReadFile(logFile.Name()) data, _ := os.ReadFile(logFile.Name())
So(string(data), ShouldContainSubstring, "setting up mgmt routes") So(string(data), ShouldContainSubstring, "setting up mgmt routes")
}) })
Convey("Verify mgmt route enabled for uploading certificates and public keys", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defaultValue := true
conf.Commit = "v1.0.0"
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
log.NewLogger("debug", logFile.Name()), monitoring.NewMetricsServer(false,
log.NewLogger("debug", logFile.Name())), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
config, layers, manifest, err := test.GetRandomImageComponents(10)
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
test.Image{
Manifest: manifest,
Layers: layers,
Config: config,
Reference: "0.0.1",
}, "repo", storeController,
)
So(err, ShouldBeNil)
sigConfig, sigLayers, sigManifest, err := test.GetRandomImageComponents(10)
So(err, ShouldBeNil)
ref, _ := test.GetCosignSignatureTagForManifest(manifest)
err = test.WriteImageToFileSystem(
test.Image{
Manifest: sigManifest,
Layers: sigLayers,
Config: sigConfig,
Reference: ref,
}, "repo", storeController,
)
So(err, ShouldBeNil)
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
rootDir := t.TempDir()
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(rootDir)
// generate a keypair
err = test.GenerateNotationCerts(rootDir, "test")
So(err, ShouldBeNil)
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "test.crt"))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "invalidTool").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
Get(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "signatures").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "config").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "invalid").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up mgmt routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
} }
func TestMgmtWithBearer(t *testing.T) { func TestMgmtWithBearer(t *testing.T) {
@ -694,7 +852,7 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix) resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS") So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
}) })
} }

View file

@ -10,7 +10,12 @@ Response depends on the user privileges:
| Supported queries | Input | Output | Description | | Supported queries | Input | Output | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration | | [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration |
| [Upload a certificate](#post-certificate) | certificate | None | Add certificate for verifying notation signatures|
| [Upload a public key](#post-public-key) | public key | None | Add public key for verifying cosign signatures |
## General usage
The mgmt endpoint accepts as a query parameter what `resource` is targeted by the request and then all other required parameters for the specified resource. The default value of this
query parameter is `config`.
## Get current configuration ## Get current configuration
@ -42,3 +47,34 @@ If ldap or htpasswd are enabled mgmt will return `{"htpasswd": {}}` indicating t
If any key is present under `'auth'` key, in the mgmt response, it means that particular authentication method is enabled. If any key is present under `'auth'` key, in the mgmt response, it means that particular authentication method is enabled.
## Configure zot for verifying signatures
If the `resource` is `signatures` then the mgmt endpoint accepts as a query parameter the `tool` that corresponds to the uploaded file and then all other required parameters for the specified tool.
### Upload a certificate
**Sample request**
| Tool | Parameter | Parameter Type | Parameter Description |
| --- | --- | --- | --- |
| notation | truststoreType | string | The type of the truststore. This parameter is optional and its default value is `ca` |
| | truststoreName | string | The name of the truststore |
```bash
curl --data-binary @certificate.crt -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=notation&truststoreType=ca&truststoreName=newtruststore
```
As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}` directory under $rootDir. And `truststores` field from `_notation/trustpolicy.json` file will be updated.
### Upload a public key
**Sample request**
| Tool | Parameter | Parameter Type | Parameter Description |
| --- | --- | --- | --- |
| cosign |
```bash
curl --data-binary @publicKey.pub -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=cosign
```
As a result of this request, the uploaded file will be stored in `_cosign` directory under $rootDir.

View file

@ -3,15 +3,8 @@ package bolt_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"os"
"path"
"testing" "testing"
"time"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/signer"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
@ -1013,169 +1006,6 @@ func TestWrapperErrors(t *testing.T) {
err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig")) err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig"))
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
Convey("VerifySignature -> untrusted signature", func() {
err := boltdbWrapper.SetManifestData(digest.FromString("dig"), repodb.ManifestData{
ManifestBlob: []byte("Bad Manifest"),
ConfigBlob: []byte("Bad Manifest"),
})
So(err, ShouldBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket))
return repoBuck.Put([]byte("repo1"), repoMetaBlob)
})
So(err, ShouldBeNil)
layerInfo := repodb.LayerInfo{LayerDigest: "", LayerContent: []byte{}, SignatureKey: ""}
err = boltdbWrapper.AddManifestSignature("repo1", digest.FromString("dig"),
repodb.SignatureMetadata{
SignatureType: signatures.CosignSignature,
SignatureDigest: string(digest.FromString("signature digest")),
LayersInfo: []repodb.LayerInfo{layerInfo},
})
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig"))
So(err, ShouldBeNil)
repoData, err := boltdbWrapper.GetRepoMeta("repo1")
So(err, ShouldBeNil)
So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Signer,
ShouldBeEmpty)
So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Date,
ShouldBeZeroValue)
})
Convey("VerifySignature -> trusted signature", func() {
_, _, manifest, _ := test.GetRandomImageComponents(10)
manifestContent, _ := json.Marshal(manifest)
manifestDigest := digest.FromBytes(manifestContent)
err := boltdbWrapper.SetManifestData(manifestDigest, repodb.ManifestData{
ManifestBlob: manifestContent,
ConfigBlob: []byte("configContent"),
})
So(err, ShouldBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket))
return repoBuck.Put([]byte("repo"), repoMetaBlob)
})
So(err, ShouldBeNil)
mediaType := jws.MediaTypeEnvelope
signOpts := notation.SignerSignOptions{
SignatureMediaType: mediaType,
PluginConfig: map[string]string{},
ExpiryDuration: 24 * time.Hour,
}
tdir := t.TempDir()
keyName := "notation-sign-test"
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(tdir)
err = test.GenerateNotationCerts(tdir, keyName)
So(err, ShouldBeNil)
// getSigner
var newSigner notation.Signer
// ResolveKey
signingKeys, err := test.LoadNotationSigningkeys(tdir)
So(err, ShouldBeNil)
idx := test.Index(signingKeys.Keys, keyName)
So(idx, ShouldBeGreaterThanOrEqualTo, 0)
key := signingKeys.Keys[idx]
if key.X509KeyPair != nil {
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
So(err, ShouldBeNil)
}
descToSign := ispec.Descriptor{
MediaType: manifest.MediaType,
Digest: manifestDigest,
Size: int64(len(manifestContent)),
}
sig, _, err := newSigner.Sign(ctx, descToSign, signOpts)
So(err, ShouldBeNil)
layerInfo := repodb.LayerInfo{
LayerDigest: string(digest.FromBytes(sig)),
LayerContent: sig, SignatureKey: mediaType,
}
err = boltdbWrapper.AddManifestSignature("repo", manifestDigest,
repodb.SignatureMetadata{
SignatureType: signatures.NotationSignature,
SignatureDigest: string(digest.FromString("signature digest")),
LayersInfo: []repodb.LayerInfo{layerInfo},
})
So(err, ShouldBeNil)
err = signatures.InitNotationDir(tdir)
So(err, ShouldBeNil)
trustpolicyPath := path.Join(tdir, "_notation/trustpolicy.json")
if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
trustPolicy := `
{
"version": "1.0",
"trustPolicies": [
{
"name": "notation-sign-test",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": ["ca:notation-sign-test"],
"trustedIdentities": [
"*"
]
}
]
}`
file, err := os.Create(trustpolicyPath)
So(err, ShouldBeNil)
defer file.Close()
_, err = file.WriteString(trustPolicy)
So(err, ShouldBeNil)
}
truststore := "_notation/truststore/x509/ca/notation-sign-test"
truststoreSrc := "notation/truststore/x509/ca/notation-sign-test"
err = os.MkdirAll(path.Join(tdir, truststore), 0o755)
So(err, ShouldBeNil)
err = test.CopyFile(path.Join(tdir, truststoreSrc, "notation-sign-test.crt"),
path.Join(tdir, truststore, "notation-sign-test.crt"))
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateSignaturesValidity("repo", manifestDigest) //nolint:contextcheck
So(err, ShouldBeNil)
repoData, err := boltdbWrapper.GetRepoMeta("repo")
So(err, ShouldBeNil)
So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Signer,
ShouldNotBeEmpty)
So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Date,
ShouldNotBeZeroValue)
})
}) })
}) })
} }

View file

@ -563,6 +563,25 @@ func TestWrapperErrors(t *testing.T) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
Convey("UpdateSignaturesValidity GetManifestData error", func() {
err := setBadManifestData(dynamoWrapper.Client, manifestDataTablename, "dig")
So(err, ShouldBeNil)
err = dynamoWrapper.UpdateSignaturesValidity("repo", "dig")
So(err, ShouldNotBeNil)
})
Convey("UpdateSignaturesValidity GetRepoMeta error", func() {
err := dynamoWrapper.SetManifestData("dig", repodb.ManifestData{})
So(err, ShouldBeNil)
err = setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo")
So(err, ShouldBeNil)
err = dynamoWrapper.UpdateSignaturesValidity("repo", "dig")
So(err, ShouldNotBeNil)
})
Convey("AddManifestSignature GetRepoMeta error", func() { Convey("AddManifestSignature GetRepoMeta error", func() {
err := dynamoWrapper.SetRepoReference("repo", "tag", "dig", "") err := dynamoWrapper.SetRepoReference("repo", "tag", "dig", "")
So(err, ShouldBeNil) So(err, ShouldBeNil)

View file

@ -618,7 +618,67 @@ func (dwr *DBWrapper) IncrementImageDownloads(repo string, reference string) err
} }
func (dwr *DBWrapper) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error { func (dwr *DBWrapper) UpdateSignaturesValidity(repo string, manifestDigest godigest.Digest) error {
return nil // get ManifestData of signed manifest
var blob []byte
manifestData, err := dwr.GetManifestData(manifestDigest)
if err != nil {
if errors.Is(err, zerr.ErrManifestDataNotFound) {
indexData, err := dwr.GetIndexData(manifestDigest)
if err != nil {
return nil //nolint: nilerr
}
blob = indexData.IndexBlob
} else {
return fmt.Errorf("%w for manifest '%s' from repo '%s'", errRepodb, manifestDigest, repo)
}
} else {
blob = manifestData.ManifestBlob
}
// update signatures with details about validity and author
repoMeta, err := dwr.GetRepoMeta(repo)
if err != nil {
return err
}
manifestSignatures := repodb.ManifestSignatures{}
for sigType, sigs := range repoMeta.Signatures[manifestDigest.String()] {
signaturesInfo := []repodb.SignatureInfo{}
for _, sigInfo := range sigs {
layersInfo := []repodb.LayerInfo{}
for _, layerInfo := range sigInfo.LayersInfo {
author, date, isTrusted, _ := signatures.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey,
manifestDigest, blob, repo)
if isTrusted {
layerInfo.Signer = author
}
if !date.IsZero() {
layerInfo.Signer = author
layerInfo.Date = date
}
layersInfo = append(layersInfo, layerInfo)
}
signaturesInfo = append(signaturesInfo, repodb.SignatureInfo{
SignatureManifestDigest: sigInfo.SignatureManifestDigest,
LayersInfo: layersInfo,
})
}
manifestSignatures[sigType] = signaturesInfo
}
repoMeta.Signatures[manifestDigest.String()] = manifestSignatures
return dwr.SetRepoMeta(repoMeta.Name, repoMeta)
} }
func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest,

View file

@ -13,6 +13,9 @@ import (
"time" "time"
guuid "github.com/gofrs/uuid" guuid "github.com/gofrs/uuid"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/signer"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go" "github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -25,6 +28,7 @@ import (
"zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/meta/repodb"
boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
dynamodb_wrapper "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper" dynamodb_wrapper "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper"
"zotregistry.io/zot/pkg/meta/signatures"
localCtx "zotregistry.io/zot/pkg/requestcontext" localCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
@ -71,7 +75,7 @@ func TestBoltDBWrapper(t *testing.T) {
So(boltdbWrapper, ShouldNotBeNil) So(boltdbWrapper, ShouldNotBeNil)
So(err, ShouldBeNil) So(err, ShouldBeNil)
RunRepoDBTests(boltdbWrapper) RunRepoDBTests(t, boltdbWrapper)
}) })
} }
@ -122,11 +126,11 @@ func TestDynamoDBWrapper(t *testing.T) {
return err return err
} }
RunRepoDBTests(dynamoDriver, resetDynamoDBTables) RunRepoDBTests(t, dynamoDriver, resetDynamoDBTables)
}) })
} }
func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { func RunRepoDBTests(t *testing.T, repoDB repodb.RepoDB, preparationFuncs ...func() error) { //nolint: thelper
Convey("Test RepoDB Interface implementation", func() { Convey("Test RepoDB Interface implementation", func() {
for _, prepFunc := range preparationFuncs { for _, prepFunc := range preparationFuncs {
err := prepFunc() err := prepFunc()
@ -994,6 +998,170 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
Convey("Test UpdateSignaturesValidity", func() {
Convey("untrusted signature", func() {
var (
repo1 = "repo1"
tag1 = "0.0.1"
manifestDigest1 = godigest.FromString("dig")
)
err := repoDB.SetRepoReference(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
err = repoDB.SetManifestMeta(repo1, manifestDigest1, repodb.ManifestMetadata{
ManifestBlob: []byte("Bad Manifest"),
ConfigBlob: []byte("Bad Manifest"),
})
So(err, ShouldBeNil)
layerInfo := repodb.LayerInfo{LayerDigest: "", LayerContent: []byte{}, SignatureKey: ""}
err = repoDB.AddManifestSignature(repo1, manifestDigest1, repodb.SignatureMetadata{
SignatureType: "cosign",
SignatureDigest: string(manifestDigest1),
LayersInfo: []repodb.LayerInfo{layerInfo},
})
So(err, ShouldBeNil)
err = repoDB.UpdateSignaturesValidity(repo1, manifestDigest1)
So(err, ShouldBeNil)
repoData, err := repoDB.GetRepoMeta(repo1)
So(err, ShouldBeNil)
So(repoData.Signatures[string(manifestDigest1)]["cosign"][0].LayersInfo[0].Signer,
ShouldBeEmpty)
So(repoData.Signatures[string(manifestDigest1)]["cosign"][0].LayersInfo[0].Date,
ShouldBeZeroValue)
})
Convey("trusted signature", func() {
_, _, manifest, _ := test.GetRandomImageComponents(10)
manifestContent, _ := json.Marshal(manifest)
manifestDigest := godigest.FromBytes(manifestContent)
repo := "repo"
tag := "0.0.1"
err := repoDB.SetRepoReference(repo, tag, manifestDigest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
err = repoDB.SetManifestMeta(repo, manifestDigest, repodb.ManifestMetadata{
ManifestBlob: manifestContent,
ConfigBlob: []byte("configContent"),
})
So(err, ShouldBeNil)
mediaType := jws.MediaTypeEnvelope
signOpts := notation.SignerSignOptions{
SignatureMediaType: mediaType,
PluginConfig: map[string]string{},
ExpiryDuration: 24 * time.Hour,
}
tdir := t.TempDir()
keyName := "notation-sign-test"
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(tdir)
err = test.GenerateNotationCerts(tdir, keyName)
So(err, ShouldBeNil)
// getSigner
var newSigner notation.Signer
// ResolveKey
signingKeys, err := test.LoadNotationSigningkeys(tdir)
So(err, ShouldBeNil)
idx := test.Index(signingKeys.Keys, keyName)
So(idx, ShouldBeGreaterThanOrEqualTo, 0)
key := signingKeys.Keys[idx]
if key.X509KeyPair != nil {
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
So(err, ShouldBeNil)
}
descToSign := ispec.Descriptor{
MediaType: manifest.MediaType,
Digest: manifestDigest,
Size: int64(len(manifestContent)),
}
ctx := context.Background()
sig, _, err := newSigner.Sign(ctx, descToSign, signOpts)
So(err, ShouldBeNil)
layerInfo := repodb.LayerInfo{
LayerDigest: string(godigest.FromBytes(sig)),
LayerContent: sig, SignatureKey: mediaType,
}
err = repoDB.AddManifestSignature(repo, manifestDigest, repodb.SignatureMetadata{
SignatureType: "notation",
SignatureDigest: string(godigest.FromString("signature digest")),
LayersInfo: []repodb.LayerInfo{layerInfo},
})
So(err, ShouldBeNil)
err = signatures.InitNotationDir(tdir)
So(err, ShouldBeNil)
trustpolicyPath := path.Join(tdir, "_notation/trustpolicy.json")
trustPolicy := `
{
"version": "1.0",
"trustPolicies": [
{
"name": "notation-sign-test",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": ["ca:notation-sign-test"],
"trustedIdentities": [
"*"
]
}
]
}`
file, err := os.Create(trustpolicyPath)
So(err, ShouldBeNil)
defer file.Close()
_, err = file.WriteString(trustPolicy)
So(err, ShouldBeNil)
truststore := "_notation/truststore/x509/ca/notation-sign-test"
truststoreSrc := "notation/truststore/x509/ca/notation-sign-test"
err = os.MkdirAll(path.Join(tdir, truststore), 0o755)
So(err, ShouldBeNil)
err = test.CopyFile(path.Join(tdir, truststoreSrc, "notation-sign-test.crt"),
path.Join(tdir, truststore, "notation-sign-test.crt"))
So(err, ShouldBeNil)
err = repoDB.UpdateSignaturesValidity(repo, manifestDigest) //nolint:contextcheck
So(err, ShouldBeNil)
repoData, err := repoDB.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,
ShouldNotBeZeroValue)
})
})
Convey("Test AddImageSignature with inverted order", func() { Convey("Test AddImageSignature with inverted order", func() {
var ( var (
repo1 = "repo1" repo1 = "repo1"

View file

@ -0,0 +1,132 @@
# Verifying signatures
## How to configure zot for verifying signatures
In order to configure zot for verifying signatures, the user should provide:
1. public keys (which correspond to the private keys used to sign images with `cosign`)
or
2. certificates (used to sign images with `notation`)
These files could be uploaded using one of these requests:
1. upload a public key
***Example of request***
```
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=cosign"
```
2. upload a certificate
***Example of request***
```
curl --data-binary @filet.crt -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=notation&truststoreType=ca&truststoreName=upload-cert"
```
Besides the requested files, the user should also specify the `tool` which should be :
- `cosign` for uploading public keys
- `notation` for uploading certificates
Also, if the uploaded file is a certificate then the user should also specify the type of the truststore through `truststoreType` param and also its name through `truststoreName` param.
Based on the uploaded files, signatures verification will be performed for all the signed images. Then the information known about the signatures will be:
- the tool used to generate the signature (`cosign` or `notation`)
- info about the trustworthiness of the signature (if there is a certificate or a public key which can successfully verify the signature)
- the author of the signature which will be:
- the public key -> for signatures generated using `cosign`
- the subject of the certificate -> for signatures generated using `notation`
**Example of GraphQL output**
```json
{
"data": {
"Image": {
"Manifests": [
{
"Digest":"sha256:6c19fba547b87bde9a45df2f8563e0c61826d098dd30192a2c8b86da1e1a6360"
}
],
"IsSigned": true,
"Tag": "latest",
"SignatureInfo":[
{
"Tool":"cosign",
"IsTrusted":false,
"Author":""
},
{
"Tool":"cosign",
"IsTrusted":false,
"Author":""
},
{
"Tool":"cosign",
"IsTrusted": true,
"Author":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9pN+/hGcFlh4YYaNvZxNvuh8Qyhl\npURz77qScOHe3DqdmiWiuqIseyhEdjEDwpL6fHRwu3a2Nd9wbKqm0la76w==\n-----END PUBLIC KEY-----\n"
},
{
"Tool":"notation",
"IsTrusted": false,
"Author":"CN=v4-test,O=Notary,L=Seattle,ST=WA,C=US"
},
{
"Tool":"notation",
"IsTrusted": true,
"Author":"CN=multipleSig,O=Notary,L=Seattle,ST=WA,C=US"
}
]
}
}
}
```
## 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`.
- `_cosign` directory will contain the uploaded public keys
```
_cosign
├── $publicKey1
└── $publicKey2
```
- `_notation` directory will have this structure:
```
_notation
├── trustpolicy.json
└── truststore
└── x509
└── $truststoreType
└── $truststoreName
└── $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:
```
{
"version": "1.0",
"trustPolicies": [
{
"name": "default-config",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": [],
"trustedIdentities": [
"*"
]
}
]
}
```

View file

@ -12,6 +12,7 @@ import (
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
sigs "github.com/sigstore/cosign/v2/pkg/signature" sigs "github.com/sigstore/cosign/v2/pkg/signature"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature/options" "github.com/sigstore/sigstore/pkg/signature/options"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
@ -111,3 +112,32 @@ func VerifyCosignSignature(
return "", false, nil return "", false, nil
} }
func UploadPublicKey(publicKeyContent []byte) error {
// validate public key
if ok, err := validatePublicKey(publicKeyContent); !ok {
return err
}
// add public key to "{rootDir}/_cosign/{name.pub}"
configDir, err := GetCosignDirPath()
if err != nil {
return err
}
name := godigest.FromBytes(publicKeyContent)
// store public key
publicKeyPath := path.Join(configDir, name.String())
return os.WriteFile(publicKeyPath, publicKeyContent, defaultFilePerms)
}
func validatePublicKey(publicKeyContent []byte) (bool, error) {
_, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent)
if err != nil {
return false, err
}
return true, nil
}

View file

@ -2,19 +2,26 @@ package signatures
import ( import (
"context" "context"
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"regexp"
"sync"
"time" "time"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/verifier" "github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore" "github.com/notaryproject/notation-go/verifier/truststore"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
@ -22,7 +29,10 @@ import (
const notationDirRelativePath = "_notation" const notationDirRelativePath = "_notation"
var notationDir = "" //nolint:gochecknoglobals var (
notationDir = "" //nolint:gochecknoglobals
TrustpolicyLock = new(sync.Mutex) //nolint: gochecknoglobals
)
func InitNotationDir(rootDir string) error { func InitNotationDir(rootDir string) error {
dir := path.Join(rootDir, notationDirRelativePath) dir := path.Join(rootDir, notationDirRelativePath)
@ -37,11 +47,48 @@ func InitNotationDir(rootDir string) error {
if err == nil { if err == nil {
notationDir = dir notationDir = dir
if _, err := LoadTrustPolicyDocument(notationDir); os.IsNotExist(err) {
return InitTrustpolicyFile(notationDir)
}
} }
return err return err
} }
func InitTrustpolicyFile(configDir string) error {
// 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`
// this default trustpolicy.json file sets the signatureVerification.level
// to `strict` which enforces all validations (this means that even if there is
// 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": [
"*"
]
}
]
}`
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
return os.WriteFile(path.Join(configDir, dir.PathTrustPolicy), []byte(trustPolicy), defaultDirPerms)
}
func GetNotationDirPath() (string, error) { func GetNotationDirPath() (string, error) {
if notationDir != "" { if notationDir != "" {
return notationDir, nil return notationDir, nil
@ -79,6 +126,9 @@ func NewFromConfig() (notation.Verifier, error) {
} }
// Load trust policy. // Load trust policy.
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
policyDocument, err := LoadTrustPolicyDocument(notationDir) policyDocument, err := LoadTrustPolicyDocument(notationDir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -165,3 +215,112 @@ func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter t
return false return false
} }
func UploadCertificate(certificateContent []byte, truststoreType, truststoreName 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 {
return err
}
// add certificate to "{rootDir}/_notation/truststore/x509/{type}/{name}/{name.crt}"
configDir, err := GetNotationDirPath()
if err != nil {
return err
}
name := godigest.FromBytes(certificateContent)
// store certificate
truststorePath := path.Join(configDir, dir.TrustStoreDir, "x509", truststoreType, truststoreName, name.String())
if err := os.MkdirAll(filepath.Dir(truststorePath), defaultDirPerms); err != nil {
return err
}
err = os.WriteFile(truststorePath, certificateContent, defaultFilePerms)
if err != nil {
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 {
return true
}
}
return false
}
func validateTruststoreName(truststoreName string) bool {
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) {
var certs []*x509.Certificate
block, rest := pem.Decode(certificateContent)
if block == nil {
// data may be in DER format
derCerts, err := x509.ParseCertificates(certificateContent)
if err != nil {
return false, err
}
certs = append(certs, derCerts...)
} else {
// data is in PEM format
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false, err
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
}
}
if len(certs) == 0 {
return false, zerr.ErrInvalidCertificateContent
}
return true, nil
}

View file

@ -14,6 +14,7 @@ const (
CosignSignature = "cosign" CosignSignature = "cosign"
NotationSignature = "notation" NotationSignature = "notation"
defaultDirPerms = 0o700 defaultDirPerms = 0o700
defaultFilePerms = 0o644
) )
func InitCosignAndNotationDirs(rootDir string) error { func InitCosignAndNotationDirs(rootDir string) error {

View file

@ -56,6 +56,9 @@ func TestInitCosignAndNotationDirs(t *testing.T) {
err = signatures.InitCosignAndNotationDirs(dir) err = signatures.InitCosignAndNotationDirs(dir)
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
err = signatures.InitNotationDir(dir)
So(err, ShouldNotBeNil)
err = os.Chmod(dir, 0o500) err = os.Chmod(dir, 0o500)
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -70,6 +73,51 @@ func TestInitCosignAndNotationDirs(t *testing.T) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet) So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet)
}) })
Convey("UploadCertificate - notationDir is not set", 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 = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet)
})
Convey("UploadPublicKey - cosignDir is not set", 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 = signatures.UploadPublicKey(publicKeyContent)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrSignConfigDirNotSet)
})
} }
func TestVerifySignatures(t *testing.T) { func TestVerifySignatures(t *testing.T) {
@ -278,6 +326,11 @@ func TestVerifySignatures(t *testing.T) {
err := signatures.InitNotationDir(dir) err := signatures.InitNotationDir(dir)
So(err, ShouldBeNil) So(err, ShouldBeNil)
notationDir, _ := signatures.GetNotationDirPath()
err = os.Remove(path.Join(notationDir, "trustpolicy.json"))
So(err, ShouldBeNil)
_, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo) _, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, repo)
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
@ -292,7 +345,7 @@ func TestVerifySignatures(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"), err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"),
0o600, false) 0o600, true)
So(err, ShouldBeNil) So(err, ShouldBeNil)
_, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent, _, _, _, err = signatures.VerifySignature("notation", []byte("signature"), "", manifestDigest, manifestContent,
@ -363,7 +416,7 @@ func TestVerifySignatures(t *testing.T) {
] ]
}` }`
err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte(trustPolicy), 0o600, false) err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte(trustPolicy), 0o600, true)
So(err, ShouldBeNil) So(err, ShouldBeNil)
indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo) indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo)
@ -437,3 +490,222 @@ func TestCheckExpiryErr(t *testing.T) {
So(isExpiryErr, ShouldBeTrue) So(isExpiryErr, ShouldBeTrue)
}) })
} }
func TestUploadPublicKey(t *testing.T) {
Convey("public key - invalid content", t, func() {
err := signatures.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 = signatures.InitCosignDir(rootDir)
So(err, ShouldBeNil)
err = signatures.UploadPublicKey(publicKeyContent)
So(err, ShouldBeNil)
})
}
func TestUploadCertificate(t *testing.T) {
Convey("invalid truststore type", t, func() {
err := signatures.UploadCertificate([]byte("certificate content"), "wrongType", "store")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrInvalidTruststoreType)
})
Convey("invalid truststore name", t, func() {
err := signatures.UploadCertificate([]byte("certificate content"), "ca", "*store?")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrInvalidTruststoreName)
})
Convey("invalid certificate content", t, func() {
err := signatures.UploadCertificate([]byte("invalid content"), "ca", "store")
So(err, ShouldNotBeNil)
content := `-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
`
err = signatures.UploadCertificate([]byte(content), "ca", "store")
So(err, ShouldNotBeNil)
content = ``
err = signatures.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 = signatures.InitNotationDir(rootDir)
So(err, ShouldBeNil)
notationDir, err := signatures.GetNotationDirPath()
So(err, ShouldBeNil)
err = os.Chmod(notationDir, 0o100)
So(err, ShouldBeNil)
err = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
So(err, ShouldNotBeNil)
err = os.Chmod(notationDir, 0o777)
So(err, ShouldBeNil)
})
Convey("certificate can't be stored", 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 = signatures.InitNotationDir(rootDir)
So(err, ShouldBeNil)
notationDir, err := signatures.GetNotationDirPath()
So(err, ShouldBeNil)
err = os.MkdirAll(path.Join(notationDir, "truststore/x509/ca/notation-upload-test"), 0o777)
So(err, ShouldBeNil)
err = os.Chmod(path.Join(notationDir, "truststore/x509/ca/notation-upload-test"), 0o100)
So(err, ShouldBeNil)
err = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
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 = signatures.InitNotationDir(rootDir)
So(err, ShouldBeNil)
notationDir, err := signatures.GetNotationDirPath()
So(err, ShouldBeNil)
err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"),
0o600, true)
So(err, ShouldBeNil)
err = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
So(err, ShouldNotBeNil)
})
Convey("trustpolicy - truststore already exists", 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 = signatures.InitNotationDir(rootDir)
So(err, ShouldBeNil)
notationDir, err := signatures.GetNotationDirPath()
So(err, ShouldBeNil)
trustpolicyDoc, err := signatures.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 = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
So(err, ShouldBeNil)
})
Convey("upload certificate successfully", 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 = signatures.InitNotationDir(rootDir)
So(err, ShouldBeNil)
err = signatures.UploadCertificate(certificateContent, "ca", "notation-upload-test")
So(err, ShouldBeNil)
})
}

View file

@ -151,6 +151,17 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"summary": "Get current server configuration", "summary": "Get current server configuration",
"parameters": [
{
"enum": [
"config"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -165,6 +176,80 @@ const docTemplate = `{
} }
} }
} }
},
"post": {
"description": "Upload certificates and public keys for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload certificates and public keys for verifying signatures",
"parameters": [
{
"enum": [
"signatures"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query",
"required": true
},
{
"enum": [
"cosign",
"notation"
],
"type": "string",
"description": "specify signing tool",
"name": "tool",
"in": "query",
"required": true
},
{
"type": "string",
"description": "truststore type",
"name": "truststoreType",
"in": "query"
},
{
"type": "string",
"description": "truststore name",
"name": "truststoreName",
"in": "query"
},
{
"description": "Public key or Certificate content",
"name": "requestBody",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
} }
}, },
"/v2/_zot/ext/userprefs": { "/v2/_zot/ext/userprefs": {

View file

@ -142,6 +142,17 @@
"application/json" "application/json"
], ],
"summary": "Get current server configuration", "summary": "Get current server configuration",
"parameters": [
{
"enum": [
"config"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -156,6 +167,80 @@
} }
} }
} }
},
"post": {
"description": "Upload certificates and public keys for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload certificates and public keys for verifying signatures",
"parameters": [
{
"enum": [
"signatures"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query",
"required": true
},
{
"enum": [
"cosign",
"notation"
],
"type": "string",
"description": "specify signing tool",
"name": "tool",
"in": "query",
"required": true
},
{
"type": "string",
"description": "truststore type",
"name": "truststoreType",
"in": "query"
},
{
"type": "string",
"description": "truststore name",
"name": "truststoreName",
"in": "query"
},
{
"description": "Public key or Certificate content",
"name": "requestBody",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
} }
}, },
"/v2/_zot/ext/userprefs": { "/v2/_zot/ext/userprefs": {

View file

@ -291,6 +291,13 @@ paths:
consumes: consumes:
- application/json - application/json
description: Get current server configuration description: Get current server configuration
parameters:
- description: specify resource
enum:
- config
in: query
name: resource
type: string
produces: produces:
- application/json - application/json
responses: responses:
@ -303,6 +310,56 @@ paths:
schema: schema:
type: string type: string
summary: Get current server configuration summary: Get current server configuration
post:
consumes:
- application/octet-stream
description: Upload certificates and public keys for verifying signatures
parameters:
- description: specify resource
enum:
- signatures
in: query
name: resource
required: true
type: string
- description: specify signing tool
enum:
- cosign
- notation
in: query
name: tool
required: true
type: string
- description: truststore type
in: query
name: truststoreType
type: string
- description: truststore name
in: query
name: truststoreName
type: string
- description: Public key or Certificate content
in: body
name: requestBody
required: true
schema:
type: string
produces:
- application/json
responses:
"200":
description: ok
schema:
type: string
"400":
description: bad request".
schema:
type: string
"500":
description: internal server error".
schema:
type: string
summary: Upload certificates and public keys for verifying signatures
/v2/_zot/ext/userprefs: /v2/_zot/ext/userprefs:
put: put:
consumes: consumes: