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
+186 -56
View File
@@ -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 {
+105 -12
View File
@@ -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
+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
}