mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
added issuer check (#2804)
* added issuer check Signed-off-by: Namanl2001 <namanlakhwani@gmail.com> * switch to using SimpleContainerImage Signed-off-by: Namanl2001 <namanlakhwani@gmail.com> * added subject check and required test cases Signed-off-by: Namanl2001 <namanlakhwani@gmail.com> * small nits Signed-off-by: Namanl2001 <namanlakhwani@gmail.com> * correcting tests Signed-off-by: Namanl2001 <namanlakhwani@gmail.com> Co-authored-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
parent
b17e76493e
commit
edafffd2bd
4 changed files with 77 additions and 37 deletions
|
@ -551,6 +551,9 @@ type ImageVerification struct {
|
|||
// Subject is the verified identity used for keyless signing, for example the email address
|
||||
Subject string `json:"subject,omitempty" yaml:"subject,omitempty"`
|
||||
|
||||
// Issuer is the certificate issuer used for keyless signing.
|
||||
Issuer string `json:"issuer,omitempty" yaml:"issuer,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"`
|
||||
|
|
1
go.sum
1
go.sum
|
@ -1393,6 +1393,7 @@ github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+
|
|||
github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.31.1 h1:d18hG4PkHnNAKNMOmFuXFaiY8Us0nird/2m60uS1AMs=
|
||||
github.com/prometheus/common v0.31.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/kyverno/kyverno/pkg/engine/common"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/sigstore/cosign/pkg/oci"
|
||||
sigs "github.com/sigstore/cosign/pkg/signature"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/sigstore/sigstore/pkg/signature/payload"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
|
@ -75,6 +77,7 @@ type Options struct {
|
|||
Key string
|
||||
Roots []byte
|
||||
Subject string
|
||||
Issuer string
|
||||
Repository string
|
||||
Log logr.Logger
|
||||
}
|
||||
|
@ -102,7 +105,7 @@ func VerifySignature(opts Options) (digest string, err error) {
|
|||
cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, opts.Key)
|
||||
}
|
||||
} else {
|
||||
cosignOpts.CertEmail = opts.Subject
|
||||
cosignOpts.CertEmail = ""
|
||||
cosignOpts.RootCerts, err = getX509CertPool(opts.Roots)
|
||||
}
|
||||
|
||||
|
@ -137,7 +140,22 @@ func VerifySignature(opts Options) (digest string, err error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
digest, err = extractDigest(opts.ImageRef, verified, log)
|
||||
payload, err := extractPayload(opts.ImageRef, verified, log)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get payload")
|
||||
}
|
||||
|
||||
issuer, err := extractIssuer(opts.ImageRef, payload, log)
|
||||
if err == nil && (issuer != opts.Issuer) {
|
||||
return "", errors.Wrap(err, "issuer mismatch")
|
||||
}
|
||||
|
||||
subject, err := extractSubject(opts.ImageRef, payload, log)
|
||||
if err == nil && wildcard.Match(opts.Subject, subject) {
|
||||
return "", errors.Wrap(err, "subject mismatch")
|
||||
}
|
||||
|
||||
digest, err = extractDigest(opts.ImageRef, payload, log)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get digest")
|
||||
}
|
||||
|
@ -321,21 +339,20 @@ func decodePEM(raw []byte) (signature.Verifier, error) {
|
|||
return signature.LoadECDSAVerifier(ed, crypto.SHA256)
|
||||
}
|
||||
|
||||
func extractDigest(imgRef string, verified []oci.Signature, log logr.Logger) (string, error) {
|
||||
var jsonMap map[string]interface{}
|
||||
for _, vp := range verified {
|
||||
payload, err := vp.Payload()
|
||||
func extractPayload(imgRef string, verified []oci.Signature, log logr.Logger) ([]payload.SimpleContainerImage, error) {
|
||||
var sigPayload []payload.SimpleContainerImage
|
||||
for _, sig := range verified {
|
||||
p, err := sig.Payload()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get payload")
|
||||
return nil, errors.Wrap(err, "failed to get payload")
|
||||
}
|
||||
|
||||
// TODO - change to using payload.SimpleContainerImage after the next Tekton release
|
||||
if err := json.Unmarshal(payload, &jsonMap); err != nil {
|
||||
return "", err
|
||||
ss := payload.SimpleContainerImage{}
|
||||
if err := json.Unmarshal(p, &ss); err != nil {
|
||||
return nil, errors.Wrap(err, "error decoding the payload")
|
||||
}
|
||||
|
||||
log.V(3).Info("image verification response", "image", imgRef, "payload", jsonMap)
|
||||
|
||||
log.V(3).Info("image verification response", "image", imgRef, "payload", ss)
|
||||
// The expected payload is in one of these JSON formats:
|
||||
// {
|
||||
// "critical": {
|
||||
|
@ -362,39 +379,44 @@ func extractDigest(imgRef string, verified []oci.Signature, log logr.Logger) (st
|
|||
// },
|
||||
// "Optional": {}
|
||||
// }
|
||||
//
|
||||
critical := getMapValue(jsonMap, "critical", "Critical")
|
||||
if critical != nil {
|
||||
image := getMapValue(critical, "image", "Image")
|
||||
if image != nil {
|
||||
digest := getStringValue(image, "docker-manifest-digest", "Docker-manifest-digest")
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
sigPayload = append(sigPayload, ss)
|
||||
}
|
||||
return sigPayload, nil
|
||||
}
|
||||
|
||||
func extractDigest(imgRef string, payload []payload.SimpleContainerImage, log logr.Logger) (string, error) {
|
||||
for _, p := range payload {
|
||||
if digest := p.Critical.Image.DockerManifestDigest; digest != "" {
|
||||
return digest, nil
|
||||
} else {
|
||||
log.Info("failed to extract image digest from verification response", "image", imgRef, "payload", jsonMap)
|
||||
log.Info("failed to extract image digest from verification response", "image", imgRef, "payload", p)
|
||||
return "", fmt.Errorf("unknown image response for " + imgRef)
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("digest not found for " + imgRef)
|
||||
}
|
||||
|
||||
func getMapValue(m map[string]interface{}, keys ...string) map[string]interface{} {
|
||||
for _, k := range keys {
|
||||
if m[k] != nil {
|
||||
return m[k].(map[string]interface{})
|
||||
func extractIssuer(imgRef string, payload []payload.SimpleContainerImage, log logr.Logger) (string, error) {
|
||||
for _, p := range payload {
|
||||
if issuer := p.Optional["Issuer"]; issuer != nil {
|
||||
return issuer.(string), nil
|
||||
} else {
|
||||
log.Info("failed to extract image issuer from verification response", "image", imgRef, "payload", p)
|
||||
return "", fmt.Errorf("unknown image response for " + imgRef)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return "", fmt.Errorf("issuer not found for " + imgRef)
|
||||
}
|
||||
|
||||
func getStringValue(m map[string]interface{}, keys ...string) string {
|
||||
for _, k := range keys {
|
||||
if m[k] != nil {
|
||||
return m[k].(string)
|
||||
func extractSubject(imgRef string, payload []payload.SimpleContainerImage, log logr.Logger) (string, error) {
|
||||
for _, p := range payload {
|
||||
if subject := p.Optional["Subject"]; subject != nil {
|
||||
return subject.(string), nil
|
||||
} else {
|
||||
log.Info("failed to extract image subject from verification response", "image", imgRef, "payload", p)
|
||||
return "", fmt.Errorf("unknown image response for " + imgRef)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return "", fmt.Errorf("image subject not found for " + imgRef)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/sigstore/cosign/pkg/oci"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
@ -33,20 +34,33 @@ const tektonPayload = `{
|
|||
},
|
||||
"Type": "Tekton container signature"
|
||||
},
|
||||
"Optional": {}
|
||||
"Optional": {
|
||||
"Issuer": "https://github.com/login/oauth",
|
||||
"Subject": "https://github.com/mycompany/demo/.github/workflows/ci.yml@refs/heads/main"
|
||||
}
|
||||
}`
|
||||
|
||||
func TestCosignPayload(t *testing.T) {
|
||||
var log logr.Logger = logr.DiscardLogger{}
|
||||
image := "registry-v2.nirmata.io/pause"
|
||||
signedPayloads := cosign.SignedPayload{Payload: []byte(cosignPayload)}
|
||||
d, err := extractDigest(image, []oci.Signature{&sig{cosignPayload: signedPayloads}}, log)
|
||||
p, err := extractPayload(image, []oci.Signature{&sig{cosignPayload: signedPayloads}}, log)
|
||||
assert.NilError(t, err)
|
||||
d, err := extractDigest(image, p, log)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, d, "sha256:4a1c4b21597c1b4415bdbecb28a3296c6b5e23ca4f9feeb599860a1dac6a0108")
|
||||
|
||||
image2 := "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/nop"
|
||||
signedPayloads2 := cosign.SignedPayload{Payload: []byte(tektonPayload)}
|
||||
d2, err := extractDigest(image2, []oci.Signature{&sig{cosignPayload: signedPayloads2}}, log)
|
||||
p2, err := extractPayload(image2, []oci.Signature{&sig{cosignPayload: signedPayloads2}}, log)
|
||||
assert.NilError(t, err)
|
||||
i2, err := extractIssuer(image2, p2, log)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, i2, "https://github.com/login/oauth")
|
||||
s2, err := extractSubject(image2, p2, log)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, wildcard.Match("https://github.com/mycompany/*/.github/workflows/*.yml@refs/heads/main", s2))
|
||||
d2, err := extractDigest(image2, p2, log)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, d2, "sha256:6a037d5ba27d9c6be32a9038bfe676fb67d2e4145b4f53e9c61fb3e69f06e816")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue