1// Copyright 2018 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
15// Package loglist allows parsing and searching of the master CT Log list.
16package loglist
17
18import (
19	"bytes"
20	"crypto"
21	"crypto/ecdsa"
22	"crypto/rsa"
23	"crypto/sha256"
24	"encoding/base64"
25	"encoding/hex"
26	"encoding/json"
27	"fmt"
28	"regexp"
29	"strings"
30	"unicode"
31
32	"github.com/google/certificate-transparency-go/tls"
33)
34
35const (
36	// LogListURL has the master URL for Google Chrome's log list.
37	LogListURL = "https://www.gstatic.com/ct/log_list/log_list.json"
38	// LogListSignatureURL has the URL for the signature over Google Chrome's log list.
39	LogListSignatureURL = "https://www.gstatic.com/ct/log_list/log_list.sig"
40)
41
42// Manually mapped from https://www.gstatic.com/ct/log_list/log_list_schema.json
43
44// LogList holds a collection of logs and their operators
45type LogList struct {
46	Logs      []Log      `json:"logs"`
47	Operators []Operator `json:"operators"`
48}
49
50// Operator describes a log operator
51type Operator struct {
52	ID   int    `json:"id"`
53	Name string `json:"name"`
54}
55
56// Log describes a log.
57type Log struct {
58	Description       string `json:"description"`
59	Key               []byte `json:"key"`
60	MaximumMergeDelay int    `json:"maximum_merge_delay"` // seconds
61	OperatedBy        []int  `json:"operated_by"`         // List of log operators
62	URL               string `json:"url"`
63	FinalSTH          *STH   `json:"final_sth,omitempty"`
64	DisqualifiedAt    int    `json:"disqualified_at,omitempty"`
65	DNSAPIEndpoint    string `json:"dns_api_endpoint,omitempty"` // DNS API endpoint for the log
66}
67
68// STH describes a signed tree head from a log.
69type STH struct {
70	TreeSize          int    `json:"tree_size"`
71	Timestamp         int    `json:"timestamp"`
72	SHA256RootHash    []byte `json:"sha256_root_hash"`
73	TreeHeadSignature []byte `json:"tree_head_signature"`
74}
75
76// NewFromJSON creates a LogList from JSON encoded data.
77func NewFromJSON(llData []byte) (*LogList, error) {
78	var ll LogList
79	if err := json.Unmarshal(llData, &ll); err != nil {
80		return nil, fmt.Errorf("failed to parse log list: %v", err)
81	}
82	return &ll, nil
83}
84
85// NewFromSignedJSON creates a LogList from JSON encoded data, checking a
86// signature along the way. The signature data should be provided as the
87// raw signature data.
88func NewFromSignedJSON(llData, rawSig []byte, pubKey crypto.PublicKey) (*LogList, error) {
89	sigAlgo := tls.Anonymous
90	switch pkType := pubKey.(type) {
91	case *rsa.PublicKey:
92		sigAlgo = tls.RSA
93	case *ecdsa.PublicKey:
94		sigAlgo = tls.ECDSA
95	default:
96		return nil, fmt.Errorf("Unsupported public key type %v", pkType)
97	}
98	tlsSig := tls.DigitallySigned{
99		Algorithm: tls.SignatureAndHashAlgorithm{
100			Hash:      tls.SHA256,
101			Signature: sigAlgo,
102		},
103		Signature: rawSig,
104	}
105	if err := tls.VerifySignature(pubKey, llData, tlsSig); err != nil {
106		return nil, fmt.Errorf("failed to verify signature: %v", err)
107	}
108	return NewFromJSON(llData)
109}
110
111// FindLogByName returns all logs whose names contain the given string.
112func (ll *LogList) FindLogByName(name string) []*Log {
113	name = strings.ToLower(name)
114	var results []*Log
115	for _, log := range ll.Logs {
116		if strings.Contains(strings.ToLower(log.Description), name) {
117			log := log
118			results = append(results, &log)
119		}
120	}
121	return results
122}
123
124// FindLogByURL finds the log with the given URL.
125func (ll *LogList) FindLogByURL(url string) *Log {
126	for _, log := range ll.Logs {
127		// Don't count trailing slashes
128		if strings.TrimRight(log.URL, "/") == strings.TrimRight(url, "/") {
129			return &log
130		}
131	}
132	return nil
133}
134
135// FindLogByKeyHash finds the log with the given key hash.
136func (ll *LogList) FindLogByKeyHash(keyhash [sha256.Size]byte) *Log {
137	for _, log := range ll.Logs {
138		h := sha256.Sum256(log.Key)
139		if bytes.Equal(h[:], keyhash[:]) {
140			return &log
141		}
142	}
143	return nil
144}
145
146// FindLogByKeyHashPrefix finds all logs whose key hash starts with the prefix.
147func (ll *LogList) FindLogByKeyHashPrefix(prefix string) []*Log {
148	var results []*Log
149	for _, log := range ll.Logs {
150		h := sha256.Sum256(log.Key)
151		hh := hex.EncodeToString(h[:])
152		if strings.HasPrefix(hh, prefix) {
153			log := log
154			results = append(results, &log)
155		}
156	}
157	return results
158}
159
160// FindLogByKey finds the log with the given DER-encoded key.
161func (ll *LogList) FindLogByKey(key []byte) *Log {
162	for _, log := range ll.Logs {
163		if bytes.Equal(log.Key[:], key) {
164			return &log
165		}
166	}
167	return nil
168}
169
170var hexDigits = regexp.MustCompile("^[0-9a-fA-F]+$")
171
172// FuzzyFindLog tries to find logs that match the given unspecified input,
173// whose format is unspecified.  This generally returns a single log, but
174// if text input that matches multiple log descriptions is provided, then
175// multiple logs may be returned.
176func (ll *LogList) FuzzyFindLog(input string) []*Log {
177	input = strings.Trim(input, " \t")
178	if logs := ll.FindLogByName(input); len(logs) > 0 {
179		return logs
180	}
181	if log := ll.FindLogByURL(input); log != nil {
182		return []*Log{log}
183	}
184	// Try assuming the input is binary data of some form.  First base64:
185	if data, err := base64.StdEncoding.DecodeString(input); err == nil {
186		if len(data) == sha256.Size {
187			var hash [sha256.Size]byte
188			copy(hash[:], data)
189			if log := ll.FindLogByKeyHash(hash); log != nil {
190				return []*Log{log}
191			}
192		}
193		if log := ll.FindLogByKey(data); log != nil {
194			return []*Log{log}
195		}
196	}
197	// Now hex, but strip all internal whitespace first.
198	input = stripInternalSpace(input)
199	if data, err := hex.DecodeString(input); err == nil {
200		if len(data) == sha256.Size {
201			var hash [sha256.Size]byte
202			copy(hash[:], data)
203			if log := ll.FindLogByKeyHash(hash); log != nil {
204				return []*Log{log}
205			}
206		}
207		if log := ll.FindLogByKey(data); log != nil {
208			return []*Log{log}
209		}
210	}
211	// Finally, allow hex strings with an odd number of digits.
212	if hexDigits.MatchString(input) {
213		if logs := ll.FindLogByKeyHashPrefix(input); len(logs) > 0 {
214			return logs
215		}
216	}
217
218	return nil
219}
220
221func stripInternalSpace(input string) string {
222	return strings.Map(func(r rune) rune {
223		if !unicode.IsSpace(r) {
224			return r
225		}
226		return -1
227	}, input)
228}
229