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

     1  //go:build imagetrust
     2  // +build imagetrust
     3  
     4  package imagetrust
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"encoding/pem"
    13  	"errors"
    14  	"fmt"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"regexp"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
    23  	"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
    24  	_ "github.com/notaryproject/notation-core-go/signature/jws"
    25  	"github.com/notaryproject/notation-go"
    26  	"github.com/notaryproject/notation-go/dir"
    27  	"github.com/notaryproject/notation-go/plugin"
    28  	"github.com/notaryproject/notation-go/verifier"
    29  	"github.com/notaryproject/notation-go/verifier/trustpolicy"
    30  	"github.com/notaryproject/notation-go/verifier/truststore"
    31  	godigest "github.com/opencontainers/go-digest"
    32  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    33  
    34  	zerr "zotregistry.dev/zot/errors"
    35  )
    36  
    37  const (
    38  	notationDirRelativePath = "_notation"
    39  	truststoreName          = "default"
    40  )
    41  
    42  type CertificateLocalStorage struct {
    43  	notationDir string
    44  }
    45  
    46  type CertificateAWSStorage struct {
    47  	secretsManagerClient SecretsManagerClient
    48  	secretsManagerCache  SecretsManagerCache
    49  }
    50  
    51  type certificateStorage interface {
    52  	LoadTrustPolicyDocument() (*trustpolicy.Document, error)
    53  	StoreCertificate(certificateContent []byte, truststoreType string) error
    54  	GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error)
    55  	InitTrustpolicy(trustpolicy []byte) error
    56  }
    57  
    58  func NewCertificateLocalStorage(rootDir string) (*CertificateLocalStorage, error) {
    59  	dir := path.Join(rootDir, notationDirRelativePath)
    60  
    61  	_, err := os.Stat(dir)
    62  	if os.IsNotExist(err) {
    63  		err = os.MkdirAll(dir, defaultDirPerms)
    64  		if err != nil {
    65  			return nil, err
    66  		}
    67  	}
    68  
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	certStorage := &CertificateLocalStorage{
    74  		notationDir: dir,
    75  	}
    76  
    77  	if err := InitTrustpolicyFile(certStorage); err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	for _, truststoreType := range truststore.Types {
    82  		defaultTruststore := path.Join(dir, "truststore", "x509", string(truststoreType), truststoreName)
    83  
    84  		_, err = os.Stat(defaultTruststore)
    85  		if os.IsNotExist(err) {
    86  			err = os.MkdirAll(defaultTruststore, defaultDirPerms)
    87  			if err != nil {
    88  				return nil, err
    89  			}
    90  		}
    91  
    92  		if err != nil {
    93  			return nil, err
    94  		}
    95  	}
    96  
    97  	return certStorage, nil
    98  }
    99  
   100  func NewCertificateAWSStorage(
   101  	secretsManagerClient SecretsManagerClient, secretsManagerCache SecretsManagerCache,
   102  ) (*CertificateAWSStorage, error) {
   103  	certStorage := &CertificateAWSStorage{
   104  		secretsManagerClient: secretsManagerClient,
   105  		secretsManagerCache:  secretsManagerCache,
   106  	}
   107  
   108  	err := InitTrustpolicyFile(certStorage)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	return certStorage, nil
   114  }
   115  
   116  func InitTrustpolicyFile(notationStorage certificateStorage) error {
   117  	truststores := []string{}
   118  
   119  	for _, truststoreType := range truststore.Types {
   120  		truststores = append(truststores, fmt.Sprintf("\"%s:%s\"", string(truststoreType), truststoreName))
   121  	}
   122  
   123  	defaultTruststores := strings.Join(truststores, ",")
   124  
   125  	// according to https://github.com/notaryproject/notation/blob/main/specs/commandline/verify.md
   126  	// the value of signatureVerification.level field from trustpolicy.json file
   127  	// could be one of these values: `strict`, `permissive`, `audit` or `skip`
   128  	// this default trustpolicy.json file sets the signatureVerification.level
   129  	// to `strict` which enforces all validations (this means that even if there is
   130  	// a certificate that verifies a signature, but that certificate has expired, then the
   131  	// signature is not trusted; if this field were set to `permissive` then the
   132  	// signature would be trusted)
   133  	trustPolicy := `{
   134  	"version": "1.0",
   135  	"trustPolicies": [
   136  		{
   137  			"name": "default-config",
   138  			"registryScopes": [ "*" ],
   139  			"signatureVerification": {
   140  				"level" : "strict" 
   141  			},
   142  			"trustStores": [` + defaultTruststores + `],
   143  			"trustedIdentities": [
   144  				"*"
   145  			]
   146  		}
   147  	]
   148  }`
   149  
   150  	return notationStorage.InitTrustpolicy([]byte(trustPolicy))
   151  }
   152  
   153  func (local *CertificateLocalStorage) InitTrustpolicy(trustpolicy []byte) error {
   154  	notationDir, err := local.GetNotationDirPath()
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	return os.WriteFile(path.Join(notationDir, dir.PathTrustPolicy), trustpolicy, defaultDirPerms)
   160  }
   161  
   162  func (cloud *CertificateAWSStorage) InitTrustpolicy(trustpolicy []byte) error {
   163  	name := "trustpolicy"
   164  	description := "notation trustpolicy file"
   165  	secret := base64.StdEncoding.EncodeToString(trustpolicy)
   166  	secretInputParam := &secretsmanager.CreateSecretInput{
   167  		Name:         &name,
   168  		Description:  &description,
   169  		SecretString: &secret,
   170  	}
   171  
   172  	_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
   173  	if err != nil && IsResourceExistsException(err) {
   174  		trustpolicyContent, err := cloud.secretsManagerCache.GetSecretString(name)
   175  		if err != nil {
   176  			return err
   177  		}
   178  
   179  		existingTrustpolicy, err := base64.StdEncoding.DecodeString(trustpolicyContent)
   180  		if err == nil && bytes.Equal(trustpolicy, existingTrustpolicy) {
   181  			return nil
   182  		}
   183  
   184  		force := true
   185  
   186  		deleteSecretParam := &secretsmanager.DeleteSecretInput{
   187  			SecretId:                   &name,
   188  			ForceDeleteWithoutRecovery: &force,
   189  		}
   190  
   191  		_, err = cloud.secretsManagerClient.DeleteSecret(context.Background(), deleteSecretParam)
   192  
   193  		if err != nil {
   194  			return err
   195  		}
   196  
   197  		err = cloud.recreateSecret(secretInputParam)
   198  
   199  		return err
   200  	}
   201  
   202  	return err
   203  }
   204  
   205  func (cloud *CertificateAWSStorage) recreateSecret(secretInputParam *secretsmanager.CreateSecretInput) error {
   206  	maxAttempts := 5
   207  	retrySec := 2
   208  
   209  	var err error
   210  
   211  	for i := 0; i < maxAttempts; i++ {
   212  		_, err = cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
   213  
   214  		if err == nil {
   215  			return nil
   216  		}
   217  
   218  		time.Sleep(time.Duration(retrySec) * time.Second)
   219  	}
   220  
   221  	return err
   222  }
   223  
   224  func (local *CertificateLocalStorage) GetNotationDirPath() (string, error) {
   225  	if local.notationDir != "" {
   226  		return local.notationDir, nil
   227  	}
   228  
   229  	return "", zerr.ErrSignConfigDirNotSet
   230  }
   231  
   232  func (cloud *CertificateAWSStorage) GetCertificates(
   233  	ctx context.Context, storeType truststore.Type, namedStore string,
   234  ) ([]*x509.Certificate, error) {
   235  	certificates := []*x509.Certificate{}
   236  
   237  	if !validateTruststoreType(string(storeType)) {
   238  		return []*x509.Certificate{}, zerr.ErrInvalidTruststoreType
   239  	}
   240  
   241  	if !validateTruststoreName(namedStore) {
   242  		return []*x509.Certificate{}, zerr.ErrInvalidTruststoreName
   243  	}
   244  
   245  	listSecretsInput := secretsmanager.ListSecretsInput{
   246  		Filters: []types.Filter{
   247  			{
   248  				Key:    types.FilterNameStringTypeName,
   249  				Values: []string{path.Join(string(storeType), namedStore)},
   250  			},
   251  		},
   252  	}
   253  
   254  	secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput)
   255  	if err != nil {
   256  		return []*x509.Certificate{}, err
   257  	}
   258  
   259  	for _, secret := range secrets.SecretList {
   260  		// get key
   261  		raw, err := cloud.secretsManagerCache.GetSecretString(*(secret.Name))
   262  		if err != nil {
   263  			return []*x509.Certificate{}, err
   264  		}
   265  
   266  		rawDecoded, err := base64.StdEncoding.DecodeString(raw)
   267  		if err != nil {
   268  			return []*x509.Certificate{}, err
   269  		}
   270  
   271  		certs, _, err := parseAndValidateCertificateContent(rawDecoded)
   272  		if err != nil {
   273  			return []*x509.Certificate{}, err
   274  		}
   275  
   276  		err = truststore.ValidateCertificates(certs)
   277  		if err != nil {
   278  			return []*x509.Certificate{}, err
   279  		}
   280  
   281  		certificates = append(certificates, certs...)
   282  	}
   283  
   284  	return certificates, nil
   285  }
   286  
   287  // Equivalent function for trustpolicy.LoadDocument() but using a specific SysFS not the one returned by ConfigFS().
   288  func (local *CertificateLocalStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) {
   289  	notationDir, err := local.GetNotationDirPath()
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	jsonFile, err := dir.NewSysFS(notationDir).Open(dir.PathTrustPolicy)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	defer jsonFile.Close()
   300  
   301  	policyDocument := &trustpolicy.Document{}
   302  
   303  	err = json.NewDecoder(jsonFile).Decode(policyDocument)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	return policyDocument, nil
   309  }
   310  
   311  func (cloud *CertificateAWSStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) {
   312  	policyDocument := &trustpolicy.Document{}
   313  
   314  	raw, err := cloud.secretsManagerCache.GetSecretString("trustpolicy")
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	rawDecoded, err := base64.StdEncoding.DecodeString(raw)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	var buf bytes.Buffer
   325  
   326  	err = json.Compact(&buf, rawDecoded)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	err = json.Unmarshal(buf.Bytes(), policyDocument)
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  
   336  	return policyDocument, nil
   337  }
   338  
   339  // NewFromConfig returns a verifier based on local file system.
   340  // Equivalent function for verifier.NewFromConfig()
   341  // but using LoadTrustPolicyDocumnt() function instead of trustpolicy.LoadDocument() function.
   342  func NewFromConfig(notationStorage certificateStorage) (notation.Verifier, error) {
   343  	// Load trust policy.
   344  	policyDocument, err := notationStorage.LoadTrustPolicyDocument()
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	return notationStorage.GetVerifier(policyDocument)
   350  }
   351  
   352  func (local *CertificateLocalStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) {
   353  	notationDir, err := local.GetNotationDirPath()
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	x509TrustStore := truststore.NewX509TrustStore(dir.NewSysFS(notationDir))
   358  
   359  	return verifier.New(policyDoc, x509TrustStore,
   360  		plugin.NewCLIManager(dir.NewSysFS(path.Join(notationDir, dir.PathPlugins))))
   361  }
   362  
   363  func (cloud *CertificateAWSStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) {
   364  	return verifier.New(policyDoc, cloud,
   365  		plugin.NewCLIManager(dir.NewSysFS(path.Join(dir.PathPlugins))))
   366  }
   367  
   368  func VerifyNotationSignature(
   369  	notationStorage certificateStorage, artifactDescriptor ispec.Descriptor, artifactReference string,
   370  	rawSignature []byte, signatureMediaType string,
   371  ) (string, time.Time, bool, error) {
   372  	var (
   373  		date   time.Time
   374  		author string
   375  	)
   376  
   377  	// If there's no signature associated with the reference.
   378  	if len(rawSignature) == 0 {
   379  		return author, date, false, notation.ErrorSignatureRetrievalFailed{
   380  			Msg: fmt.Sprintf("no signature associated with %q is provided, make sure the image was signed successfully",
   381  				artifactReference),
   382  		}
   383  	}
   384  
   385  	// Initialize verifier.
   386  	verifier, err := NewFromConfig(notationStorage)
   387  	if err != nil {
   388  		return author, date, false, err
   389  	}
   390  
   391  	ctx := context.Background()
   392  
   393  	// Set VerifyOptions.
   394  	opts := notation.VerifierVerifyOptions{
   395  		// ArtifactReference is important to validate registry scope format
   396  		// If "registryScopes" field from trustpolicy.json file is not wildcard then "domain:80/repo@" should not be hardcoded
   397  		ArtifactReference:  "domain:80/repo@" + artifactReference,
   398  		SignatureMediaType: signatureMediaType,
   399  		PluginConfig:       map[string]string{},
   400  	}
   401  
   402  	// Verify the notation signature which should be associated with the artifactDescriptor.
   403  	outcome, err := verifier.Verify(ctx, artifactDescriptor, rawSignature, opts)
   404  	if outcome.EnvelopeContent != nil {
   405  		author = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].Subject.String()
   406  
   407  		if outcome.VerificationLevel == trustpolicy.LevelStrict && (err == nil ||
   408  			CheckExpiryErr(outcome.VerificationResults, outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter, err)) {
   409  			expiry := outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry
   410  			if !expiry.IsZero() && expiry.Before(outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter) {
   411  				date = outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry
   412  			} else {
   413  				date = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter
   414  			}
   415  		}
   416  	}
   417  
   418  	if err != nil {
   419  		return author, date, false, err
   420  	}
   421  
   422  	// Verification Succeeded.
   423  	return author, date, true, nil
   424  }
   425  
   426  func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter time.Time, err error) bool {
   427  	for _, result := range verificationResults {
   428  		if result.Type == trustpolicy.TypeExpiry {
   429  			if errors.Is(err, result.Error) {
   430  				return true
   431  			}
   432  		} else if result.Type == trustpolicy.TypeAuthenticTimestamp {
   433  			if errors.Is(err, result.Error) && time.Now().After(notAfter) {
   434  				return true
   435  			} else {
   436  				return false
   437  			}
   438  		}
   439  	}
   440  
   441  	return false
   442  }
   443  
   444  func UploadCertificate(
   445  	notationStorage certificateStorage, certificateContent []byte, truststoreType string,
   446  ) error {
   447  	// validate truststore type
   448  	if !validateTruststoreType(truststoreType) {
   449  		return zerr.ErrInvalidTruststoreType
   450  	}
   451  
   452  	// validate certificate
   453  	if _, ok, err := parseAndValidateCertificateContent(certificateContent); !ok {
   454  		return err
   455  	}
   456  
   457  	// store certificate
   458  	err := notationStorage.StoreCertificate(certificateContent, truststoreType)
   459  
   460  	return err
   461  }
   462  
   463  func (local *CertificateLocalStorage) StoreCertificate(
   464  	certificateContent []byte, truststoreType string,
   465  ) error {
   466  	// add certificate to "{rootDir}/_notation/truststore/x509/{type}/default/{name.crt}"
   467  	configDir, err := local.GetNotationDirPath()
   468  	if err != nil {
   469  		return err
   470  	}
   471  
   472  	name := godigest.FromBytes(certificateContent)
   473  
   474  	// store certificate
   475  	truststorePath := path.Join(configDir, dir.TrustStoreDir, "x509", truststoreType, truststoreName, name.String())
   476  
   477  	if err := os.MkdirAll(filepath.Dir(truststorePath), defaultDirPerms); err != nil {
   478  		return err
   479  	}
   480  
   481  	return os.WriteFile(truststorePath, certificateContent, defaultFilePerms)
   482  }
   483  
   484  func (cloud *CertificateAWSStorage) StoreCertificate(
   485  	certificateContent []byte, truststoreType string,
   486  ) error {
   487  	name := path.Join(truststoreType, truststoreName, godigest.FromBytes(certificateContent).Encoded())
   488  	description := "notation certificate"
   489  	secret := base64.StdEncoding.EncodeToString(certificateContent)
   490  	secretInputParam := &secretsmanager.CreateSecretInput{
   491  		Name:         &name,
   492  		Description:  &description,
   493  		SecretString: &secret,
   494  	}
   495  
   496  	_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
   497  
   498  	if err != nil && IsResourceExistsException(err) {
   499  		return nil
   500  	}
   501  
   502  	return err
   503  }
   504  
   505  func validateTruststoreType(truststoreType string) bool {
   506  	for _, t := range truststore.Types {
   507  		if string(t) == truststoreType {
   508  			return true
   509  		}
   510  	}
   511  
   512  	return false
   513  }
   514  
   515  func validateTruststoreName(truststoreName string) bool {
   516  	if strings.Contains(truststoreName, "..") {
   517  		return false
   518  	}
   519  
   520  	return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(truststoreName)
   521  }
   522  
   523  // implementation from https://github.com/notaryproject/notation-core-go/blob/main/x509/cert.go#L20
   524  func parseAndValidateCertificateContent(certificateContent []byte) ([]*x509.Certificate, bool, error) {
   525  	var certs []*x509.Certificate
   526  
   527  	block, rest := pem.Decode(certificateContent)
   528  	if block == nil {
   529  		// data may be in DER format
   530  		derCerts, err := x509.ParseCertificates(certificateContent)
   531  		if err != nil {
   532  			return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
   533  		}
   534  
   535  		certs = append(certs, derCerts...)
   536  	} else {
   537  		// data is in PEM format
   538  		for block != nil {
   539  			cert, err := x509.ParseCertificate(block.Bytes)
   540  			if err != nil {
   541  				return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
   542  			}
   543  			certs = append(certs, cert)
   544  			block, rest = pem.Decode(rest)
   545  		}
   546  	}
   547  
   548  	if len(certs) == 0 {
   549  		return []*x509.Certificate{}, false, fmt.Errorf("%w: no valid certificates found in payload",
   550  			zerr.ErrInvalidCertificateContent)
   551  	}
   552  
   553  	return certs, true, nil
   554  }