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:
parent
49e4d93f42
commit
41b05c60dd
19 changed files with 1575 additions and 193 deletions
|
@ -97,4 +97,7 @@ var (
|
|||
ErrSyncImageNotSigned = errors.New("sync: image is not signed")
|
||||
ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config")
|
||||
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")
|
||||
)
|
||||
|
|
|
@ -361,6 +361,12 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
|
|||
|
||||
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 {
|
||||
|
|
|
@ -4,15 +4,27 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"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 {
|
||||
|
@ -70,23 +82,38 @@ type mgmt struct {
|
|||
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 {
|
||||
return http.HandlerFunc(func(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)
|
||||
var resource string
|
||||
|
||||
if queryHasParams(r.URL.Query(), []string{"resource"}) {
|
||||
resource = r.URL.Query().Get("resource")
|
||||
} 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}
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
||||
|
||||
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
"zotregistry.io/zot/pkg/scheduler"
|
||||
)
|
||||
|
||||
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," +
|
||||
"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")
|
||||
}
|
||||
|
|
54
pkg/extensions/extension_mgmt_disabled_test.go
Normal file
54
pkg/extensions/extension_mgmt_disabled_test.go
Normal 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")
|
||||
})
|
||||
}
|
|
@ -9,7 +9,9 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
|
@ -20,6 +22,10 @@ import (
|
|||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
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"
|
||||
)
|
||||
|
||||
|
@ -518,6 +524,158 @@ func TestMgmtExtension(t *testing.T) {
|
|||
data, _ := os.ReadFile(logFile.Name())
|
||||
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) {
|
||||
|
@ -694,7 +852,7 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
|
|||
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,7 +10,12 @@ Response depends on the user privileges:
|
|||
| Supported queries | Input | Output | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -3,15 +3,8 @@ package bolt_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
@ -1013,169 +1006,6 @@ func TestWrapperErrors(t *testing.T) {
|
|||
err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig"))
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("VerifySignature -> untrusted signature", func() {
|
||||
err := boltdbWrapper.SetManifestData(digest.FromString("dig"), repodb.ManifestData{
|
||||
ManifestBlob: []byte("Bad Manifest"),
|
||||
ConfigBlob: []byte("Bad Manifest"),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket))
|
||||
|
||||
return repoBuck.Put([]byte("repo1"), repoMetaBlob)
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
layerInfo := repodb.LayerInfo{LayerDigest: "", LayerContent: []byte{}, SignatureKey: ""}
|
||||
|
||||
err = boltdbWrapper.AddManifestSignature("repo1", digest.FromString("dig"),
|
||||
repodb.SignatureMetadata{
|
||||
SignatureType: signatures.CosignSignature,
|
||||
SignatureDigest: string(digest.FromString("signature digest")),
|
||||
LayersInfo: []repodb.LayerInfo{layerInfo},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.UpdateSignaturesValidity("repo1", digest.FromString("dig"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
repoData, err := boltdbWrapper.GetRepoMeta("repo1")
|
||||
So(err, ShouldBeNil)
|
||||
So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Signer,
|
||||
ShouldBeEmpty)
|
||||
So(repoData.Signatures[string(digest.FromString("dig"))][signatures.CosignSignature][0].LayersInfo[0].Date,
|
||||
ShouldBeZeroValue)
|
||||
})
|
||||
|
||||
Convey("VerifySignature -> trusted signature", func() {
|
||||
_, _, manifest, _ := test.GetRandomImageComponents(10)
|
||||
manifestContent, _ := json.Marshal(manifest)
|
||||
manifestDigest := digest.FromBytes(manifestContent)
|
||||
|
||||
err := boltdbWrapper.SetManifestData(manifestDigest, repodb.ManifestData{
|
||||
ManifestBlob: manifestContent,
|
||||
ConfigBlob: []byte("configContent"),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
repoBuck := tx.Bucket([]byte(bolt.RepoMetadataBucket))
|
||||
|
||||
return repoBuck.Put([]byte("repo"), repoMetaBlob)
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
mediaType := jws.MediaTypeEnvelope
|
||||
|
||||
signOpts := notation.SignerSignOptions{
|
||||
SignatureMediaType: mediaType,
|
||||
PluginConfig: map[string]string{},
|
||||
ExpiryDuration: 24 * time.Hour,
|
||||
}
|
||||
|
||||
tdir := t.TempDir()
|
||||
keyName := "notation-sign-test"
|
||||
|
||||
test.NotationPathLock.Lock()
|
||||
defer test.NotationPathLock.Unlock()
|
||||
|
||||
test.LoadNotationPath(tdir)
|
||||
|
||||
err = test.GenerateNotationCerts(tdir, keyName)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// getSigner
|
||||
var newSigner notation.Signer
|
||||
|
||||
// ResolveKey
|
||||
signingKeys, err := test.LoadNotationSigningkeys(tdir)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
idx := test.Index(signingKeys.Keys, keyName)
|
||||
So(idx, ShouldBeGreaterThanOrEqualTo, 0)
|
||||
|
||||
key := signingKeys.Keys[idx]
|
||||
|
||||
if key.X509KeyPair != nil {
|
||||
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
descToSign := ispec.Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Digest: manifestDigest,
|
||||
Size: int64(len(manifestContent)),
|
||||
}
|
||||
sig, _, err := newSigner.Sign(ctx, descToSign, signOpts)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
layerInfo := repodb.LayerInfo{
|
||||
LayerDigest: string(digest.FromBytes(sig)),
|
||||
LayerContent: sig, SignatureKey: mediaType,
|
||||
}
|
||||
|
||||
err = boltdbWrapper.AddManifestSignature("repo", manifestDigest,
|
||||
repodb.SignatureMetadata{
|
||||
SignatureType: signatures.NotationSignature,
|
||||
SignatureDigest: string(digest.FromString("signature digest")),
|
||||
LayersInfo: []repodb.LayerInfo{layerInfo},
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = signatures.InitNotationDir(tdir)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
trustpolicyPath := path.Join(tdir, "_notation/trustpolicy.json")
|
||||
|
||||
if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
|
||||
trustPolicy := `
|
||||
{
|
||||
"version": "1.0",
|
||||
"trustPolicies": [
|
||||
{
|
||||
"name": "notation-sign-test",
|
||||
"registryScopes": [ "*" ],
|
||||
"signatureVerification": {
|
||||
"level" : "strict"
|
||||
},
|
||||
"trustStores": ["ca:notation-sign-test"],
|
||||
"trustedIdentities": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
file, err := os.Create(trustpolicyPath)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString(trustPolicy)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
truststore := "_notation/truststore/x509/ca/notation-sign-test"
|
||||
truststoreSrc := "notation/truststore/x509/ca/notation-sign-test"
|
||||
err = os.MkdirAll(path.Join(tdir, truststore), 0o755)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = test.CopyFile(path.Join(tdir, truststoreSrc, "notation-sign-test.crt"),
|
||||
path.Join(tdir, truststore, "notation-sign-test.crt"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.UpdateSignaturesValidity("repo", manifestDigest) //nolint:contextcheck
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
repoData, err := boltdbWrapper.GetRepoMeta("repo")
|
||||
So(err, ShouldBeNil)
|
||||
So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Signer,
|
||||
ShouldNotBeEmpty)
|
||||
So(repoData.Signatures[string(manifestDigest)][signatures.NotationSignature][0].LayersInfo[0].Date,
|
||||
ShouldNotBeZeroValue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -563,6 +563,25 @@ func TestWrapperErrors(t *testing.T) {
|
|||
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() {
|
||||
err := dynamoWrapper.SetRepoReference("repo", "tag", "dig", "")
|
||||
So(err, ShouldBeNil)
|
||||
|
|
|
@ -618,7 +618,67 @@ func (dwr *DBWrapper) IncrementImageDownloads(repo string, reference string) err
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -13,6 +13,9 @@ import (
|
|||
"time"
|
||||
|
||||
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"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
@ -25,6 +28,7 @@ import (
|
|||
"zotregistry.io/zot/pkg/meta/repodb"
|
||||
boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
|
||||
dynamodb_wrapper "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper"
|
||||
"zotregistry.io/zot/pkg/meta/signatures"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
@ -71,7 +75,7 @@ func TestBoltDBWrapper(t *testing.T) {
|
|||
So(boltdbWrapper, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
RunRepoDBTests(boltdbWrapper)
|
||||
RunRepoDBTests(t, boltdbWrapper)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -122,11 +126,11 @@ func TestDynamoDBWrapper(t *testing.T) {
|
|||
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() {
|
||||
for _, prepFunc := range preparationFuncs {
|
||||
err := prepFunc()
|
||||
|
@ -994,6 +998,170 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) {
|
|||
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() {
|
||||
var (
|
||||
repo1 = "repo1"
|
||||
|
|
132
pkg/meta/signatures/README.md
Normal file
132
pkg/meta/signatures/README.md
Normal 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": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
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"
|
||||
"github.com/sigstore/sigstore/pkg/signature/options"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
|
@ -111,3 +112,32 @@ func VerifyCosignSignature(
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -2,19 +2,26 @@ package signatures
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
|
@ -22,7 +29,10 @@ import (
|
|||
|
||||
const notationDirRelativePath = "_notation"
|
||||
|
||||
var notationDir = "" //nolint:gochecknoglobals
|
||||
var (
|
||||
notationDir = "" //nolint:gochecknoglobals
|
||||
TrustpolicyLock = new(sync.Mutex) //nolint: gochecknoglobals
|
||||
)
|
||||
|
||||
func InitNotationDir(rootDir string) error {
|
||||
dir := path.Join(rootDir, notationDirRelativePath)
|
||||
|
@ -37,11 +47,48 @@ func InitNotationDir(rootDir string) error {
|
|||
|
||||
if err == nil {
|
||||
notationDir = dir
|
||||
|
||||
if _, err := LoadTrustPolicyDocument(notationDir); os.IsNotExist(err) {
|
||||
return InitTrustpolicyFile(notationDir)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if notationDir != "" {
|
||||
return notationDir, nil
|
||||
|
@ -79,6 +126,9 @@ func NewFromConfig() (notation.Verifier, error) {
|
|||
}
|
||||
|
||||
// Load trust policy.
|
||||
TrustpolicyLock.Lock()
|
||||
defer TrustpolicyLock.Unlock()
|
||||
|
||||
policyDocument, err := LoadTrustPolicyDocument(notationDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -165,3 +215,112 @@ func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter t
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ const (
|
|||
CosignSignature = "cosign"
|
||||
NotationSignature = "notation"
|
||||
defaultDirPerms = 0o700
|
||||
defaultFilePerms = 0o644
|
||||
)
|
||||
|
||||
func InitCosignAndNotationDirs(rootDir string) error {
|
||||
|
|
|
@ -56,6 +56,9 @@ func TestInitCosignAndNotationDirs(t *testing.T) {
|
|||
err = signatures.InitCosignAndNotationDirs(dir)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = signatures.InitNotationDir(dir)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = os.Chmod(dir, 0o500)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -70,6 +73,51 @@ func TestInitCosignAndNotationDirs(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
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) {
|
||||
|
@ -278,6 +326,11 @@ func TestVerifySignatures(t *testing.T) {
|
|||
err := signatures.InitNotationDir(dir)
|
||||
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)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
@ -292,7 +345,7 @@ func TestVerifySignatures(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
|
||||
err = test.WriteFileWithPermission(path.Join(notationDir, "trustpolicy.json"), []byte("invalid content"),
|
||||
0o600, false)
|
||||
0o600, true)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, _, _, 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)
|
||||
|
||||
indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo)
|
||||
|
@ -437,3 +490,222 @@ func TestCheckExpiryErr(t *testing.T) {
|
|||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -151,6 +151,17 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"summary": "Get current server configuration",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"config"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify resource",
|
||||
"name": "resource",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"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": {
|
||||
|
|
|
@ -142,6 +142,17 @@
|
|||
"application/json"
|
||||
],
|
||||
"summary": "Get current server configuration",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"config"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify resource",
|
||||
"name": "resource",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"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": {
|
||||
|
|
|
@ -291,6 +291,13 @@ paths:
|
|||
consumes:
|
||||
- application/json
|
||||
description: Get current server configuration
|
||||
parameters:
|
||||
- description: specify resource
|
||||
enum:
|
||||
- config
|
||||
in: query
|
||||
name: resource
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -303,6 +310,56 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
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:
|
||||
put:
|
||||
consumes:
|
||||
|
|
Loading…
Reference in a new issue