Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify embedded SCTs #1731

Merged
merged 3 commits into from Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cosign/cli/dockerfile.go
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion cmd/cosign/cli/fulcio/depcheck_test.go
Expand Up @@ -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",
Expand Down
229 changes: 229 additions & 0 deletions 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)
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
}
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
}