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