Skip to content

Commit

Permalink
feat: cert-extensions verify
Browse files Browse the repository at this point in the history
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
Co-authored-by: Christian Kotzbauer <@ckotzbauer1>
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
  • Loading branch information
developer-guy committed Jun 1, 2022
1 parent ae90c74 commit 517c33c
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 51 deletions.
53 changes: 53 additions & 0 deletions cmd/cosign/cli/options/certextensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package options

import (
"fmt"
"strings"

"github.com/spf13/cobra"

sigs "github.com/sigstore/cosign/pkg/signature"
)

// CertExtensionOptions is the top level wrapper for the annotations.
type CertExtensionOptions struct {
CertExtensions []string
}

var _ Interface = (*CertExtensionOptions)(nil)

func (o *CertExtensionOptions) CertExtensionsMap() (sigs.CertExtensionsMap, error) {
ce := sigs.CertExtensionsMap{}
for _, a := range o.CertExtensions {
kv := strings.Split(a, "=")
if len(kv) != 2 {
return ce, fmt.Errorf("unable to parse cert extension: %s", a)
}
if ce.CertExtensions == nil {
ce.CertExtensions = map[string]string{}
}
ce.CertExtensions[kv[0]] = kv[1]
}
return ce, nil
}

// AddFlags implements Interface
func (o *CertExtensionOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&o.CertExtensions, "cert-extensions", "", nil,
"extra key=value pairs to verify")
}
2 changes: 2 additions & 0 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type VerifyOptions struct {
Registry RegistryOptions
SignatureDigest SignatureDigestOptions
AnnotationOptions
CertExtensionOptions
}

var _ Interface = (*VerifyOptions)(nil)
Expand All @@ -46,6 +47,7 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
o.Registry.AddFlags(cmd)
o.SignatureDigest.AddFlags(cmd)
o.AnnotationOptions.AddFlags(cmd)
o.CertExtensionOptions.AddFlags(cmd)

cmd.Flags().StringVar(&o.Key, "key", "",
"path to the public key file, KMS URI or Kubernetes Secret")
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/policy_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func signPolicy() *cobra.Command {
return errors.New("error decoding certificate")
}
signerEmail := sigs.CertSubject(certs[0])
signerIssuer := sigs.CertIssuerExtension(certs[0])
signerIssuer := cosign.CertIssuerExtension(certs[0])

// Retrieve root.json from registry.
imgName := rootPath(o.ImageRef)
Expand Down
6 changes: 6 additions & 0 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ against the transparency log.`,
return err
}

certExtensions, err := o.CertExtensionsMap()
if err != nil {
return err
}

hashAlgorithm, err := o.SignatureDigest.HashAlgorithm()
if err != nil {
return err
Expand All @@ -107,6 +112,7 @@ against the transparency log.`,
RekorURL: o.Rekor.URL,
Attachment: o.Attachment,
Annotations: annotations,
CertExtensions: certExtensions,
HashAlgorithm: hashAlgorithm,
SignatureRef: o.SignatureRef,
LocalImage: o.LocalImage,
Expand Down
6 changes: 4 additions & 2 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type VerifyCommand struct {
RekorURL string
Attachment string
Annotations sigs.AnnotationsMap
CertExtensions sigs.CertExtensionsMap
SignatureRef string
HashAlgorithm crypto.Hash
LocalImage bool
Expand Down Expand Up @@ -93,6 +94,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
}
co := &cosign.CheckOpts{
Annotations: c.Annotations.Annotations,
CertExtensions: c.CertExtensions.CertExtensions,
RegistryClientOpts: ociremoteOpts,
CertEmail: c.CertEmail,
CertOidcIssuer: c.CertOidcIssuer,
Expand Down Expand Up @@ -234,7 +236,7 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) {
for _, sig := range verified {
if cert, err := sig.Cert(); err == nil && cert != nil {
fmt.Fprintln(os.Stderr, "Certificate subject: ", sigs.CertSubject(cert))
if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" {
if issuerURL := cosign.CertIssuerExtension(cert); issuerURL != "" {
fmt.Fprintln(os.Stderr, "Certificate issuer URL: ", issuerURL)
}
}
Expand Down Expand Up @@ -267,7 +269,7 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) {
ss.Optional = make(map[string]interface{})
}
ss.Optional["Subject"] = sigs.CertSubject(cert)
if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" {
if issuerURL := cosign.CertIssuerExtension(cert); issuerURL != "" {
ss.Optional["Issuer"] = issuerURL
}
}
Expand Down
1 change: 1 addition & 0 deletions doc/cosign_dockerfile_verify.md

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

