Skip to content

Commit

Permalink
Add flag to verify OIDC issuer in certificate (#1308)
Browse files Browse the repository at this point in the history
This adds support for --cert-oidc-issuer. In combination
with --cert-email, users should be able to verify and pin
the expected identity of the Fulcio certificate.

Also added certificate generation test utilities.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper authored Jan 14, 2022
1 parent 2c96cf3 commit 079e28d
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 35 deletions.
1 change: 1 addition & 0 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val
KeyRef: o.Key,
CertRef: o.CertVerify.Cert,
CertEmail: o.CertVerify.CertEmail,
CertOidcIssuer: o.CertVerify.CertOidcIssuer,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
Output: o.Output,
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ against the transparency log.`,
KeyRef: o.Key,
CertRef: o.CertVerify.Cert,
CertEmail: o.CertVerify.CertEmail,
CertOidcIssuer: o.CertVerify.CertOidcIssuer,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
Output: o.Output,
Expand Down
8 changes: 6 additions & 2 deletions cmd/cosign/cli/options/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import (

// CertVerifyOptions is the wrapper for certificate verification.
type CertVerifyOptions struct {
Cert string
CertEmail string
Cert string
CertEmail string
CertOidcIssuer string
}

var _ Interface = (*RekorOptions)(nil)
Expand All @@ -33,4 +34,7 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {

cmd.Flags().StringVar(&o.CertEmail, "cert-email", "",
"the email expected in a valid Fulcio certificate")

cmd.Flags().StringVar(&o.CertOidcIssuer, "cert-oidc-issuer", "",
"the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth")
}
4 changes: 3 additions & 1 deletion cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ against the transparency log.`,
KeyRef: o.Key,
CertRef: o.CertVerify.Cert,
CertEmail: o.CertVerify.CertEmail,
CertOidcIssuer: o.CertVerify.CertOidcIssuer,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
Output: o.Output,
Expand Down Expand Up @@ -161,6 +162,7 @@ against the transparency log.`,
CheckClaims: o.CheckClaims,
CertRef: o.CertVerify.Cert,
CertEmail: o.CertVerify.CertEmail,
CertOidcIssuer: o.CertVerify.CertOidcIssuer,
KeyRef: o.Key,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
Expand Down Expand Up @@ -238,7 +240,7 @@ The blob may be specified as a path to a file or - for stdin.`,
RekorURL: o.Rekor.URL,
}
if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert,
o.CertVerify.CertEmail, o.Signature, args[0]); err != nil {
o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.Signature, args[0]); err != nil {
return errors.Wrapf(err, "verifying blob %s", args)
}
return nil
Expand Down
28 changes: 15 additions & 13 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,20 @@ import (
// nolint
type VerifyCommand struct {
options.RegistryOptions
CheckClaims bool
KeyRef string
CertRef string
CertEmail string
Sk bool
Slot string
Output string
RekorURL string
Attachment string
Annotations sigs.AnnotationsMap
SignatureRef string
HashAlgorithm crypto.Hash
LocalImage bool
CheckClaims bool
KeyRef string
CertRef string
CertEmail string
CertOidcIssuer string
Sk bool
Slot string
Output string
RekorURL string
Attachment string
Annotations sigs.AnnotationsMap
SignatureRef string
HashAlgorithm crypto.Hash
LocalImage bool
}

// Exec runs the verification command
Expand Down Expand Up @@ -92,6 +93,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
Annotations: c.Annotations.Annotations,
RegistryClientOpts: ociremoteOpts,
CertEmail: c.CertEmail,
CertOidcIssuer: c.CertOidcIssuer,
SignatureRef: c.SignatureRef,
}
if c.CheckClaims {
Expand Down
24 changes: 13 additions & 11 deletions cmd/cosign/cli/verify/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,18 @@ import (
// nolint
type VerifyAttestationCommand struct {
options.RegistryOptions
CheckClaims bool
CertRef string
CertEmail string
KeyRef string
Sk bool
Slot string
Output string
RekorURL string
PredicateType string
Policies []string
LocalImage bool
CheckClaims bool
CertRef string
CertEmail string
CertOidcIssuer string
KeyRef string
Sk bool
Slot string
Output string
RekorURL string
PredicateType string
Policies []string
LocalImage bool
}

// Exec runs the verification command
Expand All @@ -77,6 +78,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
co := &cosign.CheckOpts{
RegistryClientOpts: ociremoteOpts,
CertEmail: c.CertEmail,
CertOidcIssuer: c.CertOidcIssuer,
}
if c.CheckClaims {
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
Expand Down
7 changes: 4 additions & 3 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func isb64(data []byte) bool {
}

// nolint
func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, sigRef, blobRef string) error {
func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, sigRef, blobRef string) error {
var verifier signature.Verifier
var cert *x509.Certificate

Expand Down Expand Up @@ -128,8 +128,9 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, sig
}

co := &cosign.CheckOpts{
RootCerts: fulcio.GetRoots(),
CertEmail: certEmail,
RootCerts: fulcio.GetRoots(),
CertEmail: certEmail,
CertOidcIssuer: certOidcIssuer,
}
cert = certs[0]
verifier, err = cosign.ValidateAndUnpackCert(cert, co)
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-attestation.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-blob.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.

16 changes: 15 additions & 1 deletion pkg/cosign/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -75,6 +76,8 @@ type CheckOpts struct {
RootCerts *x509.CertPool
// CertEmail is the email expected for a certificate to be valid. The empty string means any certificate can be valid.
CertEmail string
// CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid.
CertOidcIssuer string

// SignatureRef is the reference to the signature file
SignatureRef string
Expand Down Expand Up @@ -160,6 +163,18 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver
return nil, errors.New("expected email not found in certificate")
}
}
if co.CertOidcIssuer != "" {
issuer := ""
for _, ext := range cert.Extensions {
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) {
issuer = string(ext.Value)
break
}
}
if issuer != co.CertOidcIssuer {
return nil, errors.New("expected oidc issuer not found in certificate")
}
}
return verifier, nil
}

