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