1// Package client is a CT log client implementation and contains types and code
2// for interacting with RFC6962-compliant CT Log instances.
3// See http://tools.ietf.org/html/rfc6962 for details
4package client
5
6import (
7	"bytes"
8	"crypto/sha256"
9	"encoding/base64"
10	"encoding/json"
11	"errors"
12	"fmt"
13	"io/ioutil"
14	"log"
15	"net/http"
16	"net/url"
17	"strconv"
18	"time"
19
20	ct "github.com/google/certificate-transparency/go"
21	"golang.org/x/net/context"
22)
23
24// URI paths for CT Log endpoints
25const (
26	AddChainPath          = "/ct/v1/add-chain"
27	AddPreChainPath       = "/ct/v1/add-pre-chain"
28	AddJSONPath           = "/ct/v1/add-json"
29	GetSTHPath            = "/ct/v1/get-sth"
30	GetEntriesPath        = "/ct/v1/get-entries"
31	GetProofByHashPath    = "/ct/v1/get-proof-by-hash"
32	GetSTHConsistencyPath = "/ct/v1/get-sth-consistency"
33)
34
35// LogClient represents a client for a given CT Log instance
36type LogClient struct {
37	uri        string                // the base URI of the log. e.g. http://ct.googleapis/pilot
38	httpClient *http.Client          // used to interact with the log via HTTP
39	verifier   *ct.SignatureVerifier // nil if no public key for log available
40}
41
42//////////////////////////////////////////////////////////////////////////////////
43// JSON structures follow.
44// These represent the structures returned by the CT Log server.
45//////////////////////////////////////////////////////////////////////////////////
46
47// addChainRequest represents the JSON request body sent to the add-chain CT
48// method.
49type addChainRequest struct {
50	Chain [][]byte `json:"chain"`
51}
52
53// addChainResponse represents the JSON response to the add-chain CT method.
54// An SCT represents a Log's promise to integrate a [pre-]certificate into the
55// log within a defined period of time.
56type addChainResponse struct {
57	SCTVersion ct.Version `json:"sct_version"` // SCT structure version
58	ID         []byte     `json:"id"`          // Log ID
59	Timestamp  uint64     `json:"timestamp"`   // Timestamp of issuance
60	Extensions string     `json:"extensions"`  // Holder for any CT extensions
61	Signature  []byte     `json:"signature"`   // Log signature for this SCT
62}
63
64// addJSONRequest represents the JSON request body sent to the add-json CT
65// method.
66type addJSONRequest struct {
67	Data interface{} `json:"data"`
68}
69
70// getSTHResponse respresents the JSON response to the get-sth CT method
71type getSTHResponse struct {
72	TreeSize          uint64 `json:"tree_size"`           // Number of certs in the current tree
73	Timestamp         uint64 `json:"timestamp"`           // Time that the tree was created
74	SHA256RootHash    []byte `json:"sha256_root_hash"`    // Root hash of the tree
75	TreeHeadSignature []byte `json:"tree_head_signature"` // Log signature for this STH
76}
77
78// getConsistencyProofResponse represents the JSON response to the get-consistency-proof CT method
79type getConsistencyProofResponse struct {
80	Consistency [][]byte `json:"consistency"`
81}
82
83// getAuditProofResponse represents the JSON response to the CT get-audit-proof method
84type getAuditProofResponse struct {
85	Hash     []string `json:"hash"`      // the hashes which make up the proof
86	TreeSize uint64   `json:"tree_size"` // the tree size against which this proof is constructed
87}
88
89// getAcceptedRootsResponse represents the JSON response to the CT get-roots method.
90type getAcceptedRootsResponse struct {
91	Certificates []string `json:"certificates"`
92}
93
94// getEntryAndProodReponse represents the JSON response to the CT get-entry-and-proof method
95type getEntryAndProofResponse struct {
96	LeafInput string   `json:"leaf_input"` // the entry itself
97	ExtraData string   `json:"extra_data"` // any chain provided when the entry was added to the log
98	AuditPath []string `json:"audit_path"` // the corresponding proof
99}
100
101// GetProofByHashResponse represents the JSON response to the CT get-proof-by-hash method.
102type GetProofByHashResponse struct {
103	LeafIndex int64    `json:"leaf_index"` // The 0-based index of the end entity corresponding to the "hash" parameter.
104	AuditPath [][]byte `json:"audit_path"` // An array of base64-encoded Merkle Tree nodes proving the inclusion of the chosen certificate.
105}
106
107// New constructs a new LogClient instance.
108// |uri| is the base URI of the CT log instance to interact with, e.g.
109// http://ct.googleapis.com/pilot
110// |hc| is the underlying client to be used for HTTP requests to the CT log.
111func New(uri string, hc *http.Client) *LogClient {
112	if hc == nil {
113		hc = new(http.Client)
114	}
115	return &LogClient{uri: uri, httpClient: hc}
116}
117
118// NewWithPubKey constructs a new LogClient instance that includes public
119// key information for the log; this instance will check signatures on
120// responses from the log.
121func NewWithPubKey(uri string, hc *http.Client, pemEncodedKey string) (*LogClient, error) {
122	pubkey, _, rest, err := ct.PublicKeyFromPEM([]byte(pemEncodedKey))
123	if err != nil {
124		return nil, err
125	}
126	if len(rest) > 0 {
127		return nil, errors.New("extra data found after PEM key decoded")
128	}
129
130	verifier, err := ct.NewSignatureVerifier(pubkey)
131	if err != nil {
132		return nil, err
133	}
134
135	if hc == nil {
136		hc = new(http.Client)
137	}
138	return &LogClient{uri: uri, httpClient: hc, verifier: verifier}, nil
139}
140
141// Makes a HTTP call to |uri|, and attempts to parse the response as a
142// JSON representation of the structure in |res|. Uses |ctx| to
143// control the HTTP call (so it can have a timeout or be cancelled by
144// the caller), and |httpClient| to make the actual HTTP call.
145// Returns a non-nil |error| if there was a problem.
146func fetchAndParse(ctx context.Context, httpClient *http.Client, uri string, res interface{}) error {
147	req, err := http.NewRequest(http.MethodGet, uri, nil)
148	if err != nil {
149		return err
150	}
151	req.Cancel = ctx.Done()
152	resp, err := httpClient.Do(req)
153	if err != nil {
154		return err
155	}
156	defer resp.Body.Close()
157	// Make sure everything is read, so http.Client can reuse the connection.
158	defer ioutil.ReadAll(resp.Body)
159
160	if resp.StatusCode != 200 {
161		return fmt.Errorf("got HTTP Status %s", resp.Status)
162	}
163
164	if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
165		return err
166	}
167
168	return nil
169}
170
171// Makes a HTTP POST call to |uri|, and attempts to parse the response as a JSON
172// representation of the structure in |res|.
173// Returns a non-nil |error| if there was a problem.
174func (c *LogClient) postAndParse(uri string, req interface{}, res interface{}) (*http.Response, string, error) {
175	postBody, err := json.Marshal(req)
176	if err != nil {
177		return nil, "", err
178	}
179	httpReq, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(postBody))
180	if err != nil {
181		return nil, "", err
182	}
183	httpReq.Header.Set("Content-Type", "application/json")
184	resp, err := c.httpClient.Do(httpReq)
185	// Read all of the body, if there is one, so that the http.Client can do
186	// Keep-Alive:
187	var body []byte
188	if resp != nil {
189		body, err = ioutil.ReadAll(resp.Body)
190		resp.Body.Close()
191	}
192	if err != nil {
193		return resp, string(body), err
194	}
195	if resp.StatusCode == 200 {
196		if err != nil {
197			return resp, string(body), err
198		}
199		if err = json.Unmarshal(body, &res); err != nil {
200			return resp, string(body), err
201		}
202	}
203	return resp, string(body), nil
204}
205
206func backoffForRetry(ctx context.Context, d time.Duration) error {
207	backoffTimer := time.NewTimer(d)
208	if ctx != nil {
209		select {
210		case <-ctx.Done():
211			return ctx.Err()
212		case <-backoffTimer.C:
213		}
214	} else {
215		<-backoffTimer.C
216	}
217	return nil
218}
219
220// Attempts to add |chain| to the log, using the api end-point specified by
221// |path|. If provided context expires before submission is complete an
222// error will be returned.
223func (c *LogClient) addChainWithRetry(ctx context.Context, ctype ct.LogEntryType, path string, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
224	var resp addChainResponse
225	var req addChainRequest
226	for _, link := range chain {
227		req.Chain = append(req.Chain, link)
228	}
229	httpStatus := "Unknown"
230	backoffSeconds := 0
231	done := false
232	for !done {
233		if backoffSeconds > 0 {
234			log.Printf("Got %s, backing-off %d seconds", httpStatus, backoffSeconds)
235		}
236		err := backoffForRetry(ctx, time.Second*time.Duration(backoffSeconds))
237		if err != nil {
238			return nil, err
239		}
240		if backoffSeconds > 0 {
241			backoffSeconds = 0
242		}
243		httpResp, _, err := c.postAndParse(c.uri+path, &req, &resp)
244		if err != nil {
245			backoffSeconds = 10
246			continue
247		}
248		switch {
249		case httpResp.StatusCode == 200:
250			done = true
251		case httpResp.StatusCode == 408:
252			// request timeout, retry immediately
253		case httpResp.StatusCode == 503:
254			// Retry
255			backoffSeconds = 10
256			if retryAfter := httpResp.Header.Get("Retry-After"); retryAfter != "" {
257				if seconds, err := strconv.Atoi(retryAfter); err == nil {
258					backoffSeconds = seconds
259				}
260			}
261		default:
262			return nil, fmt.Errorf("got HTTP Status %s", httpResp.Status)
263		}
264		httpStatus = httpResp.Status
265	}
266
267	ds, err := ct.UnmarshalDigitallySigned(bytes.NewReader(resp.Signature))
268	if err != nil {
269		return nil, err
270	}
271
272	var logID ct.SHA256Hash
273	copy(logID[:], resp.ID)
274	sct := &ct.SignedCertificateTimestamp{
275		SCTVersion: resp.SCTVersion,
276		LogID:      logID,
277		Timestamp:  resp.Timestamp,
278		Extensions: ct.CTExtensions(resp.Extensions),
279		Signature:  *ds}
280	err = c.VerifySCTSignature(*sct, ctype, chain)
281	if err != nil {
282		return nil, err
283	}
284	return sct, nil
285}
286
287// AddChain adds the (DER represented) X509 |chain| to the log.
288func (c *LogClient) AddChain(chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
289	return c.addChainWithRetry(nil, ct.X509LogEntryType, AddChainPath, chain)
290}
291
292// AddPreChain adds the (DER represented) Precertificate |chain| to the log.
293func (c *LogClient) AddPreChain(chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
294	return c.addChainWithRetry(nil, ct.PrecertLogEntryType, AddPreChainPath, chain)
295}
296
297// AddChainWithContext adds the (DER represented) X509 |chain| to the log and
298// fails if the provided context expires before the chain is submitted.
299func (c *LogClient) AddChainWithContext(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
300	return c.addChainWithRetry(ctx, ct.X509LogEntryType, AddChainPath, chain)
301}
302
303// AddJSON submits arbitrary data to to XJSON server.
304func (c *LogClient) AddJSON(data interface{}) (*ct.SignedCertificateTimestamp, error) {
305	req := addJSONRequest{
306		Data: data,
307	}
308	var resp addChainResponse
309	_, _, err := c.postAndParse(c.uri+AddJSONPath, &req, &resp)
310	if err != nil {
311		return nil, err
312	}
313	ds, err := ct.UnmarshalDigitallySigned(bytes.NewReader(resp.Signature))
314	if err != nil {
315		return nil, err
316	}
317	var logID ct.SHA256Hash
318	copy(logID[:], resp.ID)
319	return &ct.SignedCertificateTimestamp{
320		SCTVersion: resp.SCTVersion,
321		LogID:      logID,
322		Timestamp:  resp.Timestamp,
323		Extensions: ct.CTExtensions(resp.Extensions),
324		Signature:  *ds}, nil
325}
326
327// GetSTH retrieves the current STH from the log.
328// Returns a populated SignedTreeHead, or a non-nil error.
329func (c *LogClient) GetSTH() (sth *ct.SignedTreeHead, err error) {
330	var resp getSTHResponse
331	if err = fetchAndParse(context.TODO(), c.httpClient, c.uri+GetSTHPath, &resp); err != nil {
332		return
333	}
334	sth = &ct.SignedTreeHead{
335		TreeSize:  resp.TreeSize,
336		Timestamp: resp.Timestamp,
337	}
338
339	if len(resp.SHA256RootHash) != sha256.Size {
340		return nil, fmt.Errorf("sha256_root_hash is invalid length, expected %d got %d", sha256.Size, len(resp.SHA256RootHash))
341	}
342	copy(sth.SHA256RootHash[:], resp.SHA256RootHash)
343
344	ds, err := ct.UnmarshalDigitallySigned(bytes.NewReader(resp.TreeHeadSignature))
345	if err != nil {
346		return nil, err
347	}
348	sth.TreeHeadSignature = *ds
349	err = c.VerifySTHSignature(*sth)
350	if err != nil {
351		return nil, err
352	}
353	return
354}
355
356// VerifySTHSignature checks the signature in sth, returning any error encountered or nil if verification is
357// successful.
358func (c *LogClient) VerifySTHSignature(sth ct.SignedTreeHead) error {
359	if c.verifier == nil {
360		// Can't verify signatures without a verifier
361		return nil
362	}
363	return c.verifier.VerifySTHSignature(sth)
364}
365
366// VerifySCTSignature checks the signature in sct for the given LogEntryType, with associated certificate chain.
367func (c *LogClient) VerifySCTSignature(sct ct.SignedCertificateTimestamp, ctype ct.LogEntryType, certData []ct.ASN1Cert) error {
368	if c.verifier == nil {
369		// Can't verify signatures without a verifier
370		return nil
371	}
372
373	if ctype == ct.PrecertLogEntryType {
374		// TODO(drysdale): cope with pre-certs, which need to have the
375		// following fields set:
376		//    leaf.PrecertEntry.TBSCertificate
377		//    leaf.PrecertEntry.IssuerKeyHash  (SHA-256 of issuer's public key)
378		return errors.New("SCT verification for pre-certificates unimplemented")
379	}
380	// Build enough of a Merkle tree leaf for the verifier to work on.
381	leaf := ct.MerkleTreeLeaf{
382		Version:  sct.SCTVersion,
383		LeafType: ct.TimestampedEntryLeafType,
384		TimestampedEntry: ct.TimestampedEntry{
385			Timestamp:  sct.Timestamp,
386			EntryType:  ctype,
387			X509Entry:  certData[0],
388			Extensions: sct.Extensions}}
389	entry := ct.LogEntry{Leaf: leaf}
390	return c.verifier.VerifySCTSignature(sct, entry)
391}
392
393// GetSTHConsistency retrieves the consistency proof between two snapshots.
394func (c *LogClient) GetSTHConsistency(ctx context.Context, first, second uint64) ([][]byte, error) {
395	u := fmt.Sprintf("%s%s?first=%d&second=%d", c.uri, GetSTHConsistencyPath, first, second)
396	var resp getConsistencyProofResponse
397	if err := fetchAndParse(ctx, c.httpClient, u, &resp); err != nil {
398		return nil, err
399	}
400	return resp.Consistency, nil
401}
402
403// GetProofByHash returns an audit path for the hash of an SCT.
404func (c *LogClient) GetProofByHash(ctx context.Context, hash []byte, treeSize uint64) (*GetProofByHashResponse, error) {
405	b64Hash := url.QueryEscape(base64.StdEncoding.EncodeToString(hash))
406	u := fmt.Sprintf("%s%s?tree_size=%d&hash=%v", c.uri, GetProofByHashPath, treeSize, b64Hash)
407	var resp GetProofByHashResponse
408	if err := fetchAndParse(ctx, c.httpClient, u, &resp); err != nil {
409		return nil, err
410	}
411	return &resp, nil
412}
413