zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/test/signature/notation.go (about)

     1  package signature
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"crypto/rsa"
     7  	"crypto/x509"
     8  	"encoding/json"
     9  	"encoding/pem"
    10  	"errors"
    11  	"fmt"
    12  	"io/fs"
    13  	"math"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"strings"
    18  	"sync"
    19  
    20  	"github.com/notaryproject/notation-core-go/signature/jws"
    21  	"github.com/notaryproject/notation-core-go/testhelper"
    22  	"github.com/notaryproject/notation-go"
    23  	notconfig "github.com/notaryproject/notation-go/config"
    24  	"github.com/notaryproject/notation-go/dir"
    25  	notreg "github.com/notaryproject/notation-go/registry"
    26  	"github.com/notaryproject/notation-go/signer"
    27  	"github.com/notaryproject/notation-go/verifier"
    28  	godigest "github.com/opencontainers/go-digest"
    29  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"oras.land/oras-go/v2/registry"
    31  	"oras.land/oras-go/v2/registry/remote"
    32  	"oras.land/oras-go/v2/registry/remote/auth"
    33  
    34  	tcommon "zotregistry.dev/zot/pkg/test/common"
    35  )
    36  
    37  var (
    38  	ErrAlreadyExists         = errors.New("already exists")
    39  	ErrKeyNotFound           = errors.New("key not found")
    40  	ErrSignatureVerification = errors.New("signature verification failed")
    41  )
    42  
    43  var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
    44  
    45  func LoadNotationPath(tdir string) {
    46  	dir.UserConfigDir = filepath.Join(tdir, "notation")
    47  
    48  	// set user libexec
    49  	dir.UserLibexecDir = dir.UserConfigDir
    50  }
    51  
    52  func GenerateNotationCerts(tdir string, certName string) error {
    53  	// generate RSA private key
    54  	bits := 2048
    55  
    56  	key, err := rsa.GenerateKey(rand.Reader, bits)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
    67  
    68  	rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert")
    69  
    70  	certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw})
    71  
    72  	// write private key
    73  	relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName)
    74  
    75  	configFS := dir.ConfigFS()
    76  
    77  	keyPath, err := configFS.SysPath(relativeKeyPath)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	certPath, err := configFS.SysPath(relativeCertPath)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	if err := tcommon.WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd
    88  		return fmt.Errorf("failed to write key file: %w", err)
    89  	}
    90  
    91  	// write self-signed certificate
    92  	if err := tcommon.WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd
    93  		return fmt.Errorf("failed to write certificate file: %w", err)
    94  	}
    95  
    96  	signingKeys, err := notconfig.LoadSigningKeys()
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	keySuite := notconfig.KeySuite{
   102  		Name: certName,
   103  		X509KeyPair: &notconfig.X509KeyPair{
   104  			KeyPath:         keyPath,
   105  			CertificatePath: certPath,
   106  		},
   107  	}
   108  
   109  	// addKeyToSigningKeys
   110  	if tcommon.Contains(signingKeys.Keys, keySuite.Name) {
   111  		return ErrAlreadyExists
   112  	}
   113  
   114  	signingKeys.Keys = append(signingKeys.Keys, keySuite)
   115  
   116  	// Add to the trust store
   117  	trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName))
   118  
   119  	if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil {
   120  		return ErrAlreadyExists
   121  	}
   122  
   123  	if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd
   124  		return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err)
   125  	}
   126  
   127  	trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension))
   128  
   129  	err = tcommon.CopyFile(certPath, trustCertPath)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	// Save to the SigningKeys.json
   135  	if err := signingKeys.Save(); err != nil {
   136  		return err
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func SignWithNotation(keyName, reference, tdir string, referrersCapability bool) error {
   143  	ctx := context.TODO()
   144  
   145  	// getSigner
   146  	var newSigner notation.Signer
   147  
   148  	mediaType := jws.MediaTypeEnvelope
   149  
   150  	// ResolveKey
   151  	signingKeys, err := LoadNotationSigningkeys(tdir)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	idx := tcommon.Index(signingKeys.Keys, keyName)
   157  	if idx < 0 {
   158  		return ErrKeyNotFound
   159  	}
   160  
   161  	key := signingKeys.Keys[idx]
   162  
   163  	if key.X509KeyPair != nil {
   164  		newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
   165  		if err != nil {
   166  			return err
   167  		}
   168  	}
   169  
   170  	// prepareSigningContent
   171  	// getRepositoryClient
   172  	authClient := &auth.Client{
   173  		Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
   174  			return auth.EmptyCredential, nil
   175  		},
   176  		Cache:    auth.NewCache(),
   177  		ClientID: "notation",
   178  	}
   179  
   180  	authClient.SetUserAgent("notation/zot_tests")
   181  
   182  	plainHTTP := true
   183  
   184  	// Resolve referance
   185  	ref, err := registry.ParseReference(reference)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	remoteRepo := &remote.Repository{
   191  		Client:    authClient,
   192  		Reference: ref,
   193  		PlainHTTP: plainHTTP,
   194  	}
   195  
   196  	if !referrersCapability {
   197  		_ = remoteRepo.SetReferrersCapability(false)
   198  	}
   199  
   200  	repositoryOpts := notreg.RepositoryOptions{}
   201  
   202  	sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
   203  
   204  	sigOpts := notation.SignOptions{
   205  		SignerSignOptions: notation.SignerSignOptions{
   206  			SignatureMediaType: mediaType,
   207  			PluginConfig:       map[string]string{},
   208  		},
   209  		ArtifactReference: ref.String(),
   210  	}
   211  
   212  	_, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  func VerifyWithNotation(reference string, tdir string) error {
   221  	// check if trustpolicy.json exists
   222  	trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json")
   223  
   224  	if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
   225  		trustPolicy := `
   226  			{
   227  				"version": "1.0",
   228  				"trustPolicies": [
   229  					{
   230  						"name": "good",
   231  						"registryScopes": [ "*" ],
   232  						"signatureVerification": {
   233  							"level" : "audit" 
   234  						},
   235  						"trustStores": ["ca:good"],
   236  						"trustedIdentities": [
   237  							"*"
   238  						]
   239  					}
   240  				]
   241  			}`
   242  
   243  		file, err := os.Create(trustpolicyPath)
   244  		if err != nil {
   245  			return err
   246  		}
   247  
   248  		defer file.Close()
   249  
   250  		_, err = file.WriteString(trustPolicy)
   251  		if err != nil {
   252  			return err
   253  		}
   254  	}
   255  
   256  	// start verifying signatures
   257  	ctx := context.TODO()
   258  
   259  	// getRepositoryClient
   260  	authClient := &auth.Client{
   261  		Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
   262  			return auth.EmptyCredential, nil
   263  		},
   264  		Cache:    auth.NewCache(),
   265  		ClientID: "notation",
   266  	}
   267  
   268  	authClient.SetUserAgent("notation/zot_tests")
   269  
   270  	plainHTTP := true
   271  
   272  	// Resolve referance
   273  	ref, err := registry.ParseReference(reference)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	remoteRepo := &remote.Repository{
   279  		Client:    authClient,
   280  		Reference: ref,
   281  		PlainHTTP: plainHTTP,
   282  	}
   283  
   284  	repositoryOpts := notreg.RepositoryOptions{}
   285  
   286  	repo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
   287  
   288  	manifestDesc, err := repo.Resolve(ctx, ref.Reference)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	if err := ref.ValidateReferenceAsDigest(); err != nil {
   294  		ref.Reference = manifestDesc.Digest.String()
   295  	}
   296  
   297  	// getVerifier
   298  	newVerifier, err := verifier.NewFromConfig()
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	remoteRepo = &remote.Repository{
   304  		Client:    authClient,
   305  		Reference: ref,
   306  		PlainHTTP: plainHTTP,
   307  	}
   308  
   309  	repo = notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
   310  
   311  	configs := map[string]string{}
   312  
   313  	verifyOpts := notation.VerifyOptions{
   314  		ArtifactReference:    ref.String(),
   315  		PluginConfig:         configs,
   316  		MaxSignatureAttempts: math.MaxInt64,
   317  	}
   318  
   319  	_, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts)
   320  	if err != nil || len(outcomes) == 0 {
   321  		return ErrSignatureVerification
   322  	}
   323  
   324  	return nil
   325  }
   326  
   327  func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) {
   328  	signatures := []godigest.Digest{}
   329  
   330  	ctx := context.TODO()
   331  
   332  	// getSignatureRepository
   333  	ref, err := registry.ParseReference(reference)
   334  	if err != nil {
   335  		return signatures, err
   336  	}
   337  
   338  	plainHTTP := true
   339  
   340  	// getRepositoryClient
   341  	authClient := &auth.Client{
   342  		Credential: func(ctx context.Context, registry string) (auth.Credential, error) {
   343  			return auth.EmptyCredential, nil
   344  		},
   345  		Cache:    auth.NewCache(),
   346  		ClientID: "notation",
   347  	}
   348  
   349  	authClient.SetUserAgent("notation/zot_tests")
   350  
   351  	remoteRepo := &remote.Repository{
   352  		Client:    authClient,
   353  		Reference: ref,
   354  		PlainHTTP: plainHTTP,
   355  	}
   356  
   357  	sigRepo := notreg.NewRepository(remoteRepo)
   358  
   359  	artifactDesc, err := sigRepo.Resolve(ctx, reference)
   360  	if err != nil {
   361  		return signatures, err
   362  	}
   363  
   364  	err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
   365  		for _, sigManifestDesc := range signatureManifests {
   366  			signatures = append(signatures, sigManifestDesc.Digest)
   367  		}
   368  
   369  		return nil
   370  	})
   371  
   372  	return signatures, err
   373  }
   374  
   375  func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) {
   376  	var err error
   377  
   378  	var signingKeysInfo *notconfig.SigningKeys
   379  
   380  	filePath := path.Join(tdir, "notation/signingkeys.json")
   381  
   382  	file, err := os.Open(filePath)
   383  	if err != nil {
   384  		if errors.Is(err, fs.ErrNotExist) {
   385  			// create file
   386  			newSigningKeys := notconfig.NewSigningKeys()
   387  
   388  			newFile, err := os.Create(filePath)
   389  			if err != nil {
   390  				return newSigningKeys, err
   391  			}
   392  
   393  			defer newFile.Close()
   394  
   395  			encoder := json.NewEncoder(newFile)
   396  			encoder.SetIndent("", "    ")
   397  
   398  			err = encoder.Encode(newSigningKeys)
   399  
   400  			return newSigningKeys, err
   401  		}
   402  
   403  		return nil, err
   404  	}
   405  
   406  	defer file.Close()
   407  
   408  	err = json.NewDecoder(file).Decode(&signingKeysInfo)
   409  
   410  	return signingKeysInfo, err
   411  }
   412  
   413  func LoadNotationConfig(tdir string) (*notconfig.Config, error) {
   414  	var configInfo *notconfig.Config
   415  
   416  	filePath := path.Join(tdir, "notation/signingkeys.json")
   417  
   418  	file, err := os.Open(filePath)
   419  	if err != nil {
   420  		return configInfo, err
   421  	}
   422  
   423  	defer file.Close()
   424  
   425  	err = json.NewDecoder(file).Decode(&configInfo)
   426  	if err != nil {
   427  		return configInfo, err
   428  	}
   429  
   430  	// set default value
   431  	configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
   432  	if configInfo.SignatureFormat == "" {
   433  		configInfo.SignatureFormat = "jws"
   434  	}
   435  
   436  	return configInfo, nil
   437  }
   438  
   439  func SignImageUsingNotary(repoTag, port string, referrersCapability bool) error {
   440  	cwd, err := os.Getwd()
   441  	if err != nil {
   442  		return err
   443  	}
   444  
   445  	defer func() { _ = os.Chdir(cwd) }()
   446  
   447  	tdir, err := os.MkdirTemp("", "notation")
   448  	if err != nil {
   449  		return err
   450  	}
   451  
   452  	defer os.RemoveAll(tdir)
   453  
   454  	_ = os.Chdir(tdir)
   455  
   456  	NotationPathLock.Lock()
   457  	defer NotationPathLock.Unlock()
   458  
   459  	LoadNotationPath(tdir)
   460  
   461  	// generate a keypair
   462  	err = GenerateNotationCerts(tdir, "notation-sign-test")
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	// sign the image
   468  	image := fmt.Sprintf("localhost:%s/%s", port, repoTag)
   469  
   470  	err = SignWithNotation("notation-sign-test", image, tdir, referrersCapability)
   471  
   472  	return err
   473  }