Files
zot/pkg/test/common.go
T
peusebiu 1df743f173 fix(gc): sync repodb when gc'ing manifests (#1819)
fix(gc): fix cleaning deduped blobs because they have the modTime of
the original blobs, fixed by updating the modTime when hard linking
the blobs.
fix(gc): failing to parse rootDir at zot startup when using s3 storage
because there are no files under rootDir and we can not create empty dirs
on s3, fixed by creating an empty file under rootDir.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
2023-09-22 11:51:20 -07:00

1610 lines
36 KiB
Go

package test
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/fs"
"log"
"math"
"math/big"
"net"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-core-go/testhelper"
"github.com/notaryproject/notation-go"
notconfig "github.com/notaryproject/notation-go/config"
"github.com/notaryproject/notation-go/dir"
notreg "github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/signer"
"github.com/notaryproject/notation-go/verifier"
godigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci"
"github.com/phayes/freeport"
"github.com/project-zot/mockoidc"
"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"
"golang.org/x/crypto/bcrypt"
"gopkg.in/resty.v1"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"zotregistry.io/zot/pkg/extensions/monitoring"
zLog "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
stypes "zotregistry.io/zot/pkg/storage/types"
testc "zotregistry.io/zot/pkg/test/common"
"zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/inject"
"zotregistry.io/zot/pkg/test/mocks"
)
const (
BaseURL = "http://127.0.0.1:%s"
BaseSecureURL = "https://127.0.0.1:%s"
SleepTime = 100 * time.Millisecond
)
var (
ErrSignatureVerification = errors.New("signature verification failed")
ErrAlreadyExists = errors.New("already exists")
ErrKeyNotFound = errors.New("key not found")
)
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
// which: manifest, config, layer
func GetTestBlobDigest(image, which string) godigest.Digest {
prePath := "../test/data"
for _, err := os.Stat(prePath); err != nil; _, err = os.Stat(prePath) {
prePath = "../" + prePath
}
imgPath := path.Join(prePath, image)
manifest, config, layer := GetOciLayoutDigests(imgPath)
switch which {
case "manifest":
return manifest
case "config":
return config
case "layer":
return layer
}
return ""
}
func GetFreePort() string {
port, err := freeport.GetFreePort()
if err != nil {
panic(err)
}
return fmt.Sprint(port)
}
func GetBaseURL(port string) string {
return fmt.Sprintf(BaseURL, port)
}
func GetSecureBaseURL(port string) string {
return fmt.Sprintf(BaseSecureURL, port)
}
func MakeHtpasswdFile() string {
// bcrypt(username="test", passwd="test")
content := "test:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n"
return MakeHtpasswdFileFromString(content)
}
func GetCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
return usernameAndHash
}
func MakeHtpasswdFileFromString(fileContent string) string {
htpasswdFile, err := os.CreateTemp("", "htpasswd-")
if err != nil {
panic(err)
}
// bcrypt(username="test", passwd="test")
content := []byte(fileContent)
if err := os.WriteFile(htpasswdFile.Name(), content, 0o600); err != nil { //nolint:gomnd
panic(err)
}
return htpasswdFile.Name()
}
func CopyFiles(sourceDir, destDir string) error {
sourceMeta, err := os.Stat(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
}
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
return fmt.Errorf("CopyFiles os.MkdirAll failed: %w", err)
}
files, err := os.ReadDir(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.ReadDir failed: %w", err)
}
for _, file := range files {
sourceFilePath := path.Join(sourceDir, file.Name())
destFilePath := path.Join(destDir, file.Name())
if file.IsDir() {
if strings.HasPrefix(file.Name(), "_") {
// Some tests create the trivy related folders under test/_trivy
continue
}
if err = CopyFiles(sourceFilePath, destFilePath); err != nil {
return err
}
} else {
sourceFile, err := os.Open(sourceFilePath)
if err != nil {
return fmt.Errorf("CopyFiles os.Open failed: %w", err)
}
defer sourceFile.Close()
destFile, err := os.Create(destFilePath)
if err != nil {
return fmt.Errorf("CopyFiles os.Create failed: %w", err)
}
defer destFile.Close()
if _, err = io.Copy(destFile, sourceFile); err != nil {
return fmt.Errorf("io.Copy failed: %w", err)
}
}
}
return nil
}
func CopyTestFiles(sourceDir, destDir string) {
err := CopyFiles(sourceDir, destDir)
if err != nil {
panic(err)
}
}
func CopyTestKeysAndCerts(destDir string) error {
files := []string{
"ca.crt", "ca.key", "client.cert", "client.csr",
"client.key", "server.cert", "server.csr", "server.key",
}
rootPath, err := testc.GetProjectRootDir()
if err != nil {
return err
}
sourceDir := filepath.Join(rootPath, "test/data")
sourceMeta, err := os.Stat(sourceDir)
if err != nil {
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
}
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
return err
}
for _, file := range files {
err = CopyFile(filepath.Join(sourceDir, file), filepath.Join(destDir, file))
if err != nil {
return err
}
}
return nil
}
type Controller interface {
Init(ctx context.Context) error
Run(ctx context.Context) error
Shutdown()
GetPort() int
}
type ControllerManager struct {
controller Controller
// used to stop background tasks(goroutines)
cancelRoutinesFunc context.CancelFunc
}
func (cm *ControllerManager) RunServer(ctx context.Context) {
// Useful to be able to call in the same goroutine for testing purposes
if err := cm.controller.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
func (cm *ControllerManager) StartServer() {
ctx, cancel := context.WithCancel(context.Background())
cm.cancelRoutinesFunc = cancel
if err := cm.controller.Init(ctx); err != nil {
panic(err)
}
go func() {
cm.RunServer(ctx)
}()
}
func (cm *ControllerManager) StopServer() {
// stop background tasks
if cm.cancelRoutinesFunc != nil {
cm.cancelRoutinesFunc()
}
cm.controller.Shutdown()
}
func (cm *ControllerManager) WaitServerToBeReady(port string) {
url := GetBaseURL(port)
WaitTillServerReady(url)
}
func (cm *ControllerManager) StartAndWait(port string) {
cm.StartServer()
url := GetBaseURL(port)
WaitTillServerReady(url)
}
func NewControllerManager(controller Controller) ControllerManager {
cm := ControllerManager{
controller: controller,
}
return cm
}
func WriteImageToFileSystem(image image.Image, repoName, ref string, storeController storage.StoreController) error {
store := storeController.GetImageStore(repoName)
err := store.InitRepo(repoName)
if err != nil {
return err
}
for _, layerBlob := range image.Layers {
layerReader := bytes.NewReader(layerBlob)
layerDigest := godigest.FromBytes(layerBlob)
_, _, err = store.FullBlobUpload(repoName, layerReader, layerDigest)
if err != nil {
return err
}
}
configBlob, err := json.Marshal(image.Config)
if err != nil {
return err
}
configReader := bytes.NewReader(configBlob)
configDigest := godigest.FromBytes(configBlob)
_, _, err = store.FullBlobUpload(repoName, configReader, configDigest)
if err != nil {
return err
}
manifestBlob, err := json.Marshal(image.Manifest)
if err != nil {
return err
}
_, _, err = store.PutImageManifest(repoName, ref, ispec.MediaTypeImageManifest, manifestBlob)
if err != nil {
return err
}
return nil
}
func WriteMultiArchImageToFileSystem(multiarchImage image.MultiarchImage, repoName, ref string,
storeController storage.StoreController,
) error {
store := storeController.GetImageStore(repoName)
err := store.InitRepo(repoName)
if err != nil {
return err
}
for _, image := range multiarchImage.Images {
err := WriteImageToFileSystem(image, repoName, image.DigestStr(), storeController)
if err != nil {
return err
}
}
indexBlob, err := json.Marshal(multiarchImage.Index)
if err != nil {
return err
}
_, _, err = store.PutImageManifest(repoName, ref, ispec.MediaTypeImageIndex,
indexBlob)
return err
}
func WaitTillServerReady(url string) {
for {
_, err := resty.R().Get(url)
if err == nil {
break
}
time.Sleep(SleepTime)
}
}
func WaitTillTrivyDBDownloadStarted(rootDir string) {
for {
if _, err := os.Stat(path.Join(rootDir, "_trivy", "db", "trivy.db")); err == nil {
break
}
time.Sleep(SleepTime)
}
}
// Adapted from https://gist.github.com/dopey/c69559607800d2f2f90b1b1ed4e550fb
func RandomString(n int) string {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for count := 0; count < n; count++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
panic(err)
}
ret[count] = letters[num.Int64()]
}
return string(ret)
}
func GetRandomImageConfig() ([]byte, godigest.Digest) {
const maxLen = 16
randomAuthor := RandomString(maxLen)
config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{},
},
Author: randomAuthor,
}
configBlobContent, err := json.MarshalIndent(&config, "", "\t")
if err != nil {
log.Fatal(err)
}
configBlobDigestRaw := godigest.FromBytes(configBlobContent)
return configBlobContent, configBlobDigestRaw
}
func GetOciLayoutDigests(imagePath string) (godigest.Digest, godigest.Digest, godigest.Digest) {
var (
manifestDigest godigest.Digest
configDigest godigest.Digest
layerDigest godigest.Digest
)
oci, err := umoci.OpenLayout(imagePath)
if err != nil {
panic(fmt.Errorf("error opening layout at '%s' : %w", imagePath, err))
}
defer oci.Close()
ctxUmoci := context.Background()
index, err := oci.GetIndex(ctxUmoci)
if err != nil {
panic(err)
}
for _, manifest := range index.Manifests {
manifestDigest = manifest.Digest
manifestBlob, err := oci.GetBlob(ctxUmoci, manifest.Digest)
if err != nil {
panic(err)
}
manifestBuf, err := io.ReadAll(manifestBlob)
if err != nil {
panic(err)
}
var manifest ispec.Manifest
err = json.Unmarshal(manifestBuf, &manifest)
if err != nil {
panic(err)
}
configDigest = manifest.Config.Digest
for _, layer := range manifest.Layers {
layerDigest = layer.Digest
}
}
return manifestDigest, configDigest, layerDigest
}
// Deprecated: Should use the new functions starting with "Create".
func GetImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manifest, error) {
config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{},
},
Author: "ZotUser",
}
configBlob, err := json.Marshal(config)
if err = inject.Error(err); err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
configDigest := godigest.FromBytes(configBlob)
layers := [][]byte{
make([]byte, layerSize),
}
schemaVersion := 2
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: godigest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
},
}
return config, layers, manifest, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manifest, error) {
config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{},
},
Author: "ZotUser",
}
configBlob, err := json.Marshal(config)
if err = inject.Error(err); err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
configDigest := godigest.FromBytes(configBlob)
layers := [][]byte{
GetRandomLayer(layerSize),
}
schemaVersion := 2
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: godigest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
},
}
return config, layers, manifest, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetVulnImageWithConfig(config ispec.Image) (image.Image, error) {
vulnerableLayer, err := image.GetLayerWithVulnerability()
if err != nil {
return image.Image{}, err
}
vulnerableConfig := ispec.Image{
Platform: config.Platform,
Config: config.Config,
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
Created: config.Created,
History: config.History,
}
img, err := GetImageWithComponents(
vulnerableConfig,
[][]byte{
vulnerableLayer,
})
if err != nil {
return image.Image{}, err
}
return img, err
}
func GetRandomLayer(size int) []byte {
layer := make([]byte, size)
_, err := rand.Read(layer)
if err != nil {
return layer
}
return layer
}
// Deprecated: Should use the new functions starting with "Create".
func GetRandomImage() (image.Image, error) {
const layerSize = 20
config, layers, manifest, err := GetRandomImageComponents(layerSize)
if err != nil {
return image.Image{}, err
}
return image.Image{
Manifest: manifest,
Layers: layers,
Config: config,
}, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetImageComponentsWithConfig(conf ispec.Image) (ispec.Image, [][]byte, ispec.Manifest, error) {
configBlob, err := json.Marshal(conf)
if err = inject.Error(err); err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
configDigest := godigest.FromBytes(configBlob)
layerSize := 100
layer := make([]byte, layerSize)
_, err = rand.Read(layer)
if err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
layers := [][]byte{
layer,
}
schemaVersion := 2
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: godigest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
},
}
return conf, layers, manifest, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetImageWithConfig(conf ispec.Image) (image.Image, error) {
config, layers, manifest, err := GetImageComponentsWithConfig(conf)
if err != nil {
return image.Image{}, err
}
return image.Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetImageWithComponents(config ispec.Image, layers [][]byte) (image.Image, error) {
configBlob, err := json.Marshal(config)
if err != nil {
return image.Image{}, err
}
manifestLayers := make([]ispec.Descriptor, 0, len(layers))
for _, layer := range layers {
manifestLayers = append(manifestLayers, ispec.Descriptor{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: godigest.FromBytes(layer),
Size: int64(len(layer)),
})
}
const schemaVersion = 2
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: godigest.FromBytes(configBlob),
Size: int64(len(configBlob)),
},
Layers: manifestLayers,
}
return image.Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, nil
}
func GetCosignSignatureTagForManifest(manifest ispec.Manifest) (string, error) {
manifestBlob, err := json.Marshal(manifest)
if err != nil {
return "", err
}
manifestDigest := godigest.FromBytes(manifestBlob)
return GetCosignSignatureTagForDigest(manifestDigest), nil
}
func GetCosignSignatureTagForDigest(manifestDigest godigest.Digest) string {
return manifestDigest.Algorithm().String() + "-" + manifestDigest.Encoded() + ".sig"
}
// Deprecated: Should use the new functions starting with "Create".
func GetImageWithSubject(subjectDigest godigest.Digest, mediaType string) (image.Image, error) {
num := 100
conf, layers, manifest, err := GetRandomImageComponents(num)
if err != nil {
return image.Image{}, err
}
manifest.Subject = &ispec.Descriptor{
Digest: subjectDigest,
MediaType: mediaType,
}
return image.Image{
Manifest: manifest,
Config: conf,
Layers: layers,
}, nil
}
func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
for {
select {
case <-ctx.Done():
return false, nil
default:
content, err := os.ReadFile(logPath)
if err != nil {
return false, err
}
if strings.Contains(string(content), stringToMatch) {
return true, nil
}
}
}
}
func ReadLogFileAndCountStringOccurence(logPath string, stringToMatch string,
timeout time.Duration, count int,
) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
for {
select {
case <-ctx.Done():
return false, nil
default:
content, err := os.ReadFile(logPath)
if err != nil {
return false, err
}
if strings.Count(string(content), stringToMatch) >= count {
return true, nil
}
}
}
}
func CopyFile(sourceFilePath, destFilePath string) error {
destFile, err := os.Create(destFilePath)
if err != nil {
return err
}
defer destFile.Close()
sourceFile, err := os.Open(sourceFilePath)
if err != nil {
return err
}
defer sourceFile.Close()
if _, err = io.Copy(destFile, sourceFile); err != nil {
return err
}
return nil
}
func LoadNotationPath(tdir string) {
dir.UserConfigDir = filepath.Join(tdir, "notation")
// set user libexec
dir.UserLibexecDir = dir.UserConfigDir
}
func GenerateNotationCerts(tdir string, certName string) error {
// generate RSA private key
bits := 2048
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return err
}
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert")
certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw})
// write private key
relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName)
configFS := dir.ConfigFS()
keyPath, err := configFS.SysPath(relativeKeyPath)
if err != nil {
return err
}
certPath, err := configFS.SysPath(relativeCertPath)
if err != nil {
return err
}
if err := WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd
return fmt.Errorf("failed to write key file: %w", err)
}
// write self-signed certificate
if err := WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd
return fmt.Errorf("failed to write certificate file: %w", err)
}
signingKeys, err := notconfig.LoadSigningKeys()
if err != nil {
return err
}
keySuite := notconfig.KeySuite{
Name: certName,
X509KeyPair: &notconfig.X509KeyPair{
KeyPath: keyPath,
CertificatePath: certPath,
},
}
// addKeyToSigningKeys
if Contains(signingKeys.Keys, keySuite.Name) {
return ErrAlreadyExists
}
signingKeys.Keys = append(signingKeys.Keys, keySuite)
// Add to the trust store
trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName))
if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil {
return ErrAlreadyExists
}
if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd
return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err)
}
trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension))
err = CopyFile(certPath, trustCertPath)
if err != nil {
return err
}
// Save to the SigningKeys.json
if err := signingKeys.Save(); err != nil {
return err
}
return nil
}
func SignWithNotation(keyName string, reference string, tdir string) error {
ctx := context.TODO()
// getSigner
var newSigner notation.Signer
mediaType := jws.MediaTypeEnvelope
// ResolveKey
signingKeys, err := LoadNotationSigningkeys(tdir)
if err != nil {
return err
}
idx := Index(signingKeys.Keys, keyName)
if idx < 0 {
return ErrKeyNotFound
}
key := signingKeys.Keys[idx]
if key.X509KeyPair != nil {
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
if err != nil {
return err
}
}
// prepareSigningContent
// getRepositoryClient
authClient := &auth.Client{
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
return auth.EmptyCredential, nil
},
Cache: auth.NewCache(),
ClientID: "notation",
}
authClient.SetUserAgent("notation/zot_tests")
plainHTTP := true
// Resolve referance
ref, err := registry.ParseReference(reference)
if err != nil {
return err
}
remoteRepo := &remote.Repository{
Client: authClient,
Reference: ref,
PlainHTTP: plainHTTP,
}
repositoryOpts := notreg.RepositoryOptions{}
sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
sigOpts := notation.SignOptions{
SignerSignOptions: notation.SignerSignOptions{
SignatureMediaType: mediaType,
PluginConfig: map[string]string{},
},
ArtifactReference: ref.String(),
}
_, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts)
if err != nil {
return err
}
return nil
}
func VerifyWithNotation(reference string, tdir string) error {
// check if trustpolicy.json exists
trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json")
if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
trustPolicy := `
{
"version": "1.0",
"trustPolicies": [
{
"name": "good",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "audit"
},
"trustStores": ["ca:good"],
"trustedIdentities": [
"*"
]
}
]
}`
file, err := os.Create(trustpolicyPath)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(trustPolicy)
if err != nil {
return err
}
}
// start verifying signatures
ctx := context.TODO()
// getRepositoryClient
authClient := &auth.Client{
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
return auth.EmptyCredential, nil
},
Cache: auth.NewCache(),
ClientID: "notation",
}
authClient.SetUserAgent("notation/zot_tests")
plainHTTP := true
// Resolve referance
ref, err := registry.ParseReference(reference)
if err != nil {
return err
}
remoteRepo := &remote.Repository{
Client: authClient,
Reference: ref,
PlainHTTP: plainHTTP,
}
repositoryOpts := notreg.RepositoryOptions{}
repo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
manifestDesc, err := repo.Resolve(ctx, ref.Reference)
if err != nil {
return err
}
if err := ref.ValidateReferenceAsDigest(); err != nil {
ref.Reference = manifestDesc.Digest.String()
}
// getVerifier
newVerifier, err := verifier.NewFromConfig()
if err != nil {
return err
}
remoteRepo = &remote.Repository{
Client: authClient,
Reference: ref,
PlainHTTP: plainHTTP,
}
repo = notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
configs := map[string]string{}
verifyOpts := notation.VerifyOptions{
ArtifactReference: ref.String(),
PluginConfig: configs,
MaxSignatureAttempts: math.MaxInt64,
}
_, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts)
if err != nil || len(outcomes) == 0 {
return ErrSignatureVerification
}
return nil
}
func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) {
signatures := []godigest.Digest{}
ctx := context.TODO()
// getSignatureRepository
ref, err := registry.ParseReference(reference)
if err != nil {
return signatures, err
}
plainHTTP := true
// getRepositoryClient
authClient := &auth.Client{
Credential: func(ctx context.Context, registry string) (auth.Credential, error) {
return auth.EmptyCredential, nil
},
Cache: auth.NewCache(),
ClientID: "notation",
}
authClient.SetUserAgent("notation/zot_tests")
remoteRepo := &remote.Repository{
Client: authClient,
Reference: ref,
PlainHTTP: plainHTTP,
}
sigRepo := notreg.NewRepository(remoteRepo)
artifactDesc, err := sigRepo.Resolve(ctx, reference)
if err != nil {
return signatures, err
}
err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
for _, sigManifestDesc := range signatureManifests {
signatures = append(signatures, sigManifestDesc.Digest)
}
return nil
})
return signatures, err
}
func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) {
var err error
var signingKeysInfo *notconfig.SigningKeys
filePath := path.Join(tdir, "notation/signingkeys.json")
file, err := os.Open(filePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// create file
newSigningKeys := notconfig.NewSigningKeys()
newFile, err := os.Create(filePath)
if err != nil {
return newSigningKeys, err
}
defer newFile.Close()
encoder := json.NewEncoder(newFile)
encoder.SetIndent("", " ")
err = encoder.Encode(newSigningKeys)
return newSigningKeys, err
}
return nil, err
}
defer file.Close()
err = json.NewDecoder(file).Decode(&signingKeysInfo)
return signingKeysInfo, err
}
func LoadNotationConfig(tdir string) (*notconfig.Config, error) {
var configInfo *notconfig.Config
filePath := path.Join(tdir, "notation/signingkeys.json")
file, err := os.Open(filePath)
if err != nil {
return configInfo, err
}
defer file.Close()
err = json.NewDecoder(file).Decode(&configInfo)
if err != nil {
return configInfo, err
}
// set default value
configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
if configInfo.SignatureFormat == "" {
configInfo.SignatureFormat = "jws"
}
return configInfo, nil
}
func WriteFileWithPermission(path string, data []byte, perm fs.FileMode, overwrite bool) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
flag := os.O_WRONLY | os.O_CREATE
if overwrite {
flag |= os.O_TRUNC
} else {
flag |= os.O_EXCL
}
file, err := os.OpenFile(path, flag, perm)
if err != nil {
return err
}
_, err = file.Write(data)
if err != nil {
file.Close()
return err
}
return file.Close()
}
func IsDigestReference(ref string) bool {
parts := strings.SplitN(ref, "/", 2) //nolint:gomnd
if len(parts) == 1 {
return false
}
index := strings.Index(parts[1], "@")
return index != -1
}
type isser interface {
Is(string) bool
}
// Index returns the index of the first occurrence of name in s,
// or -1 if not present.
func Index[E isser](s []E, name string) int {
for i, v := range s {
if v.Is(name) {
return i
}
}
return -1
}
// Contains reports whether name is present in s.
func Contains[E isser](s []E, name string) bool {
return Index(s, name) >= 0
}
func SignImageUsingCosign(repoTag, port string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer func() { _ = os.Chdir(cwd) }()
tdir, err := os.MkdirTemp("", "cosign")
if err != nil {
return err
}
defer os.RemoveAll(tdir)
_ = os.Chdir(tdir)
// generate a keypair
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
if err != nil {
return err
}
imageURL := fmt.Sprintf("localhost:%s/%s", port, repoTag)
const timeoutPeriod = 5
// sign the image
return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: timeoutPeriod * time.Minute},
options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
options.SignOptions{
Registry: options.RegistryOptions{AllowInsecure: true},
AnnotationOptions: options.AnnotationOptions{Annotations: []string{"tag=1.0"}},
Upload: true,
},
[]string{imageURL})
}
func SignImageUsingNotary(repoTag, port string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer func() { _ = os.Chdir(cwd) }()
tdir, err := os.MkdirTemp("", "notation")
if err != nil {
return err
}
defer os.RemoveAll(tdir)
_ = os.Chdir(tdir)
NotationPathLock.Lock()
defer NotationPathLock.Unlock()
LoadNotationPath(tdir)
// generate a keypair
err = GenerateNotationCerts(tdir, "notation-sign-test")
if err != nil {
return err
}
// sign the image
image := fmt.Sprintf("localhost:%s/%s", port, repoTag)
err = SignWithNotation("notation-sign-test", image, tdir)
return err
}
// Deprecated: Should use the new functions starting with "Create".
func GetRandomMultiarchImageComponents() (ispec.Index, []image.Image, error) {
const layerSize = 100
randomLayer1 := make([]byte, layerSize)
_, err := rand.Read(randomLayer1)
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
image1, err := GetImageWithComponents(
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
},
[][]byte{
randomLayer1,
})
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
randomLayer2 := make([]byte, layerSize)
_, err = rand.Read(randomLayer2)
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
image2, err := GetImageWithComponents(
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "386",
},
},
[][]byte{
randomLayer2,
})
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
randomLayer3 := make([]byte, layerSize)
_, err = rand.Read(randomLayer3)
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
image3, err := GetImageWithComponents(
ispec.Image{
Platform: ispec.Platform{
OS: "windows",
Architecture: "amd64",
},
},
[][]byte{
randomLayer3,
})
if err != nil {
return ispec.Index{}, []image.Image{}, err
}
index := ispec.Index{
MediaType: ispec.MediaTypeImageIndex,
Manifests: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageManifest,
Digest: getManifestDigest(image1.Manifest),
Size: getManifestSize(image1.Manifest),
},
{
MediaType: ispec.MediaTypeImageManifest,
Digest: getManifestDigest(image2.Manifest),
Size: getManifestSize(image2.Manifest),
},
{
MediaType: ispec.MediaTypeImageManifest,
Digest: getManifestDigest(image3.Manifest),
Size: getManifestSize(image3.Manifest),
},
},
}
return index, []image.Image{image1, image2, image3}, nil
}
// Deprecated: Should use the new functions starting with "Create".
func GetRandomMultiarchImage(reference string) (image.MultiarchImage, error) {
index, images, err := GetRandomMultiarchImageComponents()
if err != nil {
return image.MultiarchImage{}, err
}
index.SchemaVersion = 2
return image.MultiarchImage{
Index: index, Images: images, Reference: reference,
}, err
}
// Deprecated: Should use the new functions starting with "Create".
func GetMultiarchImageForImages(images []image.Image) image.MultiarchImage {
var index ispec.Index
for _, image := range images {
index.Manifests = append(index.Manifests, ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: getManifestDigest(image.Manifest),
Size: getManifestSize(image.Manifest),
})
}
index.SchemaVersion = 2
return image.MultiarchImage{Index: index, Images: images}
}
func getManifestSize(manifest ispec.Manifest) int64 {
manifestBlob, err := json.Marshal(manifest)
if err != nil {
return 0
}
return int64(len(manifestBlob))
}
func getManifestDigest(manifest ispec.Manifest) godigest.Digest {
manifestBlob, err := json.Marshal(manifest)
if err != nil {
return ""
}
return godigest.FromBytes(manifestBlob)
}
func GetIndexBlobWithManifests(manifestDigests []godigest.Digest) ([]byte, error) {
manifests := make([]ispec.Descriptor, 0, len(manifestDigests))
for _, manifestDigest := range manifestDigests {
manifests = append(manifests, ispec.Descriptor{
Digest: manifestDigest,
MediaType: ispec.MediaTypeImageManifest,
})
}
indexContent := ispec.Index{
MediaType: ispec.MediaTypeImageIndex,
Manifests: manifests,
}
return json.Marshal(indexContent)
}
func MockOIDCRun() (*mockoidc.MockOIDC, error) {
// Create a fresh RSA Private Key for token signing
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) //nolint: gomnd
// Create an unstarted MockOIDC server
mockServer, _ := mockoidc.NewServer(rsaKey)
// Create the net.Listener, kernel will chose a valid port
listener, _ := net.Listen("tcp", "127.0.0.1:0")
bearerMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, req *http.Request) {
// stateVal := req.Form.Get("state")
header := req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2) //nolint: gomnd
if header != "" {
if strings.ToLower(parts[0]) == "bearer" {
req.Header.Set("Authorization", strings.Join([]string{"Bearer", parts[1]}, " "))
}
}
next.ServeHTTP(response, req)
})
}
err := mockServer.AddMiddleware(bearerMiddleware)
if err != nil {
return mockServer, err
}
// tlsConfig can be nil if you want HTTP
return mockServer, mockServer.Start(listener, nil)
}
func CustomRedirectPolicy(noOfRedirect int) resty.RedirectPolicy {
return resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if len(via) >= noOfRedirect {
return fmt.Errorf("stopped after %d redirects", noOfRedirect) //nolint: goerr113
}
for key, val := range via[len(via)-1].Header {
req.Header[key] = val
}
respCookies := req.Response.Cookies()
for _, cookie := range respCookies {
req.AddCookie(cookie)
}
return nil
})
}
func GetDefaultImageStore(rootDir string, log zLog.Logger) stypes.ImageStore {
return local.NewImageStore(rootDir, false, false, log,
monitoring.NewMetricsServer(false, log),
mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore stypes.ImageStore) (bool, error) {
return true, nil
},
},
mocks.CacheMock{},
)
}
func GetDefaultStoreController(rootDir string, log zLog.Logger) storage.StoreController {
return storage.StoreController{
DefaultStore: GetDefaultImageStore(rootDir, log),
}
}
func RemoveLocalStorageContents(imageStore stypes.ImageStore) error {
repos, err := imageStore.GetRepositories()
if err != nil {
return err
}
for _, repo := range repos {
// take just the first path
err = os.RemoveAll(filepath.Join(imageStore.RootDir(), filepath.SplitList(repo)[0]))
if err != nil {
return err
}
}
return nil
}