1 change: 1 addition & 0 deletions doc/cosign_manifest_verify.md

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

1 change: 1 addition & 0 deletions doc/cosign_verify.md

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

59 changes: 59 additions & 0 deletions pkg/cosign/certextensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cosign

import "crypto/x509"

var (
// Fulcio cert-extensions, documented here: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md
CertExtensionOIDCIssuer = "1.3.6.1.4.1.57264.1.1"
CertExtensionGithubWorkflowTrigger = "1.3.6.1.4.1.57264.1.2"
CertExtensionGithubWorkflowSha = "1.3.6.1.4.1.57264.1.3"
CertExtensionGithubWorkflowName = "1.3.6.1.4.1.57264.1.4"
CertExtensionGithubWorkflowRepository = "1.3.6.1.4.1.57264.1.5"
CertExtensionGithubWorkflowRef = "1.3.6.1.4.1.57264.1.6"

CertExtensionMap = map[string]string{
CertExtensionOIDCIssuer: "oidcIssuer",
CertExtensionGithubWorkflowTrigger: "githubWorkflowTrigger",
CertExtensionGithubWorkflowSha: "githubWorkflowSha",
CertExtensionGithubWorkflowName: "githubWorkflowName",
CertExtensionGithubWorkflowRepository: "githubWorkflowRepository",
CertExtensionGithubWorkflowRef: "githubWorkflowRef",
}
)

func CertIssuerExtension(cert *x509.Certificate) string {
for _, ext := range cert.Extensions {
if ext.Id.String() == CertExtensionOIDCIssuer {
return string(ext.Value)
}
}
return ""
}

func CertExtensions(cert *x509.Certificate) map[string]string {
extensions := map[string]string{}
for _, ext := range cert.Extensions {
readableName, ok := CertExtensionMap[ext.Id.String()]
if ok {
extensions[readableName] = string(ext.Value)
} else {
extensions[ext.Id.String()] = string(ext.Value)
}
}
return extensions
}
11 changes: 9 additions & 2 deletions pkg/cosign/verifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

// SimpleClaimVerifier verifies that sig.Payload() is a SimpleContainerImage payload which references the given image digest and contains the given annotations.
func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error {
func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}, certExtensionsActual, certExtensionsExpected map[string]string) error {
p, err := sig.Payload()
if err != nil {
return err
Expand All @@ -51,11 +51,18 @@ func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map
return errors.New("missing or incorrect annotation")
}
}

if certExtensionsExpected != nil {
if !correctCertExtensions(certExtensionsExpected, certExtensionsActual) {
return errors.New("missing or incorrect cert extension")
}
}

return nil
}

// IntotoSubjectClaimVerifier verifies that sig.Payload() is an Intoto statement which references the given image digest.
func IntotoSubjectClaimVerifier(sig oci.Signature, imageDigest v1.Hash, _ map[string]interface{}) error {
func IntotoSubjectClaimVerifier(sig oci.Signature, imageDigest v1.Hash, _ map[string]interface{}, _ map[string]string, _ map[string]string) error {
p, err := sig.Payload()
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion pkg/cosign/verifiers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func Test_IntotoSubjectClaimVerifier(t *testing.T) {
if err != nil {
t.Fatal("Failed to create static.NewSignature: ", err)
}
got := IntotoSubjectClaimVerifier(ociSig, tc.digest, nil)
got := IntotoSubjectClaimVerifier(ociSig, tc.digest, nil, nil, nil)
if got != nil && !tc.shouldFail {
t.Error("Expected ClaimVerifier to succeed but failed: ", got)
}
Expand Down
21 changes: 18 additions & 3 deletions pkg/cosign/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ type CheckOpts struct {

// Annotations optionally specifies image signature annotations to verify.
Annotations map[string]interface{}
// CertExtensions optionally specifies image signature cert extensions to verify.
CertExtensions map[string]string
// ClaimVerifier, if provided, verifies claims present in the oci.Signature.
ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error
ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}, certExtensionsActual, certExtensionsExpected map[string]string) error

// RekorClient, if set, is used to use to verify signatures and public keys.
RekorClient *client.Rekor
Expand Down Expand Up @@ -468,6 +470,7 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C
// VerifyImageSignature verifies a signature
func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) {
verifier := co.SigVerifier
actualCertExtensions := map[string]string{}
if verifier == nil {
// If we don't have a public key to check against, we can try a root cert.
cert, err := sig.Cert()
Expand All @@ -493,6 +496,7 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co
}
co.IntermediateCerts = pool
}
actualCertExtensions = CertExtensions(cert)
verifier, err = ValidateAndUnpackCert(cert, co)
if err != nil {
return bundleVerified, err
Expand All @@ -505,7 +509,7 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co

// We can't check annotations without claims, both require unmarshalling the payload.
if co.ClaimVerifier != nil {
if err := co.ClaimVerifier(sig, h, co.Annotations); err != nil {
if err := co.ClaimVerifier(sig, h, co.Annotations, actualCertExtensions, co.CertExtensions); err != nil {
return bundleVerified, err
}
}
Expand Down Expand Up @@ -644,6 +648,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash
}

validationErrs := []string{}
actualCertExtensions := map[string]string{}
for _, att := range sl {
if err := func(att oci.Signature) error {
verifier := co.SigVerifier
Expand Down Expand Up @@ -672,6 +677,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash
}
co.IntermediateCerts = pool
}
actualCertExtensions = CertExtensions(cert)
verifier, err = ValidateAndUnpackCert(cert, co)
if err != nil {
return err
Expand All @@ -684,7 +690,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash

// We can't check annotations without claims, both require unmarshalling the payload.
if co.ClaimVerifier != nil {
if err := co.ClaimVerifier(att, h, co.Annotations); err != nil {
if err := co.ClaimVerifier(att, h, co.Annotations, actualCertExtensions, co.CertExtensions); err != nil {
return err
}
}
Expand Down Expand Up @@ -966,3 +972,12 @@ func correctAnnotations(wanted, have map[string]interface{}) bool {
}
return true
}

func correctCertExtensions(wanted, have map[string]string) bool {
for k, v := range wanted {
if have[k] != v {
return false
}
}
return true
}
47 changes: 47 additions & 0 deletions pkg/signature/certextensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import (
_ "crypto/sha256" // for `crypto.SHA256`
"fmt"
"strings"
)

type CertExtensionsMap struct {
CertExtensions map[string]string
}

func (a *CertExtensionsMap) Set(s string) error {
if a.CertExtensions == nil {
a.CertExtensions = map[string]string{}
}
kvp := strings.SplitN(s, "=", 2)
if len(kvp) != 2 {
return fmt.Errorf("invalid flag: %s, expected key=value", s)
}

a.CertExtensions[kvp[0]] = kvp[1]
return nil
}

func (a *CertExtensionsMap) String() string {
s := []string{}
for k, v := range a.CertExtensions {
s = append(s, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(s, ",")
}
Loading

0 comments on commit 517c33c

Please sign in to comment.