From 5deaca0b25c7cdb7dd7eab35ed7fd4467433d2c1 Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Wed, 3 Nov 2021 14:46:40 -0700 Subject: [PATCH] Add ability for verify-blob to find signing cert in transparency log (#991) - Uses the first tlog entry it finds - Verifies provided cert / public key. If one isn't provided by the user, it will find it in the tlog (if experimental feature flag is enabled) Authored-by: Dennis Leon Signed-off-by: Dennis Leon --- cmd/cosign/cli/verify/verify_blob.go | 122 ++++++++++++++++++++++----- pkg/cosign/tlog.go | 21 ++++- pkg/cosign/verify.go | 2 +- 3 files changed, 119 insertions(+), 26 deletions(-) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 2c2f826bce2..d927adef111 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -27,8 +27,8 @@ import ( "io" "os" + "github.com/go-openapi/runtime" "github.com/pkg/errors" - "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/sign" @@ -37,6 +37,9 @@ import ( "github.com/sigstore/cosign/pkg/cosign/pivkey" sigs "github.com/sigstore/cosign/pkg/signature" rekorClient "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" "github.com/sigstore/sigstore/pkg/cryptoutils" sigstoresigs "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" @@ -53,10 +56,36 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe var err error var cert *x509.Certificate - if !options.OneOf(ko.KeyRef, ko.Sk, certRef) { + if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() { return &options.PubKeyParseError{} } + var b64sig string + targetSig, err := blob.LoadFileOrURL(sigRef) + if err != nil { + if !os.IsNotExist(err) { + // ignore if file does not exist, it can be a base64 encoded string as well + return err + } + targetSig = []byte(sigRef) + } + + if isb64(targetSig) { + b64sig = string(targetSig) + } else { + b64sig = base64.StdEncoding.EncodeToString(targetSig) + } + + var blobBytes []byte + if blobRef == "-" { + blobBytes, err = io.ReadAll(os.Stdin) + } else { + blobBytes, err = blob.LoadFileOrURL(blobRef) + } + if err != nil { + return err + } + // Keys are optional! switch { case ko.KeyRef != "": @@ -92,32 +121,35 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe if err != nil { return err } - } + case options.EnableExperimental(): + rClient, err := rekorClient.GetRekorClient(ko.RekorURL) + if err != nil { + return err + } - var b64sig string - targetSig, err := blob.LoadFileOrURL(sigRef) - if err != nil { - if !os.IsNotExist(err) { - // ignore if file does not exist, it can be a base64 encoded string as well + uuids, err := cosign.FindTLogEntriesByPayload(rClient, blobBytes) + if err != nil { return err } - targetSig = []byte(sigRef) - } - if isb64(targetSig) { - b64sig = string(targetSig) - } else { - b64sig = base64.StdEncoding.EncodeToString(targetSig) - } + if len(uuids) == 0 { + return errors.New("could not find a tlog entry for provided blob") + } - var blobBytes []byte - if blobRef == "-" { - blobBytes, err = io.ReadAll(os.Stdin) - } else { - blobBytes, err = blob.LoadFileOrURL(blobRef) - } - if err != nil { - return err + tlogEntry, err := cosign.GetTlogEntry(rClient, uuids[0]) + if err != nil { + return err + } + + certs, err := extractCerts(tlogEntry) + if err != nil { + return err + } + cert = certs[0] + pubKey, err = sigstoresigs.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return err + } } sig, err := base64.StdEncoding.DecodeString(b64sig) @@ -165,3 +197,47 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe return nil } + +func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { + b, err := base64.StdEncoding.DecodeString(e.Body.(string)) + if err != nil { + return nil, err + } + + pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) + if err != nil { + return nil, err + } + + eimpl, err := types.NewEntry(pe) + if err != nil { + return nil, err + } + + var v001Entry *rekord.V001Entry + var ok bool + if v001Entry, ok = eimpl.(*rekord.V001Entry); !ok { + return nil, errors.New("unexpected tlog entry type") + } + + publicKeyB64, err := v001Entry.RekordObj.Signature.PublicKey.Content.MarshalText() + if err != nil { + return nil, err + } + + publicKey, err := base64.StdEncoding.DecodeString(string(publicKeyB64)) + if err != nil { + return nil, err + } + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(publicKey) + if err != nil { + return nil, err + } + + if len(certs) == 0 { + return nil, errors.New("no certs found in pem tlog") + } + + return certs, err +} diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 7b61359826d..133cd8edb84 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -17,6 +17,7 @@ package cosign import ( "bytes" "context" + "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" @@ -27,9 +28,10 @@ import ( "github.com/google/trillian/merkle/logverifier" "github.com/google/trillian/merkle/rfc6962/hasher" "github.com/pkg/errors" - "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/rekor/pkg/generated/client/index" + "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/client/pubkey" @@ -124,7 +126,7 @@ func rekorEntry(payload, signature, pubKey []byte) rekord_v001.V001Entry { } } -func getTlogEntry(rekorClient *client.Rekor, uuid string) (*models.LogEntryAnon, error) { +func GetTlogEntry(rekorClient *client.Rekor, uuid string) (*models.LogEntryAnon, error) { params := entries.NewGetLogEntryByUUIDParams() params.SetEntryUUID(uuid) resp, err := rekorClient.Entries.GetLogEntryByUUID(params) @@ -177,6 +179,21 @@ func FindTlogEntry(rekorClient *client.Rekor, b64Sig string, payload, pubKey []b return uuid, *verifiedEntry.Verification.InclusionProof.LogIndex, nil } +func FindTLogEntriesByPayload(rekorClient *client.Rekor, payload []byte) (uuids []string, err error) { + params := index.NewSearchIndexParams() + params.Query = &models.SearchIndex{} + + h := sha256.New() + h.Write(payload) + params.Query.Hash = fmt.Sprintf("sha256:%s", strings.ToLower(hex.EncodeToString(h.Sum(nil)))) + + searchIndex, err := rekorClient.Index.SearchIndex(params) + if err != nil { + return nil, err + } + return searchIndex.GetPayload(), nil +} + func verifyTLogEntry(rekorClient *client.Rekor, uuid string) (*models.LogEntryAnon, error) { params := entries.NewGetLogEntryByUUIDParams() params.EntryUUID = uuid diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index c807672bb05..9bb2d02eecc 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -260,7 +260,7 @@ func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, // if we have a cert, we should check expiry // The IntegratedTime verified in VerifyTlog if cert != nil { - e, err := getTlogEntry(rekorClient, uuid) + e, err := GetTlogEntry(rekorClient, uuid) if err != nil { return err }