0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-23 22:27:35 -05:00
zot/pkg/extensions/imagetrust/notation.go

551 lines
15 KiB
Go
Raw Normal View History

//go:build imagetrust
// +build imagetrust
package imagetrust
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
)
const (
notationDirRelativePath = "_notation"
truststoreName = "default"
)
type CertificateLocalStorage struct {
notationDir string
}
type CertificateAWSStorage struct {
secretsManagerClient SecretsManagerClient
secretsManagerCache SecretsManagerCache
}
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 nil, err
}
}
if err != nil {
return nil, err
}
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 certStorage, nil
}
func NewCertificateAWSStorage(
secretsManagerClient SecretsManagerClient, secretsManagerCache SecretsManagerCache,
) (*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`
// this default trustpolicy.json file sets the signatureVerification.level
// to `strict` which enforces all validations (this means that even if there is
// 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": [` + defaultTruststores + `],
"trustedIdentities": [
"*"
]
}
]
}`
return notationStorage.InitTrustpolicy([]byte(trustPolicy))
}
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.") {
trustpolicyContent, err := cloud.secretsManagerCache.GetSecretString(name)
if err != nil {
return err
}
existingTrustpolicy, err := base64.StdEncoding.DecodeString(trustpolicyContent)
if err == nil && bytes.Equal(trustpolicy, existingTrustpolicy) {
return nil
}
force := true
deleteSecretParam := &secretsmanager.DeleteSecretInput{
SecretId: &name,
ForceDeleteWithoutRecovery: &force,
}
_, err = cloud.secretsManagerClient.DeleteSecret(context.Background(), deleteSecretParam)
if err != nil {
return err
}
err = cloud.recreateSecret(secretInputParam)
return err
}
return err
}
func (cloud *CertificateAWSStorage) recreateSecret(secretInputParam *secretsmanager.CreateSecretInput) error {
maxAttempts := 5
retrySec := 2
var err error
for i := 0; i < maxAttempts; i++ {
_, err = cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
if err == nil {
return nil
}
time.Sleep(time.Duration(retrySec) * time.Second)
}
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 (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
}
defer jsonFile.Close()
policyDocument := &trustpolicy.Document{}
err = json.NewDecoder(jsonFile).Decode(policyDocument)
if err != nil {
return nil, err
}
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(notationStorage certificateStorage) (notation.Verifier, error) {
// Load trust policy.
policyDocument, err := notationStorage.LoadTrustPolicyDocument()
if err != nil {
return nil, err
}
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(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(
notationStorage certificateStorage, artifactDescriptor ispec.Descriptor, artifactReference string,
rawSignature []byte, signatureMediaType string,
) (string, time.Time, bool, error) {
var (
date time.Time
author string
)
// If there's no signature associated with the reference.
if len(rawSignature) == 0 {
return author, date, false, notation.ErrorSignatureRetrievalFailed{
Msg: fmt.Sprintf("no signature associated with %q is provided, make sure the image was signed successfully",
artifactReference),
}
}
// Initialize verifier.
verifier, err := NewFromConfig(notationStorage)
if err != nil {
return author, date, false, err
}
ctx := context.Background()
// Set VerifyOptions.
opts := notation.VerifierVerifyOptions{
// ArtifactReference is important to validate registry scope format
// If "registryScopes" field from trustpolicy.json file is not wildcard then "domain:80/repo@" should not be hardcoded
ArtifactReference: "domain:80/repo@" + artifactReference,
SignatureMediaType: signatureMediaType,
PluginConfig: map[string]string{},
}
// Verify the notation signature which should be associated with the artifactDescriptor.
outcome, err := verifier.Verify(ctx, artifactDescriptor, rawSignature, opts)
if outcome.EnvelopeContent != nil {
author = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].Subject.String()
if outcome.VerificationLevel == trustpolicy.LevelStrict && (err == nil ||
CheckExpiryErr(outcome.VerificationResults, outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter, err)) {
expiry := outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry
if !expiry.IsZero() && expiry.Before(outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter) {
date = outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry
} else {
date = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter
}
}
}
if err != nil {
return author, date, false, err
}
// Verification Succeeded.
return author, date, true, nil
}
func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter time.Time, err error) bool {
for _, result := range verificationResults {
if result.Type == trustpolicy.TypeExpiry {
if errors.Is(err, result.Error) {
return true
}
} else if result.Type == trustpolicy.TypeAuthenticTimestamp {
if errors.Is(err, result.Error) && time.Now().After(notAfter) {
return true
} else {
return false
}
}
}
return false
}
func UploadCertificate(
notationStorage certificateStorage, certificateContent []byte, truststoreType string,
) error {
// validate truststore type
if !validateTruststoreType(truststoreType) {
return zerr.ErrInvalidTruststoreType
}
// validate certificate
if _, ok, err := parseAndValidateCertificateContent(certificateContent); !ok {
return err
}
// 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
}
name := godigest.FromBytes(certificateContent)
// store certificate
truststorePath := path.Join(configDir, dir.TrustStoreDir, "x509", truststoreType, truststoreName, name.String())
if err := os.MkdirAll(filepath.Dir(truststorePath), defaultDirPerms); 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,
}
_, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam)
return err
}
func validateTruststoreType(truststoreType string) bool {
for _, t := range truststore.Types {
if string(t) == truststoreType {
return true
}
}
return false
}
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 parseAndValidateCertificateContent(certificateContent []byte) ([]*x509.Certificate, bool, error) {
var certs []*x509.Certificate
block, rest := pem.Decode(certificateContent)
if block == nil {
// data may be in DER format
derCerts, err := x509.ParseCertificates(certificateContent)
if err != nil {
return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, derCerts...)
} else {
// data is in PEM format
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
}
}
if len(certs) == 0 {
return []*x509.Certificate{}, false, fmt.Errorf("%w: no valid certificates found in payload",
refactor(extensions)!: refactor the extensions URLs and errors (#1636) BREAKING CHANGE: The functionality provided by the mgmt endpoint has beed redesigned - see details below BREAKING CHANGE: The API keys endpoint has been moved - see details below BREAKING CHANGE: The mgmt extension config has been removed - endpoint is now enabled by having both the search and the ui extensions enabled BREAKING CHANGE: The API keys configuration has been moved from extensions to http>auth>apikey mgmt and imagetrust extensions: - separate the _zot/ext/mgmt into 3 separate endpoints: _zot/ext/auth, _zot/ext/notation, _zot/ext/cosign - signature verification logic is in a separate `imagetrust` extension - better hanling or errors in case of signature uploads: logging and error codes (more 400 and less 500 errors) - add authz on signature uploads (and add a new middleware in common for this purpose) - remove the mgmt extension configuration - it is now enabled if the UI and the search extensions are enabled userprefs estension: - userprefs are enabled if both search and ui extensions are enabled (as opposed to just search) apikey extension is removed and logic moved into the api folder - Move apikeys code out of pkg/extensions and into pkg/api - Remove apikey configuration options from the extensions configuration and move it inside the http auth section - remove the build label apikeys other changes: - move most of the logic adding handlers to the extensions endpoints out of routes.go and into the extensions files. - add warnings in case the users are still using configurations with the obsolete settings for mgmt and api keys - add a new function in the extension package which could be a single point of starting backgroud tasks for all extensions - more clear methods for verifying specific extensions are enabled - fix http methods paired with the UI handlers - rebuild swagger docs Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-08-02 13:58:34 -05:00
zerr.ErrInvalidCertificateContent)
}
return certs, true, nil
}