fix: call notation-go libs instead of using notation binary (#1104)

fix: add loading notation path

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
Co-authored-by: Roxana Nemulescu <roxana.nemulescu@gmail.com>
This commit is contained in:
Andreea Lupu
2023-02-13 20:43:52 +02:00
committed by GitHub
parent 2377d62344
commit ee95ab0ffc
21 changed files with 1731 additions and 333 deletions
+499 -14
View File
@@ -4,20 +4,34 @@ import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/fs"
"log"
"math"
"math/big"
"net/http"
"net/url"
"os"
"os/exec"
"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"
@@ -27,6 +41,9 @@ import (
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"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/storage"
)
@@ -37,6 +54,8 @@ const (
SleepTime = 100 * time.Millisecond
)
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
// which: manifest, config, layer
func GetTestBlobDigest(image, which string) godigest.Digest {
prePath := "../test/data"
@@ -61,8 +80,11 @@ func GetTestBlobDigest(image, which string) godigest.Digest {
}
var (
ErrPostBlob = errors.New("can't post blob")
ErrPutBlob = errors.New("can't put blob")
ErrPostBlob = errors.New("can't post blob")
ErrPutBlob = errors.New("can't put blob")
ErrAlreadyExists = errors.New("already exists")
ErrKeyNotFound = errors.New("key not found")
ErrSignatureVerification = errors.New("signature verification failed")
)
type Image struct {
@@ -667,11 +689,15 @@ func UploadImage(img Image, baseURL, repo string) error {
return err
}
_, err = resty.R().
resp, err = resty.R().
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
SetBody(manifestBlob).
Put(baseURL + "/v2/" + repo + "/manifests/" + img.Tag)
if ErrStatusCode(resp.StatusCode()) != http.StatusCreated {
return ErrPutBlob
}
return err
}
@@ -745,6 +771,469 @@ func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout ti
}
}
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,
}
sigRepo := notreg.NewRepository(remoteRepo)
sigOpts := notation.SignOptions{
ArtifactReference: ref.String(),
SignatureMediaType: mediaType,
PluginConfig: map[string]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,
}
repo := notreg.NewRepository(remoteRepo)
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.NewRepository(remoteRepo)
configs := map[string]string{}
verifyOpts := notation.RemoteVerifyOptions{
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)
artifectDesc, err := sigRepo.Resolve(ctx, reference)
if err != nil {
return signatures, err
}
err = sigRepo.ListSignatures(ctx, artifectDesc, 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 UploadImageWithBasicAuth(img Image, baseURL, repo, user, password string) error {
for _, blob := range img.Layers {
resp, err := resty.R().
@@ -883,17 +1372,13 @@ func SignImageUsingNotary(repoTag, port string) error {
_ = os.Chdir(tdir)
_, err = exec.LookPath("notation")
if err != nil {
return err
}
NotationPathLock.Lock()
defer NotationPathLock.Unlock()
os.Setenv("XDG_CONFIG_HOME", tdir)
LoadNotationPath(tdir)
// generate a keypair
cmd := exec.Command("notation", "cert", "generate-test", "--trust", "notation-sign-test")
err = cmd.Run()
err = GenerateNotationCerts(tdir, "notation-sign-test")
if err != nil {
return err
}
@@ -901,7 +1386,7 @@ func SignImageUsingNotary(repoTag, port string) error {
// sign the image
image := fmt.Sprintf("localhost:%s/%s", port, repoTag)
cmd = exec.Command("notation", "sign", "--key", "notation-sign-test", "--plain-http", image)
err = SignWithNotation("notation-sign-test", image, tdir)
return cmd.Run()
return err
}