Skip to content

Commit

Permalink
feat: add full regexp support to cosign (kyverno#10815)
Browse files Browse the repository at this point in the history
Signed-off-by: Vishal Choudhary <[email protected]>
  • Loading branch information
vishal-chdhry authored Aug 16, 2024
1 parent 5a60836 commit f69ffe1
Show file tree
Hide file tree
Showing 21 changed files with 1,377 additions and 25 deletions.
8 changes: 8 additions & 0 deletions api/kyverno/v1/image_verification_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,18 @@ type KeylessAttestor struct {
// +kubebuilder:validation:Optional
Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty"`

// IssuerRegExp is the regular expression to match certificate issuer used for keyless signing.
// +kubebuilder:validation:Optional
IssuerRegExp string `json:"issuerRegExp,omitempty" yaml:"issuerRegExp,omitempty"`

// Subject is the verified identity used for keyless signing, for example the email address.
// +kubebuilder:validation:Optional
Subject string `json:"subject,omitempty" yaml:"subject,omitempty"`

// SubjectRegExp is the regular expression to match identity used for keyless signing, for example the email address.
// +kubebuilder:validation:Optional
SubjectRegExp string `json:"subjectRegExp,omitempty" yaml:"subjectRegExp,omitempty"`

// Roots is an optional set of PEM encoded trusted root certificates.
// If not provided, the system roots are used.
// +kubebuilder:validation:Optional
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions cmd/cli/kubectl-kyverno/data/crds/kyverno.io_clusterpolicies.yaml

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions cmd/cli/kubectl-kyverno/data/crds/kyverno.io_policies.yaml

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions config/crds/kyverno/kyverno.io_clusterpolicies.yaml

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions config/crds/kyverno/kyverno.io_policies.yaml

Large diffs are not rendered by default.

272 changes: 272 additions & 0 deletions config/install-latest-testing.yaml

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions docs/user/crd/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2700,6 +2700,17 @@ <h3 id="kyverno.io/v1.KeylessAttestor">KeylessAttestor
</tr>
<tr>
<td>
<code>issuerRegExp</code><br/>
<em>
string
</em>
</td>
<td>
<p>IssuerRegExp is the regular expression to match certificate issuer used for keyless signing.</p>
</td>
</tr>
<tr>
<td>
<code>subject</code><br/>
<em>
string
Expand All @@ -2711,6 +2722,17 @@ <h3 id="kyverno.io/v1.KeylessAttestor">KeylessAttestor
</tr>
<tr>
<td>
<code>subjectRegExp</code><br/>
<em>
string
</em>
</td>
<td>
<p>SubjectRegExp is the regular expression to match identity used for keyless signing, for example the email address.</p>
</td>
</tr>
<tr>
<td>
<code>roots</code><br/>
<em>
string
Expand Down
58 changes: 58 additions & 0 deletions docs/user/crd/kyverno.v1.html
Original file line number Diff line number Diff line change
Expand Up @@ -5436,6 +5436,35 @@ <H3 id="kyverno-io-v1-KeylessAttestor">KeylessAttestor



<tr>
<td><code>issuerRegExp</code>

<span style="color:blue;"> *</span>

</br>




<span style="font-family: monospace">string</span>


</td>
<td>


<p>IssuerRegExp is the regular expression to match certificate issuer used for keyless signing.</p>





</td>
</tr>




<tr>
<td><code>subject</code>

Expand Down Expand Up @@ -5465,6 +5494,35 @@ <H3 id="kyverno-io-v1-KeylessAttestor">KeylessAttestor



<tr>
<td><code>subjectRegExp</code>

<span style="color:blue;"> *</span>

</br>




<span style="font-family: monospace">string</span>


</td>
<td>


<p>SubjectRegExp is the regular expression to match identity used for keyless signing, for example the email address.</p>





</td>
</tr>




<tr>
<td><code>roots</code>

Expand Down
18 changes: 18 additions & 0 deletions pkg/client/applyconfigurations/kyverno/v1/keylessattestor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 52 additions & 16 deletions pkg/cosign/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
"strings"

"github.com/google/go-containerregistry/pkg/name"
Expand Down Expand Up @@ -74,7 +75,7 @@ func (v *cosignVerifier) VerifySignature(ctx context.Context, opts images.Option
return nil, err
}

if err := matchSignatures(signatures, opts.Subject, opts.Issuer, opts.AdditionalExtensions); err != nil {
if err := matchSignatures(signatures, opts.Subject, opts.SubjectRegExp, opts.Issuer, opts.IssuerRegExp, opts.AdditionalExtensions); err != nil {
return nil, err
}

Expand Down Expand Up @@ -308,7 +309,7 @@ func (v *cosignVerifier) FetchAttestations(ctx context.Context, opts images.Opti
continue
}

if err := matchSignatures([]oci.Signature{signature}, opts.Subject, opts.Issuer, opts.AdditionalExtensions); err != nil {
if err := matchSignatures([]oci.Signature{signature}, opts.Subject, opts.SubjectRegExp, opts.Issuer, opts.IssuerRegExp, opts.AdditionalExtensions); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -500,7 +501,7 @@ func extractDigest(imgRef string, payload []payload.SimpleContainerImage) (strin
return "", fmt.Errorf("digest not found for %s", imgRef)
}

func matchSignatures(signatures []oci.Signature, subject, issuer string, extensions map[string]string) error {
func matchSignatures(signatures []oci.Signature, subject, subjectRegExp, issuer, issuerRegExp string, extensions map[string]string) error {
if subject == "" && issuer == "" && len(extensions) == 0 {
return nil
}
Expand All @@ -516,7 +517,7 @@ func matchSignatures(signatures []oci.Signature, subject, issuer string, extensi
return fmt.Errorf("certificate not found")
}

if err := matchCertificateData(cert, subject, issuer, extensions); err != nil {
if err := matchCertificateData(cert, subject, subjectRegExp, issuer, issuerRegExp, extensions); err != nil {
errs = append(errs, err)
} else {
// only one signature certificate needs to match the required subject, issuer, and extensions
Expand All @@ -532,31 +533,66 @@ func matchSignatures(signatures []oci.Signature, subject, issuer string, extensi
return fmt.Errorf("invalid signature")
}

func matchCertificateData(cert *x509.Certificate, subject, issuer string, extensions map[string]string) error {
if subject != "" {
s := ""
func matchCertificateData(cert *x509.Certificate, subject, subjectRegExp, issuer, issuerRegExp string, extensions map[string]string) error {
if subject != "" || subjectRegExp != "" {
if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 {
s = sans[0]
}
if !wildcard.Match(subject, s) {
return fmt.Errorf("subject mismatch: expected %s, received %s", subject, s)
subjectMatched := false
if subject != "" {
for _, s := range sans {
if wildcard.Match(subject, s) {
subjectMatched = true
break
}
}
}
if subjectRegExp != "" {
regex, err := regexp.Compile(subjectRegExp)
if err != nil {
return fmt.Errorf("invalid regexp for subject: %s : %w", subjectRegExp, err)
}
for _, s := range sans {
if regex.MatchString(s) {
subjectMatched = true
break
}
}
}

if !subjectMatched {
sub := ""
if subject != "" {
sub = subject
} else if subjectRegExp != "" {
sub = subjectRegExp
}
return fmt.Errorf("subject mismatch: expected %s, received %s", sub, strings.Join(sans, ", "))
}
}
}

if err := matchExtensions(cert, issuer, extensions); err != nil {
if err := matchExtensions(cert, issuer, issuerRegExp, extensions); err != nil {
return err
}

return nil
}

func matchExtensions(cert *x509.Certificate, issuer string, extensions map[string]string) error {
func matchExtensions(cert *x509.Certificate, issuer, issuerRegExp string, extensions map[string]string) error {
ce := cosign.CertExtensions{Cert: cert}

if issuer != "" {
if issuer != "" || issuerRegExp != "" {
val := ce.GetIssuer()
if !wildcard.Match(issuer, val) {
return fmt.Errorf("issuer mismatch: expected %s, received %s", issuer, val)
if issuer != "" {
if !wildcard.Match(issuer, val) {
return fmt.Errorf("issuer mismatch: expected %s, received %s", issuer, val)
}
}
if issuerRegExp != "" {
if regex, err := regexp.Compile(issuerRegExp); err != nil {
return fmt.Errorf("invalid regexp for issuer: %s : %w", issuerRegExp, err)
} else if !regex.MatchString(val) {
return fmt.Errorf("issuer mismatch: expected %s, received %s", issuerRegExp, val)
}
}
}

Expand Down
53 changes: 44 additions & 9 deletions pkg/cosign/cosign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,28 +227,52 @@ func TestCosignMatchCertificateData(t *testing.T) {
assert.NilError(t, err)

subject1 := "https://github.com/JimBugwadia/demo-java-tomcat/.github/workflows/publish.yaml@refs/tags/*"
subject1RegExp := `https://github\.com/JimBugwadia/demo-java-tomcat/.+`
issuer1 := "https://token.actions.githubusercontent.com"
issuer1RegExp := `https://token\.actions\..+`

extensions := map[string]string{
"githubWorkflowTrigger": "push",
"githubWorkflowSha": "c7645284fa7aebe554618eee879b4d6947f8564e",
"githubWorkflowName": "build-sign-attest",
"githubWorkflowRepository": "JimBugwadia/demo-java-tomcat",
}

matchErr := matchCertificateData(cert1, subject1, issuer1, extensions)
matchErr := matchCertificateData(cert1, subject1, "", issuer1, "", extensions)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, "", "", issuer1, "", extensions)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, subject1, "", issuer1, "", nil)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, "", subject1RegExp, "", issuer1RegExp, nil)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, "", issuer1, extensions)
matchErr = matchCertificateData(cert1, "", "", "", issuer1RegExp, nil)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, subject1, issuer1, nil)
matchErr = matchCertificateData(cert1, subject1, subject1RegExp, issuer1, issuer1RegExp, nil)
assert.NilError(t, matchErr)

matchErr = matchCertificateData(cert1, "wrong-subject", issuer1, extensions)
matchErr = matchCertificateData(cert1, "", `^wrong-regex$`, issuer1, issuer1RegExp, nil)
assert.Error(t, matchErr, "subject mismatch: expected ^wrong-regex$, received https://github.com/JimBugwadia/demo-java-tomcat/.github/workflows/publish.yaml@refs/tags/v0.0.22")

matchErr = matchCertificateData(cert1, "", "", "", `^wrong-regex$`, nil)
assert.Error(t, matchErr, "issuer mismatch: expected ^wrong-regex$, received https://token.actions.githubusercontent.com")

matchErr = matchCertificateData(cert1, "wrong-subject", "", issuer1, "", extensions)
assert.Error(t, matchErr, "subject mismatch: expected wrong-subject, received https://github.com/JimBugwadia/demo-java-tomcat/.github/workflows/publish.yaml@refs/tags/v0.0.22")

matchErr = matchCertificateData(cert1, "", "*", "", issuer1RegExp, nil)
assert.Error(t, matchErr, "invalid regexp for subject: * : error parsing regexp: missing argument to repetition operator: `*`")

matchErr = matchCertificateData(cert1, "", subject1RegExp, "", "?", nil)
assert.Error(t, matchErr, "invalid regexp for issuer: ? : error parsing regexp: missing argument to repetition operator: `?`")

extensions["githubWorkflowTrigger"] = "pull"
matchErr = matchCertificateData(cert1, subject1, issuer1, extensions)
matchErr = matchCertificateData(cert1, subject1, "", issuer1, "", extensions)
assert.Error(t, matchErr, "extension mismatch: expected pull for key githubWorkflowTrigger, received push")
}

Expand Down Expand Up @@ -431,17 +455,28 @@ func TestCosignMatchSignatures(t *testing.T) {
}

subject2 := "*@nirmata.com"
subject2RegExp := `.+@nirmata\.com`
issuer2 := "https://github.com/login/oauth"
issuer2RegExp := `https://github\.com/login/.+`

matchErr := matchSignatures(sigs, subject1, "", issuer1, "", extensions)
assert.NilError(t, matchErr)

matchErr = matchSignatures(sigs, subject2, "", issuer2, "", nil)
assert.NilError(t, matchErr)

matchErr := matchSignatures(sigs, subject1, issuer1, extensions)
matchErr = matchSignatures(sigs, "", subject2RegExp, issuer2, "", nil)
assert.NilError(t, matchErr)

matchErr = matchSignatures(sigs, subject2, issuer2, nil)
matchErr = matchSignatures(sigs, "", "", "", issuer2RegExp, nil)
assert.NilError(t, matchErr)

matchErr = matchSignatures(sigs, subject2, issuer1, nil)
matchErr = matchSignatures(sigs, subject2, "", issuer1, "", nil)
assert.Error(t, matchErr, "subject mismatch: expected *@nirmata.com, received https://github.com/JimBugwadia/demo-java-tomcat/.github/workflows/publish.yaml@refs/tags/v0.0.22; issuer mismatch: expected https://token.actions.githubusercontent.com, received https://github.com/login/oauth")

matchErr = matchSignatures(sigs, subject2, issuer2, extensions)
matchErr = matchSignatures(sigs, "", subject2RegExp, issuer1, "", nil)
assert.Error(t, matchErr, `subject mismatch: expected .+@nirmata\.com, received https://github.com/JimBugwadia/demo-java-tomcat/.github/workflows/publish.yaml@refs/tags/v0.0.22; issuer mismatch: expected https://token.actions.githubusercontent.com, received https://github.com/login/oauth`)

matchErr = matchSignatures(sigs, subject2, "", issuer2, "", extensions)
assert.ErrorContains(t, matchErr, "extension mismatch")
}
2 changes: 2 additions & 0 deletions pkg/engine/internal/imageverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,9 @@ func (iv *ImageVerifier) buildCosignVerifier(

opts.Roots = attestor.Keyless.Roots
opts.Issuer = attestor.Keyless.Issuer
opts.IssuerRegExp = attestor.Keyless.IssuerRegExp
opts.Subject = attestor.Keyless.Subject
opts.SubjectRegExp = attestor.Keyless.SubjectRegExp
opts.AdditionalExtensions = attestor.Keyless.AdditionalExtensions
}

Expand Down
Loading

0 comments on commit f69ffe1

Please sign in to comment.