0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00
zot/pkg/extensions/extension_image_trust_test.go
Andrei Aaron ba6f347d8d
refactor(pkg/test): split logic in pkg/test/common.go into multiple packages (#1861)
Which could be imported independently. See more details:
1. "zotregistry.io/zot/pkg/test/common" - currently used as
   tcommon "zotregistry.io/zot/pkg/test/common" - inside pkg/test
   test "zotregistry.io/zot/pkg/test/common" - in tests
   . "zotregistry.io/zot/pkg/test/common" - in tests
Decouple zb from code in test/pkg in order to keep the size small.

2. "zotregistry.io/zot/pkg/test/image-utils" - curently used as
   . "zotregistry.io/zot/pkg/test/image-utils"

3. "zotregistry.io/zot/pkg/test/deprecated" -  curently used as
   "zotregistry.io/zot/pkg/test/deprecated"
This one will bre replaced gradually by image-utils in the future.

4. "zotregistry.io/zot/pkg/test/signature" - (cosign + notation) use as
   "zotregistry.io/zot/pkg/test/signature"

5. "zotregistry.io/zot/pkg/test/auth" - (bearer + oidc)  curently used as
   authutils "zotregistry.io/zot/pkg/test/auth"

 6. "zotregistry.io/zot/pkg/test/oci-utils" -  curently used as
   ociutils "zotregistry.io/zot/pkg/test/oci-utils"

Some unused functions were removed, some were replaced, and in
a few cases specific funtions were moved to the files they were used in.

Added an interface for the StoreController, this reduces the number of imports
of the entire image store, decreasing binary size for tests.
If the zb code was still coupled with pkg/test, this would have reflected in zb size.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-09-27 11:34:48 -07:00

1010 lines
35 KiB
Go

//go:build search && imagetrust
// +build search,imagetrust
package extensions_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"testing"
"time"
guuid "github.com/gofrs/uuid"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
test "zotregistry.io/zot/pkg/test/common"
. "zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/signature"
)
type errReader int
func (errReader) Read(p []byte) (int, error) {
return 0, fmt.Errorf("test error") //nolint:goerr113
}
func TestSignatureHandlers(t *testing.T) {
conf := config.New()
log := log.NewLogger("debug", "")
trust := extensions.ImageTrust{
Conf: conf,
Log: log,
}
Convey("Test error handling when Cosign handler reads the request body", t, func() {
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, "baseURL", errReader(0))
response := httptest.NewRecorder()
trust.HandleCosignPublicKeyUpload(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
Convey("Test error handling when Notation handler reads the request body", t, func() {
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, "baseURL", errReader(0))
query := request.URL.Query()
request.URL.RawQuery = query.Encode()
response := httptest.NewRecorder()
trust.HandleNotationCertificateUpload(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
}
func TestSignaturesAllowedMethodsHeader(t *testing.T) {
defaultVal := true
Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultVal
conf.Extensions.Trust.Cosign = defaultVal
conf.Extensions.Trust.Notation = defaultVal
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctrlManager := test.NewControllerManager(ctlr)
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullCosign)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
resp, _ = resty.R().Options(baseURL + constants.FullNotation)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}
func TestSignatureUploadAndVerificationLocal(t *testing.T) {
Convey("test with local storage", t, func() {
var cacheDriverParams map[string]interface{}
RunSignatureUploadAndVerificationTests(t, cacheDriverParams)
})
}
func TestSignatureUploadAndVerificationAWS(t *testing.T) {
skipIt(t)
Convey("test with AWS", t, func() {
uuid, err := guuid.NewV4()
So(err, ShouldBeNil)
cacheTablename := "BlobTable" + uuid.String()
repoMetaTablename := "RepoMetadataTable" + uuid.String()
manifestDataTablename := "ManifestDataTable" + uuid.String()
versionTablename := "Version" + uuid.String()
indexDataTablename := "IndexDataTable" + uuid.String()
userDataTablename := "UserDataTable" + uuid.String()
apiKeyTablename := "ApiKeyTable" + uuid.String()
cacheDriverParams := map[string]interface{}{
"name": "dynamoDB",
"endpoint": os.Getenv("DYNAMODBMOCK_ENDPOINT"),
"region": "us-east-2",
"cacheTablename": cacheTablename,
"repoMetaTablename": repoMetaTablename,
"manifestDataTablename": manifestDataTablename,
"indexDataTablename": indexDataTablename,
"userDataTablename": userDataTablename,
"apiKeyTablename": apiKeyTablename,
"versionTablename": versionTablename,
}
t.Logf("using dynamo driver options: %v", cacheDriverParams)
RunSignatureUploadAndVerificationTests(t, cacheDriverParams)
})
}
func RunSignatureUploadAndVerificationTests(t *testing.T, cacheDriverParams map[string]interface{}) { //nolint: thelper
repo := "repo"
tag := "0.0.1"
certName := "test"
defaultValue := true
imageQuery := `
{
Image(image:"%s:%s"){
RepoName Tag Digest IsSigned
Manifests {
Digest
SignatureInfo { Tool IsTrusted Author }
}
SignatureInfo { Tool IsTrusted Author }
}
}`
Convey("Verify cosign public key upload without search or notation being enabled", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// upload the public key
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
options.KeyOpts{KeyRef: path.Join(keyDir, "cosign.key"), PassFunc: generate.GetPass},
options.SignOptions{
Registry: options.RegistryOptions{AllowInsecure: true},
AnnotationOptions: options.AnnotationOptions{Annotations: []string{fmt.Sprintf("tag=%s", tag)}},
Upload: true,
},
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repo, image.DigestStr())})
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity completed", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished generating tasks for updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished resetting task generator for updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
Convey("Verify notation certificate upload without search or cosign being enabled", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
rootDir := t.TempDir()
signature.NotationPathLock.Lock()
defer signature.NotationPathLock.Unlock()
signature.LoadNotationPath(rootDir)
// generate a keypair
err = signature.GenerateNotationCerts(rootDir, certName)
So(err, ShouldBeNil)
// upload the certificate
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", fmt.Sprintf("%s.crt", certName)))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag))
err = signature.SignWithNotation(certName, imageURL, rootDir)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity completed", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
Convey("Verify uploading notation certificates", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
// Verify the image is initially shown as not being signed
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.IsSigned, ShouldEqual, false)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 0)
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 0)
rootDir := t.TempDir()
signature.NotationPathLock.Lock()
defer signature.NotationPathLock.Unlock()
signature.LoadNotationPath(rootDir)
// generate a keypair
err = signature.GenerateNotationCerts(rootDir, certName)
So(err, ShouldBeNil)
// upload the certificate
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", fmt.Sprintf("%s.crt", certName)))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag))
err = signature.SignWithNotation(certName, imageURL, rootDir)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity completed", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err = resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse = zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary = imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.IsSigned, ShouldEqual, true)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "notation")
So(imgSummary.SignatureInfo[0].Author,
ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US")
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "notation")
So(imgSummary.Manifests[0].SignatureInfo[0].Author,
ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US")
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Verify uploading cosign public keys", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
// Verify the image is initially shown as not being signed
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.IsSigned, ShouldEqual, false)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 0)
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 0)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// upload the public key
client := resty.New()
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
options.KeyOpts{KeyRef: path.Join(keyDir, "cosign.key"), PassFunc: generate.GetPass},
options.SignOptions{
Registry: options.RegistryOptions{AllowInsecure: true},
AnnotationOptions: options.AnnotationOptions{Annotations: []string{fmt.Sprintf("tag=%s", tag)}},
Upload: true,
},
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repo, image.DigestStr())})
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity completed", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err = resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse = zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary = imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(imgSummary.IsSigned, ShouldEqual, true)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.SignatureInfo[0].Author, ShouldEqual, string(publicKeyContent))
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.Manifests[0].SignatureInfo[0].Author, ShouldEqual, string(publicKeyContent))
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Verify uploading cosign public keys with auth configured", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
testCreds := test.GetCredString("admin", "admin") + "\n" + test.GetCredString("test", "test")
htpasswdPath := test.MakeHtpasswdFileFromString(testCreds)
defer os.Remove(htpasswdPath)
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.AccessControl = &config.AccessControlConfig{
AdminPolicy: config.Policy{
Users: []string{"admin"},
Actions: []string{},
},
}
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// fail to upload the public key without credentials
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// fail to upload the public key with bad credentials
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// upload the public key using credentials and non-admin user
resp, err = client.R().SetBasicAuth("test", "test").SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// upload the public key using credentials and admin user
resp, err = client.R().SetBasicAuth("admin", "admin").SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
Convey("Verify signatures are read from the disk and updated in the DB when zot starts", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
// Write image
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
// Write signature
sig := CreateImageWith().RandomLayers(1, 2).RandomConfig().Build()
So(err, ShouldBeNil)
ref, err := signature.GetCosignSignatureTagForManifest(image.Manifest)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(sig, repo, ref, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity completed", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(imgSummary.IsSigned, ShouldEqual, true)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, false)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.SignatureInfo[0].Author, ShouldEqual, "")
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, false)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.Manifests[0].SignatureInfo[0].Author, ShouldEqual, "")
})
Convey("Verify failures when saving uploaded certificates and public keys", func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
if cacheDriverParams != nil {
conf.Storage.CacheDriver = cacheDriverParams
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
rootDir := t.TempDir()
signature.NotationPathLock.Lock()
defer signature.NotationPathLock.Unlock()
signature.LoadNotationPath(rootDir)
// generate Notation cert
err := signature.GenerateNotationCerts(rootDir, "test")
So(err, ShouldBeNil)
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "test.crt"))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
// generate Cosign keys
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// Make sure the write to disk fails
So(os.Chmod(globalDir, 0o000), ShouldBeNil)
defer func() {
So(os.Chmod(globalDir, 0o755), ShouldBeNil)
}()
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
}
func skipIt(t *testing.T) {
t.Helper()
if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" {
t.Skip("Skipping testing without AWS mock server")
}
}