mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 12:58:02 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user