Skip to content

Commit 920dcfa

Browse files
Verify embedded SCTs (#1731)
* Add verification for embedded SCTs This adds support for verifying SCTs embedded in certificates in addition to being detached. Embedded SCTs will be verified while parsing the certificate on verification (for verify, verify-blob, and verify-attestation), and on signing if a certificate chain is provided. Signed-off-by: Hayden Blauzvern <[email protected]> * Add policy flag to enforce SCT Signed-off-by: Hayden Blauzvern <[email protected]> * Added comment for imported package Signed-off-by: Hayden Blauzvern <[email protected]>
1 parent 70a3d8c commit 920dcfa

30 files changed

+1239
-211
lines changed

cmd/cosign/cli/dockerfile.go

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val
9292
CertEmail: o.CertVerify.CertEmail,
9393
CertOidcIssuer: o.CertVerify.CertOidcIssuer,
9494
CertChain: o.CertVerify.CertChain,
95+
EnforceSCT: o.CertVerify.EnforceSCT,
9596
Sk: o.SecurityKey.Use,
9697
Slot: o.SecurityKey.Slot,
9798
Output: o.Output,

cmd/cosign/cli/fulcio/depcheck_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ func TestNoDeps(t *testing.T) {
2525
depcheck.AssertNoDependency(t, map[string][]string{
2626
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio": {
2727
// Avoid pulling in a variety of things that are massive dependencies.
28-
"github.com/google/certificate-transparency-go",
2928
"github.com/google/trillian",
3029
"github.com/envoyproxy/go-control-plane",
3130
"github.com/gogo/protobuf/protoc-gen-gogo",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright 2022 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ctl
16+
17+
import (
18+
"context"
19+
"crypto"
20+
"crypto/ecdsa"
21+
"crypto/sha256"
22+
"crypto/x509"
23+
"encoding/json"
24+
"encoding/pem"
25+
"fmt"
26+
"os"
27+
28+
ct "github.com/google/certificate-transparency-go"
29+
ctx509 "github.com/google/certificate-transparency-go/x509"
30+
"github.com/google/certificate-transparency-go/x509util"
31+
"github.com/pkg/errors"
32+
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctutil"
33+
34+
"github.com/sigstore/cosign/pkg/cosign/tuf"
35+
"github.com/sigstore/sigstore/pkg/cryptoutils"
36+
)
37+
38+
// This is the CT log public key target name
39+
var ctPublicKeyStr = `ctfe.pub`
40+
41+
// Setting this env variable will over ride what is used to validate
42+
// the SCT coming back from Fulcio.
43+
const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE"
44+
45+
// logIDMetadata holds information for mapping a key ID hash (log ID) to associated data.
46+
type logIDMetadata struct {
47+
pubKey crypto.PublicKey
48+
status tuf.StatusKind
49+
}
50+
51+
// ContainsSCT checks if the certificate contains embedded SCTs. cert can either be
52+
// DER or PEM encoded.
53+
func ContainsSCT(cert []byte) (bool, error) {
54+
embeddedSCTs, err := x509util.ParseSCTsFromCertificate(cert)
55+
if err != nil {
56+
return false, err
57+
}
58+
if len(embeddedSCTs) != 0 {
59+
return true, nil
60+
}
61+
return false, nil
62+
}
63+
64+
// VerifySCT verifies SCTs against the Fulcio CT log public key.
65+
//
66+
// The SCT is a `Signed Certificate Timestamp`, which promises that
67+
// the certificate issued by Fulcio was also added to the public CT log within
68+
// some defined time period.
69+
//
70+
// VerifySCT can verify an SCT list embedded in the certificate, or a detached
71+
// SCT provided by Fulcio.
72+
//
73+
// By default the public keys comes from TUF, but you can override this for test
74+
// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using
75+
// an alternate, the file can be PEM, or DER format.
76+
func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error {
77+
// fetch SCT verification key
78+
pubKeys := make(map[[sha256.Size]byte]logIDMetadata)
79+
rootEnv := os.Getenv(altCTLogPublicKeyLocation)
80+
if rootEnv == "" {
81+
tufClient, err := tuf.NewFromEnv(ctx)
82+
if err != nil {
83+
return err
84+
}
85+
defer tufClient.Close()
86+
87+
targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr})
88+
if err != nil {
89+
return err
90+
}
91+
for _, t := range targets {
92+
pub, err := cryptoutils.UnmarshalPEMToPublicKey(t.Target)
93+
if err != nil {
94+
return err
95+
}
96+
ctPub, ok := pub.(*ecdsa.PublicKey)
97+
if !ok {
98+
return fmt.Errorf("invalid public key: was %T, require *ecdsa.PublicKey", pub)
99+
}
100+
keyID, err := ctutil.GetCTLogID(ctPub)
101+
if err != nil {
102+
return errors.Wrap(err, "error getting CTFE public key hash")
103+
}
104+
pubKeys[keyID] = logIDMetadata{ctPub, t.Status}
105+
}
106+
} else {
107+
fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv)
108+
raw, err := os.ReadFile(rootEnv)
109+
if err != nil {
110+
return errors.Wrap(err, "error reading alternate public key file")
111+
}
112+
pubKey, err := getAlternatePublicKey(raw)
113+
if err != nil {
114+
return errors.Wrap(err, "error parsing alternate public key from the file")
115+
}
116+
keyID, err := ctutil.GetCTLogID(pubKey)
117+
if err != nil {
118+
return errors.Wrap(err, "error getting CTFE public key hash")
119+
}
120+
pubKeys[keyID] = logIDMetadata{pubKey, tuf.Active}
121+
}
122+
if len(pubKeys) == 0 {
123+
return errors.New("none of the CTFE keys have been found")
124+
}
125+
126+
// parse certificate and chain
127+
cert, err := x509util.CertificateFromPEM(certPEM)
128+
if err != nil {
129+
return err
130+
}
131+
certChain, err := x509util.CertificatesFromPEM(chainPEM)
132+
if err != nil {
133+
return err
134+
}
135+
if len(certChain) == 0 {
136+
return errors.New("no certificate chain found")
137+
}
138+
139+
// fetch embedded SCT if present
140+
embeddedSCTs, err := x509util.ParseSCTsFromCertificate(certPEM)
141+
if err != nil {
142+
return err
143+
}
144+
// SCT must be either embedded or in header
145+
if len(embeddedSCTs) == 0 && len(rawSCT) == 0 {
146+
return errors.New("no SCT found")
147+
}
148+
149+
// check SCT embedded in certificate
150+
if len(embeddedSCTs) != 0 {
151+
for _, sct := range embeddedSCTs {
152+
pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID]
153+
if !ok {
154+
return errors.New("ctfe public key not found for embedded SCT")
155+
}
156+
err := ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert, certChain[0]}, sct, true)
157+
if err != nil {
158+
return errors.Wrap(err, "error verifying embedded SCT")
159+
}
160+
if pubKeyMetadata.status != tuf.Active {
161+
fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n")
162+
}
163+
}
164+
return nil
165+
}
166+
167+
// check SCT in response header
168+
var addChainResp ct.AddChainResponse
169+
if err := json.Unmarshal(rawSCT, &addChainResp); err != nil {
170+
return errors.Wrap(err, "unmarshal")
171+
}
172+
sct, err := addChainResp.ToSignedCertificateTimestamp()
173+
if err != nil {
174+
return err
175+
}
176+
pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID]
177+
if !ok {
178+
return errors.New("ctfe public key not found")
179+
}
180+
err = ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert}, sct, false)
181+
if err != nil {
182+
return errors.Wrap(err, "error verifying SCT")
183+
}
184+
if pubKeyMetadata.status != tuf.Active {
185+
fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n")
186+
}
187+
return nil
188+
}
189+
190+
// VerifyEmbeddedSCT verifies an embedded SCT in a certificate.
191+
func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate) error {
192+
if len(chain) < 2 {
193+
return errors.New("certificate chain must contain at least a certificate and its issuer")
194+
}
195+
certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0])
196+
if err != nil {
197+
return err
198+
}
199+
chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:])
200+
if err != nil {
201+
return err
202+
}
203+
return VerifySCT(ctx, certPEM, chainPEM, []byte{})
204+
}
205+
206+
// Given a byte array, try to construct a public key from it.
207+
// Will try first to see if it's PEM formatted, if not, then it will
208+
// try to parse it as der publics, and failing that
209+
func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) {
210+
var pubKey crypto.PublicKey
211+
var err error
212+
var derBytes []byte
213+
pemBlock, _ := pem.Decode(in)
214+
if pemBlock == nil {
215+
fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER")
216+
derBytes = in
217+
} else {
218+
derBytes = pemBlock.Bytes
219+
}
220+
pubKey, err = x509.ParsePKIXPublicKey(derBytes)
221+
if err != nil {
222+
// Try using the PKCS1 before giving up.
223+
pubKey, err = x509.ParsePKCS1PublicKey(derBytes)
224+
if err != nil {
225+
return nil, errors.Wrap(err, "failed to parse alternate public key")
226+
}
227+
}
228+
return pubKey, nil
229+
}

0 commit comments

Comments
 (0)