diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index fdcf524c49e..27e37b2b778 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -92,6 +92,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/fulcio/depcheck_test.go b/cmd/cosign/cli/fulcio/depcheck_test.go index 39493e5a86c..bc7d45f2f26 100644 --- a/cmd/cosign/cli/fulcio/depcheck_test.go +++ b/cmd/cosign/cli/fulcio/depcheck_test.go @@ -25,7 +25,6 @@ func TestNoDeps(t *testing.T) { depcheck.AssertNoDependency(t, map[string][]string{ "github.com/sigstore/cosign/cmd/cosign/cli/fulcio": { // Avoid pulling in a variety of things that are massive dependencies. - "github.com/google/certificate-transparency-go", "github.com/google/trillian", "github.com/envoyproxy/go-control-plane", "github.com/gogo/protobuf/protoc-gen-gogo", diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/google b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/google rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go new file mode 100644 index 00000000000..49ca970fb82 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go @@ -0,0 +1,229 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ctl + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "os" + + ct "github.com/google/certificate-transparency-go" + ctx509 "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctutil" + + "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +// This is the CT log public key target name +var ctPublicKeyStr = `ctfe.pub` + +// Setting this env variable will over ride what is used to validate +// the SCT coming back from Fulcio. +const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + +// logIDMetadata holds information for mapping a key ID hash (log ID) to associated data. +type logIDMetadata struct { + pubKey crypto.PublicKey + status tuf.StatusKind +} + +// ContainsSCT checks if the certificate contains embedded SCTs. cert can either be +// DER or PEM encoded. +func ContainsSCT(cert []byte) (bool, error) { + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(cert) + if err != nil { + return false, err + } + if len(embeddedSCTs) != 0 { + return true, nil + } + return false, nil +} + +// VerifySCT verifies SCTs against the Fulcio CT log public key. +// +// The SCT is a `Signed Certificate Timestamp`, which promises that +// the certificate issued by Fulcio was also added to the public CT log within +// some defined time period. +// +// VerifySCT can verify an SCT list embedded in the certificate, or a detached +// SCT provided by Fulcio. +// +// By default the public keys comes from TUF, but you can override this for test +// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using +// an alternate, the file can be PEM, or DER format. +func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error { + // fetch SCT verification key + pubKeys := make(map[[sha256.Size]byte]logIDMetadata) + rootEnv := os.Getenv(altCTLogPublicKeyLocation) + if rootEnv == "" { + tufClient, err := tuf.NewFromEnv(ctx) + if err != nil { + return err + } + defer tufClient.Close() + + targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) + if err != nil { + return err + } + for _, t := range targets { + pub, err := cryptoutils.UnmarshalPEMToPublicKey(t.Target) + if err != nil { + return err + } + ctPub, ok := pub.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("invalid public key: was %T, require *ecdsa.PublicKey", pub) + } + keyID, err := ctutil.GetCTLogID(ctPub) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{ctPub, t.Status} + } + } else { + fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) + raw, err := os.ReadFile(rootEnv) + if err != nil { + return errors.Wrap(err, "error reading alternate public key file") + } + pubKey, err := getAlternatePublicKey(raw) + if err != nil { + return errors.Wrap(err, "error parsing alternate public key from the file") + } + keyID, err := ctutil.GetCTLogID(pubKey) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{pubKey, tuf.Active} + } + if len(pubKeys) == 0 { + return errors.New("none of the CTFE keys have been found") + } + + // parse certificate and chain + cert, err := x509util.CertificateFromPEM(certPEM) + if err != nil { + return err + } + certChain, err := x509util.CertificatesFromPEM(chainPEM) + if err != nil { + return err + } + if len(certChain) == 0 { + return errors.New("no certificate chain found") + } + + // fetch embedded SCT if present + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(certPEM) + if err != nil { + return err + } + // SCT must be either embedded or in header + if len(embeddedSCTs) == 0 && len(rawSCT) == 0 { + return errors.New("no SCT found") + } + + // check SCT embedded in certificate + if len(embeddedSCTs) != 0 { + for _, sct := range embeddedSCTs { + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found for embedded SCT") + } + err := ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert, certChain[0]}, sct, true) + if err != nil { + return errors.Wrap(err, "error verifying embedded SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") + } + } + return nil + } + + // check SCT in response header + var addChainResp ct.AddChainResponse + if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { + return errors.Wrap(err, "unmarshal") + } + sct, err := addChainResp.ToSignedCertificateTimestamp() + if err != nil { + return err + } + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found") + } + err = ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert}, sct, false) + if err != nil { + return errors.Wrap(err, "error verifying SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") + } + return nil +} + +// VerifyEmbeddedSCT verifies an embedded SCT in a certificate. +func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate) error { + if len(chain) < 2 { + return errors.New("certificate chain must contain at least a certificate and its issuer") + } + certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) + if err != nil { + return err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) + if err != nil { + return err + } + return VerifySCT(ctx, certPEM, chainPEM, []byte{}) +} + +// Given a byte array, try to construct a public key from it. +// Will try first to see if it's PEM formatted, if not, then it will +// try to parse it as der publics, and failing that +func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { + var pubKey crypto.PublicKey + var err error + var derBytes []byte + pemBlock, _ := pem.Decode(in) + if pemBlock == nil { + fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") + derBytes = in + } else { + derBytes = pemBlock.Bytes + } + pubKey, err = x509.ParsePKIXPublicKey(derBytes) + if err != nil { + // Try using the PKCS1 before giving up. + pubKey, err = x509.ParsePKCS1PublicKey(derBytes) + if err != nil { + return nil, errors.Wrap(err, "failed to parse alternate public key") + } + } + return pubKey, nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go new file mode 100644 index 00000000000..751e1423551 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go @@ -0,0 +1,250 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ctl + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func TestGetAlternatePublicKey(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get cwd: %v", err) + } + tests := []struct { + file string + wantErrSub string + wantType string + }{ + {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, + // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ + {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, + // This needs to be parsed with the pkcs1, pkix won't do. + {file: "rsa", wantType: "*rsa.PublicKey"}, + // This works with pkix, from: + // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem + {file: "google", wantType: "*rsa.PublicKey"}, + } + for _, tc := range tests { + filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) + bytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("Failed to read testfile %s : %v", tc.file, err) + } + got, err := getAlternatePublicKey(bytes) + switch { + case err == nil && tc.wantErrSub != "": + t.Errorf("Wanted Error for %s but got none", tc.file) + case err != nil && tc.wantErrSub == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case err != nil && tc.wantErrSub != "": + if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) + } + } + switch { + case got == nil && tc.wantType != "": + t.Errorf("Wanted public key for %s but got none", tc.file) + case got != nil && tc.wantType == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case got != nil && tc.wantType != "": + if reflect.TypeOf(got).String() != tc.wantType { + t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) + } + } + } +} + +func TestContainsSCT(t *testing.T) { + // test certificate without embedded SCT + contains, err := ContainsSCT([]byte(testdata.TestCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if contains { + t.Fatalf("certificate unexpectedly contained SCT") + } + + // test certificate with embedded SCT + contains, err = ContainsSCT([]byte(testdata.TestEmbeddedCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if !contains { + t.Fatalf("certificate unexpectedly did not contain SCT") + } +} + +// From https://github.com/google/certificate-transparency-go/blob/e76f3f637053b90c8168d29b01ca162cd235ace5/ctutil/ctutil_test.go +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + chainPEM string + sct []byte + embedded bool + wantErr bool + errMsg string + }{ + { + desc: "cert", + certPEM: testdata.TestCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "invalid SCT", + certPEM: testdata.TestPreCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + certPEM: testdata.TestEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + certPEM: testdata.TestInvalidEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + errMsg: "failed to verify ECDSA signature", + }, + } + + writePubKey(t, testdata.LogPublicKeyPEM) + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // convert SCT to response struct if detached + var sctBytes []byte + if !test.embedded { + var sct ct.SignedCertificateTimestamp + if _, err := tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + chainResp, err := toAddChainResponse(&sct) + if err != nil { + t.Fatalf("error generating chain response: %v", err) + } + sctBytes, err = json.Marshal(chainResp) + if err != nil { + t.Fatalf("error marshalling chain: %v", err) + } + } + + err := VerifySCT(context.Background(), []byte(test.certPEM), []byte(test.chainPEM), sctBytes) + if gotErr := err != nil; gotErr != test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTError(t *testing.T) { + // verify fails with mismatched verifcation key + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("unexpected error generating ECDSA key: %v", err) + } + pemKey, err := cryptoutils.MarshalPublicKeyToPEM(key.Public()) + if err != nil { + t.Fatalf("unexpected error marshalling ECDSA key: %v", err) + } + writePubKey(t, string(pemKey)) + err = VerifySCT(context.Background(), []byte(testdata.TestEmbeddedCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "ctfe public key not found") { + t.Fatalf("expected error verifying SCT with mismatched key: %v", err) + } + + // verify fails without either a detached SCT or embedded SCT + err = VerifySCT(context.Background(), []byte(testdata.TestCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "no SCT found") { + t.Fatalf("expected error verifying SCT without SCT: %v", err) + } +} + +func TestVerifyEmbeddedSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + // verify fails without a certificate chain + err = VerifyEmbeddedSCT(context.Background(), chain[:1]) + if err == nil || err.Error() != "certificate chain must contain at least a certificate and its issuer" { + t.Fatalf("expected error verifying SCT without chain: %v", err) + } + + writePubKey(t, testdata.LogPublicKeyPEM) + err = VerifyEmbeddedSCT(context.Background(), chain) + if err != nil { + t.Fatalf("unexpected error verifying embedded SCT: %v", err) + } +} + +// toAddChainResponse converts an SCT to a response struct, the expected structure for detached SCTs +func toAddChainResponse(sct *ct.SignedCertificateTimestamp) (*ct.AddChainResponse, error) { + sig, err := tls.Marshal(sct.Signature) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature: %w", err) + } + addChainResp := &ct.AddChainResponse{ + SCTVersion: sct.SCTVersion, + Timestamp: sct.Timestamp, + Extensions: base64.StdEncoding.EncodeToString(sct.Extensions), + ID: sct.LogID.KeyID[:], + Signature: sig, + } + + return addChainResp, nil +} + +// writePubKey writes the SCT verification key to disk, since there is not a TUF +// test setup +func writePubKey(t *testing.T, keyPEM string) { + t.Helper() + + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + t.Cleanup(func() { tmpPrivFile.Close() }) + if _, err := tmpPrivFile.Write([]byte(keyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + t.Cleanup(func() { os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") }) +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go new file mode 100644 index 00000000000..a764b8e32f8 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go @@ -0,0 +1,224 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package copied from +// https://github.com/google/certificate-transparency-go/blob/master/ctutil/ctutil.go + +// Package ctutil contains utilities for Certificate Transparency. +package ctutil + +import ( + "bytes" + "crypto" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/x509" +) + +var emptyHash = [sha256.Size]byte{} + +// LeafHashB64 does as LeafHash does, but returns the leaf hash base64-encoded. +// The base64-encoded leaf hash returned by B64LeafHash can be used with the +// get-proof-by-hash API endpoint of Certificate Transparency Logs. +func LeafHashB64(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (string, error) { + hash, err := LeafHash(chain, sct, embedded) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(hash[:]), nil +} + +// LeafHash calculates the leaf hash of the certificate or precertificate at +// chain[0] that sct was issued for. +// +// sct is required because the SCT timestamp is used to calculate the leaf hash. +// Leaf hashes are unique to (pre)certificate-SCT pairs. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to calculate the leaf hash for a normal X.509 +// certificate then it is enough to just provide the end entity +// certificate in chain. This case assumes that the SCT being provided is +// not embedded within the leaf certificate provided, i.e. the certificate +// is what was submitted to the Certificate Transparency Log in order to +// obtain the SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to calculate the leaf hash for a precertificate +// then the issuing certificate must also be provided in chain. The +// precertificate should be at chain[0], and its issuer at chain[1]. For +// this case, set embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If using this function to calculate the leaf hash for a certificate +// where the SCT provided is embedded within the certificate you +// are providing at chain[0], set embedded to true. LeafHash will +// calculate the leaf hash by building the corresponding precertificate. +// LeafHash will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +// +// Note: LeafHash doesn't check that the provided SCT verifies for the given +// chain. It simply calculates what the leaf hash would be for the given +// (pre)certificate-SCT pair. +func LeafHash(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) ([sha256.Size]byte, error) { + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return emptyHash, err + } + return ct.LeafHashForLeaf(leaf) +} + +// VerifySCT takes the public key of a Certificate Transparency Log, a +// certificate chain, and an SCT and verifies whether the SCT is a valid SCT for +// the certificate at chain[0], signed by the Log that the public key belongs +// to. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCT(pubKey crypto.PublicKey, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + s, err := ct.NewSignatureVerifier(pubKey) + if err != nil { + return fmt.Errorf("error creating signature verifier: %w", err) + } + + return VerifySCTWithVerifier(s, chain, sct, embedded) +} + +// VerifySCTWithVerifier takes a ct.SignatureVerifier, a certificate chain, and +// an SCT and verifies whether the SCT is a valid SCT for the certificate at +// chain[0], signed by the Log whose public key was used to set up the +// ct.SignatureVerifier. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCTWithVerifier(sv *ct.SignatureVerifier, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + if sv == nil { + return errors.New("ct.SignatureVerifier is nil") + } + + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return err + } + + return sv.VerifySCTSignature(*sct, ct.LogEntry{Leaf: *leaf}) +} + +func createLeaf(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (*ct.MerkleTreeLeaf, error) { + if len(chain) == 0 { + return nil, errors.New("chain is empty") + } + if sct == nil { + return nil, errors.New("sct is nil") + } + + if embedded { + sctPresent, err := ContainsSCT(chain[0], sct) + if err != nil { + return nil, fmt.Errorf("error checking for SCT in leaf certificate: %w", err) + } + if !sctPresent { + return nil, errors.New("SCT provided is not embedded within leaf certificate") + } + } + + certType := ct.X509LogEntryType + if chain[0].IsPrecertificate() || embedded { + certType = ct.PrecertLogEntryType + } + + var leaf *ct.MerkleTreeLeaf + var err error + if embedded { + leaf, err = ct.MerkleTreeLeafForEmbeddedSCT(chain, sct.Timestamp) + } else { + leaf, err = ct.MerkleTreeLeafFromChain(chain, certType, sct.Timestamp) + } + if err != nil { + return nil, fmt.Errorf("error creating MerkleTreeLeaf: %w", err) + } + return leaf, nil +} + +// ContainsSCT checks to see whether the given SCT is embedded within the given +// certificate. +func ContainsSCT(cert *x509.Certificate, sct *ct.SignedCertificateTimestamp) (bool, error) { + if cert == nil || sct == nil { + return false, nil + } + + sctBytes, err := tls.Marshal(*sct) + if err != nil { + return false, fmt.Errorf("error tls.Marshalling SCT: %w", err) + } + for _, s := range cert.SCTList.SCTList { + if bytes.Equal(sctBytes, s.Val) { + return true, nil + } + } + return false, nil +} + +// GetCTLogID takes the key manager for a log and returns the LogID. (see RFC 6962 S3.2) +// In CT V1 the log id is a hash of the public key. +func GetCTLogID(pk crypto.PublicKey) ([sha256.Size]byte, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return [sha256.Size]byte{}, err + } + return sha256.Sum256(pubBytes), nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go new file mode 100644 index 00000000000..8476d866d0c --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go @@ -0,0 +1,385 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ctutil + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + ttestdata "github.com/google/certificate-transparency-go/trillian/testdata" + "github.com/google/certificate-transparency-go/x509util" +) + +var ( + demoLogID = [32]byte{19, 56, 222, 93, 229, 36, 102, 128, 227, 214, 3, 121, 93, 175, 126, 236, 97, 217, 34, 32, 40, 233, 98, 27, 46, 179, 164, 251, 84, 10, 60, 57} +) + +func TestLeafHash(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + want string + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + want: testdata.TestCertB64LeafHash, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + want: testdata.TestPreCertB64LeafHash, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + want: testdata.TestPreCertB64LeafHash, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test LeafHash() + wantSl, err := base64.StdEncoding.DecodeString(test.want) + if err != nil { + t.Fatalf("error base64-decoding leaf hash %q: %s", test.want, err) + } + var want [32]byte + copy(want[:], wantSl) + + got, err := LeafHash(chain, &sct, test.embedded) + if got != want || err != nil { + t.Errorf("LeafHash(_,_) = %v, %v, want %v, nil", got, err, want) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, &sct, test.embedded) + if gotB64 != test.want || err != nil { + t.Errorf("LeafHashB64(_,_) = %v, %v, want %v, nil", gotB64, err, test.want) + } + }) + } +} + +func TestLeafHashErrors(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + }{ + { + desc: "empty chain", + chainPEM: "", + sct: testdata.TestCertProof, + }, + { + desc: "nil SCT", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: nil, + }, + { + desc: "no SCTs embedded in cert, embedded true", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + { + desc: "cert contains embedded SCTs, but not the SCT provided", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct *ct.SignedCertificateTimestamp + if test.sct != nil { + sct = &ct.SignedCertificateTimestamp{} + if _, err = tls.Unmarshal(test.sct, sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + } + + // Test LeafHash() + got, err := LeafHash(chain, sct, test.embedded) + if got != emptyHash || err == nil { + t.Errorf("LeafHash(_,_) = %s, %v, want %v, error", got, err, emptyHash) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, sct, test.embedded) + if gotB64 != "" || err == nil { + t.Errorf("LeafHashB64(_,_) = %s, %v, want \"\", error", gotB64, err) + } + }) + } +} + +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCT() + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + err = VerifySCT(pk, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTWithVerifier(t *testing.T) { + // Parse public key + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + // Create signature verifier + sv, err := ct.NewSignatureVerifier(pk) + if err != nil { + t.Errorf("couldn't create signature verifier: %s", err) + } + + tests := []struct { + desc string + sv *ct.SignatureVerifier + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "nil signature verifier", + sv: nil, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert", + sv: sv, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + sv: sv, + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + sv: sv, + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCTWithVerifier() + err = VerifySCTWithVerifier(test.sv, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestContainsSCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + sct []byte + want bool + }{ + { + desc: "cert doesn't contain any SCTs", + certPEM: testdata.TestCertPEM, + sct: testdata.TestPreCertProof, + want: false, + }, + { + desc: "cert contains SCT but not specified SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestInvalidProof, + want: false, + }, + { + desc: "cert contains SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestPreCertProof, + want: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse cert + cert, err := x509util.CertificateFromPEM([]byte(test.certPEM)) + if err != nil { + t.Fatalf("error parsing certificate: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test ContainsSCT() + got, err := ContainsSCT(cert, &sct) + if err != nil { + t.Fatalf("ContainsSCT(_,_) = false, %s, want no error", err) + } + + if got != test.want { + t.Errorf("ContainsSCT(_,_) = %t, nil, want %t, nil", got, test.want) + } + }) + } +} + +func TestGetCTLogID(t *testing.T) { + block, _ := pem.Decode([]byte(ttestdata.DemoPublicKey)) + pk, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatalf("unexpected error loading public key: %v", err) + } + + got, err := GetCTLogID(pk) + if err != nil { + t.Fatalf("error getting logid: %v", err) + } + + if want := demoLogID; got != want { + t.Errorf("logID: \n%v want \n%v", got, want) + } +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 819585a9985..3687f5db01f 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -17,103 +17,16 @@ package fulcioverifier import ( "context" - "crypto" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "os" - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/ctutil" - ctx509 "github.com/google/certificate-transparency-go/x509" - "github.com/google/certificate-transparency-go/x509util" "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/fulcio/pkg/api" ) -// This is the CT log public key target name -var ctPublicKeyStr = `ctfe.pub` - -// Setting this env variable will over ride what is used to validate -// the SCT coming back from Fulcio. -const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" - -// verifySCT verifies the SCT against the Fulcio CT log public key. -// By default this comes from TUF, but you can override this (for test) -// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using -// an alternate, the file can be PEM, or DER format. -// -// The SCT is a `Signed Certificate Timestamp`, which promises that -// the certificate issued by Fulcio was also added to the public CT log within -// some defined time period -func verifySCT(ctx context.Context, certPEM, rawSCT []byte) error { - pubKeys := make(map[crypto.PublicKey]tuf.StatusKind) - rootEnv := os.Getenv(altCTLogPublicKeyLocation) - if rootEnv == "" { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return err - } - defer tufClient.Close() - - targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) - if err != nil { - return err - } - for _, t := range targets { - ctPub, err := cosign.PemToECDSAKey(t.Target) - if err != nil { - return errors.Wrap(err, "converting Public CT to ECDSAKey") - } - pubKeys[ctPub] = t.Status - } - } else { - fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) - raw, err := os.ReadFile(rootEnv) - if err != nil { - return errors.Wrap(err, "error reading alternate public key file") - } - pubKey, err := getAlternatePublicKey(raw) - if err != nil { - return errors.Wrap(err, "error parsing alternate public key from the file") - } - pubKeys[pubKey] = tuf.Active - } - if len(pubKeys) == 0 { - return errors.New("none of the CTFE keys have been found") - } - cert, err := x509util.CertificateFromPEM(certPEM) - if err != nil { - return err - } - var addChainResp ct.AddChainResponse - if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { - return errors.Wrap(err, "unmarshal") - } - sct, err := addChainResp.ToSignedCertificateTimestamp() - if err != nil { - return err - } - var verifySctErr error - for pubKey, status := range pubKeys { - verifySctErr = ctutil.VerifySCT(pubKey, []*ctx509.Certificate{cert}, sct, false) - // Exit after successful verification of the SCT - if verifySctErr == nil { - if status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") - } - return nil - } - } - // Return the last error that occurred during verification. - return verifySctErr -} - func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*fulcio.Signer, error) { fs, err := fulcio.NewSigner(ctx, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) if err != nil { @@ -121,35 +34,10 @@ func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClien } // verify the sct - if err := verifySCT(ctx, fs.Cert, fs.SCT); err != nil { + if err := ctl.VerifySCT(ctx, fs.Cert, fs.Chain, fs.SCT); err != nil { return nil, errors.Wrap(err, "verifying SCT") } fmt.Fprintln(os.Stderr, "Successfully verified SCT...") return fs, nil } - -// Given a byte array, try to construct a public key from it. -// Will try first to see if it's PEM formatted, if not, then it will -// try to parse it as der publics, and failing that -func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { - var pubKey crypto.PublicKey - var err error - var derBytes []byte - pemBlock, _ := pem.Decode(in) - if pemBlock == nil { - fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") - derBytes = in - } else { - derBytes = pemBlock.Bytes - } - pubKey, err = x509.ParsePKIXPublicKey(derBytes) - if err != nil { - // Try using the PKCS1 before giving up. - pubKey, err = x509.ParsePKCS1PublicKey(derBytes) - if err != nil { - return nil, errors.Wrap(err, "failed to parse alternate public key") - } - } - return pubKey, nil -} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go deleted file mode 100644 index a4588e7ace5..00000000000 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// -// Copyright 2021 The Sigstore Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fulcioverifier - -import ( - "fmt" - "os" - "reflect" - "strings" - "testing" -) - -func TestGetAlternatePublicKey(t *testing.T) { - wd, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get cwd: %v", err) - } - tests := []struct { - file string - wantErrSub string - wantType string - }{ - {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, - // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ - {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, - // This needs to be parsed with the pkcs1, pkix won't do. - {file: "rsa", wantType: "*rsa.PublicKey"}, - // This works with pkix, from: - // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem - {file: "google", wantType: "*rsa.PublicKey"}, - } - for _, tc := range tests { - filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) - bytes, err := os.ReadFile(filepath) - if err != nil { - t.Fatalf("Failed to read testfile %s : %v", tc.file, err) - } - got, err := getAlternatePublicKey(bytes) - switch { - case err == nil && tc.wantErrSub != "": - t.Errorf("Wanted Error for %s but got none", tc.file) - case err != nil && tc.wantErrSub == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case err != nil && tc.wantErrSub != "": - if !strings.Contains(err.Error(), tc.wantErrSub) { - t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) - } - } - switch { - case got == nil && tc.wantType != "": - t.Errorf("Wanted public key for %s but got none", tc.file) - case got != nil && tc.wantType == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case got != nil && tc.wantType != "": - if reflect.TypeOf(got).String() != tc.wantType { - t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) - } - } - } -} diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 07c213cbe58..5854065f43e 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -87,6 +87,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index f801f4f801a..615842c810e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -24,6 +24,7 @@ type CertVerifyOptions struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -44,4 +45,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "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") + + cmd.Flags().BoolVar(&o.EnforceSCT, "enforce-sct", false, + "whether to enforce that a certificate contain an embedded SCT, a proof of "+ + "inclusion in a certificate transparency log") } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 4f3a12843ed..44da104baf8 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -32,6 +32,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" icos "github.com/sigstore/cosign/internal/pkg/cosign" @@ -414,9 +415,22 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin for _, c := range certChain[:len(certChain)-1] { subPool.AddCert(c) } - if err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { + if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { return nil, errors.Wrap(err, "unable to validate certificate chain") } + // Verify SCT if present in the leaf certificate. + contains, err := ctl.ContainsSCT(leafCert.Raw) + if err != nil { + return nil, err + } + if contains { + var chain []*x509.Certificate + chain = append(chain, leafCert) + chain = append(chain, certChain...) + if err := ctl.VerifyEmbeddedSCT(context.Background(), chain); err != nil { + return nil, err + } + } certSigner.Chain = certChainBytes return certSigner, nil diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 680e30239a1..5471da9a1b0 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -97,6 +97,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -174,6 +175,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -252,7 +254,8 @@ The blob may be specified as a path to a file or - for stdin.`, BundlePath: o.BundlePath, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, - o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, o.Signature, args[0]); err != nil { + o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, + o.Signature, args[0], o.CertVerify.EnforceSCT); err != nil { return errors.Wrapf(err, "verifying blob %s", args) } return nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 79cc1325c6d..e55381e0b1d 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -54,6 +54,7 @@ type VerifyCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -95,6 +96,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, SignatureRef: c.SignatureRef, } if c.CheckClaims { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index fb4cb7713b0..8ab95fd7dac 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -50,6 +50,7 @@ type VerifyAttestationCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -77,6 +78,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d0741f13044..0341ed0a025 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -61,7 +61,8 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string) error { +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, + certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -119,6 +120,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer co := &cosign.CheckOpts{ CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { @@ -162,7 +164,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if len(uuids) == 0 { return errors.New("could not find a tlog entry for provided blob") } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes) + return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) } // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. @@ -184,7 +186,8 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer return nil } -func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte) error { +func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, + uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) @@ -202,6 +205,7 @@ func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor IntermediateCerts: fulcio.GetIntermediates(), CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } cert := certs[0] verifier, err := cosign.ValidateAndUnpackCert(cert, co) diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index bf0ae43bf8d..fa6ec41bfe3 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -62,6 +62,7 @@ cosign dockerfile verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index a78e08bfb59..81f314d83f8 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -56,6 +56,7 @@ cosign manifest verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 90c7e7a4f09..65ca6994699 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -66,6 +66,7 @@ cosign verify-attestation [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-attestation --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index cb02172985a..894e46afde6 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -68,6 +68,7 @@ cosign verify-blob [flags] --cert-chain string 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 --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-blob --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index c27f4d50808..68bf94f6d90 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -72,6 +72,7 @@ cosign verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/go.mod b/go.mod index 9d72519daf1..d622821d8cf 100644 --- a/go.mod +++ b/go.mod @@ -314,3 +314,5 @@ require ( sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) + +exclude github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b diff --git a/go.sum b/go.sum index 209d61d5652..538d3d7f87b 100644 --- a/go.sum +++ b/go.sum @@ -1052,7 +1052,6 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1717,7 +1716,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index cdcc05074d5..b00ed733c75 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -31,6 +31,7 @@ import ( "strings" "time" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/tuf" @@ -83,6 +84,9 @@ type CheckOpts struct { CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. CertOidcIssuer string + // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a + // certificate transparency log. + EnforceSCT bool // SignatureRef is the reference to the signature file SignatureRef string @@ -158,7 +162,8 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } // Now verify the cert, then the signature. - if err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts); err != nil { + chains, err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts) + if err != nil { return nil, err } if co.CertEmail != "" { @@ -221,6 +226,22 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } return nil, errors.New("none of the expected identities matched what was in the certificate") } + contains, err := ctl.ContainsSCT(cert.Raw) + if err != nil { + return nil, err + } + if co.EnforceSCT && !contains { + return nil, errors.New("certificate does not include required embedded SCT") + } + if contains { + // handle if chains has more than one chain - grab first and print message + if len(chains) > 1 { + fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first to verify the SCT.\n") + } + if err := ctl.VerifyEmbeddedSCT(context.Background(), chains[0]); err != nil { + return nil, err + } + } return verifier, nil } @@ -901,8 +922,8 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa. return nil } -func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) error { - if _, err := cert.Verify(x509.VerifyOptions{ +func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) ([][]*x509.Certificate, error) { + chains, err := cert.Verify(x509.VerifyOptions{ // THIS IS IMPORTANT: WE DO NOT CHECK TIMES HERE // THE CERTIFICATE IS TREATED AS TRUSTED FOREVER // WE CHECK THAT THE SIGNATURES WERE CREATED DURING THIS WINDOW @@ -912,10 +933,11 @@ func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x5 KeyUsages: []x509.ExtKeyUsage{ x509.ExtKeyUsageCodeSigning, }, - }); err != nil { - return err + }) + if err != nil { + return nil, err } - return nil + return chains, nil } func correctAnnotations(wanted, have map[string]interface{}) bool { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 83e91a3ee5c..3f735c6d3ec 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -28,12 +28,14 @@ import ( "io" "net" "net/url" + "os" "strings" "testing" "time" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/strfmt" + "github.com/google/certificate-transparency-go/testdata" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" @@ -45,6 +47,7 @@ import ( "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/cosign/test" rtypes "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" @@ -349,6 +352,64 @@ func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { } } +func TestValidateAndUnpackCertWithSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[1]) + co := &CheckOpts{ + RootCerts: rootPool, + } + + // write SCT verification key to disk + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + defer os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + + // validate again, explicitly setting enforce SCT + co.EnforceSCT = true + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + EnforceSCT: true, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "certificate does not include required embedded SCT") +} + func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -621,10 +682,16 @@ func TestTrustedCertSuccess(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + chains, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } + if len(chains) != 1 { + t.Fatalf("unexpected number of chains found, expected 1, got %v", len(chains)) + } + if len(chains[0]) != 3 { + t.Fatalf("unexpected number of certs in chain, expected 3, got %v", len(chains[0])) + } } func TestTrustedCertSuccessNoIntermediates(t *testing.T) { @@ -634,7 +701,7 @@ func TestTrustedCertSuccessNoIntermediates(t *testing.T) { rootPool := x509.NewCertPool() rootPool.AddCert(rootCert) - err := TrustedCert(leafCert, rootPool, nil) + _, err := TrustedCert(leafCert, rootPool, nil) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } @@ -652,7 +719,7 @@ func TestTrustedCertSuccessChainFromRoot(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + _, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } diff --git a/test/e2e_test.go b/test/e2e_test.go index c62280ba28a..1302b9d8478 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -501,8 +501,8 @@ func TestSignBlob(t *testing.T) { KeyRef: pubKeyPath2, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -514,8 +514,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) } func TestSignBlobBundle(t *testing.T) { @@ -540,7 +540,7 @@ func TestSignBlobBundle(t *testing.T) { BundlePath: bundlePath, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -553,7 +553,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -563,7 +563,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) } func TestGenerate(t *testing.T) { diff --git a/third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE b/third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE similarity index 100% rename from third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE rename to third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE