1
0
Fork 0
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:
Vishal Choudhary 2025-02-18 12:53:39 +05:30 committed by GitHub
parent 2898048511
commit 219f25ace2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 664 additions and 26 deletions

View file

@ -127,10 +127,10 @@ type Attestor struct {
Name string `json:"name"` Name string `json:"name"`
// Cosign defines attestor configuration for Cosign based signatures // Cosign defines attestor configuration for Cosign based signatures
// +optional // +optional
Cosign Cosign `json:"cosign,omitempty"` Cosign *Cosign `json:"cosign,omitempty"`
// Notary defines attestor configuration for Notary based signatures // Notary defines attestor configuration for Notary based signatures
// +optional // +optional
Notary Notary `json:"notary,omitempty"` Notary *Notary `json:"notary,omitempty"`
} }
// Cosign defines attestor configuration for Cosign based signatures // 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 // InToto defines the details of attestation attached using intoto format
// +optional // +optional
InToto InToto `json:"intoto,omitempty"` InToto *InToto `json:"intoto,omitempty"`
// Referrer defines the details of attestation attached using OCI 1.1 format // Referrer defines the details of attestation attached using OCI 1.1 format
// +optional // +optional
Referrer Referrer `json:"referrer,omitempty"` Referrer *Referrer `json:"referrer,omitempty"`
} }
type InToto struct { type InToto struct {

View file

@ -31,8 +31,16 @@ import (
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Attestation) DeepCopyInto(out *Attestation) { func (in *Attestation) DeepCopyInto(out *Attestation) {
*out = *in *out = *in
out.InToto = in.InToto if in.InToto != nil {
out.Referrer = in.Referrer 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 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Attestor) DeepCopyInto(out *Attestor) { func (in *Attestor) DeepCopyInto(out *Attestor) {
*out = *in *out = *in
in.Cosign.DeepCopyInto(&out.Cosign) if in.Cosign != nil {
out.Notary = in.Notary 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 return
} }
@ -498,7 +514,9 @@ func (in *ImageVerificationPolicySpec) DeepCopyInto(out *ImageVerificationPolicy
if in.Attestations != nil { if in.Attestations != nil {
in, out := &in.Attestations, &out.Attestations in, out := &in.Attestations, &out.Attestations
*out = make([]Attestation, len(*in)) *out = make([]Attestation, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.Verifications != nil { if in.Verifications != nil {
in, out := &in.Verifications, &out.Verifications in, out := &in.Verifications, &out.Verifications

View file

@ -19,8 +19,6 @@ var (
) )
type imagedatafetcher struct { type imagedatafetcher struct {
// TODO: Add caching and prefetching
lister k8scorev1.SecretInterface lister k8scorev1.SecretInterface
defaultOptions []remote.Option 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) { func (i *imagedatafetcher) FetchImageData(ctx context.Context, image string, options ...Option) (*ImageData, error) {
img := ImageData{ img := ImageData{
Image: image, Image: image,
referrersData: make(map[string]referrerData),
} }
var err error var err error
@ -146,10 +145,30 @@ type ImageData struct {
nameRef name.Reference nameRef name.Reference
desc *remote.Descriptor desc *remote.Descriptor
referrersManifest *gcrv1.IndexManifest referrersManifest *gcrv1.IndexManifest
referrersData map[string]referrerData
verifiedReferrers []gcrv1.Descriptor
} }
func (i *ImageData) Descriptor() ocispec.Descriptor { type referrerData struct {
return GCRtoOCISpecDesc(i.desc.Descriptor) 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 { func (i *ImageData) loadReferrers() error {
@ -157,25 +176,56 @@ func (i *ImageData) loadReferrers() error {
return nil return nil
} }
referrers, err := remote.Referrers(i.nameRef.Context().Digest(i.Digest), i.remoteOpts...) referrersDescs, err := i.fetchReferrersFromRemote(i.Digest)
if err != nil { if err != nil {
return err 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 i.referrersManifest = referrersDescs
return nil 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) { func (i *ImageData) FetchRefererrs(artifactType string) ([]gcrv1.Descriptor, error) {
if err := i.loadReferrers(); err != nil { if err := i.loadReferrers(); err != nil {
return nil, err 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) { 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...) img, err := remote.Image(i.nameRef.Context().Digest(desc.Digest.String()), i.remoteOpts...)
if err != nil { if err != nil {
return nil, nil, err 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)) b, err := io.ReadAll(io.LimitReader(reader, maxPayloadSize))
i.referrersData[desc.Digest.String()] = referrerData{
data: b,
layerDescriptor: layerDesc,
}
return b, layerDesc, err 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)
}

View file

@ -2,6 +2,7 @@ package imagedataloader
import ( import (
gcrv1 "github.com/google/go-containerregistry/pkg/v1" gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
@ -26,3 +27,30 @@ func GCRtoOCISpecDesc(v1desc gcrv1.Descriptor) ocispec.Descriptor {
} }
return ociDesc 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
}

View 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
}

View 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 &notaryVerifier{
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...)
}

View 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")
}

View 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")
}

View 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
}

View file

@ -11,6 +11,7 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
gcrremote "github.com/google/go-containerregistry/pkg/v1/remote" gcrremote "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/kyverno/kyverno/pkg/images" "github.com/kyverno/kyverno/pkg/images"
"github.com/kyverno/kyverno/pkg/imageverifiers/notary"
"github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/logging"
_ "github.com/notaryproject/notation-core-go/signature/cose" _ "github.com/notaryproject/notation-core-go/signature/cose"
_ "github.com/notaryproject/notation-core-go/signature/jws" _ "github.com/notaryproject/notation-core-go/signature/jws"
@ -69,7 +70,7 @@ func (v *notaryVerifier) VerifySignature(ctx context.Context, opts images.Option
MaxSignatureAttempts: 10, 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 { if err != nil {
return nil, errors.Wrapf(err, "failed to verify %s", ref) return nil, errors.Wrapf(err, "failed to verify %s", ref)
} }