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")
|
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")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
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/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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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"
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue