mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 04:48:26 +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:
@@ -13,10 +13,14 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"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"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
|
||||
sigs "github.com/sigstore/cosign/v2/pkg/signature"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
sigstoreSigs "github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/sigstore/sigstore/pkg/signature/options"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
@@ -24,116 +28,242 @@ import (
|
||||
|
||||
const cosignDirRelativePath = "_cosign"
|
||||
|
||||
var cosignDir = "" //nolint:gochecknoglobals
|
||||
type PublicKeyLocalStorage struct {
|
||||
cosignDir string
|
||||
}
|
||||
|
||||
func InitCosignDir(rootDir string) error {
|
||||
type PublicKeyAWSStorage struct {
|
||||
secretsManagerClient *secretsmanager.Client
|
||||
secretsManagerCache *secretcache.Cache
|
||||
}
|
||||
|
||||
type publicKeyStorage interface {
|
||||
StorePublicKey(name godigest.Digest, publicKeyContent []byte) error
|
||||
GetPublicKeyVerifier(name string) (sigstoreSigs.Verifier, []byte, error)
|
||||
GetPublicKeys() ([]string, error)
|
||||
}
|
||||
|
||||
func NewPublicKeyLocalStorage(rootDir string) (*PublicKeyLocalStorage, error) {
|
||||
dir := path.Join(rootDir, cosignDirRelativePath)
|
||||
|
||||
_, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, defaultDirPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
cosignDir = dir
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return err
|
||||
return &PublicKeyLocalStorage{
|
||||
cosignDir: dir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetCosignDirPath() (string, error) {
|
||||
if cosignDir != "" {
|
||||
return cosignDir, nil
|
||||
func NewPublicKeyAWSStorage(
|
||||
secretsManagerClient *secretsmanager.Client, secretsManagerCache *secretcache.Cache,
|
||||
) *PublicKeyAWSStorage {
|
||||
return &PublicKeyAWSStorage{
|
||||
secretsManagerClient: secretsManagerClient,
|
||||
secretsManagerCache: secretsManagerCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (local *PublicKeyLocalStorage) GetCosignDirPath() (string, error) {
|
||||
if local.cosignDir != "" {
|
||||
return local.cosignDir, nil
|
||||
}
|
||||
|
||||
return "", zerr.ErrSignConfigDirNotSet
|
||||
}
|
||||
|
||||
func VerifyCosignSignature(
|
||||
repo string, digest godigest.Digest, signatureKey string, layerContent []byte,
|
||||
cosignStorage publicKeyStorage, repo string, digest godigest.Digest, signatureKey string, layerContent []byte,
|
||||
) (string, bool, error) {
|
||||
cosignDir, err := GetCosignDirPath()
|
||||
publicKeys, err := cosignStorage.GetPublicKeys()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(cosignDir)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
for _, publicKey := range publicKeys {
|
||||
// cosign verify the image
|
||||
pubKeyVerifier, pubKeyContent, err := cosignStorage.GetPublicKeyVerifier(publicKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
// cosign verify the image
|
||||
ctx := context.Background()
|
||||
keyRef := path.Join(cosignDir, file.Name())
|
||||
hashAlgorithm := crypto.SHA256
|
||||
pkcs11Key, ok := pubKeyVerifier.(*pkcs11key.Key)
|
||||
if ok {
|
||||
defer pkcs11Key.Close()
|
||||
}
|
||||
|
||||
pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
verifier := pubKeyVerifier
|
||||
|
||||
pkcs11Key, ok := pubKey.(*pkcs11key.Key)
|
||||
if ok {
|
||||
defer pkcs11Key.Close()
|
||||
}
|
||||
b64sig := signatureKey
|
||||
|
||||
verifier := pubKey
|
||||
signature, err := base64.StdEncoding.DecodeString(b64sig)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
b64sig := signatureKey
|
||||
compressed := io.NopCloser(bytes.NewReader(layerContent))
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(b64sig)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
payload, err := io.ReadAll(compressed)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
compressed := io.NopCloser(bytes.NewReader(layerContent))
|
||||
err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload),
|
||||
options.WithContext(context.Background()))
|
||||
|
||||
payload, err := io.ReadAll(compressed)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx))
|
||||
|
||||
if err == nil {
|
||||
publicKey, err := os.ReadFile(keyRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return string(publicKey), true, nil
|
||||
}
|
||||
if err == nil {
|
||||
return string(pubKeyContent), true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func UploadPublicKey(publicKeyContent []byte) error {
|
||||
func (local *PublicKeyLocalStorage) GetPublicKeyVerifier(fileName string) (sigstoreSigs.Verifier, []byte, error) {
|
||||
cosignDir, err := local.GetCosignDirPath()
|
||||
if err != nil {
|
||||
return nil, []byte{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
keyRef := path.Join(cosignDir, fileName)
|
||||
hashAlgorithm := crypto.SHA256
|
||||
|
||||
pubKeyContent, err := os.ReadFile(keyRef)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pubKey, err := sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pubKey, pubKeyContent, nil
|
||||
}
|
||||
|
||||
func (cloud *PublicKeyAWSStorage) GetPublicKeyVerifier(secretName string) (sigstoreSigs.Verifier, []byte, error) {
|
||||
hashAlgorithm := crypto.SHA256
|
||||
|
||||
// get key
|
||||
raw, err := cloud.secretsManagerCache.GetSecretString(secretName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rawDecoded, err := base64.StdEncoding.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// PEM encoded file.
|
||||
key, err := cryptoutils.UnmarshalPEMToPublicKey(rawDecoded)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pubKey, err := sigstoreSigs.LoadVerifier(key, hashAlgorithm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pubKey, rawDecoded, nil
|
||||
}
|
||||
|
||||
func (local *PublicKeyLocalStorage) GetPublicKeys() ([]string, error) {
|
||||
cosignDir, err := local.GetCosignDirPath()
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(cosignDir)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
publicKeys := []string{}
|
||||
for _, file := range files {
|
||||
publicKeys = append(publicKeys, file.Name())
|
||||
}
|
||||
|
||||
return publicKeys, nil
|
||||
}
|
||||
|
||||
func (cloud *PublicKeyAWSStorage) GetPublicKeys() ([]string, error) {
|
||||
ctx := context.Background()
|
||||
listSecretsInput := secretsmanager.ListSecretsInput{
|
||||
Filters: []types.Filter{
|
||||
{
|
||||
Key: types.FilterNameStringTypeDescription,
|
||||
Values: []string{"cosign public key"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
publicKeys := []string{}
|
||||
|
||||
for _, secret := range secrets.SecretList {
|
||||
publicKeys = append(publicKeys, *(secret.Name))
|
||||
}
|
||||
|
||||
return publicKeys, nil
|
||||
}
|
||||
|
||||
func UploadPublicKey(cosignStorage publicKeyStorage, publicKeyContent []byte) error {
|
||||
// validate public key
|
||||
if ok, err := validatePublicKey(publicKeyContent); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
name := godigest.FromBytes(publicKeyContent)
|
||||
|
||||
return cosignStorage.StorePublicKey(name, publicKeyContent)
|
||||
}
|
||||
|
||||
func (local *PublicKeyLocalStorage) StorePublicKey(name godigest.Digest, publicKeyContent []byte) error {
|
||||
// add public key to "{rootDir}/_cosign/{name.pub}"
|
||||
configDir, err := GetCosignDirPath()
|
||||
cosignDir, err := local.GetCosignDirPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := godigest.FromBytes(publicKeyContent)
|
||||
|
||||
// store public key
|
||||
publicKeyPath := path.Join(configDir, name.String())
|
||||
publicKeyPath := path.Join(cosignDir, name.String())
|
||||
|
||||
return os.WriteFile(publicKeyPath, publicKeyContent, defaultFilePerms)
|
||||
}
|
||||
|
||||
func (cloud *PublicKeyAWSStorage) StorePublicKey(name godigest.Digest, publicKeyContent []byte) error {
|
||||
n := name.Encoded()
|
||||
description := "cosign public key"
|
||||
secret := base64.StdEncoding.EncodeToString(publicKeyContent)
|
||||
secretInputParam := &secretsmanager.CreateSecretInput{
|
||||
Name: &n,
|
||||
Description: &description,
|
||||
SecretString: &secret,
|
||||
}
|
||||
|
||||
_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePublicKey(publicKeyContent []byte) (bool, error) {
|
||||
_, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,14 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
aws1 "github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
smanager "github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/aws/aws-secretsmanager-caching-go/secretcache"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
@@ -23,18 +31,103 @@ const (
|
||||
defaultFilePerms = 0o644
|
||||
)
|
||||
|
||||
func InitCosignAndNotationDirs(rootDir string) error {
|
||||
err := InitCosignDir(rootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = InitNotationDir(rootDir)
|
||||
|
||||
return err
|
||||
type ImageTrustStore struct {
|
||||
CosignStorage publicKeyStorage
|
||||
NotationStorage certificateStorage
|
||||
}
|
||||
|
||||
func VerifySignature(
|
||||
func NewLocalImageTrustStore(rootDir string) (*ImageTrustStore, error) {
|
||||
publicKeyStorage, err := NewPublicKeyLocalStorage(rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certStorage, err := NewCertificateLocalStorage(rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageTrustStore{
|
||||
CosignStorage: publicKeyStorage,
|
||||
NotationStorage: certStorage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAWSImageTrustStore(region, endpoint string) (*ImageTrustStore, error) {
|
||||
secretsManagerClient, err := GetSecretsManagerClient(region, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretsManagerCache := GetSecretsManagerRetrieval(region, endpoint)
|
||||
|
||||
publicKeyStorage := NewPublicKeyAWSStorage(secretsManagerClient, secretsManagerCache)
|
||||
|
||||
certStorage, err := NewCertificateAWSStorage(secretsManagerClient, secretsManagerCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageTrustStore{
|
||||
CosignStorage: publicKeyStorage,
|
||||
NotationStorage: certStorage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetSecretsManagerClient(region, endpoint string) (*secretsmanager.Client, error) {
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(
|
||||
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{
|
||||
PartitionID: "aws",
|
||||
URL: endpoint,
|
||||
SigningRegion: region,
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Using the SDK's default configuration, loading additional config
|
||||
// and credentials values from the environment variables, shared
|
||||
// credentials, and shared configuration files
|
||||
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region),
|
||||
config.WithEndpointResolverWithOptions(customResolver))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretsmanager.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
func GetSecretsManagerRetrieval(region, endpoint string) *secretcache.Cache {
|
||||
endpointFunc := func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
|
||||
return endpoints.ResolvedEndpoint{
|
||||
PartitionID: "aws",
|
||||
URL: endpoint,
|
||||
SigningRegion: region,
|
||||
}, nil
|
||||
}
|
||||
customResolver := endpoints.ResolverFunc(endpointFunc)
|
||||
|
||||
cfg := aws1.NewConfig().WithRegion(region).WithEndpointResolver(customResolver)
|
||||
|
||||
newSession := session.Must(session.NewSession())
|
||||
|
||||
client := smanager.New(newSession, cfg)
|
||||
// Create a custom CacheConfig struct
|
||||
config := secretcache.CacheConfig{
|
||||
MaxCacheSize: secretcache.DefaultMaxCacheSize,
|
||||
VersionStage: secretcache.DefaultVersionStage,
|
||||
CacheItemTTL: secretcache.DefaultCacheItemTTL,
|
||||
}
|
||||
|
||||
// Instantiate the cache
|
||||
cache, _ := secretcache.New(
|
||||
func(c *secretcache.Cache) { c.CacheConfig = config },
|
||||
func(c *secretcache.Cache) { c.Client = client },
|
||||
)
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func (imgTrustStore *ImageTrustStore) VerifySignature(
|
||||
signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte,
|
||||
repo string,
|
||||
) (string, time.Time, bool, error) {
|
||||
@@ -55,11 +148,11 @@ func VerifySignature(
|
||||
|
||||
switch signatureType {
|
||||
case zcommon.CosignSignature:
|
||||
author, isValid, err := VerifyCosignSignature(repo, manifestDigest, sigKey, rawSignature)
|
||||
author, isValid, err := VerifyCosignSignature(imgTrustStore.CosignStorage, repo, manifestDigest, sigKey, rawSignature)
|
||||
|
||||
return author, time.Time{}, isValid, err
|
||||
case zcommon.NotationSignature:
|
||||
return VerifyNotationSignature(desc, manifestDigest.String(), rawSignature, sigKey)
|
||||
return VerifyNotationSignature(imgTrustStore.NotationStorage, desc, manifestDigest.String(), rawSignature, sigKey)
|
||||
default:
|
||||
return "", time.Time{}, false, zerr.ErrInvalidSignatureType
|
||||
}
|
||||
|
||||
@@ -9,19 +9,17 @@ import (
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
func InitCosignAndNotationDirs(rootDir string) error {
|
||||
return nil
|
||||
func NewLocalImageTrustStore(dir string) (*imageTrustDisabled, error) {
|
||||
return &imageTrustDisabled{}, nil
|
||||
}
|
||||
|
||||
func InitCosignDir(rootDir string) error {
|
||||
return nil
|
||||
func NewAWSImageTrustStore(region, endpoint string) (*imageTrustDisabled, error) {
|
||||
return &imageTrustDisabled{}, nil
|
||||
}
|
||||
|
||||
func InitNotationDir(rootDir string) error {
|
||||
return nil
|
||||
}
|
||||
type imageTrustDisabled struct{}
|
||||
|
||||
func VerifySignature(
|
||||
func (imgTrustStore *imageTrustDisabled) VerifySignature(
|
||||
signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, manifestContent []byte,
|
||||
repo string,
|
||||
) (string, time.Time, bool, error) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package imagetrust_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@@ -10,35 +11,56 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"zotregistry.io/zot/pkg/extensions/imagetrust"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
func TestImageTrust(t *testing.T) {
|
||||
Convey("binary doesn't include imagetrust", t, func() {
|
||||
rootDir := t.TempDir()
|
||||
|
||||
err := imagetrust.InitCosignDir(rootDir)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
cosignDir := path.Join(rootDir, "_cosign")
|
||||
_, err = os.Stat(cosignDir)
|
||||
_, err := os.Stat(cosignDir)
|
||||
So(os.IsNotExist(err), ShouldBeTrue)
|
||||
|
||||
err = imagetrust.InitNotationDir(rootDir)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
notationDir := path.Join(rootDir, "_notation")
|
||||
_, err = os.Stat(notationDir)
|
||||
So(os.IsNotExist(err), ShouldBeTrue)
|
||||
|
||||
err = imagetrust.InitCosignAndNotationDirs(rootDir)
|
||||
repo := "repo"
|
||||
|
||||
image, err := test.GetRandomImage() //nolint:staticcheck
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
manifestContent, err := json.Marshal(image.Manifest)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
manifestDigest := image.Digest()
|
||||
|
||||
localImgTrustStore, err := imagetrust.NewLocalImageTrustStore(rootDir)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
author, expTime, ok, err := localImgTrustStore.VerifySignature("cosign",
|
||||
[]byte(""), "", manifestDigest, manifestContent, repo,
|
||||
)
|
||||
So(author, ShouldBeEmpty)
|
||||
So(expTime, ShouldBeZeroValue)
|
||||
So(ok, ShouldBeFalse)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = os.Stat(cosignDir)
|
||||
So(os.IsNotExist(err), ShouldBeTrue)
|
||||
|
||||
_, err = os.Stat(notationDir)
|
||||
So(os.IsNotExist(err), ShouldBeTrue)
|
||||
|
||||
author, expTime, ok, err := imagetrust.VerifySignature("", []byte{}, "", "", []byte{}, "")
|
||||
cloudImgTrustStore, err := imagetrust.NewAWSImageTrustStore("region",
|
||||
"endpoint",
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
author, expTime, ok, err = cloudImgTrustStore.VerifySignature("cosign",
|
||||
[]byte(""), "", manifestDigest, manifestContent, repo,
|
||||
)
|
||||
So(author, ShouldBeEmpty)
|
||||
So(expTime, ShouldBeZeroValue)
|
||||
So(ok, ShouldBeFalse)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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