Skip to content

Commit

Permalink
Add certificate chain flag for signing (sigstore#1656)
Browse files Browse the repository at this point in the history
* Add certificate chain flag for signing

This allows users to pass their own certificate chain to include in the
OCI signature. The chain is checked for validity using the provided
certificate.

Also refactored the check for matching public keys using a method from
sigstore/sigstore, comparing the certificate's key with the provided
key. Also added this check when extracting the PKCS11 certificate.

Certificate chains must be PEM-encoded. I changed the text of the
certificate flag to also specify a preference for PEM encoding, but
didn't remove the code that handles DER encoding for backwards
compatibility.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Adding 3rd party licenses

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Added check for empty chain

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper committed Mar 26, 2022
1 parent 4fb8950 commit db90d13
Show file tree
Hide file tree
Showing 22 changed files with 498 additions and 99 deletions.
5 changes: 4 additions & 1 deletion cmd/cosign/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func Attest() *cobra.Command {
# attach an attestation to a container image with a key pair stored in Hashicorp Vault
cosign attest --predicate <FILE> --type <TYPE> --key hashivault://[KEY] <IMAGE>
# attach an attestation to a container image with a local key pair file, including a certificate and certificate chain
cosign attest --predicate <FILE> --type <TYPE> --key cosign.key --cert cosign.crt --cert-chain chain.crt <IMAGE>
# attach an attestation to a container image which does not fully support OCI media types
COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest --predicate <FILE> --type <TYPE> --key cosign.key legacy-registry.example.com/my/image`,

Expand All @@ -70,7 +73,7 @@ func Attest() *cobra.Command {
OIDCClientSecret: o.OIDC.ClientSecret,
}
for _, img := range args {
if err := attest.AttestCmd(cmd.Context(), ko, o.Registry, img, o.Cert, o.NoUpload,
if err := attest.AttestCmd(cmd.Context(), ko, o.Registry, img, o.Cert, o.CertChain, o.NoUpload,
o.Predicate.Path, o.Force, o.Predicate.Type, o.Replace, ro.Timeout); err != nil {
return errors.Wrapf(err, "signing %s", img)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string,
}

//nolint
func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string,
func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string,
noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error {
// A key file or token is required unless we're in experimental mode!
if options.EnableExperimental() {
Expand Down Expand Up @@ -117,7 +117,7 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt
// each access.
ref = digest // nolint

sv, err := sign.SignerFromKeyOpts(ctx, certPath, ko)
sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko)
if err != nil {
return errors.Wrap(err, "getting signer")
}
Expand Down
9 changes: 8 additions & 1 deletion cmd/cosign/cli/options/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type AttestOptions struct {
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool
Expand Down Expand Up @@ -51,7 +52,13 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) {
"path to the private key file, KMS URI or Kubernetes Secret")

cmd.Flags().StringVar(&o.Cert, "cert", "",
"path to the x509 certificate to include in the Signature")
"path to the X.509 certificate in PEM format to include in the OCI Signature")

cmd.Flags().StringVar(&o.CertChain, "cert-chain", "",
"path to a list of CA X.509 certificates in PEM format which will be needed "+
"when building the certificate chain for the signing certificate. "+
"Must start with the parent intermediate CA certificate of the "+
"signing certificate and end with the root certificate. Included in the OCI Signature")

cmd.Flags().BoolVar(&o.NoUpload, "no-upload", false,
"do not upload the generated attestation")
Expand Down
9 changes: 8 additions & 1 deletion cmd/cosign/cli/options/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type SignOptions struct {
Key string
Cert string
CertChain string
Upload bool
Output string // deprecated: TODO remove when the output flag is fully deprecated
OutputSignature string // TODO: this should be the root output file arg.
Expand Down Expand Up @@ -55,7 +56,13 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) {
"path to the private key file, KMS URI or Kubernetes Secret")

cmd.Flags().StringVar(&o.Cert, "cert", "",
"path to the x509 certificate to include in the Signature")
"path to the X.509 certificate in PEM format to include in the OCI Signature")

cmd.Flags().StringVar(&o.CertChain, "cert-chain", "",
"path to a list of CA X.509 certificates in PEM format which will be needed "+
"when building the certificate chain for the signing certificate. "+
"Must start with the parent intermediate CA certificate of the "+
"signing certificate and end with the root certificate. Included in the OCI Signature")

cmd.Flags().BoolVar(&o.Upload, "upload", true,
"whether to upload the signature")
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 @@ -175,7 +175,7 @@ func signPolicy() *cobra.Command {
}

// Get Fulcio signer
sv, err := sign.SignerFromKeyOpts(ctx, "", sign.KeyOpts{
sv, err := sign.SignerFromKeyOpts(ctx, "", "", sign.KeyOpts{
FulcioURL: o.Fulcio.URL,
IDToken: o.Fulcio.IdentityToken,
InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify,
Expand Down
6 changes: 5 additions & 1 deletion cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func Sign() *cobra.Command {
# sign a container image with a key pair stored in a Kubernetes secret
cosign sign --key k8s://[NAMESPACE]/[KEY] <IMAGE>
# sign a container image with a key, attaching a certificate and certificate chain
cosign sign --key cosign.key --cert cosign.crt --cert-chain chain.crt <IMAGE>
# sign a container in a registry which does not fully support OCI media types
COSIGN_DOCKER_MEDIA_TYPES=1 cosign sign --key cosign.key legacy-registry.example.com/my/image`,
Args: cobra.MinimumNArgs(1),
Expand Down Expand Up @@ -89,7 +92,8 @@ func Sign() *cobra.Command {
if err != nil {
return err
}
if err := sign.SignCmd(ro, ko, o.Registry, annotationsMap.Annotations, args, o.Cert, o.Upload, o.OutputSignature, o.OutputCertificate, o.PayloadPath, o.Force, o.Recursive, o.Attachment); err != nil {
if err := sign.SignCmd(ro, ko, o.Registry, annotationsMap.Annotations, args, o.Cert, o.CertChain, o.Upload,
o.OutputSignature, o.OutputCertificate, o.PayloadPath, o.Force, o.Recursive, o.Attachment); err != nil {
if o.Attachment == "" {
return errors.Wrapf(err, "signing %v", args)
}
Expand Down
90 changes: 62 additions & 28 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package sign
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
Expand Down Expand Up @@ -95,7 +93,8 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot

// nolint
func SignCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{},
imgs []string, certPath string, upload bool, outputSignature, outputCertificate string, payloadPath string, force bool, recursive bool, attachment string) error {
imgs []string, certPath string, certChainPath string, upload bool, outputSignature, outputCertificate string,
payloadPath string, force bool, recursive bool, attachment string) error {
if options.EnableExperimental() {
if options.NOf(ko.KeyRef, ko.Sk) > 1 {
return &options.KeyParseError{}
Expand All @@ -109,7 +108,7 @@ func SignCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOption
ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout)
defer cancel()

sv, err := SignerFromKeyOpts(ctx, certPath, ko)
sv, err := SignerFromKeyOpts(ctx, certPath, certChainPath, ko)
if err != nil {
return errors.Wrap(err, "getting signer")
}
Expand Down Expand Up @@ -310,29 +309,40 @@ func signerFromSecurityKey(keySlot string) (*SignerVerifier, error) {
}, nil
}

func signerFromKeyRef(ctx context.Context, certPath, keyRef string, passFunc cosign.PassFunc) (*SignerVerifier, error) {
func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc) (*SignerVerifier, error) {
k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc)
if err != nil {
return nil, errors.Wrap(err, "reading key")
}

// Handle the -cert flag
// Attempt to extract certificate from PKCS11 token
// With PKCS11, we assume the certificate is in the same slot on the PKCS11
// token as the private key. If it's not there, show a warning to the
// user.
if pkcs11Key, ok := k.(*pkcs11key.Key); ok {
certFromPKCS11, _ := pkcs11Key.Certificate()
var pemBytes []byte
if certFromPKCS11 == nil {
fmt.Fprintln(os.Stderr, "warning: no x509 certificate retrieved from the PKCS11 token")
} else {
pemBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPKCS11)
if err != nil {
pkcs11Key.Close()
return nil, err
}
return &SignerVerifier{
SignerVerifier: k,
close: pkcs11Key.Close,
}, nil
}
pemBytes, err := cryptoutils.MarshalCertificateToPEM(certFromPKCS11)
if err != nil {
pkcs11Key.Close()
return nil, err
}
// Check that provided public key and certificate key match
pubKey, err := k.PublicKey()
if err != nil {
pkcs11Key.Close()
return nil, err
}
if cryptoutils.EqualKeys(pubKey, certFromPKCS11.PublicKey) != nil {
pkcs11Key.Close()
return nil, errors.New("pkcs11 key and certificate do not match")
}

return &SignerVerifier{
Cert: pemBytes,
SignerVerifier: k,
Expand All @@ -342,15 +352,18 @@ func signerFromKeyRef(ctx context.Context, certPath, keyRef string, passFunc cos
certSigner := &SignerVerifier{
SignerVerifier: k,
}

if certPath == "" {
return certSigner, nil
}

// Handle --cert flag
// Allow both DER and PEM encoding
certBytes, err := os.ReadFile(certPath)
if err != nil {
return nil, errors.Wrap(err, "read certificate")
}
// Handle PEM.
// Handle PEM
if bytes.HasPrefix(certBytes, []byte("-----")) {
decoded, _ := pem.Decode(certBytes)
if decoded.Type != "CERTIFICATE" {
Expand All @@ -366,23 +379,44 @@ func signerFromKeyRef(ctx context.Context, certPath, keyRef string, passFunc cos
if err != nil {
return nil, errors.Wrap(err, "get public key")
}
switch kt := parsedCert.PublicKey.(type) {
case *ecdsa.PublicKey:
if !kt.Equal(pk) {
return nil, errors.New("public key in certificate does not match that in the signing key")
}
case *rsa.PublicKey:
if !kt.Equal(pk) {
return nil, errors.New("public key in certificate does not match that in the signing key")
}
default:
return nil, fmt.Errorf("unsupported key type: %T", parsedCert.PublicKey)
if cryptoutils.EqualKeys(pk, parsedCert.PublicKey) != nil {
return nil, errors.New("public key in certificate does not match the provided public key")
}
pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert)
if err != nil {
return nil, errors.Wrap(err, "marshaling certificate to PEM")
}
certSigner.Cert = pemBytes

if certChainPath == "" {
return certSigner, nil
}

// Handle --cert-chain flag
// Accept only PEM encoded certificate chain
certChainBytes, err := os.ReadFile(certChainPath)
if err != nil {
return nil, errors.Wrap(err, "reading certificate chain from path")
}
certChain, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certChainBytes))
if err != nil {
return nil, errors.Wrap(err, "loading certificate chain")
}
if len(certChain) == 0 {
return nil, errors.New("no certificates in certificate chain")
}
// Verify certificate chain is valid
rootPool := x509.NewCertPool()
rootPool.AddCert(certChain[len(certChain)-1])
subPool := x509.NewCertPool()
for _, c := range certChain[:len(certChain)-1] {
subPool.AddCert(c)
}
if err := cosign.TrustedCert(parsedCert, rootPool, subPool); err != nil {
return nil, errors.Wrap(err, "unable to validate certificate chain")
}
certSigner.Chain = certChainBytes

return certSigner, nil
}

Expand Down Expand Up @@ -418,13 +452,13 @@ func keylessSigner(ctx context.Context, ko KeyOpts) (*SignerVerifier, error) {
}, nil
}

func SignerFromKeyOpts(ctx context.Context, certPath string, ko KeyOpts) (*SignerVerifier, error) {
func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko KeyOpts) (*SignerVerifier, error) {
if ko.Sk {
return signerFromSecurityKey(ko.Slot)
}

if ko.KeyRef != "" {
return signerFromKeyRef(ctx, certPath, ko.KeyRef, ko.PassFunc)
return signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc)
}

// Default Keyless!
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/sign/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func SignBlobCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOp
ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout)
defer cancel()

sv, err := SignerFromKeyOpts(ctx, "", ko)
sv, err := SignerFromKeyOpts(ctx, "", "", ko)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit db90d13

Please sign in to comment.