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"`
|
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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
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"
|
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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue