Skip to content

Commit

Permalink
Add --ca-intermediates flag
Browse files Browse the repository at this point in the history
Add --ca-intermediates flag to enable to pass a PEM file
with intermediate CA certificates.
One can use either --ca-roots, optionally together with
--ca-intermediates - or --certificate-chain, which contains
zero, one or several intermediate CA certificate followed
by the root CA certificate.

Expand the helper Go program test/gencert/main.go to
allow to generate root and intermediate CA certificates,
and a certificate signed by the intermediate CA.
Expand the functional test e2e_tsa_certbundle.sh
to test the --ca-intermediates flag (together with --ca-roots).

Fixed sigstore#3462.

Signed-off-by: Dmitry S <dsavints@gmail.com>
  • Loading branch information
dmitris committed Feb 1, 2024
1 parent 072195d commit 2583a55
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 68 deletions.
8 changes: 7 additions & 1 deletion cmd/cosign/cli/options/certificate.go
Expand Up @@ -33,6 +33,7 @@ type CertVerifyOptions struct {
CertGithubWorkflowName string
CertGithubWorkflowRepository string
CertGithubWorkflowRef string
CAIntermediates string
CARoots string
CertChain string
SCT string
Expand Down Expand Up @@ -76,6 +77,10 @@ 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.CAIntermediates, "ca-intermediates", "",
"path to a file of intermediate 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("ca-intermediates", cobra.BashCompFilenameExt, []string{"cert"})
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.")
Expand All @@ -85,9 +90,10 @@ 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 --ca-roots.")
"signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.")
_ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"})
cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain")
cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain")

cmd.Flags().StringVar(&o.SCT, "sct", "",
"path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+
Expand Down
5 changes: 5 additions & 0 deletions cmd/cosign/cli/verify.go
Expand Up @@ -62,6 +62,10 @@ against the transparency log.`,
# verify image with local certificate and certificate chain
cosign verify --cert cosign.crt --cert-chain chain.crt <IMAGE>
# verify image with local certificate and certificate bundles of CA roots
# and (optionally) CA intermediates
cosign verify --cert cosign.crt --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem <IMAGE>
# verify image using keyless verification with the given certificate
# chain and identity parameters, without Fulcio roots (for BYO PKI):
cosign verify --cert-chain chain.crt --certificate-oidc-issuer https://issuer.example.com --certificate-identity foo@example.com <IMAGE>
Expand Down Expand Up @@ -115,6 +119,7 @@ against the transparency log.`,
CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName,
CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository,
CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef,
CAIntermediates: o.CertVerify.CAIntermediates,
CARoots: o.CertVerify.CARoots,
CertChain: o.CertVerify.CertChain,
IgnoreSCT: o.CertVerify.IgnoreSCT,
Expand Down
13 changes: 13 additions & 0 deletions cmd/cosign/cli/verify/verify.go
Expand Up @@ -60,6 +60,7 @@ type VerifyCommand struct {
CertGithubWorkflowName string
CertGithubWorkflowRepository string
CertGithubWorkflowRef string
CAIntermediates string
CARoots string
CertChain string
CertOidcProvider string
Expand Down Expand Up @@ -206,6 +207,18 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
co.RootCerts.AddCert(cert)
}
}
if c.CAIntermediates != "" {
caIntermediates, err := loadCertChainFromFileOrURL(c.CAIntermediates)
if err != nil {
return err
}
if len(caIntermediates) > 0 {
co.IntermediateCerts = x509.NewCertPool()
for _, cert := range caIntermediates {
co.IntermediateCerts.AddCert(cert)
}
}
}
}
default:
{
Expand Down
2 changes: 1 addition & 1 deletion pkg/cosign/verify.go
Expand Up @@ -433,7 +433,7 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif
}

// ValidateAndUnpackCertWithCertPools creates a Verifier from a certificate. Verifies that the certificate
// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates
// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates.
// Optionally verifies the subject and issuer of the certificate.
func ValidateAndUnpackCertWithCertPools(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) {
if co.RootCerts == nil {
Expand Down
63 changes: 19 additions & 44 deletions test/e2e_tsa_certbundle.sh
Expand Up @@ -27,40 +27,11 @@ set -exuo pipefail

CERT_BASE="test/testdata"

# the certificates listed below are generated with the `gen-tsa-mtls-certs.sh` script.
TIMESTAMP_CACERT=$CERT_BASE/tsa-mtls-ca.crt
TIMESTAMP_CLIENT_CERT=$CERT_BASE/tsa-mtls-client.crt
TIMESTAMP_CLIENT_KEY=$CERT_BASE/tsa-mtls-client.key
TIMESTAMP_SERVER_CERT=$CERT_BASE/tsa-mtls-server.crt
TIMESTAMP_SERVER_KEY=$CERT_BASE/tsa-mtls-server.key
TIMESTAMP_SERVER_NAME="server.example.com"
TIMESTAMP_SERVER_URL=https://localhost:3000/api/v1/timestamp
export TIMESTAMP_SERVER_URL=https://freetsa.org/tsr
TIMESTAMP_CHAIN_FILE="timestamp-chain"

set +e
curl -s https://www.freetsa.org/files/cacert.pem > $TIMESTAMP_CHAIN_FILE
echo "TIMESTAMP_CHAIN_FILE: $(ls -l $TIMESTAMP_CHAIN_FILE)"
COSIGN_CLI=./cosign
command -v timestamp-server >& /dev/null
exit_code=$?
set -e
if [[ $exit_code != 0 ]]; then
rm -fr /tmp/timestamp-authority
git clone https://github.com/sigstore/timestamp-authority /tmp/timestamp-authority
pushd /tmp/timestamp-authority
make
export PATH="/tmp/timestamp-authority/bin:$PATH"
popd
fi

timestamp-server serve --disable-ntp-monitoring --tls-host 0.0.0.0 --tls-port 3000 \
--scheme https --tls-ca $TIMESTAMP_CACERT --tls-key $TIMESTAMP_SERVER_KEY \
--tls-certificate $TIMESTAMP_SERVER_CERT &

sleep 1
curl -k -s --key test/testdata/tsa-mtls-client.key \
--cert test/testdata/tsa-mtls-client.crt \
--cacert test/testdata/tsa-mtls-ca.crt https://localhost:3000/api/v1/timestamp/certchain \
> $TIMESTAMP_CHAIN_FILE
echo "DONE: $(ls -l $TIMESTAMP_CHAIN_FILE)"

# unlike e2e_tsa_mtls.sh, there is no option to pass an image as a command-line parameter.

Expand All @@ -76,36 +47,40 @@ done

echo "IMG01: $IMG01, IMG02: $IMG02, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL"

rm -f *.pem import-cosign.* key.pem

# use gencert to generate two CAs (for testing certificate bundle feature),
# keys and certificates
echo "generate CAs, keys and certificates with gencert"

passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z')
rm -f *.pem import-cosign.*
for i in 01 02; do
go run test/gencert/main.go && mv cacert.pem cacert${i}.pem && mv ca-key.pem ca-key${i}.pem && mv cert.pem cert${i}.pem && mv key.pem key${i}.pem
COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key key${i}.pem --output-key-prefix import-cosign${i}
go run test/gencert/main.go -output-suffix "$i" -intermediate
COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key ca-intermediate-key${i}.pem --output-key-prefix import-cosign${i}
IMG="IMG${i}"
cat ca-intermediate${i}.pem ca-root${i}.pem > certchain${i}.pem
COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \
--timestamp-client-cacert ${TIMESTAMP_CACERT} --timestamp-client-cert ${TIMESTAMP_CLIENT_CERT} \
--timestamp-client-key ${TIMESTAMP_CLIENT_KEY} --timestamp-server-name ${TIMESTAMP_SERVER_NAME}\
--upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain cacert${i}.pem --cert cert${i}.pem "${!IMG}"
--upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain certchain${i}.pem --cert cert${i}.pem "${!IMG}"
# key is now longer needed
rm -f key${i}.pem import-cosign${i}.*
done

# create a certificate bundle - concatenate both generated CA certificates
cat cacert01.pem cacert02.pem > ca-roots.pem
ls -l *.pem
cat ca-root01.pem ca-root02.pem > ca-roots.pem
cat ca-intermediate01.pem ca-intermediate02.pem > ca-intermediates.pem

echo "cosign verify:"
for i in 01 02; do
IMG="IMG${i}"
# first try with --certificate-chain parameter
$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \
--certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \
--certificate-chain certchain${i}.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"

# then do the same but now with --ca-roots and --ca-intermediates parameters
$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \
--certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \
--ca-roots ca-roots.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"
--ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"
done

# cleanup
rm -fr ca-key*.pem ca-roots.pem cacert*.pem cert*.pem timestamp-chain /tmp/timestamp-authority
pkill -f 'timestamp-server'
rm -fr *.pem timestamp-chain
124 changes: 102 additions & 22 deletions test/gencert/main.go
Expand Up @@ -15,6 +15,9 @@
// code is based on the snippet https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251
// with a permissive (Public Domain) License.

// TODO(dmitris) - refactor to move the code generating certificates and writing to files
// to a separate function.

package main

import (
Expand All @@ -23,6 +26,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"log"
"math/big"
"net"
Expand All @@ -31,16 +35,21 @@ import (
)

func main() {
var genIntermediate = flag.Bool("intermediate", false, "generate intermediate CA")
var outputSuffix = flag.String("output-suffix", "", "suffix to append to generated files before the extension")
flag.Parse()

// set up our CA certificate
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"CA Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
Organization: []string{"CA Company, INC."},
OrganizationalUnit: []string{"CA Root Team"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
Expand All @@ -64,19 +73,20 @@ func main() {
}

// pem encode
caCertFile, err := os.OpenFile("cacert.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
fname := "ca-root" + *outputSuffix + ".pem"
caCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatalf("unable to write to cacert.pem: %v", err)
log.Fatalf("unable to write to %s: %v", fname, err)
}
err = pem.Encode(caCertFile, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
if err != nil {
log.Fatalf("unable to write to cacert.pem: %v", err)
log.Fatalf("unable to write to %s: %v", fname, err)
}

caPrivKeyFile, err := os.OpenFile("ca-key.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
fname = "ca-key" + *outputSuffix + ".pem"
caPrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
if err != nil {
log.Fatal(err)
}
Expand All @@ -86,19 +96,82 @@ func main() {
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
})
if err != nil {
log.Fatalf("unable to create to ca-key.pem: %v", err) //nolint:gocritic
log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic
}

// generate intermediate CA if requested
var caIntermediate *x509.Certificate
var caIntermediatePrivKey *rsa.PrivateKey
if *genIntermediate {
caIntermediate = &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"CA Company, INC."},
OrganizationalUnit: []string{"CA Intermediate Team"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
EmailAddresses: []string{"ca@example.com"},
}
// create our private and public key
caIntermediatePrivKey, err = rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
log.Fatal(err)
}

// create the Intermediate CA
caIntermediateBytes, err := x509.CreateCertificate(rand.Reader, caIntermediate, ca, &caIntermediatePrivKey.PublicKey, caPrivKey)
if err != nil {
log.Fatal(err)
}

// pem encode
fname = "ca-intermediate" + *outputSuffix + ".pem"
caIntermediateCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatalf("unable to write to %s: %v", fname, err)
}
err = pem.Encode(caIntermediateCertFile, &pem.Block{
Type: "CERTIFICATE",
Bytes: caIntermediateBytes,
})
if err != nil {
log.Fatalf("unable to write to %s: %v", fname, err)
}
fname = "ca-intermediate-key" + *outputSuffix + ".pem"
caIntermediatePrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
if err != nil {
log.Fatal(err)
}
defer caIntermediatePrivKeyFile.Close()
err = pem.Encode(caIntermediatePrivKeyFile, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caIntermediatePrivKey),
})
if err != nil {
log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic
}
}
// set up our server certificate
cert := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
Organization: []string{"End User"},
OrganizationalUnit: []string{"End Node Team"},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
NotBefore: time.Now(),
Expand All @@ -115,12 +188,18 @@ func main() {
log.Fatal(err)
}

certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
var certBytes []byte
if !*genIntermediate {
certBytes, err = x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
} else {
certBytes, err = x509.CreateCertificate(rand.Reader, cert, caIntermediate, &caIntermediatePrivKey.PublicKey, caIntermediatePrivKey)
}
if err != nil {
log.Fatal(err)
}

certFile, err := os.OpenFile("cert.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
fname = "cert" + *outputSuffix + ".pem"
certFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatal(err)
}
Expand All @@ -133,7 +212,8 @@ func main() {
log.Fatalf("failed to encode cert: %v", err)
}

keyFile, err := os.OpenFile("key.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
fname = "key" + *outputSuffix + ".pem"
keyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
if err != nil {
log.Fatal(err)
}
Expand All @@ -143,6 +223,6 @@ func main() {
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
if err != nil {
log.Fatalf("failed to encode private key: %v", err)
log.Fatalf("failed to encode private key to %s: %v", fname, err)
}
}

0 comments on commit 2583a55

Please sign in to comment.