Expand Down Expand Up @@ -697,7 +712,6 @@ func TrustedCert(cert *x509.Certificate, roots *x509.CertPool) error {
CurrentTime: cert.NotBefore,
Roots: roots,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsage(x509.KeyUsageDigitalSignature),
x509.ExtKeyUsageCodeSigning,
},
}); err != nil {
Expand Down
107 changes: 107 additions & 0 deletions pkg/cosign/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cosign
import (
"context"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"io"
Expand All @@ -26,7 +27,9 @@ import (
"github.com/pkg/errors"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/cosign/test"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/stretchr/testify/require"
)

type mockVerifier struct {
Expand Down Expand Up @@ -89,3 +92,107 @@ func Test_verifyOCIAttestation(t *testing.T) {
t.Error("verifyOCIAttestation() expected invalid payload type error, got nil")
}
}

func TestValidateAndUnpackCertSuccess(t *testing.T) {
subject := "email@email"
oidcIssuer := "https://accounts.google.com"

rootCert, rootKey, _ := test.GenerateRootCa()
leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey)

rootPool := x509.NewCertPool()
rootPool.AddCert(rootCert)

co := &CheckOpts{
RootCerts: rootPool,
CertEmail: subject,
CertOidcIssuer: oidcIssuer,
}

_, err := ValidateAndUnpackCert(leafCert, co)
if err != nil {
t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err)
}
}

func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) {
subject := "email@email"
oidcIssuer := "https://accounts.google.com"

rootCert, rootKey, _ := test.GenerateRootCa()
leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey)

rootPool := x509.NewCertPool()
rootPool.AddCert(rootCert)

co := &CheckOpts{
RootCerts: rootPool,
}

_, err := ValidateAndUnpackCert(leafCert, co)
if err != nil {
t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err)
}
}

func TestValidateAndUnpackCertInvalidRoot(t *testing.T) {
subject := "email@email"
oidcIssuer := "https://accounts.google.com"

rootCert, rootKey, _ := test.GenerateRootCa()
leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey)

otherRoot, _, _ := test.GenerateRootCa()

rootPool := x509.NewCertPool()
rootPool.AddCert(otherRoot)

co := &CheckOpts{
RootCerts: rootPool,
CertEmail: subject,
CertOidcIssuer: oidcIssuer,
}

_, err := ValidateAndUnpackCert(leafCert, co)
require.Contains(t, err.Error(), "certificate signed by unknown authority")
}

func TestValidateAndUnpackCertInvalidOidcIssuer(t *testing.T) {
subject := "email@email"
oidcIssuer := "https://accounts.google.com"

rootCert, rootKey, _ := test.GenerateRootCa()
leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey)

rootPool := x509.NewCertPool()
rootPool.AddCert(rootCert)

co := &CheckOpts{
RootCerts: rootPool,
CertEmail: subject,
CertOidcIssuer: "other",
}

_, err := ValidateAndUnpackCert(leafCert, co)
require.Contains(t, err.Error(), "expected oidc issuer not found in certificate")
}

func TestValidateAndUnpackCertInvalidEmail(t *testing.T) {
subject := "email@email"
oidcIssuer := "https://accounts.google.com"

rootCert, rootKey, _ := test.GenerateRootCa()
leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey)

rootPool := x509.NewCertPool()
rootPool.AddCert(rootCert)

co := &CheckOpts{
RootCerts: rootPool,
CertEmail: "other",
CertOidcIssuer: oidcIssuer,
}

_, err := ValidateAndUnpackCert(leafCert, co)
require.Contains(t, err.Error(), "expected email not found in certificate")
}
Loading

0 comments on commit 079e28d

Please sign in to comment.