Skip to content

Commit

Permalink
Add --ca-roots flag for 'cosign verify'
Browse files Browse the repository at this point in the history
Add --ca-roots command-line flag for 'cosign verify'
to enable verifying cosign signatures using PEM bundles
of CA roots. Whether to also add --ca-intermediates flag
is TBD.  Unit tests will be added in the next commit(s).

Fixes #3462.

Signed-off-by: Dmitry S <dsavints@gmail.com>
  • Loading branch information
dmitris committed Jan 9, 2024
1 parent 88a2079 commit b6c572c
Show file tree
Hide file tree
Showing 20 changed files with 285 additions and 186 deletions.
23 changes: 20 additions & 3 deletions cmd/cosign/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,26 @@ func attachSignature() *cobra.Command {
o := &options.AttachSignatureOptions{}

cmd := &cobra.Command{
Use: "signature",
Short: "Attach signatures to the supplied container image",
Example: " cosign attach signature <image uri>",
Use: "signature",
Short: "Attach signatures to the supplied container image",
Example: ` cosign attach signature [--payload <path>] [--signature < path>] [--rekor-response < path>] <image uri>
cosign attach signature command attaches payload, signature, rekor-bundle, etc in a new layer of provided image.
# Attach signature can attach payload to a supplied image
cosign attach signature --payload <payload.json> $IMAGE
# Attach signature can attach payload, signature to a supplied image
cosign attach signature --payload <payload.json> --signature <base64 signature file> $IMAGE
# Attach signature can attach payload, signature, time stamped response to a supplied image
cosign attach signature --payload <payload.json> --signature <base64 signature file> --tsr=<file> $IMAGE
# Attach signature attaches payload, signature and rekor-bundle via rekor-response to a supplied image
cosign attach signature --payload <payload.json> --signature <base64 signature file> --rekor-response <proper rekor-response format file> $IMAGE
# Attach signature attaches payload, signature and rekor-bundle directly to a supplied image
cosign attach signature --payload <payload.json> --signature <base64 signature file> --rekor-response <rekor-bundle file> $IMAGE`,
PersistentPreRun: options.BindViper,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
26 changes: 23 additions & 3 deletions cmd/cosign/cli/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,24 @@ func CleanCmd(ctx context.Context, regOpts options.RegistryOptions, cleanType op
for _, t := range cleanTags {
if err := remote.Delete(t, remoteOpts...); err != nil {
var te *transport.Error
if errors.As(err, &te) && te.StatusCode == http.StatusNotFound { //nolint: revive
switch {
case errors.As(err, &te) && te.StatusCode == http.StatusNotFound:
// If the tag doesn't exist, some registries may
// respond with a 404, which shouldn't be considered an
// error.
} else {
fmt.Fprintf(os.Stderr, "could not delete %s from %s\n: %v\n", t, imageRef, err)
case errors.As(err, &te) && te.StatusCode == http.StatusBadRequest:
// Docker registry >=v2.3 requires does not allow deleting the OCI object name directly, must use the digest instead.
// See https://github.com/distribution/distribution/blob/main/docs/content/spec/api.md#deleting-an-image
if err := deleteByDigest(t, remoteOpts...); err != nil {
if errors.As(err, &te) && te.StatusCode == http.StatusNotFound { //nolint: revive
} else {
fmt.Fprintf(os.Stderr, "could not delete %s by digest from %s:\n%v\n", t, imageRef, err)
}
} else {
fmt.Fprintf(os.Stderr, "Removed %s from %s\n", t, imageRef)
}
default:
fmt.Fprintf(os.Stderr, "could not delete %s from %s:\n%v\n", t, imageRef, err)
}
} else {
fmt.Fprintf(os.Stderr, "Removed %s from %s\n", t, imageRef)
Expand All @@ -111,6 +123,14 @@ func CleanCmd(ctx context.Context, regOpts options.RegistryOptions, cleanType op
return nil
}

func deleteByDigest(tag name.Tag, opts ...remote.Option) error {
digestTag, err := ociremote.DockerContentDigest(tag, ociremote.WithRemoteOptions(opts...))
if err != nil {
return err
}
return remote.Delete(digestTag, opts...)
}

func prompt(cleanType options.CleanType) string {
switch cleanType {
case options.CleanTypeSignature:
Expand Down
8 changes: 4 additions & 4 deletions cmd/cosign/cli/options/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type CertVerifyOptions struct {
CertGithubWorkflowName string
CertGithubWorkflowRepository string
CertGithubWorkflowRef string
CertBundle string
CARoots string
CertChain string
SCT string
IgnoreSCT bool
Expand Down Expand Up @@ -76,7 +76,7 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.CertGithubWorkflowRef, "certificate-github-workflow-ref", "",
"contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon.")
// -- Cert extensions end --
cmd.Flags().StringVar(&o.CertBundle, "certificate-bundle", "",
cmd.Flags().StringVar(&o.CARoots, "ca-roots", "",
"path to a bundle file of CA certificates in PEM format which will be needed "+
"when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.")
_ = cmd.Flags().SetAnnotation("certificate-bundle", cobra.BashCompFilenameExt, []string{"cert"})
Expand All @@ -85,9 +85,9 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
"path to a list of CA 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. Conflicts with --certificate-bundle.")
"signing certificate and end with the root certificate. Conflicts with --ca-roots.")
_ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"})
cmd.MarkFlagsMutuallyExclusive("certificate-bundle", "certificate-chain")
cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain")

cmd.Flags().StringVar(&o.SCT, "sct", "",
"path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ against the transparency log.`,
CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName,
CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository,
CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef,
CertBundle: o.CertVerify.CertBundle,
CARoots: o.CertVerify.CARoots,
CertChain: o.CertVerify.CertChain,
IgnoreSCT: o.CertVerify.IgnoreSCT,
SCTRef: o.CertVerify.SCT,
Expand Down
99 changes: 66 additions & 33 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type VerifyCommand struct {
CertGithubWorkflowName string
CertGithubWorkflowRepository string
CertGithubWorkflowRef string
CertBundle string
CARoots string
CertChain string
CertOidcProvider string
IgnoreSCT bool
Expand Down Expand Up @@ -178,29 +178,47 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
}
}
if keylessVerification(c.KeyRef, c.Sk) {
if c.CertChain != "" {
chain, err := loadCertChainFromFileOrURL(c.CertChain)
if err != nil {
return err
}
co.RootCerts = x509.NewCertPool()
co.RootCerts.AddCert(chain[len(chain)-1])
if len(chain) > 1 {
co.IntermediateCerts = x509.NewCertPool()
for _, cert := range chain[:len(chain)-1] {
co.IntermediateCerts.AddCert(cert)
switch {
case c.CertChain != "":
{
chain, err := loadCertChainFromFileOrURL(c.CertChain)
if err != nil {
return err
}
co.RootCerts = x509.NewCertPool()
co.RootCerts.AddCert(chain[len(chain)-1])
if len(chain) > 1 {
co.IntermediateCerts = x509.NewCertPool()
for _, cert := range chain[:len(chain)-1] {
co.IntermediateCerts.AddCert(cert)
}
}
}
} else {
// This performs an online fetch of the Fulcio roots. This is needed
// for verifying keyless certificates (both online and offline).
co.RootCerts, err = fulcio.GetRoots()
if err != nil {
return fmt.Errorf("getting Fulcio roots: %w", err)
case c.CARoots != "":
{
caRoots, err := loadCertChainFromFileOrURL(c.CARoots)
if err != nil {
return err
}
co.RootCerts = x509.NewCertPool()
if len(caRoots) > 0 {
for _, cert := range caRoots {
co.RootCerts.AddCert(cert)
}
}
}
co.IntermediateCerts, err = fulcio.GetIntermediates()
if err != nil {
return fmt.Errorf("getting Fulcio intermediates: %w", err)
default:
{
// This performs an online fetch of the Fulcio roots. This is needed
// for verifying keyless certificates (both online and offline).
co.RootCerts, err = fulcio.GetRoots()
if err != nil {
return fmt.Errorf("getting Fulcio roots: %w", err)
}
co.IntermediateCerts, err = fulcio.GetIntermediates()
if err != nil {
return fmt.Errorf("getting Fulcio intermediates: %w", err)
}
}
}
}
Expand Down Expand Up @@ -242,8 +260,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
if err != nil {
return err
}
if c.CertChain == "" {
// If no certChain is passed, the Fulcio root certificate will be used
if c.CertChain == "" && c.CARoots == "" {
// If no certChain and no CARoots are passed, the Fulcio root certificate will be used
co.RootCerts, err = fulcio.GetRoots()
if err != nil {
return fmt.Errorf("getting Fulcio roots: %w", err)
Expand All @@ -257,14 +275,21 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
return err
}
} else {
// Verify certificate with chain
chain, err := loadCertChainFromFileOrURL(c.CertChain)
if err != nil {
return err
}
pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
if err != nil {
return err
if c.CARoots == "" {
// Verify certificate with chain
chain, err := loadCertChainFromFileOrURL(c.CertChain)
if err != nil {
return err
}
pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
if err != nil {
return err
}
} else {
pubKey, err = cosign.ValidateAndUnpackCertWithCertPools(cert, co)
if err != nil {
return err
}
}
}
if c.SCTRef != "" {
Expand Down Expand Up @@ -346,7 +371,11 @@ func PrintVerification(ctx context.Context, verified []oci.Signature, output str
for _, sig := range verified {
if cert, err := sig.Cert(); err == nil && cert != nil {
ce := cosign.CertExtensions{Cert: cert}
ui.Infof(ctx, "Certificate subject: %s", sigs.CertSubject(cert))
sub := ""
if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 {
sub = sans[0]
}
ui.Infof(ctx, "Certificate subject: %s", sub)
if issuerURL := ce.GetIssuer(); issuerURL != "" {
ui.Infof(ctx, "Certificate issuer URL: %s", issuerURL)
}
Expand Down Expand Up @@ -399,7 +428,11 @@ func PrintVerification(ctx context.Context, verified []oci.Signature, output str
if ss.Optional == nil {
ss.Optional = make(map[string]interface{})
}
ss.Optional["Subject"] = sigs.CertSubject(cert)
sub := ""
if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 {
sub = sans[0]
}
ss.Optional["Subject"] = sub
if issuerURL := ce.GetIssuer(); issuerURL != "" {
ss.Optional["Issuer"] = issuerURL
ss.Optional[cosign.CertExtensionOIDCIssuer] = issuerURL
Expand Down
19 changes: 18 additions & 1 deletion doc/cosign_attach_signature.md

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

4 changes: 2 additions & 2 deletions doc/cosign_dockerfile_verify.md

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

4 changes: 2 additions & 2 deletions doc/cosign_manifest_verify.md

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

Loading

0 comments on commit b6c572c

Please sign in to comment.