// Copyright 2017 Google Inc. 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 client import ( "context" "crypto/sha256" "errors" "fmt" "io/ioutil" "net/http" "time" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/client/configpb" "github.com/google/certificate-transparency-go/jsonclient" "github.com/google/certificate-transparency-go/x509" ) type interval struct { lower *time.Time // nil => no lower bound upper *time.Time // nil => no upper bound } // TemporalLogConfigFromFile creates a TemporalLogConfig object from the given // filename, which should contain text-protobuf encoded configuration data. func TemporalLogConfigFromFile(filename string) (*configpb.TemporalLogConfig, error) { if len(filename) == 0 { return nil, errors.New("log config filename empty") } cfgText, err := ioutil.ReadFile(filename) if err != nil { return nil, fmt.Errorf("failed to read log config: %v", err) } var cfg configpb.TemporalLogConfig if err := proto.UnmarshalText(string(cfgText), &cfg); err != nil { return nil, fmt.Errorf("failed to parse log config: %v", err) } if len(cfg.Shard) == 0 { return nil, errors.New("empty log config found") } return &cfg, nil } // AddLogClient is an interface that allows adding certificates and pre-certificates to a log. // Both LogClient and TemporalLogClient implement this interface, which allows users to // commonize code for adding certs to normal/temporal logs. type AddLogClient interface { AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) } // TemporalLogClient allows [pre-]certificates to be uploaded to a temporal log. type TemporalLogClient struct { Clients []*LogClient intervals []interval } // NewTemporalLogClient builds a new client for interacting with a temporal log. // The provided config should be contiguous and chronological. func NewTemporalLogClient(cfg configpb.TemporalLogConfig, hc *http.Client) (*TemporalLogClient, error) { if len(cfg.Shard) == 0 { return nil, errors.New("empty config") } overall, err := shardInterval(cfg.Shard[0]) if err != nil { return nil, fmt.Errorf("cfg.Shard[0] invalid: %v", err) } intervals := make([]interval, 0, len(cfg.Shard)) intervals = append(intervals, overall) for i := 1; i < len(cfg.Shard); i++ { interval, err := shardInterval(cfg.Shard[i]) if err != nil { return nil, fmt.Errorf("cfg.Shard[%d] invalid: %v", i, err) } if overall.upper == nil { return nil, fmt.Errorf("cfg.Shard[%d] extends an interval with no upper bound", i) } if interval.lower == nil { return nil, fmt.Errorf("cfg.Shard[%d] has no lower bound but extends an interval", i) } if !interval.lower.Equal(*overall.upper) { return nil, fmt.Errorf("cfg.Shard[%d] starts at %v but previous interval ended at %v", i, interval.lower, overall.upper) } overall.upper = interval.upper intervals = append(intervals, interval) } clients := make([]*LogClient, 0, len(cfg.Shard)) for i, shard := range cfg.Shard { opts := jsonclient.Options{} opts.PublicKeyDER = shard.GetPublicKeyDer() c, err := New(shard.Uri, hc, opts) if err != nil { return nil, fmt.Errorf("failed to create client for cfg.Shard[%d]: %v", i, err) } clients = append(clients, c) } tlc := TemporalLogClient{ Clients: clients, intervals: intervals, } return &tlc, nil } // GetAcceptedRoots retrieves the set of acceptable root certificates for all // of the shards of a temporal log (i.e. the union). func (tlc *TemporalLogClient) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) { type result struct { roots []ct.ASN1Cert err error } results := make(chan result, len(tlc.Clients)) for _, c := range tlc.Clients { go func(c *LogClient) { var r result r.roots, r.err = c.GetAcceptedRoots(ctx) results <- r }(c) } var allRoots []ct.ASN1Cert seen := make(map[[sha256.Size]byte]bool) for range tlc.Clients { r := <-results if r.err != nil { return nil, r.err } for _, root := range r.roots { h := sha256.Sum256(root.Data) if seen[h] { continue } seen[h] = true allRoots = append(allRoots, root) } } return allRoots, nil } // AddChain adds the (DER represented) X509 chain to the appropriate log. func (tlc *TemporalLogClient) AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { return tlc.addChain(ctx, ct.X509LogEntryType, ct.AddChainPath, chain) } // AddPreChain adds the (DER represented) Precertificate chain to the appropriate log. func (tlc *TemporalLogClient) AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { return tlc.addChain(ctx, ct.PrecertLogEntryType, ct.AddPreChainPath, chain) } func (tlc *TemporalLogClient) addChain(ctx context.Context, ctype ct.LogEntryType, path string, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { // Parse the first entry in the chain if len(chain) == 0 { return nil, errors.New("missing chain") } cert, err := x509.ParseCertificate(chain[0].Data) if err != nil { return nil, fmt.Errorf("failed to parse initial chain entry: %v", err) } cidx, err := tlc.IndexByDate(cert.NotAfter) if err != nil { return nil, fmt.Errorf("failed to find log to process cert: %v", err) } return tlc.Clients[cidx].addChainWithRetry(ctx, ctype, path, chain) } // IndexByDate returns the index of the Clients entry that is appropriate for the given // date. func (tlc *TemporalLogClient) IndexByDate(when time.Time) (int, error) { for i, interval := range tlc.intervals { if (interval.lower != nil) && when.Before(*interval.lower) { continue } if (interval.upper != nil) && !when.Before(*interval.upper) { continue } return i, nil } return -1, fmt.Errorf("no log found encompassing date %v", when) } func shardInterval(cfg *configpb.LogShardConfig) (interval, error) { var interval interval if cfg.NotAfterStart != nil { t, err := ptypes.Timestamp(cfg.NotAfterStart) if err != nil { return interval, fmt.Errorf("failed to parse NotAfterStart: %v", err) } interval.lower = &t } if cfg.NotAfterLimit != nil { t, err := ptypes.Timestamp(cfg.NotAfterLimit) if err != nil { return interval, fmt.Errorf("failed to parse NotAfterLimit: %v", err) } interval.upper = &t } if interval.lower != nil && interval.upper != nil && !(*interval.lower).Before(*interval.upper) { return interval, errors.New("inverted interval") } return interval, nil }