mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 16:06:56 +00:00
feat: add notary verifier with tsa support (#12160)
* feat: add notary repository Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * feat: add notary verifier Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * feat: tests Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * feat: more tests Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * fix: more tests Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * fix: ci Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * feat: update types Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> --------- Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
parent
2898048511
commit
219f25ace2
11 changed files with 664 additions and 26 deletions
|
@ -127,10 +127,10 @@ type Attestor struct {
|
|||
Name string `json:"name"`
|
||||
// Cosign defines attestor configuration for Cosign based signatures
|
||||
// +optional
|
||||
Cosign Cosign `json:"cosign,omitempty"`
|
||||
Cosign *Cosign `json:"cosign,omitempty"`
|
||||
// Notary defines attestor configuration for Notary based signatures
|
||||
// +optional
|
||||
Notary Notary `json:"notary,omitempty"`
|
||||
Notary *Notary `json:"notary,omitempty"`
|
||||
}
|
||||
|
||||
// Cosign defines attestor configuration for Cosign based signatures
|
||||
|
@ -294,11 +294,11 @@ type Attestation struct {
|
|||
|
||||
// InToto defines the details of attestation attached using intoto format
|
||||
// +optional
|
||||
InToto InToto `json:"intoto,omitempty"`
|
||||
InToto *InToto `json:"intoto,omitempty"`
|
||||
|
||||
// Referrer defines the details of attestation attached using OCI 1.1 format
|
||||
// +optional
|
||||
Referrer Referrer `json:"referrer,omitempty"`
|
||||
Referrer *Referrer `json:"referrer,omitempty"`
|
||||
}
|
||||
|
||||
type InToto struct {
|
||||
|
|
|
@ -31,8 +31,16 @@ import (
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Attestation) DeepCopyInto(out *Attestation) {
|
||||
*out = *in
|
||||
out.InToto = in.InToto
|
||||
out.Referrer = in.Referrer
|
||||
if in.InToto != nil {
|
||||
in, out := &in.InToto, &out.InToto
|
||||
*out = new(InToto)
|
||||
**out = **in
|
||||
}
|
||||
if in.Referrer != nil {
|
||||
in, out := &in.Referrer, &out.Referrer
|
||||
*out = new(Referrer)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -49,8 +57,16 @@ func (in *Attestation) DeepCopy() *Attestation {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Attestor) DeepCopyInto(out *Attestor) {
|
||||
*out = *in
|
||||
in.Cosign.DeepCopyInto(&out.Cosign)
|
||||
out.Notary = in.Notary
|
||||
if in.Cosign != nil {
|
||||
in, out := &in.Cosign, &out.Cosign
|
||||
*out = new(Cosign)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Notary != nil {
|
||||
in, out := &in.Notary, &out.Notary
|
||||
*out = new(Notary)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -498,7 +514,9 @@ func (in *ImageVerificationPolicySpec) DeepCopyInto(out *ImageVerificationPolicy
|
|||
if in.Attestations != nil {
|
||||
in, out := &in.Attestations, &out.Attestations
|
||||
*out = make([]Attestation, len(*in))
|
||||
copy(*out, *in)
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Verifications != nil {
|
||||
in, out := &in.Verifications, &out.Verifications
|
||||
|
|
|
@ -19,8 +19,6 @@ var (
|
|||
)
|
||||
|
||||
type imagedatafetcher struct {
|
||||
// TODO: Add caching and prefetching
|
||||
|
||||
lister k8scorev1.SecretInterface
|
||||
defaultOptions []remote.Option
|
||||
}
|
||||
|
@ -43,7 +41,8 @@ func New(lister k8scorev1.SecretInterface, opts ...Option) (*imagedatafetcher, e
|
|||
|
||||
func (i *imagedatafetcher) FetchImageData(ctx context.Context, image string, options ...Option) (*ImageData, error) {
|
||||
img := ImageData{
|
||||
Image: image,
|
||||
Image: image,
|
||||
referrersData: make(map[string]referrerData),
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -146,10 +145,30 @@ type ImageData struct {
|
|||
nameRef name.Reference
|
||||
desc *remote.Descriptor
|
||||
referrersManifest *gcrv1.IndexManifest
|
||||
referrersData map[string]referrerData
|
||||
verifiedReferrers []gcrv1.Descriptor
|
||||
}
|
||||
|
||||
func (i *ImageData) Descriptor() ocispec.Descriptor {
|
||||
return GCRtoOCISpecDesc(i.desc.Descriptor)
|
||||
type referrerData struct {
|
||||
layerDescriptor *gcrv1.Descriptor
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (i *ImageData) FetchReference(identifier string) (ocispec.Descriptor, error) {
|
||||
if identifier == i.Digest {
|
||||
return GCRtoOCISpecDesc(i.desc.Descriptor), nil
|
||||
}
|
||||
|
||||
d, err := remote.Head(i.nameRef.Context().Digest(identifier), i.remoteOpts...)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
return GCRtoOCISpecDesc(*d), nil
|
||||
}
|
||||
|
||||
func (i *ImageData) WithDigest(digest string) string {
|
||||
return i.nameRef.Context().Digest(digest).String()
|
||||
}
|
||||
|
||||
func (i *ImageData) loadReferrers() error {
|
||||
|
@ -157,25 +176,56 @@ func (i *ImageData) loadReferrers() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
referrers, err := remote.Referrers(i.nameRef.Context().Digest(i.Digest), i.remoteOpts...)
|
||||
referrersDescs, err := i.fetchReferrersFromRemote(i.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
referrersDescs, err := referrers.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This check ensures that the manifest does not have an abnormal amount of referrers attached to it to protect against compromised images
|
||||
if len(referrersDescs.Manifests) > maxReferrersCount {
|
||||
return fmt.Errorf("failed to fetch referrers: to many referrers found, max limit is %d", maxReferrersCount)
|
||||
}
|
||||
|
||||
i.referrersManifest = referrersDescs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ImageData) fetchReferrersFromRemote(digest string) (*gcrv1.IndexManifest, error) {
|
||||
referrers, err := remote.Referrers(i.nameRef.Context().Digest(digest), i.remoteOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
referrersDescs, err := referrers.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// This check ensures that the manifest does not have an abnormal amount of referrers attached to it to protect against compromised images
|
||||
if len(referrersDescs.Manifests) > maxReferrersCount {
|
||||
return nil, fmt.Errorf("failed to fetch referrers: to many referrers found, max limit is %d", maxReferrersCount)
|
||||
}
|
||||
|
||||
return referrersDescs, nil
|
||||
}
|
||||
|
||||
func (i *ImageData) FetchRefererrsForDigest(digest string, artifactType string) ([]gcrv1.Descriptor, error) {
|
||||
// If the call is for image referrers, return prefetched referrers
|
||||
if digest == i.Digest {
|
||||
return i.FetchRefererrs(artifactType)
|
||||
}
|
||||
|
||||
// this is most likely a call to fetch notary signatures for an attesatation
|
||||
idx, err := i.fetchReferrersFromRemote(digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refList := make([]gcrv1.Descriptor, 0)
|
||||
for _, ref := range idx.Manifests {
|
||||
if ref.ArtifactType == artifactType {
|
||||
refList = append(refList, ref)
|
||||
}
|
||||
}
|
||||
|
||||
return refList, nil
|
||||
}
|
||||
|
||||
func (i *ImageData) FetchRefererrs(artifactType string) ([]gcrv1.Descriptor, error) {
|
||||
if err := i.loadReferrers(); err != nil {
|
||||
return nil, err
|
||||
|
@ -192,6 +242,10 @@ func (i *ImageData) FetchRefererrs(artifactType string) ([]gcrv1.Descriptor, err
|
|||
}
|
||||
|
||||
func (i *ImageData) FetchReferrerData(desc gcrv1.Descriptor) ([]byte, *gcrv1.Descriptor, error) {
|
||||
if v, found := i.referrersData[desc.Digest.String()]; found {
|
||||
return v.data, v.layerDescriptor, nil
|
||||
}
|
||||
|
||||
img, err := remote.Image(i.nameRef.Context().Digest(desc.Digest.String()), i.remoteOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -233,5 +287,17 @@ func (i *ImageData) FetchReferrerData(desc gcrv1.Descriptor) ([]byte, *gcrv1.Des
|
|||
|
||||
b, err := io.ReadAll(io.LimitReader(reader, maxPayloadSize))
|
||||
|
||||
i.referrersData[desc.Digest.String()] = referrerData{
|
||||
data: b,
|
||||
layerDescriptor: layerDesc,
|
||||
}
|
||||
return b, layerDesc, err
|
||||
}
|
||||
|
||||
func (i *ImageData) AddVerifiedReferrer(desc gcrv1.Descriptor) {
|
||||
if i.verifiedReferrers == nil {
|
||||
i.verifiedReferrers = make([]gcrv1.Descriptor, 0)
|
||||
}
|
||||
|
||||
i.verifiedReferrers = append(i.verifiedReferrers, desc)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package imagedataloader
|
|||
|
||||
import (
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
@ -26,3 +27,30 @@ func GCRtoOCISpecDesc(v1desc gcrv1.Descriptor) ocispec.Descriptor {
|
|||
}
|
||||
return ociDesc
|
||||
}
|
||||
|
||||
func OCISpectoGCRDesc(ocidesc ocispec.Descriptor) (*gcrv1.Descriptor, error) {
|
||||
gcrDesc := &gcrv1.Descriptor{
|
||||
MediaType: types.MediaType(ocidesc.MediaType),
|
||||
Size: ocidesc.Size,
|
||||
URLs: ocidesc.URLs,
|
||||
Annotations: ocidesc.Annotations,
|
||||
Data: ocidesc.Data,
|
||||
ArtifactType: ocidesc.ArtifactType,
|
||||
}
|
||||
|
||||
digest, err := gcrv1.NewHash(ocidesc.Digest.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcrDesc.Digest = digest
|
||||
if ocidesc.Platform != nil {
|
||||
gcrDesc.Platform = &gcrv1.Platform{
|
||||
Architecture: ocidesc.Platform.Architecture,
|
||||
OS: ocidesc.Platform.OS,
|
||||
OSVersion: ocidesc.Platform.OSVersion,
|
||||
}
|
||||
}
|
||||
|
||||
return gcrDesc, nil
|
||||
}
|
||||
|
|
107
pkg/imageverifiers/notary/helpers.go
Normal file
107
pkg/imageverifiers/notary/helpers.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package notary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
|
||||
"github.com/kyverno/kyverno/pkg/imagedataloader"
|
||||
"github.com/notaryproject/notation-go"
|
||||
notationregistry "github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
type simpleTrustStore struct {
|
||||
name string
|
||||
cacerts []*x509.Certificate
|
||||
tsacerts []*x509.Certificate
|
||||
}
|
||||
|
||||
func NewTrustStore(name string, certs []*x509.Certificate, tsaCerts []*x509.Certificate) truststore.X509TrustStore {
|
||||
return &simpleTrustStore{
|
||||
name: name,
|
||||
cacerts: certs,
|
||||
tsacerts: tsaCerts,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *simpleTrustStore) GetCertificates(ctx context.Context, storeType truststore.Type, name string) ([]*x509.Certificate, error) {
|
||||
if name != ts.name {
|
||||
return nil, errors.Errorf("truststore not found")
|
||||
}
|
||||
|
||||
switch storeType {
|
||||
case truststore.TypeCA:
|
||||
return ts.cacerts, nil
|
||||
case truststore.TypeTSA:
|
||||
return ts.tsacerts, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("entry not found in trust store")
|
||||
}
|
||||
|
||||
func buildTrustPolicy(tsa []*x509.Certificate) *trustpolicy.Document {
|
||||
truststores := []string{"ca:kyverno"}
|
||||
if len(tsa) != 0 {
|
||||
truststores = append(truststores, "tsa:kyverno")
|
||||
}
|
||||
|
||||
return &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "kyverno",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name},
|
||||
TrustStores: truststores,
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func checkVerificationOutcomes(outcomes []*notation.VerificationOutcome) error {
|
||||
var errs []error
|
||||
for _, outcome := range outcomes {
|
||||
if outcome.Error != nil {
|
||||
errs = append(errs, outcome.Error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
|
||||
type verificationInfo struct {
|
||||
Verifier notation.Verifier
|
||||
Repo notationregistry.Repository
|
||||
}
|
||||
|
||||
func getVerificationInfo(image *imagedataloader.ImageData, att *policiesv1alpha1.Notary) (*verificationInfo, error) {
|
||||
certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader([]byte(att.Certs)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tsacerts, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader([]byte(att.TSACerts)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notationVerifier, err := verifier.New(buildTrustPolicy(tsacerts), NewTrustStore("kyverno", certs, tsacerts), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &verificationInfo{
|
||||
Verifier: notationVerifier,
|
||||
Repo: NewRepository(image),
|
||||
}, nil
|
||||
}
|
123
pkg/imageverifiers/notary/notary.go
Normal file
123
pkg/imageverifiers/notary/notary.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package notary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
|
||||
"github.com/kyverno/kyverno/pkg/imagedataloader"
|
||||
"github.com/kyverno/kyverno/pkg/logging"
|
||||
"github.com/notaryproject/notation-go"
|
||||
notationlog "github.com/notaryproject/notation-go/log"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
func NewVerifier() *notaryVerifier {
|
||||
return ¬aryVerifier{
|
||||
log: logging.WithName("Notary"),
|
||||
}
|
||||
}
|
||||
|
||||
type notaryVerifier struct {
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func (v *notaryVerifier) VerifyImageSignature(ctx context.Context, image *imagedataloader.ImageData, attestor *policiesv1alpha1.Attestor) error {
|
||||
if attestor.Notary == nil {
|
||||
return fmt.Errorf("notary verifier only supports notary attestor")
|
||||
}
|
||||
|
||||
logger := v.log.WithValues("image", image.Image, "digest", image.Digest, "attestor", attestor.Name)
|
||||
logger.V(2).Info("verifying notary image signature", "image", image.Image)
|
||||
|
||||
vInfo, err := getVerificationInfo(image, attestor.Notary)
|
||||
if err != nil {
|
||||
err := errors.Wrapf(err, "failed to setup notation verification data")
|
||||
logger.Error(err, "image verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
opts := notation.VerifyOptions{
|
||||
ArtifactReference: image.WithDigest(image.Digest),
|
||||
MaxSignatureAttempts: 10,
|
||||
}
|
||||
_, outcomes, err := notation.Verify(notationlog.WithLogger(ctx, NotaryLoggerAdapter(v.log.WithName("Notary Verifier Debug"))), vInfo.Verifier,
|
||||
vInfo.Repo, opts)
|
||||
if err != nil {
|
||||
err := errors.Wrapf(err, "failed to verify image %s", image.Image)
|
||||
logger.Error(err, "image verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkVerificationOutcomes(outcomes); err != nil {
|
||||
err := errors.Wrapf(err, "notation failed to verify signatures")
|
||||
logger.Error(err, "image verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *notaryVerifier) VerifyAttestationSignature(ctx context.Context, image *imagedataloader.ImageData, attestation *policiesv1alpha1.Attestation, attestor *policiesv1alpha1.Attestor) error {
|
||||
if attestation.Referrer == nil {
|
||||
return fmt.Errorf("notary verifier only supports oci 1.1 referrers as attestations")
|
||||
}
|
||||
if attestor.Notary == nil {
|
||||
return fmt.Errorf("notary verifier only supports notary attestor")
|
||||
}
|
||||
|
||||
logger := v.log.WithValues("image", image.Image, "digest", image.Digest, "attestation", attestation.Name, "attestor", attestor.Name) // TODO: use attestor and attestation names
|
||||
logger.V(2).Info("verifying notary image signature", "image", image.Image)
|
||||
|
||||
vInfo, err := getVerificationInfo(image, attestor.Notary)
|
||||
if err != nil {
|
||||
err := errors.Wrapf(err, "failed to setup notation verification data")
|
||||
logger.Error(err, "image verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
referrers, err := image.FetchRefererrs(attestation.Referrer.Type)
|
||||
if err != nil {
|
||||
err := errors.Wrapf(err, "failed to fetch referrers")
|
||||
logger.Error(err, "image attestation verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, r := range referrers {
|
||||
reference := image.WithDigest(r.Digest.String())
|
||||
logger := logger.WithValues("attestation ref", reference)
|
||||
|
||||
logger.V(2).Info("verifying attestation")
|
||||
opts := notation.VerifyOptions{
|
||||
ArtifactReference: reference,
|
||||
MaxSignatureAttempts: 10,
|
||||
}
|
||||
|
||||
_, outcomes, err := notation.Verify(notationlog.WithLogger(ctx, NotaryLoggerAdapter(v.log.WithName("Notary Verifier Debug"))), vInfo.Verifier,
|
||||
vInfo.Repo, opts)
|
||||
if err != nil {
|
||||
err := errors.Wrapf(err, "failed to verify attestation %s, digest %s", r.ArtifactType, r.Digest)
|
||||
logger.Error(err, "attestation verification failed")
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := checkVerificationOutcomes(outcomes); err != nil {
|
||||
err := errors.Wrapf(err, "notation failed to verify attesattion signatures")
|
||||
logger.Error(err, "attesatation verification failed")
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
image.AddVerifiedReferrer(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return fmt.Errorf("attestation verification failed, no attestations found for type: %s", attestation.Referrer.Type)
|
||||
}
|
||||
return multierr.Combine(errs...)
|
||||
}
|
169
pkg/imageverifiers/notary/notary_test.go
Normal file
169
pkg/imageverifiers/notary/notary_test.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package notary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
|
||||
"github.com/kyverno/kyverno/pkg/imagedataloader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
cert = `-----BEGIN CERTIFICATE-----
|
||||
MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
|
||||
BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
|
||||
Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
|
||||
MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
|
||||
dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
|
||||
b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
|
||||
hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
|
||||
Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
|
||||
Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
|
||||
ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
|
||||
A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
|
||||
CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
|
||||
kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
|
||||
Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
|
||||
ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
|
||||
5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
|
||||
uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
|
||||
-----END CERTIFICATE-----`
|
||||
unsignedImage = "ghcr.io/kyverno/test-verify-image:unsigned"
|
||||
)
|
||||
|
||||
func Test_ImageSignatureVerificationStandard(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyImageSignature(ctx, img, attestor)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ImageSignatureVerificationUnsigned(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, unsignedImage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyImageSignature(ctx, img, attestor)
|
||||
assert.ErrorContains(t, err, "make sure the artifact was signed successfully")
|
||||
}
|
||||
|
||||
func Test_ImageAttestationVerificationStandard(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
attestation := &v1alpha1.Attestation{
|
||||
Name: "attestation",
|
||||
Referrer: &v1alpha1.Referrer{
|
||||
Type: "sbom/cyclone-dx",
|
||||
},
|
||||
}
|
||||
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyAttestationSignature(ctx, img, attestation, attestor)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ImageAttestationVerificationFailNotFound(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
attestation := &v1alpha1.Attestation{
|
||||
Name: "attestation",
|
||||
Referrer: &v1alpha1.Referrer{
|
||||
Type: "invalid",
|
||||
},
|
||||
}
|
||||
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyAttestationSignature(ctx, img, attestation, attestor)
|
||||
assert.ErrorContains(t, err, "attestation verification failed, no attestations found for type: invalid")
|
||||
}
|
||||
|
||||
func Test_ImageAttestationVerificationFailUntrusted(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
attestation := &v1alpha1.Attestation{
|
||||
Name: "attestation",
|
||||
Referrer: &v1alpha1.Referrer{
|
||||
Type: "trivy/vulnerability-fail-test",
|
||||
},
|
||||
}
|
||||
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyAttestationSignature(ctx, img, attestation, attestor)
|
||||
assert.ErrorContains(t, err, "failed to verify signature with digest sha256:5e52184f10b19c69105e5dd5d3c875753cfd824d3d2f86cd2122e4107bd13d16, signature is not produced by a trusted signer")
|
||||
}
|
||||
|
||||
func Test_ImageAttestationVerificationFailUnsigned(t *testing.T) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
attestor := &v1alpha1.Attestor{
|
||||
Name: "test",
|
||||
Notary: &v1alpha1.Notary{
|
||||
Certs: cert,
|
||||
},
|
||||
}
|
||||
|
||||
attestation := &v1alpha1.Attestation{
|
||||
Name: "attestation",
|
||||
Referrer: &v1alpha1.Referrer{
|
||||
Type: "application/vnd.cncf.notary.signature",
|
||||
},
|
||||
}
|
||||
v := notaryVerifier{log: logr.Discard()}
|
||||
err = v.VerifyAttestationSignature(ctx, img, attestation, attestor)
|
||||
assert.ErrorContains(t, err, "make sure the artifact was signed successfully")
|
||||
}
|
57
pkg/imageverifiers/notary/repo.go
Normal file
57
pkg/imageverifiers/notary/repo.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package notary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kyverno/kyverno/pkg/imagedataloader"
|
||||
notationregistry "github.com/notaryproject/notation-go/registry"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type repositoryClient struct {
|
||||
image *imagedataloader.ImageData
|
||||
}
|
||||
|
||||
func NewRepository(image *imagedataloader.ImageData) notationregistry.Repository {
|
||||
return &repositoryClient{
|
||||
image: image,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *repositoryClient) Resolve(_ context.Context, img string) (ocispec.Descriptor, error) {
|
||||
fmt.Println(img)
|
||||
return c.image.FetchReference(img)
|
||||
}
|
||||
|
||||
func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error {
|
||||
gcrDesc, err := c.image.FetchRefererrsForDigest(desc.Digest.String(), notationregistry.ArtifactTypeNotation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
descriptorList := make([]ocispec.Descriptor, 0, len(gcrDesc))
|
||||
for _, d := range gcrDesc {
|
||||
descriptorList = append(descriptorList, imagedataloader.GCRtoOCISpecDesc(d))
|
||||
}
|
||||
|
||||
return fn(descriptorList)
|
||||
}
|
||||
|
||||
func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) {
|
||||
gcrDesc, err := imagedataloader.OCISpectoGCRDesc(desc)
|
||||
if err != nil {
|
||||
return nil, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
data, layerDesc, err := c.image.FetchReferrerData(*gcrDesc)
|
||||
if err != nil {
|
||||
return nil, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
return data, imagedataloader.GCRtoOCISpecDesc(*layerDesc), nil
|
||||
}
|
||||
|
||||
func (c *repositoryClient) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("push signature is not implemented")
|
||||
}
|
69
pkg/imageverifiers/notary/repo_test.go
Normal file
69
pkg/imageverifiers/notary/repo_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package notary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/kyverno/kyverno/pkg/imagedataloader"
|
||||
notationregistry "github.com/notaryproject/notation-go/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
image = "ghcr.io/kyverno/test-verify-image:signed"
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
repositoryClient, img := setuprepo(t)
|
||||
|
||||
desc, err := repositoryClient.Resolve(ctx, img.Digest)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, desc.Digest.String(), "sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105")
|
||||
assert.Equal(t, desc.MediaType, "application/vnd.docker.distribution.manifest.v2+json")
|
||||
}
|
||||
|
||||
func TestListSignatures(t *testing.T) {
|
||||
repositoryClient, img := setuprepo(t)
|
||||
sigs := 0
|
||||
|
||||
fn := func(l []ocispec.Descriptor) error {
|
||||
sigs = len(l)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := repositoryClient.ListSignatures(ctx, ocispec.Descriptor{Digest: digest.Digest(img.Digest)}, fn)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sigs, 2)
|
||||
}
|
||||
|
||||
func TestFetchSignatureBlob(t *testing.T) {
|
||||
repositoryClient, img := setuprepo(t)
|
||||
ref, err := name.ParseReference(image)
|
||||
assert.NoError(t, err)
|
||||
|
||||
referrers, err := remote.Referrers(ref.Context().Digest(img.Digest))
|
||||
assert.NoError(t, err)
|
||||
referrersDescs, err := referrers.IndexManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, d := range referrersDescs.Manifests {
|
||||
if d.ArtifactType == notationregistry.ArtifactTypeNotation {
|
||||
_, desc, err := repositoryClient.FetchSignatureBlob(ctx, imagedataloader.GCRtoOCISpecDesc(d))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, desc.MediaType, "application/jose+json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setuprepo(t *testing.T) (notationregistry.Repository, *imagedataloader.ImageData) {
|
||||
idf, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
img, err := idf.FetchImageData(ctx, image)
|
||||
assert.NoError(t, err)
|
||||
return NewRepository(img), img
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
gcrremote "github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/kyverno/kyverno/pkg/images"
|
||||
"github.com/kyverno/kyverno/pkg/imageverifiers/notary"
|
||||
"github.com/kyverno/kyverno/pkg/logging"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
|
@ -69,7 +70,7 @@ func (v *notaryVerifier) VerifySignature(ctx context.Context, opts images.Option
|
|||
MaxSignatureAttempts: 10,
|
||||
}
|
||||
|
||||
targetDesc, outcomes, err := notation.Verify(notationlog.WithLogger(ctx, NotaryLoggerAdapter(v.log.WithName("Notary Verifier Debug"))), notationVerifier, parsedRef.Repo, remoteVerifyOptions)
|
||||
targetDesc, outcomes, err := notation.Verify(notationlog.WithLogger(ctx, notary.NotaryLoggerAdapter(v.log.WithName("Notary Verifier Debug"))), notationVerifier, parsedRef.Repo, remoteVerifyOptions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to verify %s", ref)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue