Skip to content

Commit 2583a55

Browse files
committed
Add --ca-intermediates flag
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>
1 parent 072195d commit 2583a55

File tree

6 files changed

+147
-68
lines changed

6 files changed

+147
-68
lines changed

cmd/cosign/cli/options/certificate.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type CertVerifyOptions struct {
3333
CertGithubWorkflowName string
3434
CertGithubWorkflowRepository string
3535
CertGithubWorkflowRef string
36+
CAIntermediates string
3637
CARoots string
3738
CertChain string
3839
SCT string
@@ -76,6 +77,10 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
7677
cmd.Flags().StringVar(&o.CertGithubWorkflowRef, "certificate-github-workflow-ref", "",
7778
"contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon.")
7879
// -- Cert extensions end --
80+
cmd.Flags().StringVar(&o.CAIntermediates, "ca-intermediates", "",
81+
"path to a file of intermediate CA certificates in PEM format which will be needed "+
82+
"when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.")
83+
_ = cmd.Flags().SetAnnotation("ca-intermediates", cobra.BashCompFilenameExt, []string{"cert"})
7984
cmd.Flags().StringVar(&o.CARoots, "ca-roots", "",
8085
"path to a bundle file of CA certificates in PEM format which will be needed "+
8186
"when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.")
@@ -85,9 +90,10 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
8590
"path to a list of CA certificates in PEM format which will be needed "+
8691
"when building the certificate chain for the signing certificate. "+
8792
"Must start with the parent intermediate CA certificate of the "+
88-
"signing certificate and end with the root certificate. Conflicts with --ca-roots.")
93+
"signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.")
8994
_ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"})
9095
cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain")
96+
cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain")
9197

9298
cmd.Flags().StringVar(&o.SCT, "sct", "",
9399
"path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+

cmd/cosign/cli/verify.go

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ against the transparency log.`,
6262
# verify image with local certificate and certificate chain
6363
cosign verify --cert cosign.crt --cert-chain chain.crt <IMAGE>
6464
65+
# verify image with local certificate and certificate bundles of CA roots
66+
# and (optionally) CA intermediates
67+
cosign verify --cert cosign.crt --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem <IMAGE>
68+
6569
# verify image using keyless verification with the given certificate
6670
# chain and identity parameters, without Fulcio roots (for BYO PKI):
6771
cosign verify --cert-chain chain.crt --certificate-oidc-issuer https://issuer.example.com --certificate-identity foo@example.com <IMAGE>
@@ -115,6 +119,7 @@ against the transparency log.`,
115119
CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName,
116120
CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository,
117121
CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef,
122+
CAIntermediates: o.CertVerify.CAIntermediates,
118123
CARoots: o.CertVerify.CARoots,
119124
CertChain: o.CertVerify.CertChain,
120125
IgnoreSCT: o.CertVerify.IgnoreSCT,

cmd/cosign/cli/verify/verify.go

+13
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type VerifyCommand struct {
6060
CertGithubWorkflowName string
6161
CertGithubWorkflowRepository string
6262
CertGithubWorkflowRef string
63+
CAIntermediates string
6364
CARoots string
6465
CertChain string
6566
CertOidcProvider string
@@ -206,6 +207,18 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
206207
co.RootCerts.AddCert(cert)
207208
}
208209
}
210+
if c.CAIntermediates != "" {
211+
caIntermediates, err := loadCertChainFromFileOrURL(c.CAIntermediates)
212+
if err != nil {
213+
return err
214+
}
215+
if len(caIntermediates) > 0 {
216+
co.IntermediateCerts = x509.NewCertPool()
217+
for _, cert := range caIntermediates {
218+
co.IntermediateCerts.AddCert(cert)
219+
}
220+
}
221+
}
209222
}
210223
default:
211224
{

pkg/cosign/verify.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif
433433
}
434434

435435
// ValidateAndUnpackCertWithCertPools creates a Verifier from a certificate. Verifies that the certificate
436-
// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates
436+
// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates.
437437
// Optionally verifies the subject and issuer of the certificate.
438438
func ValidateAndUnpackCertWithCertPools(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) {
439439
if co.RootCerts == nil {

test/e2e_tsa_certbundle.sh

+19-44
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,11 @@ set -exuo pipefail
2727

2828
CERT_BASE="test/testdata"
2929

30-
# the certificates listed below are generated with the `gen-tsa-mtls-certs.sh` script.
31-
TIMESTAMP_CACERT=$CERT_BASE/tsa-mtls-ca.crt
32-
TIMESTAMP_CLIENT_CERT=$CERT_BASE/tsa-mtls-client.crt
33-
TIMESTAMP_CLIENT_KEY=$CERT_BASE/tsa-mtls-client.key
34-
TIMESTAMP_SERVER_CERT=$CERT_BASE/tsa-mtls-server.crt
35-
TIMESTAMP_SERVER_KEY=$CERT_BASE/tsa-mtls-server.key
36-
TIMESTAMP_SERVER_NAME="server.example.com"
37-
TIMESTAMP_SERVER_URL=https://localhost:3000/api/v1/timestamp
30+
export TIMESTAMP_SERVER_URL=https://freetsa.org/tsr
3831
TIMESTAMP_CHAIN_FILE="timestamp-chain"
39-
40-
set +e
32+
curl -s https://www.freetsa.org/files/cacert.pem > $TIMESTAMP_CHAIN_FILE
33+
echo "TIMESTAMP_CHAIN_FILE: $(ls -l $TIMESTAMP_CHAIN_FILE)"
4134
COSIGN_CLI=./cosign
42-
command -v timestamp-server >& /dev/null
43-
exit_code=$?
44-
set -e
45-
if [[ $exit_code != 0 ]]; then
46-
rm -fr /tmp/timestamp-authority
47-
git clone https://github.com/sigstore/timestamp-authority /tmp/timestamp-authority
48-
pushd /tmp/timestamp-authority
49-
make
50-
export PATH="/tmp/timestamp-authority/bin:$PATH"
51-
popd
52-
fi
53-
54-
timestamp-server serve --disable-ntp-monitoring --tls-host 0.0.0.0 --tls-port 3000 \
55-
--scheme https --tls-ca $TIMESTAMP_CACERT --tls-key $TIMESTAMP_SERVER_KEY \
56-
--tls-certificate $TIMESTAMP_SERVER_CERT &
57-
58-
sleep 1
59-
curl -k -s --key test/testdata/tsa-mtls-client.key \
60-
--cert test/testdata/tsa-mtls-client.crt \
61-
--cacert test/testdata/tsa-mtls-ca.crt https://localhost:3000/api/v1/timestamp/certchain \
62-
> $TIMESTAMP_CHAIN_FILE
63-
echo "DONE: $(ls -l $TIMESTAMP_CHAIN_FILE)"
6435

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

@@ -76,36 +47,40 @@ done
7647

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

79-
rm -f *.pem import-cosign.* key.pem
80-
8150
# use gencert to generate two CAs (for testing certificate bundle feature),
8251
# keys and certificates
8352
echo "generate CAs, keys and certificates with gencert"
84-
8553
passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z')
8654
rm -f *.pem import-cosign.*
8755
for i in 01 02; do
88-
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
89-
COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key key${i}.pem --output-key-prefix import-cosign${i}
56+
go run test/gencert/main.go -output-suffix "$i" -intermediate
57+
COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key ca-intermediate-key${i}.pem --output-key-prefix import-cosign${i}
9058
IMG="IMG${i}"
59+
cat ca-intermediate${i}.pem ca-root${i}.pem > certchain${i}.pem
9160
COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \
92-
--timestamp-client-cacert ${TIMESTAMP_CACERT} --timestamp-client-cert ${TIMESTAMP_CLIENT_CERT} \
93-
--timestamp-client-key ${TIMESTAMP_CLIENT_KEY} --timestamp-server-name ${TIMESTAMP_SERVER_NAME}\
94-
--upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain cacert${i}.pem --cert cert${i}.pem "${!IMG}"
61+
--upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain certchain${i}.pem --cert cert${i}.pem "${!IMG}"
9562
# key is now longer needed
9663
rm -f key${i}.pem import-cosign${i}.*
9764
done
65+
9866
# create a certificate bundle - concatenate both generated CA certificates
99-
cat cacert01.pem cacert02.pem > ca-roots.pem
67+
ls -l *.pem
68+
cat ca-root01.pem ca-root02.pem > ca-roots.pem
69+
cat ca-intermediate01.pem ca-intermediate02.pem > ca-intermediates.pem
10070

10171
echo "cosign verify:"
10272
for i in 01 02; do
10373
IMG="IMG${i}"
74+
# first try with --certificate-chain parameter
75+
$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \
76+
--certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \
77+
--certificate-chain certchain${i}.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"
78+
79+
# then do the same but now with --ca-roots and --ca-intermediates parameters
10480
$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \
10581
--certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \
106-
--ca-roots ca-roots.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"
82+
--ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}"
10783
done
10884

10985
# cleanup
110-
rm -fr ca-key*.pem ca-roots.pem cacert*.pem cert*.pem timestamp-chain /tmp/timestamp-authority
111-
pkill -f 'timestamp-server'
86+
rm -fr *.pem timestamp-chain

test/gencert/main.go

+102-22
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
// code is based on the snippet https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251
1616
// with a permissive (Public Domain) License.
1717

18+
// TODO(dmitris) - refactor to move the code generating certificates and writing to files
19+
// to a separate function.
20+
1821
package main
1922

2023
import (
@@ -23,6 +26,7 @@ import (
2326
"crypto/x509"
2427
"crypto/x509/pkix"
2528
"encoding/pem"
29+
"flag"
2630
"log"
2731
"math/big"
2832
"net"
@@ -31,16 +35,21 @@ import (
3135
)
3236

3337
func main() {
38+
var genIntermediate = flag.Bool("intermediate", false, "generate intermediate CA")
39+
var outputSuffix = flag.String("output-suffix", "", "suffix to append to generated files before the extension")
40+
flag.Parse()
41+
3442
// set up our CA certificate
3543
ca := &x509.Certificate{
3644
SerialNumber: big.NewInt(2019),
3745
Subject: pkix.Name{
38-
Organization: []string{"CA Company, INC."},
39-
Country: []string{"US"},
40-
Province: []string{""},
41-
Locality: []string{"San Francisco"},
42-
StreetAddress: []string{"Golden Gate Bridge"},
43-
PostalCode: []string{"94016"},
46+
Organization: []string{"CA Company, INC."},
47+
OrganizationalUnit: []string{"CA Root Team"},
48+
Country: []string{"US"},
49+
Province: []string{""},
50+
Locality: []string{"San Francisco"},
51+
StreetAddress: []string{"Golden Gate Bridge"},
52+
PostalCode: []string{"94016"},
4453
},
4554
NotBefore: time.Now(),
4655
NotAfter: time.Now().AddDate(10, 0, 0),
@@ -64,19 +73,20 @@ func main() {
6473
}
6574

6675
// pem encode
67-
caCertFile, err := os.OpenFile("cacert.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
76+
fname := "ca-root" + *outputSuffix + ".pem"
77+
caCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
6878
if err != nil {
69-
log.Fatalf("unable to write to cacert.pem: %v", err)
79+
log.Fatalf("unable to write to %s: %v", fname, err)
7080
}
7181
err = pem.Encode(caCertFile, &pem.Block{
7282
Type: "CERTIFICATE",
7383
Bytes: caBytes,
7484
})
7585
if err != nil {
76-
log.Fatalf("unable to write to cacert.pem: %v", err)
86+
log.Fatalf("unable to write to %s: %v", fname, err)
7787
}
78-
79-
caPrivKeyFile, err := os.OpenFile("ca-key.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
88+
fname = "ca-key" + *outputSuffix + ".pem"
89+
caPrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
8090
if err != nil {
8191
log.Fatal(err)
8292
}
@@ -86,19 +96,82 @@ func main() {
8696
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
8797
})
8898
if err != nil {
89-
log.Fatalf("unable to create to ca-key.pem: %v", err) //nolint:gocritic
99+
log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic
90100
}
91101

102+
// generate intermediate CA if requested
103+
var caIntermediate *x509.Certificate
104+
var caIntermediatePrivKey *rsa.PrivateKey
105+
if *genIntermediate {
106+
caIntermediate = &x509.Certificate{
107+
SerialNumber: big.NewInt(2019),
108+
Subject: pkix.Name{
109+
Organization: []string{"CA Company, INC."},
110+
OrganizationalUnit: []string{"CA Intermediate Team"},
111+
Country: []string{"US"},
112+
Province: []string{""},
113+
Locality: []string{"San Francisco"},
114+
StreetAddress: []string{"Golden Gate Bridge"},
115+
PostalCode: []string{"94016"},
116+
},
117+
NotBefore: time.Now(),
118+
NotAfter: time.Now().AddDate(10, 0, 0),
119+
IsCA: true,
120+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */},
121+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
122+
BasicConstraintsValid: true,
123+
EmailAddresses: []string{"ca@example.com"},
124+
}
125+
// create our private and public key
126+
caIntermediatePrivKey, err = rsa.GenerateKey(rand.Reader, 4096)
127+
if err != nil {
128+
log.Fatal(err)
129+
}
130+
131+
// create the Intermediate CA
132+
caIntermediateBytes, err := x509.CreateCertificate(rand.Reader, caIntermediate, ca, &caIntermediatePrivKey.PublicKey, caPrivKey)
133+
if err != nil {
134+
log.Fatal(err)
135+
}
136+
137+
// pem encode
138+
fname = "ca-intermediate" + *outputSuffix + ".pem"
139+
caIntermediateCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
140+
if err != nil {
141+
log.Fatalf("unable to write to %s: %v", fname, err)
142+
}
143+
err = pem.Encode(caIntermediateCertFile, &pem.Block{
144+
Type: "CERTIFICATE",
145+
Bytes: caIntermediateBytes,
146+
})
147+
if err != nil {
148+
log.Fatalf("unable to write to %s: %v", fname, err)
149+
}
150+
fname = "ca-intermediate-key" + *outputSuffix + ".pem"
151+
caIntermediatePrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
152+
if err != nil {
153+
log.Fatal(err)
154+
}
155+
defer caIntermediatePrivKeyFile.Close()
156+
err = pem.Encode(caIntermediatePrivKeyFile, &pem.Block{
157+
Type: "RSA PRIVATE KEY",
158+
Bytes: x509.MarshalPKCS1PrivateKey(caIntermediatePrivKey),
159+
})
160+
if err != nil {
161+
log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic
162+
}
163+
}
92164
// set up our server certificate
93165
cert := &x509.Certificate{
94166
SerialNumber: big.NewInt(2019),
95167
Subject: pkix.Name{
96-
Organization: []string{"Company, INC."},
97-
Country: []string{"US"},
98-
Province: []string{""},
99-
Locality: []string{"San Francisco"},
100-
StreetAddress: []string{"Golden Gate Bridge"},
101-
PostalCode: []string{"94016"},
168+
Organization: []string{"End User"},
169+
OrganizationalUnit: []string{"End Node Team"},
170+
Country: []string{"US"},
171+
Province: []string{""},
172+
Locality: []string{"San Francisco"},
173+
StreetAddress: []string{"Golden Gate Bridge"},
174+
PostalCode: []string{"94016"},
102175
},
103176
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
104177
NotBefore: time.Now(),
@@ -115,12 +188,18 @@ func main() {
115188
log.Fatal(err)
116189
}
117190

118-
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
191+
var certBytes []byte
192+
if !*genIntermediate {
193+
certBytes, err = x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
194+
} else {
195+
certBytes, err = x509.CreateCertificate(rand.Reader, cert, caIntermediate, &caIntermediatePrivKey.PublicKey, caIntermediatePrivKey)
196+
}
119197
if err != nil {
120198
log.Fatal(err)
121199
}
122200

123-
certFile, err := os.OpenFile("cert.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
201+
fname = "cert" + *outputSuffix + ".pem"
202+
certFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
124203
if err != nil {
125204
log.Fatal(err)
126205
}
@@ -133,7 +212,8 @@ func main() {
133212
log.Fatalf("failed to encode cert: %v", err)
134213
}
135214

136-
keyFile, err := os.OpenFile("key.pem", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
215+
fname = "key" + *outputSuffix + ".pem"
216+
keyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400)
137217
if err != nil {
138218
log.Fatal(err)
139219
}
@@ -143,6 +223,6 @@ func main() {
143223
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
144224
})
145225
if err != nil {
146-
log.Fatalf("failed to encode private key: %v", err)
226+
log.Fatalf("failed to encode private key to %s: %v", fname, err)
147227
}
148228
}

0 commit comments

Comments
 (0)