1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

fix attestation checks (#3999)

* fix attestation checks

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make fmt

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix linter issues

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make codegen

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* dos2unix

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Jim Bugwadia 2022-05-23 23:57:01 -07:00 committed by GitHub
parent 88f769cb39
commit 8fe9163f4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 454 additions and 152 deletions

View file

@ -13,7 +13,6 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/in-toto/in-toto-golang/in_toto"
wildcard "github.com/kyverno/go-wildcard"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/registryclient"
"github.com/kyverno/kyverno/pkg/utils"
"github.com/pkg/errors"
@ -35,6 +34,7 @@ var ImageSignatureRepository string
type Options struct {
ImageRef string
FetchAttestations bool
Key string
Cert string
CertChain string
@ -47,30 +47,95 @@ type Options struct {
RekorURL string
}
// VerifySignature verifies that the image has the expected signatures
func VerifySignature(opts Options) (digest string, err error) {
ctx := context.Background()
var remoteOpts []remote.Option
ro := options.RegistryOptions{}
remoteOpts, err = ro.ClientOpts(ctx)
if err != nil {
return "", errors.Wrap(err, "constructing client options")
type Response struct {
Digest string
Statements []map[string]interface{}
}
func Verify(opts Options) (*Response, error) {
if opts.FetchAttestations {
return fetchAttestations(opts)
} else {
return verifySignature(opts)
}
}
// verifySignature verifies that the image has the expected signatures
func verifySignature(opts Options) (*Response, error) {
ref, err := name.ParseReference(opts.ImageRef)
if err != nil {
return nil, fmt.Errorf("failed to parse image %s", opts.ImageRef)
}
cosignOpts, err := buildCosignOptions(opts)
if err != nil {
return nil, err
}
signatures, bundleVerified, err := client.VerifyImageSignatures(context.Background(), ref, cosignOpts)
if err != nil {
logger.Info("image verification failed", "error", err.Error())
return nil, err
}
logger.V(3).Info("verified image", "count", len(signatures), "bundleVerified", bundleVerified)
payload, err := extractPayload(signatures)
if err != nil {
return nil, err
}
if err := matchSubjectAndIssuer(signatures, opts.Subject, opts.Issuer); err != nil {
return nil, err
}
if err := matchExtensions(signatures, opts.AdditionalExtensions); err != nil {
return nil, err
}
err = checkAnnotations(payload, opts.Annotations)
if err != nil {
return nil, err
}
digest, err := extractDigest(opts.ImageRef, payload)
if err != nil {
return nil, err
}
return &Response{Digest: digest}, nil
}
func buildCosignOptions(opts Options) (*cosign.CheckOpts, error) {
var remoteOpts []remote.Option
var err error
ro := options.RegistryOptions{}
remoteOpts, err = ro.ClientOpts(context.Background())
if err != nil {
return nil, errors.Wrap(err, "constructing client options")
}
o, err := registryclient.GetOptions()
if err != nil {
return "", errors.Wrap(err, "getting remote options")
return nil, errors.Wrap(err, "getting remote options")
}
remoteOpts = append(remoteOpts, remote.WithRemoteOptions(o))
cosignOpts := &cosign.CheckOpts{
Annotations: map[string]interface{}{},
RegistryClientOpts: remoteOpts,
ClaimVerifier: cosign.SimpleClaimVerifier,
}
if opts.FetchAttestations {
cosignOpts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
} else {
cosignOpts.ClaimVerifier = cosign.SimpleClaimVerifier
}
if opts.Roots != "" {
cp, err := loadCertPool([]byte(opts.Roots))
if err != nil {
return "", errors.Wrapf(err, "failed to load Root certificates")
return nil, errors.Wrap(err, "failed to load Root certificates")
}
cosignOpts.RootCerts = cp
}
@ -79,13 +144,13 @@ func VerifySignature(opts Options) (digest string, err error) {
if strings.HasPrefix(opts.Key, "-----BEGIN PUBLIC KEY-----") {
cosignOpts.SigVerifier, err = decodePEM([]byte(opts.Key))
if err != nil {
return "", errors.Wrap(err, "failed to load public key from PEM")
return nil, errors.Wrap(err, "failed to load public key from PEM")
}
} else {
// this supports Kubernetes secrets and KMS
cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, opts.Key)
cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(context.Background(), opts.Key)
if err != nil {
return "", errors.Wrapf(err, "failed to load public key from %s", opts.Key)
return nil, errors.Wrapf(err, "failed to load public key from %s", opts.Key)
}
}
} else {
@ -93,30 +158,30 @@ func VerifySignature(opts Options) (digest string, err error) {
// load cert and optionally a cert chain as a verifier
cert, err := loadCert([]byte(opts.Cert))
if err != nil {
return "", errors.Wrapf(err, "failed to load certificate from %s", opts.Cert)
return nil, errors.Wrapf(err, "failed to load certificate from %s", string(opts.Cert))
}
if opts.CertChain == "" {
cosignOpts.SigVerifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
if err != nil {
return "", errors.Wrapf(err, "failed to load signature from certificate")
return nil, errors.Wrap(err, "failed to load signature from certificate")
}
} else {
// Verify certificate with chain
chain, err := loadCertChain([]byte(opts.CertChain))
if err != nil {
return "", err
return nil, errors.Wrap(err, "failed to load load certificate chain")
}
cosignOpts.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, cosignOpts)
if err != nil {
return "", err
return nil, errors.Wrap(err, "failed to load validate certificate chain")
}
}
} else if opts.CertChain != "" {
// load cert chain as roots
cp, err := loadCertPool([]byte(opts.CertChain))
if err != nil {
return "", errors.Wrapf(err, "failed to load cert chain")
return nil, errors.Wrap(err, "failed to load certificates")
}
cosignOpts.RootCerts = cp
} else {
@ -130,63 +195,20 @@ func VerifySignature(opts Options) (digest string, err error) {
if opts.RekorURL != "" {
cosignOpts.RekorClient, err = rekor.NewClient(opts.RekorURL)
if err != nil {
return "", errors.Wrapf(err, "failed to create Rekor client from URL %s", opts.RekorURL)
return nil, errors.Wrapf(err, "failed to create Rekor client from URL %s", opts.RekorURL)
}
}
if opts.Repository != "" {
signatureRepo, err := name.NewRepository(opts.Repository)
if err != nil {
return "", errors.Wrapf(err, "failed to parse signature repository %s", opts.Repository)
return nil, errors.Wrapf(err, "failed to parse signature repository %s", opts.Repository)
}
cosignOpts.RegistryClientOpts = append(cosignOpts.RegistryClientOpts, remote.WithTargetRepository(signatureRepo))
}
ref, err := name.ParseReference(opts.ImageRef)
if err != nil {
return "", errors.Wrap(err, "failed to parse image")
}
signatures, bundleVerified, err := client.VerifyImageSignatures(ctx, ref, cosignOpts)
if err != nil {
logger.Info("image verification failed", "error", err.Error())
return "", err
}
logger.V(3).Info("verified image", "count", len(signatures), "bundleVerified", bundleVerified)
pld, err := extractPayload(signatures)
if err != nil {
return "", errors.Wrap(err, "failed to get pld")
}
if err := matchSubjectAndIssuer(signatures, opts.Subject, opts.Issuer); err != nil {
return "", err
}
if err := matchExtensions(signatures, opts.AdditionalExtensions); err != nil {
return "", errors.Wrap(err, "extensions mismatch")
}
err = checkAnnotations(pld, opts.Annotations)
if err != nil {
return "", errors.Wrap(err, "annotation mismatch")
}
digest, err = extractDigest(opts.ImageRef, pld)
if err != nil {
return "", errors.Wrap(err, "failed to get digest")
}
return digest, nil
}
func getFulcioRoots(roots []byte) (*x509.CertPool, error) {
if len(roots) == 0 {
return fulcio.GetRoots(), nil
}
return loadCertPool(roots)
return cosignOpts, nil
}
func loadCertPool(roots []byte) (*x509.CertPool, error) {
@ -220,35 +242,15 @@ func loadCertChain(pem []byte) ([]*x509.Certificate, error) {
return cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pem))
}
// FetchAttestations retrieves signed attestations and decodes them into in-toto statements
// fetchAttestations retrieves signed attestations and decodes them into in-toto statements
// https://github.com/in-toto/attestation/blob/main/spec/README.md#statement
func FetchAttestations(imageRef string, imageVerify kyvernov1.ImageVerification) ([]map[string]interface{}, error) {
ctx := context.Background()
var err error
cosignOpts := &cosign.CheckOpts{
ClaimVerifier: cosign.IntotoSubjectClaimVerifier,
}
if imageVerify.Key != "" {
if strings.HasPrefix(imageVerify.Key, "-----BEGIN PUBLIC KEY-----") {
cosignOpts.SigVerifier, err = decodePEM([]byte(imageVerify.Key))
} else {
cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, imageVerify.Key)
}
} else {
cosignOpts.CertEmail = ""
cosignOpts.RootCerts, err = getFulcioRoots([]byte(imageVerify.Roots))
if err == nil {
cosignOpts.RekorClient, err = rekor.NewClient("https://rekor.sigstore.dev")
}
}
func fetchAttestations(opts Options) (*Response, error) {
cosignOpts, err := buildCosignOptions(opts)
if err != nil {
return nil, errors.Wrap(err, "loading credentials")
return nil, err
}
ref, err := name.ParseReference(imageRef)
ref, err := name.ParseReference(opts.ImageRef)
if err != nil {
return nil, errors.Wrap(err, "failed to parse image")
}
@ -258,51 +260,61 @@ func FetchAttestations(imageRef string, imageVerify kyvernov1.ImageVerification)
msg := err.Error()
logger.Info("failed to fetch attestations", "error", msg)
if strings.Contains(msg, "MANIFEST_UNKNOWN: manifest unknown") {
return nil, fmt.Errorf("not found")
return nil, errors.Wrap(fmt.Errorf("not found"), "")
}
return nil, err
}
logger.V(3).Info("verified images", "count", len(signatures), "bundleVerified", bundleVerified)
inTotoStatements, err := decodeStatements(signatures)
logger.V(3).Info("verified images", "signatures", len(signatures), "bundleVerified", bundleVerified)
inTotoStatements, digest, err := decodeStatements(signatures)
if err != nil {
return nil, err
}
return inTotoStatements, nil
return &Response{Digest: digest, Statements: inTotoStatements}, nil
}
func decodeStatements(sigs []oci.Signature) ([]map[string]interface{}, error) {
func decodeStatements(sigs []oci.Signature) ([]map[string]interface{}, string, error) {
if len(sigs) == 0 {
return []map[string]interface{}{}, nil
return []map[string]interface{}{}, "", nil
}
var digest string
decodedStatements := make([]map[string]interface{}, len(sigs))
for i, sig := range sigs {
pld, err := sig.Payload()
if err != nil {
return nil, errors.Wrap(err, "failed to decode payload")
return nil, "", errors.Wrap(err, "failed to decode payload")
}
sci := payload.SimpleContainerImage{}
if err := json.Unmarshal(pld, &sci); err != nil {
return nil, "", errors.Wrap(err, "error decoding the payload")
}
if d := sci.Critical.Image.DockerManifestDigest; d != "" {
digest = d
}
data := make(map[string]interface{})
if err := json.Unmarshal(pld, &data); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal JSON payload: %v", sig)
return nil, "", errors.Wrapf(err, "failed to unmarshal JSON payload: %v", sig)
}
if dataPayload, ok := data["payload"]; !ok {
return nil, fmt.Errorf("missing payload in %v", data)
return nil, "", fmt.Errorf("missing payload in %v", data)
} else {
decodedStatement, err := decodeStatement(dataPayload.(string))
if err != nil {
return nil, errors.Wrapf(err, "failed to decode statement %s", string(pld))
return nil, "", errors.Wrapf(err, "failed to decode statement %s", string(pld))
}
decodedStatements[i] = decodedStatement
}
}
return decodedStatements, nil
return decodedStatements, digest, nil
}
func decodeStatement(payloadBase64 string) (map[string]interface{}, error) {
@ -482,7 +494,8 @@ func checkAnnotations(payload []payload.SimpleContainerImage, annotations map[st
for _, p := range payload {
for key, val := range annotations {
if val != p.Optional[key] {
return fmt.Errorf("annotation value for %s does not match", key)
return fmt.Errorf("annotations mismatch: %s does not match expected value %s for key %s",
p.Optional[key], val, key)
}
}
}

View file

@ -70,14 +70,14 @@ func TestCosignKeyless(t *testing.T) {
Subject: "jim",
}
_, err := VerifySignature(opts)
_, err := verifySignature(opts)
assert.Error(t, err, "subject mismatch: expected jim@nirmata.com, got jim")
opts.Subject = "jim@nirmata.com"
_, err = VerifySignature(opts)
_, err = verifySignature(opts)
assert.Error(t, err, "issuer mismatch: expected https://github.com/login/oauth, got https://github.com/")
opts.Issuer = "https://github.com/login/oauth"
_, err = VerifySignature(opts)
_, err = verifySignature(opts)
assert.NilError(t, err)
}

View file

@ -0,0 +1,264 @@
package engine
import (
"encoding/json"
"testing"
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/utils/api"
"github.com/kyverno/kyverno/pkg/utils/image"
"gotest.tools/assert"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/log"
)
var scanPredicate = `
{
"predicate": {
"matches": [
{
"vulnerability": {
"id": "CVE-2021-22946",
"dataSource": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22946",
"namespace": "alpine:3.11",
"severity": "High",
"urls": [
"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22946"
],
"cvss": [],
"fix": {
"versions": [
"7.79.0-r0"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [
{
"id": "CVE-2021-22946",
"dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2021-22946",
"namespace": "nvd",
"severity": "High",
"urls": [
"https://hackerone.com/reports/1334111",
"https://lists.debian.org/debian-lts-announce/2021/09/msg00022.html",
"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RWLEC6YVEM2HWUBX67SDGPSY4CQB72OE/",
"https://www.oracle.com/security-alerts/cpuoct2021.html",
"https://security.netapp.com/advisory/ntap-20211029-0003/",
"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/APOAK4X73EJTAPTSVT7IRVDMUWVXNWGD/",
"https://security.netapp.com/advisory/ntap-20220121-0008/",
"https://www.oracle.com/security-alerts/cpujan2022.html",
"https://cert-portal.siemens.com/productcert/pdf/ssa-389290.pdf",
"https://support.apple.com/kb/HT213183",
"http://seclists.org/fulldisclosure/2022/Mar/29",
"https://www.oracle.com/security-alerts/cpuapr2022.html"
],
"description": "A user can tell curl >= 7.20.0 and <= 7.78.0 to require a successful upgrade to TLS when speaking to an IMAP, POP3 or FTP server (--ssl-reqd on the command line or CURLOPT_USE_SSL set to CURLUSESSL_CONTROL or CURLUSESSL_ALL withlibcurl). This requirement could be bypassed if the server would return a properly crafted but perfectly legitimate response.This flaw would then make curl silently continue its operations **withoutTLS** contrary to the instructions and expectations, exposing possibly sensitive data in clear text over the network.",
"cvss": [
{
"version": "2.0",
"vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
"metrics": {
"baseScore": 5,
"exploitabilityScore": 10,
"impactScore": 2.9
},
"vendorMetadata": {}
},
{
"version": "3.1",
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"metrics": {
"baseScore": 7.5,
"exploitabilityScore": 3.9,
"impactScore": 3.6
},
"vendorMetadata": {}
}
]
}
],
"matchDetails": [
{
"matcher": "apk-matcher",
"searchedBy": {
"distro": {
"type": "alpine",
"version": "3.11.7"
},
"namespace": "alpine:3.11",
"package": {
"name": "curl",
"version": "7.67.0-r3"
}
},
"found": {
"versionConstraint": "< 7.79.0-r0 (apk)"
}
}
],
"artifact": {
"name": "libcurl",
"version": "7.67.0-r3",
"type": "apk",
"locations": [
{
"path": "/lib/apk/db/installed",
"layerID": "sha256:165c22a332e306497ffa210ce9f284906fe0bf6340d20c5f8521e064323ba52a"
}
],
"language": "",
"licenses": [
"MIT"
],
"cpes": [
"cpe:2.3:a:libcurl:libcurl:7.67.0-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/libcurl@7.67.0-r3?arch=x86_64",
"metadata": {
"OriginPackage": "curl"
}
}
}
],
"source": {
"type": "image",
"target": {
"userInput": "ghcr.io/tap8stry/git-init:v0.21.0@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b",
"imageID": "sha256:ebbe9df4abf4dd9a739b33ab75d1fee2086713829a437f9d1e5e3de7b21e8d5f",
"manifestDigest": "sha256:5fe577767eba4cca2fe7594f6df94ca2b0f639a2ee8794f99f2ac49b81b5d419",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"ghcr.io/tap8stry/git-init:v0.21.0"
],
"imageSize": 81343568,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:0fcbbeeeb0d7fc5c06362d7a6717b999e605574c7210eff4f7418f6e9be9fbfe",
"size": 5610661
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:8f1d7de99bcffd39d4461b917a5313cfa0415f33eac9412a9b6138a27121c7e6",
"size": 4686
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:165c22a332e306497ffa210ce9f284906fe0bf6340d20c5f8521e064323ba52a",
"size": 37280295
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:0519ec3ee06ceaaa19b5682db6a01d408f9be6d74dc0f453e416fc92b654ce2f",
"size": 9503892
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:a79b016ff7714845775dd921c15fab70652344015e91ebff2ccec49d6792ac11",
"size": 28944034
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNzM1LCJkaWdlc3QiOiJzaGEyNTY6ZWJiZTlkZjRhYmY0ZGQ5YTczOWIzM2FiNzVkMWZlZTIwODY3MTM4MjlhNDM3ZjlkMWU1ZTNkZTdiMjFlOGQ1ZiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODgxMzQ0LCJkaWdlc3QiOiJzaGEyNTY6MGZjYmJlZWViMGQ3ZmM1YzA2MzYyZDdhNjcxN2I5OTllNjA1NTc0YzcyMTBlZmY0Zjc0MThmNmU5YmU5ZmJmZSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjExNzc2LCJkaWdlc3QiOiJzaGEyNTY6OGYxZDdkZTk5YmNmZmQzOWQ0NDYxYjkxN2E1MzEzY2ZhMDQxNWYzM2VhYzk0MTJhOWI2MTM4YTI3MTIxYzdlNiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM3NzQ5NzYwLCJkaWdlc3QiOiJzaGEyNTY6MTY1YzIyYTMzMmUzMDY0OTdmZmEyMTBjZTlmMjg0OTA2ZmUwYmY2MzQwZDIwYzVmODUyMWUwNjQzMjNiYTUyYSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjk2NTIyMjQsImRpZ2VzdCI6InNoYTI1NjowNTE5ZWMzZWUwNmNlYWFhMTliNTY4MmRiNmEwMWQ0MDhmOWJlNmQ3NGRjMGY0NTNlNDE2ZmM5MmI2NTRjZTJmIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6Mjg5NDY0MzIsImRpZ2VzdCI6InNoYTI1NjphNzliMDE2ZmY3NzE0ODQ1Nzc1ZGQ5MjFjMTVmYWI3MDY1MjM0NDAxNWU5MWViZmYyY2NlYzQ5ZDY3OTJhYzExIn1dfQ==",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vZ29vZ2xlL2tvIiwiY3JlYXRlZCI6IjIwMjEtMDItMTZUMTk6MzU6NDNaIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjAtMTItMTdUMDA6MTk6NDkuMTEyNDQ1OTY1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTo4ZWQ4MDAxMGU0NDNkYTE5ZDcyNTQ2YmNlZTlhMzVlMGE4ZDI0NGM3MjA1MmIxOTk0NjEwYmY1OTM5ZDQ3OWMyIGluIC8gIn0seyJjcmVhdGVkIjoiMjAyMC0xMi0xN1QwMDoxOTo0OS4yODQyMTExNDhaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApICBDTUQgW1wiL2Jpbi9zaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIxLTAyLTE2VDE5OjM1OjI4LjkzODk1MjU1M1oiLCJjcmVhdGVkX2J5IjoiUlVOIC9iaW4vc2ggLWMgYWRkZ3JvdXAgLVMgLWcgNjU1MzIgbm9ucm9vdCBcdTAwMjZcdTAwMjYgYWRkdXNlciAtUyAtdSA2NTUzMiBub25yb290IC1HIG5vbnJvb3QgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0wMi0xNlQxOTozNTozMi41NTU3NTk2NTRaIiwiY3JlYXRlZF9ieSI6IlJVTiAvYmluL3NoIC1jIGFwayBhZGQgLS11cGRhdGUgZ2l0IGdpdC1sZnMgb3BlbnNzaC1jbGllbnQgICAgIFx1MDAyNlx1MDAyNiBhcGsgdXBkYXRlICAgICBcdTAwMjZcdTAwMjYgYXBrIHVwZ3JhZGUgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJhdXRob3IiOiJrbyIsImNyZWF0ZWQiOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsImNyZWF0ZWRfYnkiOiJrbyBwdWJsaXNoIGtvOi8vZ2l0aHViLmNvbS90ZWt0b25jZC9waXBlbGluZS9jbWQvZ2l0LWluaXQiLCJjb21tZW50Ijoia29kYXRhIGNvbnRlbnRzLCBhdCAkS09fREFUQV9QQVRIIn0seyJhdXRob3IiOiJrbyIsImNyZWF0ZWQiOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsImNyZWF0ZWRfYnkiOiJrbyBwdWJsaXNoIGtvOi8vZ2l0aHViLmNvbS90ZWt0b25jZC9waXBlbGluZS9jbWQvZ2l0LWluaXQiLCJjb21tZW50IjoiZ28gYnVpbGQgb3V0cHV0LCBhdCAva28tYXBwL2dpdC1pbml0In1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MGZjYmJlZWViMGQ3ZmM1YzA2MzYyZDdhNjcxN2I5OTllNjA1NTc0YzcyMTBlZmY0Zjc0MThmNmU5YmU5ZmJmZSIsInNoYTI1Njo4ZjFkN2RlOTliY2ZmZDM5ZDQ0NjFiOTE3YTUzMTNjZmEwNDE1ZjMzZWFjOTQxMmE5YjYxMzhhMjcxMjFjN2U2Iiwic2hhMjU2OjE2NWMyMmEzMzJlMzA2NDk3ZmZhMjEwY2U5ZjI4NDkwNmZlMGJmNjM0MGQyMGM1Zjg1MjFlMDY0MzIzYmE1MmEiLCJzaGEyNTY6MDUxOWVjM2VlMDZjZWFhYTE5YjU2ODJkYjZhMDFkNDA4ZjliZTZkNzRkYzBmNDUzZTQxNmZjOTJiNjU0Y2UyZiIsInNoYTI1NjphNzliMDE2ZmY3NzE0ODQ1Nzc1ZGQ5MjFjMTVmYWI3MDY1MjM0NDAxNWU5MWViZmYyY2NlYzQ5ZDY3OTJhYzExIl19LCJjb25maWciOnsiQ21kIjpbIi9iaW4vc2giXSwiRW50cnlwb2ludCI6WyIva28tYXBwL2dpdC1pbml0Il0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2Jpbjova28tYXBwIiwiS09fREFUQV9QQVRIPS92YXIvcnVuL2tvIl19fQ==",
"repoDigests": [
"ghcr.io/tap8stry/git-init@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b"
]
}
},
"distro": {
"name": "Alpine Linux",
"version": "",
"idLike": null
},
"descriptor": {
"name": "grype",
"version": "0.32.0",
"configuration": {
"configPath": "",
"output": "json",
"file": "",
"output-template-file": "",
"quiet": false,
"check-for-app-update": true,
"only-fixed": false,
"search": {
"scope": "Squashed",
"unindexed-archives": false,
"indexed-archives": true
},
"ignore": null,
"exclude": [],
"db": {
"cache-dir": "/home/jim/.cache/grype/db",
"update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json",
"ca-cert": "",
"auto-update": true,
"validate-by-hash-on-start": false
},
"dev": {
"profile-cpu": false,
"profile-mem": false
},
"fail-on-severity": "",
"registry": {
"insecure-skip-tls-verify": false,
"insecure-use-http": false,
"auth": []
},
"log": {
"structured": false,
"level": "",
"file": ""
}
},
"db": {
"built": "2022-05-15T08:15:19Z",
"schemaVersion": 3,
"location": "/home/jim/.cache/grype/db/3",
"checksum": "sha256:4e6836ac8db4fbe1488c6a81f37cdc044e3d22041573c7a552aa0e053efa5c29",
"error": null
}
}
}
}
`
func Test_Conditions(t *testing.T) {
conditions := []v1.AnyAllConditions{
{
AnyConditions: []v1.Condition{
{
RawKey: &apiextv1.JSON{Raw: []byte("\"{{ matches[].vulnerability[].cvss[?metrics.impactScore > '8.0'][] | length(@) }}\"")},
Operator: "Equals",
RawValue: &apiextv1.JSON{Raw: []byte("\"0\"")},
},
{
RawKey: &apiextv1.JSON{Raw: []byte("\"{{ source.target.userInput }}\"")},
Operator: "Equals",
RawValue: &apiextv1.JSON{Raw: []byte("\"ghcr.io/tap8stry/git-init:v0.21.0@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b\"")},
},
},
},
}
ctx := context.NewContext()
img := api.ImageInfo{Pointer: "/spec/containers/0/image"}
img.ImageInfo = image.ImageInfo{
Registry: "docker.io",
Name: "nginx",
Path: "test/nginx",
Tag: "latest",
}
var dataMap map[string]interface{}
err := json.Unmarshal([]byte(scanPredicate), &dataMap)
assert.NilError(t, err)
pass, err := evaluateConditions(conditions, ctx, dataMap, img, log.Log)
assert.NilError(t, err)
assert.Equal(t, pass, true)
}

View file

@ -163,17 +163,14 @@ func (iv *imageVerifier) verify(imageVerify kyvernov1.ImageVerification, images
continue
}
var ruleResp *response.RuleResponse
var digest string
if len(imageVerify.Attestors) > 0 {
if len(imageVerify.Attestations) > 0 {
ruleResp = iv.verifyAttestations(imageVerify, imageInfo)
} else {
ruleResp, digest = iv.verifySignatures(imageVerify, imageInfo)
}
verified, err := isImageVerified(iv.policyContext.NewResource, image, iv.logger)
if err == nil && verified {
iv.logger.Info("image was previously verified, skipping check", "image", image)
continue
}
ruleResp, digest := iv.verifyImage(imageVerify, imageInfo)
if imageVerify.MutateDigest {
patch, retrievedDigest, err := iv.handleMutateDigest(digest, imageInfo)
if err != nil {
@ -261,15 +258,20 @@ func imageMatches(image string, imagePatterns []string) bool {
return false
}
func (iv *imageVerifier) verifySignatures(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) (*response.RuleResponse, string) {
image := imageInfo.String()
iv.logger.V(2).Info("verifying image signatures", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations))
func (iv *imageVerifier) verifyImage(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) (*response.RuleResponse, string) {
if len(imageVerify.Attestors) <= 0 {
return nil, ""
}
var digest string
image := imageInfo.String()
iv.logger.V(2).Info("verifying image signatures", "image", image,
"attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations))
var cosignResponse *cosign.Response
for i, attestorSet := range imageVerify.Attestors {
var err error
path := fmt.Sprintf(".attestors[%d]", i)
digest, err = iv.verifyAttestorSet(attestorSet, imageVerify, image, path)
cosignResponse, err = iv.verifyAttestorSet(attestorSet, imageVerify, imageInfo, path)
if err != nil {
iv.logger.Error(err, "failed to verify signature")
msg := fmt.Sprintf("failed to verify signature for %s: %s", image, err.Error())
@ -277,19 +279,26 @@ func (iv *imageVerifier) verifySignatures(imageVerify kyvernov1.ImageVerificatio
}
}
if cosignResponse == nil {
return ruleError(iv.rule, response.ImageVerify, "invalid response", fmt.Errorf("nil")), ""
}
msg := fmt.Sprintf("verified image signatures for %s", image)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), digest
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), cosignResponse.Digest
}
func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, imageVerify kyvernov1.ImageVerification, image, path string) (string, error) {
func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, imageVerify kyvernov1.ImageVerification,
imageInfo apiutils.ImageInfo, path string) (*cosign.Response, error) {
var errorList []error
verifiedCount := 0
attestorSet = expandStaticKeys(attestorSet)
requiredCount := getRequiredCount(attestorSet)
image := imageInfo.String()
for i, a := range attestorSet.Entries {
var digest string
var entryError error
var cosignResp *cosign.Response
attestorPath := fmt.Sprintf("%s.entries[%d]", path, i)
if a.Attestor != nil {
@ -298,11 +307,15 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im
entryError = errors.Wrapf(err, "failed to unmarshal nested attestor %s", attestorPath)
} else {
attestorPath += ".attestor"
digest, entryError = iv.verifyAttestorSet(*nestedAttestorSet, imageVerify, image, attestorPath)
cosignResp, entryError = iv.verifyAttestorSet(*nestedAttestorSet, imageVerify, imageInfo, attestorPath)
}
} else {
opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image)
digest, entryError = cosign.VerifySignature(*opts)
cosignResp, entryError = cosign.Verify(*opts)
if opts.FetchAttestations && entryError == nil {
entryError = iv.verifyAttestations(cosignResp.Statements, imageVerify, imageInfo)
}
if entryError != nil {
entryError = fmt.Errorf("%s: %s", attestorPath+subPath, entryError.Error())
}
@ -312,7 +325,7 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im
verifiedCount++
if verifiedCount >= requiredCount {
iv.logger.V(2).Info("image verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount)
return digest, nil
return cosignResp, nil
}
} else {
errorList = append(errorList, entryError)
@ -321,7 +334,7 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im
iv.logger.Info("image verification failed", "verifiedCount", verifiedCount, "requiredCount", requiredCount, "errors", errorList)
err := engineUtils.CombineErrors(errorList)
return "", err
return nil, err
}
func expandStaticKeys(attestorSet kyvernov1.AttestorSet) kyvernov1.AttestorSet {
@ -388,6 +401,10 @@ func (iv *imageVerifier) buildOptionsAndPath(attestor kyvernov1.Attestor, imageV
opts.Roots = imageVerify.Roots
}
if len(imageVerify.Attestations) > 0 {
opts.FetchAttestations = true
}
if attestor.Keys != nil {
path = path + ".keys"
opts.Key = attestor.Keys.PublicKeys
@ -432,46 +449,39 @@ func makeAddDigestPatch(imageInfo apiutils.ImageInfo, digest string) ([]byte, er
return json.Marshal(patch)
}
func (iv *imageVerifier) verifyAttestations(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) *response.RuleResponse {
func (iv *imageVerifier) verifyAttestations(statements []map[string]interface{}, imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) error {
image := imageInfo.String()
start := time.Now()
statements, err := cosign.FetchAttestations(image, imageVerify)
if err != nil {
iv.logger.Info("failed to fetch attestations", "image", image, "error", err, "duration", time.Since(start).Seconds())
return ruleError(iv.rule, response.ImageVerify, fmt.Sprintf("failed to fetch attestations for %s", image), err)
}
iv.logger.V(4).Info("received attestations", "count", len(statements))
statementsByPredicate := buildStatementMap(statements)
statementsByPredicate, types := buildStatementMap(statements)
iv.logger.V(4).Info("checking attestations", "predicates", types, "image", image)
for _, ac := range imageVerify.Attestations {
statements := statementsByPredicate[ac.PredicateType]
if statements == nil {
msg := fmt.Sprintf("predicate type %s not found", ac.PredicateType)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil)
iv.logger.Info("attestation predicate type %s not found", "predicates", types, "image", imageInfo.String())
return fmt.Errorf("predicate type %s not found", ac.PredicateType)
}
iv.logger.Info("checking attestation predicate type %s", "predicates", types, "image", imageInfo.String())
for _, s := range statements {
val, err := iv.checkAttestations(ac, s, imageInfo)
if err != nil {
return ruleError(iv.rule, response.ImageVerify, "failed to check attestation", err)
return errors.Wrap(err, "failed to check attestations")
}
if !val {
msg := fmt.Sprintf("attestation checks failed for %s and predicate %s", imageInfo.String(), ac.PredicateType)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil)
return fmt.Errorf("attestation checks failed for %s and predicate %s", imageInfo.String(), ac.PredicateType)
}
}
}
msg := fmt.Sprintf("attestation checks passed for %s", imageInfo.String())
iv.logger.V(2).Info(msg)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil)
iv.logger.V(3).Info("attestation checks passed for %s", imageInfo.String())
return nil
}
func buildStatementMap(statements []map[string]interface{}) map[string][]map[string]interface{} {
func buildStatementMap(statements []map[string]interface{}) (map[string][]map[string]interface{}, []string) {
results := map[string][]map[string]interface{}{}
var predicateTypes []string
for _, s := range statements {
predicateType := s["predicateType"].(string)
if results[predicateType] != nil {
@ -479,9 +489,11 @@ func buildStatementMap(statements []map[string]interface{}) map[string][]map[str
} else {
results[predicateType] = []map[string]interface{}{s}
}
predicateTypes = append(predicateTypes, predicateType)
}
return results
return results, predicateTypes
}
func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string]interface{}, img apiutils.ImageInfo) (bool, error) {
@ -492,24 +504,34 @@ func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string
iv.policyContext.JSONContext.Checkpoint()
defer iv.policyContext.JSONContext.Restore()
return evaluateConditions(a.Conditions, iv.policyContext.JSONContext, s, img, iv.logger)
}
func evaluateConditions(
conditions []kyvernov1.AnyAllConditions,
ctx context.Interface,
s map[string]interface{},
img apiutils.ImageInfo,
log logr.Logger) (bool, error) {
predicate, ok := s["predicate"].(map[string]interface{})
if !ok {
return false, fmt.Errorf("failed to extract predicate from statement: %v", s)
}
if err := context.AddJSONObject(iv.policyContext.JSONContext, predicate); err != nil {
if err := context.AddJSONObject(ctx, predicate); err != nil {
return false, errors.Wrapf(err, fmt.Sprintf("failed to add Statement to the context %v", s))
}
if err := iv.policyContext.JSONContext.AddImageInfo(img); err != nil {
if err := ctx.AddImageInfo(img); err != nil {
return false, errors.Wrapf(err, fmt.Sprintf("failed to add image to the context %v", s))
}
conditions, err := variables.SubstituteAllInConditions(iv.logger, iv.policyContext.JSONContext, a.Conditions)
c, err := variables.SubstituteAllInConditions(log, ctx, conditions)
if err != nil {
return false, errors.Wrapf(err, "failed to substitute variables in attestation conditions")
}
pass := variables.EvaluateAnyAllConditions(iv.logger, iv.policyContext.JSONContext, conditions)
pass := variables.EvaluateAnyAllConditions(log, ctx, c)
return pass, nil
}

View file

@ -52,12 +52,15 @@ func evaluateAnyAllConditions(log logr.Logger, ctx context.EvalInterface, condit
break
}
}
log.Info("no condition passed for 'any' block", "any", anyConditions)
}
// update the allConditionsResult if they are present
for _, condition := range allConditions {
if !Evaluate(log, ctx, condition) {
allConditionsResult = false
log.Info("a condition failed in 'all' block", "condition", condition)
break
}
}