mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-05 07:26:55 +00:00
start attestation support
Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
parent
af944b9cd5
commit
0dbe7ea675
5 changed files with 441 additions and 109 deletions
20
go.mod
20
go.mod
|
@ -24,29 +24,27 @@ require (
|
|||
github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a
|
||||
github.com/minio/pkg v1.1.3
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.15.0
|
||||
github.com/onsi/gomega v1.11.0
|
||||
github.com/onsi/ginkgo v1.16.4
|
||||
github.com/onsi/gomega v1.15.0
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sigstore/cosign v1.0.0
|
||||
github.com/sigstore/rekor v0.3.0 // indirect
|
||||
github.com/sigstore/sigstore v0.0.0-20210726180807-7e34e36ecda1
|
||||
github.com/sigstore/cosign v1.2.1
|
||||
github.com/sigstore/sigstore v0.0.0-20210729211320-56a91f560f44
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gotest.tools v2.2.0+incompatible
|
||||
k8s.io/api v0.21.3
|
||||
k8s.io/apiextensions-apiserver v0.21.1
|
||||
k8s.io/apimachinery v0.21.3
|
||||
k8s.io/api v0.21.4
|
||||
k8s.io/apiextensions-apiserver v0.21.4
|
||||
k8s.io/apimachinery v0.21.4
|
||||
k8s.io/cli-runtime v0.21.1
|
||||
k8s.io/client-go v0.21.3
|
||||
k8s.io/klog/v2 v2.9.0
|
||||
k8s.io/client-go v0.21.4
|
||||
k8s.io/klog/v2 v2.10.0
|
||||
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7
|
||||
sigs.k8s.io/controller-runtime v0.8.1
|
||||
sigs.k8s.io/kustomize/api v0.8.8
|
||||
|
|
|
@ -458,12 +458,16 @@ type ImageVerification struct {
|
|||
// Wildcards ('*' and '?') are allowed. See: https://kubernetes.io/docs/concepts/containers/images.
|
||||
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
||||
|
||||
// Key is the PEM encoded public key that the image is signed with.
|
||||
// Key is the PEM encoded public key that the image or attestation is signed with.
|
||||
// Deprecated. Use Keys instead.
|
||||
Key string `json:"key,omitempty" yaml:"key,omitempty"`
|
||||
|
||||
// Repository is an optional alternate OCI repository to use for image signatures that match this rule.
|
||||
// If specified Repository will override the default OCI image repository configured for the installation.
|
||||
Repository string `json:"repository,omitempty" yaml:"repository,omitempty"`
|
||||
|
||||
// Attestations are optional statements used to verify the image.
|
||||
Attestations []*AnyAllConditions `json:"attestations,omitempty" yaml:"attestations,omitempty"`
|
||||
}
|
||||
|
||||
// Generation defines how new resources should be created and managed.
|
||||
|
|
|
@ -3,8 +3,11 @@ package cosign
|
|||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
|
||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
"strings"
|
||||
|
||||
"github.com/gardener/controller-manager-library/pkg/logger"
|
||||
|
@ -20,7 +23,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// Alternate signature repository
|
||||
// ImageSignatureRepository is an alternate signature repository
|
||||
ImageSignatureRepository string
|
||||
)
|
||||
|
||||
|
@ -42,13 +45,14 @@ func Initialize(client kubernetes.Interface, namespace, serviceAccount string, i
|
|||
return nil
|
||||
}
|
||||
|
||||
func Verify(imageRef string, key []byte, repository string, log logr.Logger) (digest string, err error) {
|
||||
func VerifySignature(imageRef string, key []byte, repository string, log logr.Logger) (digest string, err error) {
|
||||
pubKey, err := decodePEM(key)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to decode PEM %v", string(key))
|
||||
}
|
||||
|
||||
cosignOpts := &cosign.CheckOpts{
|
||||
RootCerts: fulcio.GetRoots(),
|
||||
Annotations: map[string]interface{}{},
|
||||
SigVerifier: pubKey,
|
||||
RegistryClientOpts: []remote.Option{
|
||||
|
@ -92,6 +96,82 @@ func Verify(imageRef string, key []byte, repository string, log logr.Logger) (di
|
|||
return digest, nil
|
||||
}
|
||||
|
||||
func FetchAttestations(imageRef string, key []byte, repository string) (map[string]interface{}, error) {
|
||||
pubKey, err := decodePEM(key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to decode PEM %v", string(key))
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(imageRef)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse image")
|
||||
}
|
||||
|
||||
cosignOpts := &cosign.CheckOpts{
|
||||
RootCerts: fulcio.GetRoots(),
|
||||
ClaimVerifier: cosign.IntotoSubjectClaimVerifier,
|
||||
SigTagSuffixOverride: cosign.AttestationTagSuffix,
|
||||
SigVerifier: dsse.WrapVerifier(pubKey),
|
||||
VerifyBundle: false,
|
||||
}
|
||||
|
||||
if err := setSignatureRepo(cosignOpts, ref, repository); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set signature repository")
|
||||
}
|
||||
|
||||
verified, err := cosign.Verify(context.Background(), ref, cosignOpts)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to verify image attestations")
|
||||
}
|
||||
|
||||
inTotoAttestation, err := decodeAttestation(verified)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return inTotoAttestation, nil
|
||||
}
|
||||
|
||||
func decodeAttestation(payloads []cosign.SignedPayload) (map[string]interface{}, error) {
|
||||
if len(payloads) == 0 {
|
||||
return nil, fmt.Errorf("empty payloads")
|
||||
}
|
||||
|
||||
payload := payloads[0].Payload
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(string(payload))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to base64 decode payload")
|
||||
}
|
||||
|
||||
inTotoAttestation := make(map[string]interface{})
|
||||
if err := json.Unmarshal(data, &inTotoAttestation); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal JSON payload")
|
||||
}
|
||||
|
||||
attestationPayloadBase64 := inTotoAttestation["payload"].(string)
|
||||
statement, err := base64.StdEncoding.DecodeString(attestationPayloadBase64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to base64 decode payload")
|
||||
}
|
||||
|
||||
inTotoAttestation["payload"] = statement
|
||||
return inTotoAttestation, nil
|
||||
}
|
||||
|
||||
func setSignatureRepo(cosignOpts *cosign.CheckOpts, ref name.Reference, repository string) error {
|
||||
cosignOpts.SignatureRepo = ref.Context()
|
||||
if repository != "" {
|
||||
signatureRepo, err := name.NewRepository(repository)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse signature repository %s", repository)
|
||||
}
|
||||
|
||||
cosignOpts.SignatureRepo = signatureRepo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePEM(raw []byte) (signature.Verifier, error) {
|
||||
// PEM encoded file.
|
||||
ed, err := cosign.PemToECDSAKey(raw)
|
||||
|
|
|
@ -65,10 +65,7 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
|
|||
func verifyAndPatchImages(logger logr.Logger, policyContext *PolicyContext, rule *v1.Rule, imageVerify *v1.ImageVerification, images map[string]*context.ImageInfo, resp *response.EngineResponse) {
|
||||
imagePattern := imageVerify.Image
|
||||
key := imageVerify.Key
|
||||
repository := cosign.ImageSignatureRepository
|
||||
if imageVerify.Repository != "" {
|
||||
repository = imageVerify.Repository
|
||||
}
|
||||
repository := getSignatureRepository(imageVerify)
|
||||
|
||||
for _, imageInfo := range images {
|
||||
image := imageInfo.String()
|
||||
|
@ -84,38 +81,76 @@ func verifyAndPatchImages(logger logr.Logger, policyContext *PolicyContext, rule
|
|||
continue
|
||||
}
|
||||
|
||||
logger.Info("verifying image", "image", image)
|
||||
ruleResp := verifyImage(logger, rule.Name, repository, key, imageInfo, imageVerify.Attestations)
|
||||
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
|
||||
incrementAppliedCount(resp)
|
||||
}
|
||||
}
|
||||
|
||||
ruleResp := response.RuleResponse{
|
||||
Name: rule.Name,
|
||||
Type: utils.Validation.String(),
|
||||
}
|
||||
func getSignatureRepository(imageVerify *v1.ImageVerification) string {
|
||||
repository := cosign.ImageSignatureRepository
|
||||
if imageVerify.Repository != "" {
|
||||
repository = imageVerify.Repository
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
digest, err := cosign.Verify(image, []byte(key), repository, logger)
|
||||
return repository
|
||||
}
|
||||
|
||||
func verifyImage(logger logr.Logger, ruleName, repository, key string, imageInfo *context.ImageInfo, attestations []*v1.AnyAllConditions) *response.RuleResponse {
|
||||
image := imageInfo.String()
|
||||
logger.Info("verifying image", "image", image)
|
||||
|
||||
ruleResp := &response.RuleResponse{
|
||||
Name: ruleName,
|
||||
Type: utils.Validation.String(),
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
if len(attestations) == 0 {
|
||||
digest, err := cosign.VerifySignature(image, []byte(key), repository, logger)
|
||||
if err != nil {
|
||||
logger.Info("failed to verify image", "image", image, "key", key, "error", err, "duration", time.Since(start).Seconds())
|
||||
logger.Info("failed to verify image signature", "image", image, "error", err, "duration", time.Since(start).Seconds())
|
||||
ruleResp.Success = false
|
||||
ruleResp.Message = fmt.Sprintf("image verification failed for %s: %v", image, err)
|
||||
} else {
|
||||
logger.V(3).Info("verified image", "image", image, "digest", digest, "duration", time.Since(start).Seconds())
|
||||
ruleResp.Success = true
|
||||
ruleResp.Message = fmt.Sprintf("image %s verified", image)
|
||||
|
||||
// add digest to image
|
||||
if imageInfo.Digest == "" {
|
||||
patch, err := makeAddDigestPatch(imageInfo, digest)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", imageInfo.JSONPointer)
|
||||
} else {
|
||||
logger.V(4).Info("patching verified image with digest", "patch", string(patch))
|
||||
ruleResp.Patches = [][]byte{patch}
|
||||
}
|
||||
}
|
||||
return ruleResp
|
||||
}
|
||||
|
||||
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResp)
|
||||
ruleResp.Success = true
|
||||
ruleResp.Message = fmt.Sprintf("image %s verified", image)
|
||||
logger.V(3).Info("verified image", "image", image, "digest", digest, "duration", time.Since(start).Seconds())
|
||||
|
||||
addDigest(logger, imageInfo, digest, *ruleResp)
|
||||
return ruleResp
|
||||
}
|
||||
|
||||
inTotoAttestation, err := cosign.FetchAttestations(image, []byte(key), repository)
|
||||
if err != nil {
|
||||
logger.Info("failed to verify image attestations", "image", image, "error", err, "duration", time.Since(start).Seconds())
|
||||
ruleResp.Success = false
|
||||
ruleResp.Message = fmt.Sprintf("image verification failed for %s: %v", image, err)
|
||||
return ruleResp
|
||||
}
|
||||
|
||||
logger.Info("received attestation", "in-toto-attestation", inTotoAttestation)
|
||||
|
||||
|
||||
// add to context
|
||||
|
||||
// process any / all conditions
|
||||
|
||||
|
||||
return ruleResp
|
||||
}
|
||||
|
||||
func addDigest(logger logr.Logger, imageInfo *context.ImageInfo, digest string, ruleResp response.RuleResponse) {
|
||||
if imageInfo.Digest == "" {
|
||||
patch, err := makeAddDigestPatch(imageInfo, digest)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", imageInfo.JSONPointer)
|
||||
} else {
|
||||
logger.V(4).Info("patching verified image with digest", "patch", string(patch))
|
||||
ruleResp.Patches = [][]byte{patch}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue