// Copyright 2014 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package client is a CT log client implementation and contains types and code // for interacting with RFC6962-compliant CT Log instances. // See http://tools.ietf.org/html/rfc6962 for details package client import ( "context" "encoding/base64" "fmt" "net/http" "strconv" ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/jsonclient" "github.com/google/certificate-transparency-go/tls" ) // LogClient represents a client for a given CT Log instance type LogClient struct { jsonclient.JSONClient } // CheckLogClient is an interface that allows (just) checking of various log contents. type CheckLogClient interface { BaseURI() string GetSTH(context.Context) (*ct.SignedTreeHead, error) GetSTHConsistency(ctx context.Context, first, second uint64) ([][]byte, error) GetProofByHash(ctx context.Context, hash []byte, treeSize uint64) (*ct.GetProofByHashResponse, error) } // New constructs a new LogClient instance. // |uri| is the base URI of the CT log instance to interact with, e.g. // https://ct.googleapis.com/pilot // |hc| is the underlying client to be used for HTTP requests to the CT log. // |opts| can be used to provide a custom logger interface and a public key // for signature verification. func New(uri string, hc *http.Client, opts jsonclient.Options) (*LogClient, error) { logClient, err := jsonclient.New(uri, hc, opts) if err != nil { return nil, err } return &LogClient{*logClient}, err } // RspError represents an error that occurred when processing a response from a server, // and also includes key details from the http.Response that triggered the error. type RspError struct { Err error StatusCode int Body []byte } // Error formats the RspError instance, focusing on the error. func (e RspError) Error() string { return e.Err.Error() } // Attempts to add |chain| to the log, using the api end-point specified by // |path|. If provided context expires before submission is complete an // error will be returned. func (c *LogClient) addChainWithRetry(ctx context.Context, ctype ct.LogEntryType, path string, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { var resp ct.AddChainResponse var req ct.AddChainRequest for _, link := range chain { req.Chain = append(req.Chain, link.Data) } httpRsp, body, err := c.PostAndParseWithRetry(ctx, path, &req, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } var ds ct.DigitallySigned if rest, err := tls.Unmarshal(resp.Signature, &ds); err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } else if len(rest) > 0 { return nil, RspError{ Err: fmt.Errorf("trailing data (%d bytes) after DigitallySigned", len(rest)), StatusCode: httpRsp.StatusCode, Body: body, } } exts, err := base64.StdEncoding.DecodeString(resp.Extensions) if err != nil { return nil, RspError{ Err: fmt.Errorf("invalid base64 data in Extensions (%q): %v", resp.Extensions, err), StatusCode: httpRsp.StatusCode, Body: body, } } var logID ct.LogID copy(logID.KeyID[:], resp.ID) sct := &ct.SignedCertificateTimestamp{ SCTVersion: resp.SCTVersion, LogID: logID, Timestamp: resp.Timestamp, Extensions: ct.CTExtensions(exts), Signature: ds, } if err := c.VerifySCTSignature(*sct, ctype, chain); err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return sct, nil } // AddChain adds the (DER represented) X509 |chain| to the log. func (c *LogClient) AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { return c.addChainWithRetry(ctx, ct.X509LogEntryType, ct.AddChainPath, chain) } // AddPreChain adds the (DER represented) Precertificate |chain| to the log. func (c *LogClient) AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) { return c.addChainWithRetry(ctx, ct.PrecertLogEntryType, ct.AddPreChainPath, chain) } // AddJSON submits arbitrary data to to XJSON server. func (c *LogClient) AddJSON(ctx context.Context, data interface{}) (*ct.SignedCertificateTimestamp, error) { req := ct.AddJSONRequest{Data: data} var resp ct.AddChainResponse httpRsp, body, err := c.PostAndParse(ctx, ct.AddJSONPath, &req, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } var ds ct.DigitallySigned if rest, err := tls.Unmarshal(resp.Signature, &ds); err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } else if len(rest) > 0 { return nil, RspError{ Err: fmt.Errorf("trailing data (%d bytes) after DigitallySigned", len(rest)), StatusCode: httpRsp.StatusCode, Body: body, } } var logID ct.LogID copy(logID.KeyID[:], resp.ID) return &ct.SignedCertificateTimestamp{ SCTVersion: resp.SCTVersion, LogID: logID, Timestamp: resp.Timestamp, Extensions: ct.CTExtensions(resp.Extensions), Signature: ds, }, nil } // GetSTH retrieves the current STH from the log. // Returns a populated SignedTreeHead, or a non-nil error (which may be of type // RspError if a raw http.Response is available). func (c *LogClient) GetSTH(ctx context.Context) (*ct.SignedTreeHead, error) { var resp ct.GetSTHResponse httpRsp, body, err := c.GetAndParse(ctx, ct.GetSTHPath, nil, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } sth, err := resp.ToSignedTreeHead() if err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } if err := c.VerifySTHSignature(*sth); err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return sth, nil } // VerifySTHSignature checks the signature in sth, returning any error encountered or nil if verification is // successful. func (c *LogClient) VerifySTHSignature(sth ct.SignedTreeHead) error { if c.Verifier == nil { // Can't verify signatures without a verifier return nil } return c.Verifier.VerifySTHSignature(sth) } // VerifySCTSignature checks the signature in sct for the given LogEntryType, with associated certificate chain. func (c *LogClient) VerifySCTSignature(sct ct.SignedCertificateTimestamp, ctype ct.LogEntryType, certData []ct.ASN1Cert) error { if c.Verifier == nil { // Can't verify signatures without a verifier return nil } leaf, err := ct.MerkleTreeLeafFromRawChain(certData, ctype, sct.Timestamp) if err != nil { return fmt.Errorf("failed to build MerkleTreeLeaf: %v", err) } entry := ct.LogEntry{Leaf: *leaf} return c.Verifier.VerifySCTSignature(sct, entry) } // GetSTHConsistency retrieves the consistency proof between two snapshots. func (c *LogClient) GetSTHConsistency(ctx context.Context, first, second uint64) ([][]byte, error) { base10 := 10 params := map[string]string{ "first": strconv.FormatUint(first, base10), "second": strconv.FormatUint(second, base10), } var resp ct.GetSTHConsistencyResponse httpRsp, body, err := c.GetAndParse(ctx, ct.GetSTHConsistencyPath, params, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } return resp.Consistency, nil } // GetProofByHash returns an audit path for the hash of an SCT. func (c *LogClient) GetProofByHash(ctx context.Context, hash []byte, treeSize uint64) (*ct.GetProofByHashResponse, error) { b64Hash := base64.StdEncoding.EncodeToString(hash) base10 := 10 params := map[string]string{ "tree_size": strconv.FormatUint(treeSize, base10), "hash": b64Hash, } var resp ct.GetProofByHashResponse httpRsp, body, err := c.GetAndParse(ctx, ct.GetProofByHashPath, params, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } return &resp, nil } // GetAcceptedRoots retrieves the set of acceptable root certificates for a log. func (c *LogClient) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) { var resp ct.GetRootsResponse httpRsp, body, err := c.GetAndParse(ctx, ct.GetRootsPath, nil, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } var roots []ct.ASN1Cert for _, cert64 := range resp.Certificates { cert, err := base64.StdEncoding.DecodeString(cert64) if err != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } roots = append(roots, ct.ASN1Cert{Data: cert}) } return roots, nil } // GetEntryAndProof returns a log entry and audit path for the index of a leaf. func (c *LogClient) GetEntryAndProof(ctx context.Context, index, treeSize uint64) (*ct.GetEntryAndProofResponse, error) { base10 := 10 params := map[string]string{ "leaf_index": strconv.FormatUint(index, base10), "tree_size": strconv.FormatUint(treeSize, base10), } var resp ct.GetEntryAndProofResponse httpRsp, body, err := c.GetAndParse(ctx, ct.GetEntryAndProofPath, params, &resp) if err != nil { if httpRsp != nil { return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body} } return nil, err } return &resp, nil }