| // 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 |
| } |