1// Copyright 2017 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package client 16 17import ( 18 "context" 19 "crypto/sha256" 20 "errors" 21 "fmt" 22 "io/ioutil" 23 "net/http" 24 "time" 25 26 "github.com/golang/protobuf/proto" 27 "github.com/golang/protobuf/ptypes" 28 ct "github.com/google/certificate-transparency-go" 29 "github.com/google/certificate-transparency-go/client/configpb" 30 "github.com/google/certificate-transparency-go/jsonclient" 31 "github.com/google/certificate-transparency-go/x509" 32) 33 34type interval struct { 35 lower *time.Time // nil => no lower bound 36 upper *time.Time // nil => no upper bound 37} 38 39// TemporalLogConfigFromFile creates a TemporalLogConfig object from the given 40// filename, which should contain text-protobuf encoded configuration data. 41func TemporalLogConfigFromFile(filename string) (*configpb.TemporalLogConfig, error) { 42 if len(filename) == 0 { 43 return nil, errors.New("log config filename empty") 44 } 45 46 cfgText, err := ioutil.ReadFile(filename) 47 if err != nil { 48 return nil, fmt.Errorf("failed to read log config: %v", err) 49 } 50 51 var cfg configpb.TemporalLogConfig 52 if err := proto.UnmarshalText(string(cfgText), &cfg); err != nil { 53 return nil, fmt.Errorf("failed to parse log config: %v", err) 54 } 55 56 if len(cfg.Shard) == 0 { 57 return nil, errors.New("empty log config found") 58 } 59 return &cfg, nil 60} 61 62// AddLogClient is an interface that allows adding certificates and pre-certificates to a log. 63// Both LogClient and TemporalLogClient implement this interface, which allows users to 64// commonize code for adding certs to normal/temporal logs. 65type AddLogClient interface { 66 AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) 67 AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) 68 GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) 69} 70 71// TemporalLogClient allows [pre-]certificates to be uploaded to a temporal log. 72type TemporalLogClient struct { 73 Clients []*LogClient 74 intervals []interval 75} 76 77// NewTemporalLogClient builds a new client for interacting with a temporal log. 78// The provided config should be contiguous and chronological. 79func NewTemporalLogClient(cfg configpb.TemporalLogConfig, hc *http.Client) (*TemporalLogClient, error) { 80 if len(cfg.Shard) == 0 { 81 return nil, errors.New("empty config") 82 } 83 84 overall, err := shardInterval(cfg.Shard[0]) 85 if err != nil { 86 return nil, fmt.Errorf("cfg.Shard[0] invalid: %v", err) 87 } 88 intervals := make([]interval, 0, len(cfg.Shard)) 89 intervals = append(intervals, overall) 90 for i := 1; i < len(cfg.Shard); i++ { 91 interval, err := shardInterval(cfg.Shard[i]) 92 if err != nil { 93 return nil, fmt.Errorf("cfg.Shard[%d] invalid: %v", i, err) 94 } 95 if overall.upper == nil { 96 return nil, fmt.Errorf("cfg.Shard[%d] extends an interval with no upper bound", i) 97 } 98 if interval.lower == nil { 99 return nil, fmt.Errorf("cfg.Shard[%d] has no lower bound but extends an interval", i) 100 } 101 if !interval.lower.Equal(*overall.upper) { 102 return nil, fmt.Errorf("cfg.Shard[%d] starts at %v but previous interval ended at %v", i, interval.lower, overall.upper) 103 } 104 overall.upper = interval.upper 105 intervals = append(intervals, interval) 106 } 107 clients := make([]*LogClient, 0, len(cfg.Shard)) 108 for i, shard := range cfg.Shard { 109 opts := jsonclient.Options{} 110 opts.PublicKeyDER = shard.GetPublicKeyDer() 111 c, err := New(shard.Uri, hc, opts) 112 if err != nil { 113 return nil, fmt.Errorf("failed to create client for cfg.Shard[%d]: %v", i, err) 114 } 115 clients = append(clients, c) 116 } 117 tlc := TemporalLogClient{ 118 Clients: clients, 119 intervals: intervals, 120 } 121 return &tlc, nil 122} 123 124// GetAcceptedRoots retrieves the set of acceptable root certificates for all 125// of the shards of a temporal log (i.e. the union). 126func (tlc *TemporalLogClient) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) { 127 type result struct { 128 roots []ct.ASN1Cert 129 err error 130 } 131 results := make(chan result, len(tlc.Clients)) 132 for _, c := range tlc.Clients { 133 go func(c *LogClient) { 134 var r result 135 r.roots, r.err = c.GetAcceptedRoots(ctx) 136 results <- r 137 }(c) 138 } 139 140 var allRoots []ct.ASN1Cert 141 seen := make(map[[sha256.Size]byte]bool) 142 for range tlc.Clients { 143 r := <-results 144 if r.err != nil { 145 return nil, r.err 146 } 147 for _, root := range r.roots { 148 h := sha256.Sum256(root.Data) 149 if seen[h] { 150 continue 151 } 152 seen[h] = true 153 allRoots = append(allRoots, root) 154 } 155 } 156 return allRoots, nil 157} 158 159// AddChain adds the (DER represented) X509 chain to the appropriate log. 160func (tlc *TemporalLogClient) AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { 161 return tlc.addChain(ctx, ct.X509LogEntryType, ct.AddChainPath, chain) 162} 163 164// AddPreChain adds the (DER represented) Precertificate chain to the appropriate log. 165func (tlc *TemporalLogClient) AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { 166 return tlc.addChain(ctx, ct.PrecertLogEntryType, ct.AddPreChainPath, chain) 167} 168 169func (tlc *TemporalLogClient) addChain(ctx context.Context, ctype ct.LogEntryType, path string, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { 170 // Parse the first entry in the chain 171 if len(chain) == 0 { 172 return nil, errors.New("missing chain") 173 } 174 cert, err := x509.ParseCertificate(chain[0].Data) 175 if err != nil { 176 return nil, fmt.Errorf("failed to parse initial chain entry: %v", err) 177 } 178 cidx, err := tlc.IndexByDate(cert.NotAfter) 179 if err != nil { 180 return nil, fmt.Errorf("failed to find log to process cert: %v", err) 181 } 182 return tlc.Clients[cidx].addChainWithRetry(ctx, ctype, path, chain) 183} 184 185// IndexByDate returns the index of the Clients entry that is appropriate for the given 186// date. 187func (tlc *TemporalLogClient) IndexByDate(when time.Time) (int, error) { 188 for i, interval := range tlc.intervals { 189 if (interval.lower != nil) && when.Before(*interval.lower) { 190 continue 191 } 192 if (interval.upper != nil) && !when.Before(*interval.upper) { 193 continue 194 } 195 return i, nil 196 } 197 return -1, fmt.Errorf("no log found encompassing date %v", when) 198} 199 200func shardInterval(cfg *configpb.LogShardConfig) (interval, error) { 201 var interval interval 202 if cfg.NotAfterStart != nil { 203 t, err := ptypes.Timestamp(cfg.NotAfterStart) 204 if err != nil { 205 return interval, fmt.Errorf("failed to parse NotAfterStart: %v", err) 206 } 207 interval.lower = &t 208 } 209 if cfg.NotAfterLimit != nil { 210 t, err := ptypes.Timestamp(cfg.NotAfterLimit) 211 if err != nil { 212 return interval, fmt.Errorf("failed to parse NotAfterLimit: %v", err) 213 } 214 interval.upper = &t 215 } 216 217 if interval.lower != nil && interval.upper != nil && !(*interval.lower).Before(*interval.upper) { 218 return interval, errors.New("inverted interval") 219 } 220 return interval, nil 221} 222