feat: upload cosign public key and notation certificates to cloud (#1744)

- using secrets manager for storing public keys and certificates
- adding a default truststore for notation verification and upload all certificates to this default truststore
- removig `truststoreName` query param from notation api for uploading certificates


(cherry picked from commit eafcc1a213)

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea Lupu
2023-09-08 10:03:58 +03:00
committed by GitHub
parent 6115eed4ec
commit 5a3fac40db
27 changed files with 1661 additions and 563 deletions
+286 -94
View File
@@ -4,8 +4,10 @@
package imagetrust
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
@@ -14,9 +16,12 @@ import (
"path"
"path/filepath"
"regexp"
"sync"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
"github.com/aws/aws-secretsmanager-caching-go/secretcache"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
@@ -30,36 +35,94 @@ import (
zerr "zotregistry.io/zot/errors"
)
const notationDirRelativePath = "_notation"
var (
notationDir = "" //nolint:gochecknoglobals
TrustpolicyLock = new(sync.Mutex) //nolint: gochecknoglobals
const (
notationDirRelativePath = "_notation"
truststoreName = "default"
)
func InitNotationDir(rootDir string) error {
type CertificateLocalStorage struct {
notationDir string
}
type CertificateAWSStorage struct {
secretsManagerClient *secretsmanager.Client
secretsManagerCache *secretcache.Cache
}
type certificateStorage interface {
LoadTrustPolicyDocument() (*trustpolicy.Document, error)
StoreCertificate(certificateContent []byte, truststoreType string) error
GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error)
InitTrustpolicy(trustpolicy []byte) error
}
func NewCertificateLocalStorage(rootDir string) (*CertificateLocalStorage, error) {
dir := path.Join(rootDir, notationDirRelativePath)
_, err := os.Stat(dir)
if os.IsNotExist(err) {
err = os.MkdirAll(dir, defaultDirPerms)
if err != nil {
return err
return nil, err
}
}
if err == nil {
notationDir = dir
if err != nil {
return nil, err
}
if _, err := LoadTrustPolicyDocument(notationDir); os.IsNotExist(err) {
return InitTrustpolicyFile(notationDir)
certStorage := &CertificateLocalStorage{
notationDir: dir,
}
if err := InitTrustpolicyFile(certStorage); err != nil {
return nil, err
}
for _, truststoreType := range truststore.Types {
defaultTruststore := path.Join(dir, "truststore", "x509", string(truststoreType), truststoreName)
_, err = os.Stat(defaultTruststore)
if os.IsNotExist(err) {
err = os.MkdirAll(defaultTruststore, defaultDirPerms)
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
}
return err
return certStorage, nil
}
func InitTrustpolicyFile(configDir string) error {
func NewCertificateAWSStorage(
secretsManagerClient *secretsmanager.Client, secretsManagerCache *secretcache.Cache,
) (*CertificateAWSStorage, error) {
certStorage := &CertificateAWSStorage{
secretsManagerClient: secretsManagerClient,
secretsManagerCache: secretsManagerCache,
}
err := InitTrustpolicyFile(certStorage)
if err != nil {
return nil, err
}
return certStorage, nil
}
func InitTrustpolicyFile(notationStorage certificateStorage) error {
truststores := []string{}
for _, truststoreType := range truststore.Types {
truststores = append(truststores, fmt.Sprintf("\"%s:%s\"", string(truststoreType), truststoreName))
}
defaultTruststores := strings.Join(truststores, ",")
// according to https://github.com/notaryproject/notation/blob/main/specs/commandline/verify.md
// the value of signatureVerification.level field from trustpolicy.json file
// could be one of these values: `strict`, `permissive`, `audit` or `skip`
@@ -68,40 +131,138 @@ func InitTrustpolicyFile(configDir string) error {
// a certificate that verifies a signature, but that certificate has expired, then the
// signature is not trusted; if this field were set to `permissive` then the
// signature would be trusted)
trustPolicy := `
{
"version": "1.0",
"trustPolicies": [
{
"name": "default-config",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": [],
"trustedIdentities": [
"*"
]
}
]
}`
trustPolicy := `{
"version": "1.0",
"trustPolicies": [
{
"name": "default-config",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": [` + defaultTruststores + `],
"trustedIdentities": [
"*"
]
}
]
}`
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
return os.WriteFile(path.Join(configDir, dir.PathTrustPolicy), []byte(trustPolicy), defaultDirPerms)
return notationStorage.InitTrustpolicy([]byte(trustPolicy))
}
func GetNotationDirPath() (string, error) {
if notationDir != "" {
return notationDir, nil
func (local *CertificateLocalStorage) InitTrustpolicy(trustpolicy []byte) error {
notationDir, err := local.GetNotationDirPath()
if err != nil {
return err
}
return os.WriteFile(path.Join(notationDir, dir.PathTrustPolicy), trustpolicy, defaultDirPerms)
}
func (cloud *CertificateAWSStorage) InitTrustpolicy(trustpolicy []byte) error {
name := "trustpolicy"
description := "notation trustpolicy file"
secret := base64.StdEncoding.EncodeToString(trustpolicy)
secretInputParam := &secretsmanager.CreateSecretInput{
Name: &name,
Description: &description,
SecretString: &secret,
}
_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
if err != nil && strings.Contains(err.Error(), "the secret trustpolicy already exists.") {
force := true
deleteSecretParam := &secretsmanager.DeleteSecretInput{
SecretId: &name,
ForceDeleteWithoutRecovery: &force,
}
_, err = cloud.secretsManagerClient.DeleteSecret(context.Background(), deleteSecretParam)
if err != nil {
return err
}
_, err = cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
return err
}
return err
}
func (local *CertificateLocalStorage) GetNotationDirPath() (string, error) {
if local.notationDir != "" {
return local.notationDir, nil
}
return "", zerr.ErrSignConfigDirNotSet
}
func (cloud *CertificateAWSStorage) GetCertificates(
ctx context.Context, storeType truststore.Type, namedStore string,
) ([]*x509.Certificate, error) {
certificates := []*x509.Certificate{}
if !validateTruststoreType(string(storeType)) {
return []*x509.Certificate{}, zerr.ErrInvalidTruststoreType
}
if !validateTruststoreName(namedStore) {
return []*x509.Certificate{}, zerr.ErrInvalidTruststoreName
}
listSecretsInput := secretsmanager.ListSecretsInput{
Filters: []types.Filter{
{
Key: types.FilterNameStringTypeName,
Values: []string{path.Join(string(storeType), namedStore)},
},
},
}
secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput)
if err != nil {
return []*x509.Certificate{}, err
}
for _, secret := range secrets.SecretList {
// get key
raw, err := cloud.secretsManagerCache.GetSecretString(*(secret.Name))
if err != nil {
return []*x509.Certificate{}, err
}
rawDecoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return []*x509.Certificate{}, err
}
certs, _, err := parseAndValidateCertificateContent(rawDecoded)
if err != nil {
return []*x509.Certificate{}, err
}
err = truststore.ValidateCertificates(certs)
if err != nil {
return []*x509.Certificate{}, err
}
certificates = append(certificates, certs...)
}
return certificates, nil
}
// Equivalent function for trustpolicy.LoadDocument() but using a specific SysFS not the one returned by ConfigFS().
func LoadTrustPolicyDocument(notationDir string) (*trustpolicy.Document, error) {
func (local *CertificateLocalStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) {
notationDir, err := local.GetNotationDirPath()
if err != nil {
return nil, err
}
jsonFile, err := dir.NewSysFS(notationDir).Open(dir.PathTrustPolicy)
if err != nil {
return nil, err
@@ -119,33 +280,66 @@ func LoadTrustPolicyDocument(notationDir string) (*trustpolicy.Document, error)
return policyDocument, nil
}
func (cloud *CertificateAWSStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) {
policyDocument := &trustpolicy.Document{}
raw, err := cloud.secretsManagerCache.GetSecretString("trustpolicy")
if err != nil {
return nil, err
}
rawDecoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = json.Compact(&buf, rawDecoded)
if err != nil {
return nil, err
}
err = json.Unmarshal(buf.Bytes(), policyDocument)
if err != nil {
return nil, err
}
return policyDocument, nil
}
// NewFromConfig returns a verifier based on local file system.
// Equivalent function for verifier.NewFromConfig()
// but using LoadTrustPolicyDocumnt() function instead of trustpolicy.LoadDocument() function.
func NewFromConfig() (notation.Verifier, error) {
notationDir, err := GetNotationDirPath()
if err != nil {
return nil, err
}
func NewFromConfig(notationStorage certificateStorage) (notation.Verifier, error) {
// Load trust policy.
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
policyDocument, err := LoadTrustPolicyDocument(notationDir)
policyDocument, err := notationStorage.LoadTrustPolicyDocument()
if err != nil {
return nil, err
}
// Load trust store.
return notationStorage.GetVerifier(policyDocument)
}
func (local *CertificateLocalStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) {
notationDir, err := local.GetNotationDirPath()
if err != nil {
return nil, err
}
x509TrustStore := truststore.NewX509TrustStore(dir.NewSysFS(notationDir))
return verifier.New(policyDocument, x509TrustStore,
return verifier.New(policyDoc, x509TrustStore,
plugin.NewCLIManager(dir.NewSysFS(path.Join(notationDir, dir.PathPlugins))))
}
func (cloud *CertificateAWSStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) {
return verifier.New(policyDoc, cloud,
plugin.NewCLIManager(dir.NewSysFS(path.Join(dir.PathPlugins))))
}
func VerifyNotationSignature(
artifactDescriptor ispec.Descriptor, artifactReference string, rawSignature []byte, signatureMediaType string,
notationStorage certificateStorage, artifactDescriptor ispec.Descriptor, artifactReference string,
rawSignature []byte, signatureMediaType string,
) (string, time.Time, bool, error) {
var (
date time.Time
@@ -161,7 +355,7 @@ func VerifyNotationSignature(
}
// Initialize verifier.
verifier, err := NewFromConfig()
verifier, err := NewFromConfig(notationStorage)
if err != nil {
return author, date, false, err
}
@@ -219,24 +413,30 @@ func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter t
return false
}
func UploadCertificate(certificateContent []byte, truststoreType, truststoreName string) error {
func UploadCertificate(
notationStorage certificateStorage, certificateContent []byte, truststoreType string,
) error {
// validate truststore type
if !validateTruststoreType(truststoreType) {
return zerr.ErrInvalidTruststoreType
}
// validate truststore name
if !validateTruststoreName(truststoreName) {
return zerr.ErrInvalidTruststoreName
}
// validate certificate
if ok, err := validateCertificate(certificateContent); !ok {
if _, ok, err := parseAndValidateCertificateContent(certificateContent); !ok {
return err
}
// add certificate to "{rootDir}/_notation/truststore/x509/{type}/{name}/{name.crt}"
configDir, err := GetNotationDirPath()
// store certificate
err := notationStorage.StoreCertificate(certificateContent, truststoreType)
return err
}
func (local *CertificateLocalStorage) StoreCertificate(
certificateContent []byte, truststoreType string,
) error {
// add certificate to "{rootDir}/_notation/truststore/x509/{type}/default/{name.crt}"
configDir, err := local.GetNotationDirPath()
if err != nil {
return err
}
@@ -250,36 +450,24 @@ func UploadCertificate(certificateContent []byte, truststoreType, truststoreName
return err
}
err = os.WriteFile(truststorePath, certificateContent, defaultFilePerms)
if err != nil {
return err
return os.WriteFile(truststorePath, certificateContent, defaultFilePerms)
}
func (cloud *CertificateAWSStorage) StoreCertificate(
certificateContent []byte, truststoreType string,
) error {
name := path.Join(truststoreType, truststoreName, godigest.FromBytes(certificateContent).Encoded())
description := "notation certificate"
secret := base64.StdEncoding.EncodeToString(certificateContent)
secretInputParam := &secretsmanager.CreateSecretInput{
Name: &name,
Description: &description,
SecretString: &secret,
}
// add certificate to "trustpolicy.json"
TrustpolicyLock.Lock()
defer TrustpolicyLock.Unlock()
_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
trustpolicyDoc, err := LoadTrustPolicyDocument(configDir)
if err != nil {
return err
}
truststoreToAppend := fmt.Sprintf("%s:%s", truststoreType, truststoreName)
for _, t := range trustpolicyDoc.TrustPolicies[0].TrustStores {
if t == truststoreToAppend {
return nil
}
}
trustpolicyDoc.TrustPolicies[0].TrustStores = append(trustpolicyDoc.TrustPolicies[0].TrustStores, truststoreToAppend)
trustpolicyDocContent, err := json.Marshal(trustpolicyDoc)
if err != nil {
return err
}
return os.WriteFile(path.Join(configDir, dir.PathTrustPolicy), trustpolicyDocContent, defaultFilePerms)
return err
}
func validateTruststoreType(truststoreType string) bool {
@@ -293,11 +481,15 @@ func validateTruststoreType(truststoreType string) bool {
}
func validateTruststoreName(truststoreName string) bool {
if strings.Contains(truststoreName, "..") {
return false
}
return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(truststoreName)
}
// implementation from https://github.com/notaryproject/notation-core-go/blob/main/x509/cert.go#L20
func validateCertificate(certificateContent []byte) (bool, error) {
func parseAndValidateCertificateContent(certificateContent []byte) ([]*x509.Certificate, bool, error) {
var certs []*x509.Certificate
block, rest := pem.Decode(certificateContent)
@@ -305,7 +497,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
// data may be in DER format
derCerts, err := x509.ParseCertificates(certificateContent)
if err != nil {
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, derCerts...)
@@ -314,7 +506,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
@@ -322,9 +514,9 @@ func validateCertificate(certificateContent []byte) (bool, error) {
}
if len(certs) == 0 {
return false, fmt.Errorf("%w: no valid certificates found in payload",
return []*x509.Certificate{}, false, fmt.Errorf("%w: no valid certificates found in payload",
zerr.ErrInvalidCertificateContent)
}
return true, nil
return certs, true, nil
}