// 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 core import ( "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "time" ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/scanner" "github.com/google/certificate-transparency-go/trillian/migrillian/configpb" "github.com/google/certificate-transparency-go/x509" "github.com/google/trillian" "github.com/google/trillian/client/backoff" "github.com/google/trillian/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/klog/v2" ) var errRetry = errors.New("retry") // PreorderedLogClient is a means of communicating with a single Trillian // pre-ordered log tree. type PreorderedLogClient struct { cli trillian.TrillianLogClient treeID int64 idFunc func(int64, *ct.RawLogEntry) []byte prefix string // TODO(pavelkalinnikov): Get rid of this. } // NewPreorderedLogClient creates and initializes a pre-ordered log client. func NewPreorderedLogClient( cli trillian.TrillianLogClient, tree *trillian.Tree, idFuncType configpb.IdentityFunction, prefix string, ) (*PreorderedLogClient, error) { if tree == nil { return nil, errors.New("missing Tree") } if got, want := tree.TreeType, trillian.TreeType_PREORDERED_LOG; got != want { return nil, fmt.Errorf("tree %d is %v, want %v", tree.TreeId, got, want) } ret := PreorderedLogClient{cli: cli, treeID: tree.TreeId, prefix: prefix} switch idFuncType { case configpb.IdentityFunction_SHA256_CERT_DATA: ret.idFunc = idHashCertData case configpb.IdentityFunction_SHA256_LEAF_INDEX: ret.idFunc = idHashLeafIndex default: return nil, fmt.Errorf("unknown identity function: %v", idFuncType) } return &ret, nil } // getRoot returns the current root of the Trillian tree. func (c *PreorderedLogClient) getRoot(ctx context.Context) (uint64, []byte, error) { req := trillian.GetLatestSignedLogRootRequest{LogId: c.treeID} rsp, err := c.cli.GetLatestSignedLogRoot(ctx, &req) if err != nil { return 0, nil, err } else if rsp == nil || rsp.SignedLogRoot == nil { return 0, nil, errors.New("missing SignedLogRoot") } var logRoot types.LogRootV1 if err := logRoot.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil { return 0, nil, err } return logRoot.TreeSize, logRoot.RootHash, nil } // addSequencedLeaves converts a batch of CT log entries into Trillian log // leaves and submits them to Trillian via AddSequencedLeaves API. // // If and while Trillian returns "quota exceeded" errors, the function will // retry the request with a limited exponential back-off. // // Returns an error if Trillian replies with a severe/unknown error. func (c *PreorderedLogClient) addSequencedLeaves(ctx context.Context, b *scanner.EntryBatch) error { // TODO(pavelkalinnikov): Verify range inclusion against the remote STH. leaves := make([]*trillian.LogLeaf, len(b.Entries)) for i, e := range b.Entries { var err error if leaves[i], err = c.buildLogLeaf(b.Start+int64(i), &e); err != nil { return err } } req := trillian.AddSequencedLeavesRequest{LogId: c.treeID, Leaves: leaves} // TODO(pavelkalinnikov): Make this strategy configurable. bo := backoff.Backoff{ Min: 1 * time.Second, Max: 1 * time.Minute, Factor: 3, Jitter: true, } var err error boerr := bo.Retry(ctx, func() error { var rsp *trillian.AddSequencedLeavesResponse rsp, err = c.cli.AddSequencedLeaves(ctx, &req) switch status.Code(err) { case codes.ResourceExhausted: // There was (probably) a quota error. end := b.Start + int64(len(b.Entries)) klog.Errorf("%d: retrying batch [%d, %d) due to error: %v", c.treeID, b.Start, end, err) return errRetry case codes.OK: if rsp == nil { err = errors.New("missing AddSequencedLeaves response") } // TODO(pavelkalinnikov): Check rsp.Results statuses. return nil default: // There was another (probably serious) error. return nil // Stop backing off, and return err as is below. } }) if err != nil { // Return the more specific error if available return err } // Return the timeout, or nil on success return boerr } func (c *PreorderedLogClient) buildLogLeaf(index int64, entry *ct.LeafEntry) (*trillian.LogLeaf, error) { rle, err := ct.RawLogEntryFromLeaf(index, entry) if err != nil { return nil, err } // Don't return on x509 parsing errors because we want to migrate this log // entry as is. But log the error so that it can be flagged by monitoring. if _, err = rle.ToLogEntry(); x509.IsFatal(err) { klog.Errorf("%s: index=%d: x509 fatal error: %v", c.prefix, index, err) } else if err != nil { klog.Infof("%s: index=%d: x509 non-fatal error: %v", c.prefix, index, err) } // TODO(pavelkalinnikov): Verify cert chain if error is nil or non-fatal. leafIDHash := c.idFunc(index, rle) return &trillian.LogLeaf{ LeafValue: entry.LeafInput, ExtraData: entry.ExtraData, LeafIndex: index, LeafIdentityHash: leafIDHash[:], }, nil } func idHashCertData(_ int64, entry *ct.RawLogEntry) []byte { hash := sha256.Sum256(entry.Cert.Data) return hash[:] } func idHashLeafIndex(index int64, _ *ct.RawLogEntry) []byte { data := make([]byte, 8) binary.LittleEndian.PutUint64(data, uint64(index)) hash := sha256.Sum256(data) return hash[:] }