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