This commit is contained in:
Mikaël Cluseau
2018-06-17 18:32:44 +11:00
parent f92c531f5d
commit 4d889632f6
500 changed files with 133832 additions and 0 deletions

View File

@ -0,0 +1,17 @@
// Copyright 2017 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 configpb
//go:generate protoc -I=. -I=$GOPATH/src --go_out=:. multilog.proto

View File

@ -0,0 +1,124 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: multilog.proto
/*
Package configpb is a generated protocol buffer package.
It is generated from these files:
multilog.proto
It has these top-level messages:
TemporalLogConfig
LogShardConfig
*/
package configpb
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// TemporalLogConfig is a set of LogShardConfig messages, whose
// time limits should be contiguous.
type TemporalLogConfig struct {
Shard []*LogShardConfig `protobuf:"bytes,1,rep,name=shard" json:"shard,omitempty"`
}
func (m *TemporalLogConfig) Reset() { *m = TemporalLogConfig{} }
func (m *TemporalLogConfig) String() string { return proto.CompactTextString(m) }
func (*TemporalLogConfig) ProtoMessage() {}
func (*TemporalLogConfig) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *TemporalLogConfig) GetShard() []*LogShardConfig {
if m != nil {
return m.Shard
}
return nil
}
// LogShardConfig describes the acceptable date range for a single shard of a temporal
// log.
type LogShardConfig struct {
Uri string `protobuf:"bytes,1,opt,name=uri" json:"uri,omitempty"`
// The log's public key in DER-encoded PKIX form.
PublicKeyDer []byte `protobuf:"bytes,2,opt,name=public_key_der,json=publicKeyDer,proto3" json:"public_key_der,omitempty"`
// not_after_start defines the start of the range of acceptable NotAfter
// values, inclusive.
// Leaving this unset implies no lower bound to the range.
NotAfterStart *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=not_after_start,json=notAfterStart" json:"not_after_start,omitempty"`
// not_after_limit defines the end of the range of acceptable NotAfter values,
// exclusive.
// Leaving this unset implies no upper bound to the range.
NotAfterLimit *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=not_after_limit,json=notAfterLimit" json:"not_after_limit,omitempty"`
}
func (m *LogShardConfig) Reset() { *m = LogShardConfig{} }
func (m *LogShardConfig) String() string { return proto.CompactTextString(m) }
func (*LogShardConfig) ProtoMessage() {}
func (*LogShardConfig) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *LogShardConfig) GetUri() string {
if m != nil {
return m.Uri
}
return ""
}
func (m *LogShardConfig) GetPublicKeyDer() []byte {
if m != nil {
return m.PublicKeyDer
}
return nil
}
func (m *LogShardConfig) GetNotAfterStart() *google_protobuf.Timestamp {
if m != nil {
return m.NotAfterStart
}
return nil
}
func (m *LogShardConfig) GetNotAfterLimit() *google_protobuf.Timestamp {
if m != nil {
return m.NotAfterLimit
}
return nil
}
func init() {
proto.RegisterType((*TemporalLogConfig)(nil), "configpb.TemporalLogConfig")
proto.RegisterType((*LogShardConfig)(nil), "configpb.LogShardConfig")
}
func init() { proto.RegisterFile("multilog.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 241 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x8f, 0xb1, 0x4e, 0xc3, 0x30,
0x14, 0x45, 0x65, 0x02, 0x08, 0xdc, 0x12, 0xc0, 0x93, 0xd5, 0x85, 0xa8, 0x62, 0xc8, 0xe4, 0x4a,
0xe5, 0x0b, 0xa0, 0x6c, 0x64, 0x4a, 0xbb, 0x47, 0x4e, 0xeb, 0x18, 0x0b, 0x3b, 0xcf, 0x72, 0x5e,
0x86, 0xfe, 0x25, 0x9f, 0x84, 0x1c, 0x2b, 0x43, 0x37, 0xb6, 0xa7, 0x77, 0xcf, 0xb9, 0xd2, 0xa5,
0xb9, 0x1b, 0x2d, 0x1a, 0x0b, 0x5a, 0xf8, 0x00, 0x08, 0xec, 0xee, 0x08, 0x7d, 0x67, 0xb4, 0x6f,
0x57, 0x2f, 0x1a, 0x40, 0x5b, 0xb5, 0x99, 0xfe, 0xed, 0xd8, 0x6d, 0xd0, 0x38, 0x35, 0xa0, 0x74,
0x3e, 0xa1, 0xeb, 0x1d, 0x7d, 0x3e, 0x28, 0xe7, 0x21, 0x48, 0x5b, 0x81, 0xde, 0x4d, 0x1e, 0x13,
0xf4, 0x66, 0xf8, 0x96, 0xe1, 0xc4, 0x49, 0x91, 0x95, 0x8b, 0x2d, 0x17, 0x73, 0x9f, 0xa8, 0x40,
0xef, 0x63, 0x92, 0xc0, 0x3a, 0x61, 0xeb, 0x5f, 0x42, 0xf3, 0xcb, 0x84, 0x3d, 0xd1, 0x6c, 0x0c,
0x86, 0x93, 0x82, 0x94, 0xf7, 0x75, 0x3c, 0xd9, 0x2b, 0xcd, 0xfd, 0xd8, 0x5a, 0x73, 0x6c, 0x7e,
0xd4, 0xb9, 0x39, 0xa9, 0xc0, 0xaf, 0x0a, 0x52, 0x2e, 0xeb, 0x65, 0xfa, 0x7e, 0xa9, 0xf3, 0xa7,
0x0a, 0xec, 0x83, 0x3e, 0xf6, 0x80, 0x8d, 0xec, 0x50, 0x85, 0x66, 0x40, 0x19, 0x90, 0x67, 0x05,
0x29, 0x17, 0xdb, 0x95, 0x48, 0x53, 0xc4, 0x3c, 0x45, 0x1c, 0xe6, 0x29, 0xf5, 0x43, 0x0f, 0xf8,
0x1e, 0x8d, 0x7d, 0x14, 0x2e, 0x3b, 0xac, 0x71, 0x06, 0xf9, 0xf5, 0xff, 0x3b, 0xaa, 0x28, 0xb4,
0xb7, 0x13, 0xf2, 0xf6, 0x17, 0x00, 0x00, 0xff, 0xff, 0xf8, 0xd9, 0x50, 0x5b, 0x5b, 0x01, 0x00,
0x00,
}

View File

@ -0,0 +1,43 @@
// Copyright 2017 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.
syntax = "proto3";
package configpb;
import "google/protobuf/timestamp.proto";
// TemporalLogConfig is a set of LogShardConfig messages, whose
// time limits should be contiguous.
message TemporalLogConfig {
repeated LogShardConfig shard = 1;
}
// LogShardConfig describes the acceptable date range for a single shard of a temporal
// log.
message LogShardConfig {
string uri = 1;
// The log's public key in DER-encoded PKIX form.
bytes public_key_der = 2;
// not_after_start defines the start of the range of acceptable NotAfter
// values, inclusive.
// Leaving this unset implies no lower bound to the range.
google.protobuf.Timestamp not_after_start = 3;
// not_after_limit defines the end of the range of acceptable NotAfter values,
// exclusive.
// Leaving this unset implies no upper bound to the range.
google.protobuf.Timestamp not_after_limit = 4;
}

View File

@ -0,0 +1,75 @@
// Copyright 2016 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
import (
"context"
"errors"
"strconv"
ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/x509"
)
// GetRawEntries exposes the /ct/v1/get-entries result with only the JSON parsing done.
func (c *LogClient) GetRawEntries(ctx context.Context, start, end int64) (*ct.GetEntriesResponse, error) {
if end < 0 {
return nil, errors.New("end should be >= 0")
}
if end < start {
return nil, errors.New("start should be <= end")
}
params := map[string]string{
"start": strconv.FormatInt(start, 10),
"end": strconv.FormatInt(end, 10),
}
if ctx == nil {
ctx = context.TODO()
}
var resp ct.GetEntriesResponse
httpRsp, body, err := c.GetAndParse(ctx, ct.GetEntriesPath, params, &resp)
if err != nil {
if httpRsp != nil {
return nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body}
}
return nil, err
}
return &resp, nil
}
// GetEntries attempts to retrieve the entries in the sequence [start, end] from the CT log server
// (RFC6962 s4.6) as parsed [pre-]certificates for convenience, held in a slice of ct.LogEntry structures.
// However, this does mean that any certificate parsing failures will cause a failure of the whole
// retrieval operation; for more robust retrieval of parsed certificates, use GetRawEntries() and invoke
// ct.LogEntryFromLeaf() on each individual entry.
func (c *LogClient) GetEntries(ctx context.Context, start, end int64) ([]ct.LogEntry, error) {
resp, err := c.GetRawEntries(ctx, start, end)
if err != nil {
return nil, err
}
entries := make([]ct.LogEntry, len(resp.Entries))
for i, entry := range resp.Entries {
index := start + int64(i)
logEntry, err := ct.LogEntryFromLeaf(index, &entry)
if _, ok := err.(x509.NonFatalErrors); !ok && err != nil {
return nil, err
}
entries[i] = *logEntry
}
return entries, nil
}

View File

@ -0,0 +1,283 @@
// 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"
"crypto/sha256"
"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
}
// New constructs a new LogClient instance.
// |uri| is the base URI of the CT log instance to interact with, e.g.
// http://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 customer 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 := ct.SignedTreeHead{
TreeSize: resp.TreeSize,
Timestamp: resp.Timestamp,
}
if len(resp.SHA256RootHash) != sha256.Size {
return nil, RspError{
Err: fmt.Errorf("sha256_root_hash is invalid length, expected %d got %d", sha256.Size, len(resp.SHA256RootHash)),
StatusCode: httpRsp.StatusCode,
Body: body,
}
}
copy(sth.SHA256RootHash[:], resp.SHA256RootHash)
var ds ct.DigitallySigned
if rest, err := tls.Unmarshal(resp.TreeHeadSignature, &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,
}
}
sth.TreeHeadSignature = ds
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
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,221 @@
// Copyright 2017 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
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/protobuf/ptypes"
ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/client/configpb"
"github.com/google/certificate-transparency-go/jsonclient"
"github.com/google/certificate-transparency-go/x509"
)
type interval struct {
lower *time.Time // nil => no lower bound
upper *time.Time // nil => no upper bound
}
// TemporalLogConfigFromFile creates a TemporalLogConfig object from the given
// filename, which should contain text-protobuf encoded configuration data.
func TemporalLogConfigFromFile(filename string) (*configpb.TemporalLogConfig, error) {
if len(filename) == 0 {
return nil, errors.New("log config filename empty")
}
cfgText, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read log config: %v", err)
}
var cfg configpb.TemporalLogConfig
if err := proto.UnmarshalText(string(cfgText), &cfg); err != nil {
return nil, fmt.Errorf("failed to parse log config: %v", err)
}
if len(cfg.Shard) == 0 {
return nil, errors.New("empty log config found")
}
return &cfg, nil
}
// AddLogClient is an interface that allows adding certificates and pre-certificates to a log.
// Both LogClient and TemporalLogClient implement this interface, which allows users to
// commonize code for adding certs to normal/temporal logs.
type AddLogClient interface {
AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error)
AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error)
GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error)
}
// TemporalLogClient allows [pre-]certificates to be uploaded to a temporal log.
type TemporalLogClient struct {
Clients []*LogClient
intervals []interval
}
// NewTemporalLogClient builds a new client for interacting with a temporal log.
// The provided config should be contiguous and chronological.
func NewTemporalLogClient(cfg configpb.TemporalLogConfig, hc *http.Client) (*TemporalLogClient, error) {
if len(cfg.Shard) == 0 {
return nil, errors.New("empty config")
}
overall, err := shardInterval(cfg.Shard[0])
if err != nil {
return nil, fmt.Errorf("cfg.Shard[0] invalid: %v", err)
}
intervals := make([]interval, 0, len(cfg.Shard))
intervals = append(intervals, overall)
for i := 1; i < len(cfg.Shard); i++ {
interval, err := shardInterval(cfg.Shard[i])
if err != nil {
return nil, fmt.Errorf("cfg.Shard[%d] invalid: %v", i, err)
}
if overall.upper == nil {
return nil, fmt.Errorf("cfg.Shard[%d] extends an interval with no upper bound", i)
}
if interval.lower == nil {
return nil, fmt.Errorf("cfg.Shard[%d] has no lower bound but extends an interval", i)
}
if !interval.lower.Equal(*overall.upper) {
return nil, fmt.Errorf("cfg.Shard[%d] starts at %v but previous interval ended at %v", i, interval.lower, overall.upper)
}
overall.upper = interval.upper
intervals = append(intervals, interval)
}
clients := make([]*LogClient, 0, len(cfg.Shard))
for i, shard := range cfg.Shard {
opts := jsonclient.Options{}
opts.PublicKeyDER = shard.GetPublicKeyDer()
c, err := New(shard.Uri, hc, opts)
if err != nil {
return nil, fmt.Errorf("failed to create client for cfg.Shard[%d]: %v", i, err)
}
clients = append(clients, c)
}
tlc := TemporalLogClient{
Clients: clients,
intervals: intervals,
}
return &tlc, nil
}
// GetAcceptedRoots retrieves the set of acceptable root certificates for all
// of the shards of a temporal log (i.e. the union).
func (tlc *TemporalLogClient) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Cert, error) {
type result struct {
roots []ct.ASN1Cert
err error
}
results := make(chan result, len(tlc.Clients))
for _, c := range tlc.Clients {
go func(c *LogClient) {
var r result
r.roots, r.err = c.GetAcceptedRoots(ctx)
results <- r
}(c)
}
var allRoots []ct.ASN1Cert
seen := make(map[[sha256.Size]byte]bool)
for range tlc.Clients {
r := <-results
if r.err != nil {
return nil, r.err
}
for _, root := range r.roots {
h := sha256.Sum256(root.Data)
if seen[h] {
continue
}
seen[h] = true
allRoots = append(allRoots, root)
}
}
return allRoots, nil
}
// AddChain adds the (DER represented) X509 chain to the appropriate log.
func (tlc *TemporalLogClient) AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
return tlc.addChain(ctx, ct.X509LogEntryType, ct.AddChainPath, chain)
}
// AddPreChain adds the (DER represented) Precertificate chain to the appropriate log.
func (tlc *TemporalLogClient) AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
return tlc.addChain(ctx, ct.PrecertLogEntryType, ct.AddPreChainPath, chain)
}
func (tlc *TemporalLogClient) addChain(ctx context.Context, ctype ct.LogEntryType, path string, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) {
// Parse the first entry in the chain
if len(chain) == 0 {
return nil, errors.New("missing chain")
}
cert, err := x509.ParseCertificate(chain[0].Data)
if err != nil {
return nil, fmt.Errorf("failed to parse initial chain entry: %v", err)
}
cidx, err := tlc.IndexByDate(cert.NotAfter)
if err != nil {
return nil, fmt.Errorf("failed to find log to process cert: %v", err)
}
return tlc.Clients[cidx].addChainWithRetry(ctx, ctype, path, chain)
}
// IndexByDate returns the index of the Clients entry that is appropriate for the given
// date.
func (tlc *TemporalLogClient) IndexByDate(when time.Time) (int, error) {
for i, interval := range tlc.intervals {
if (interval.lower != nil) && when.Before(*interval.lower) {
continue
}
if (interval.upper != nil) && !when.Before(*interval.upper) {
continue
}
return i, nil
}
return -1, fmt.Errorf("no log found encompassing date %v", when)
}
func shardInterval(cfg *configpb.LogShardConfig) (interval, error) {
var interval interval
if cfg.NotAfterStart != nil {
t, err := ptypes.Timestamp(cfg.NotAfterStart)
if err != nil {
return interval, fmt.Errorf("failed to parse NotAfterStart: %v", err)
}
interval.lower = &t
}
if cfg.NotAfterLimit != nil {
t, err := ptypes.Timestamp(cfg.NotAfterLimit)
if err != nil {
return interval, fmt.Errorf("failed to parse NotAfterLimit: %v", err)
}
interval.upper = &t
}
if interval.lower != nil && interval.upper != nil && !(*interval.lower).Before(*interval.upper) {
return interval, errors.New("inverted interval")
}
return interval, nil
}

View File

@ -0,0 +1,481 @@
// Copyright 2017 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
import (
"context"
"encoding/pem"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/golang/protobuf/ptypes"
tspb "github.com/golang/protobuf/ptypes/timestamp"
ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/client/configpb"
"github.com/google/certificate-transparency-go/testdata"
"github.com/google/certificate-transparency-go/x509util"
)
func TestNewTemporalLogClient(t *testing.T) {
ts0, _ := ptypes.TimestampProto(time.Date(2010, 9, 19, 11, 00, 00, 00, time.UTC))
ts1, _ := ptypes.TimestampProto(time.Date(2011, 9, 19, 11, 00, 00, 00, time.UTC))
ts2, _ := ptypes.TimestampProto(time.Date(2012, 9, 19, 11, 00, 00, 00, time.UTC))
ts2_5, _ := ptypes.TimestampProto(time.Date(2013, 3, 19, 11, 00, 00, 00, time.UTC))
ts3, _ := ptypes.TimestampProto(time.Date(2013, 9, 19, 11, 00, 00, 00, time.UTC))
ts4, _ := ptypes.TimestampProto(time.Date(2014, 9, 19, 11, 00, 00, 00, time.UTC))
tests := []struct {
cfg configpb.TemporalLogConfig
wantErr string
}{
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: nil, NotAfterLimit: nil},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "threeA", NotAfterStart: ts2_5, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
},
},
wantErr: "previous interval ended at",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
},
},
wantErr: "previous interval ended at",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: nil, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts1},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
},
},
wantErr: "inverted",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: nil},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
},
},
wantErr: "no upper bound",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: nil, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: nil},
},
},
wantErr: "has no lower bound",
},
{
wantErr: "empty",
},
{
cfg: configpb.TemporalLogConfig{Shard: []*configpb.LogShardConfig{}},
wantErr: "empty",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts1},
},
},
wantErr: "inverted",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts2, NotAfterLimit: ts1},
},
},
wantErr: "inverted",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: &tspb.Timestamp{Seconds: -1, Nanos: -1}, NotAfterLimit: ts2},
},
},
wantErr: "failed to parse",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "one", NotAfterStart: ts1, NotAfterLimit: &tspb.Timestamp{Seconds: -1, Nanos: -1}},
},
},
wantErr: "failed to parse",
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{
Uri: "one",
NotAfterStart: nil,
NotAfterLimit: nil,
PublicKeyDer: []byte{0x01, 0x02},
},
},
},
wantErr: "invalid public key",
},
}
for _, test := range tests {
_, err := NewTemporalLogClient(test.cfg, nil)
if err != nil {
if test.wantErr == "" {
t.Errorf("NewTemporalLogClient(%+v)=nil,%v; want _,nil", test.cfg, err)
} else if !strings.Contains(err.Error(), test.wantErr) {
t.Errorf("NewTemporalLogClient(%+v)=nil,%v; want _,%q", test.cfg, err, test.wantErr)
}
continue
}
if test.wantErr != "" {
t.Errorf("NewTemporalLogClient(%+v)=_, nil; want _,%q", test.cfg, test.wantErr)
}
}
}
func TestIndexByDate(t *testing.T) {
time0 := time.Date(2010, 9, 19, 11, 00, 00, 00, time.UTC)
time1 := time.Date(2011, 9, 19, 11, 00, 00, 00, time.UTC)
time1_9 := time.Date(2012, 9, 19, 10, 59, 59, 00, time.UTC)
time2 := time.Date(2012, 9, 19, 11, 00, 00, 00, time.UTC)
time2_5 := time.Date(2013, 3, 19, 11, 00, 00, 00, time.UTC)
time3 := time.Date(2013, 9, 19, 11, 00, 00, 00, time.UTC)
time4 := time.Date(2014, 9, 19, 11, 00, 00, 00, time.UTC)
ts0, _ := ptypes.TimestampProto(time0)
ts1, _ := ptypes.TimestampProto(time1)
ts2, _ := ptypes.TimestampProto(time2)
ts3, _ := ptypes.TimestampProto(time3)
ts4, _ := ptypes.TimestampProto(time4)
allCfg := configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "zero", NotAfterStart: nil, NotAfterLimit: ts0},
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
{Uri: "five", NotAfterStart: ts4, NotAfterLimit: nil},
},
}
uptoCfg := configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "zero", NotAfterStart: nil, NotAfterLimit: ts0},
{Uri: "one", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "two", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "three", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "four", NotAfterStart: ts3, NotAfterLimit: ts4},
},
}
fromCfg :=
configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "zero", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "two", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "three", NotAfterStart: ts3, NotAfterLimit: ts4},
{Uri: "four", NotAfterStart: ts4, NotAfterLimit: nil},
},
}
boundedCfg :=
configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: "zero", NotAfterStart: ts0, NotAfterLimit: ts1},
{Uri: "one", NotAfterStart: ts1, NotAfterLimit: ts2},
{Uri: "two", NotAfterStart: ts2, NotAfterLimit: ts3},
{Uri: "three", NotAfterStart: ts3, NotAfterLimit: ts4},
},
}
tests := []struct {
cfg configpb.TemporalLogConfig
when time.Time
want int
wantErr bool
}{
{cfg: allCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), want: 0},
{cfg: allCfg, when: time0, want: 1},
{cfg: allCfg, when: time1, want: 2},
{cfg: allCfg, when: time1_9, want: 2},
{cfg: allCfg, when: time2, want: 3},
{cfg: allCfg, when: time2_5, want: 3},
{cfg: allCfg, when: time3, want: 4},
{cfg: allCfg, when: time4, want: 5},
{cfg: allCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), want: 5},
{cfg: uptoCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), want: 0},
{cfg: uptoCfg, when: time0, want: 1},
{cfg: uptoCfg, when: time1, want: 2},
{cfg: uptoCfg, when: time2, want: 3},
{cfg: uptoCfg, when: time2_5, want: 3},
{cfg: uptoCfg, when: time3, want: 4},
{cfg: uptoCfg, when: time4, wantErr: true},
{cfg: uptoCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
{cfg: fromCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
{cfg: fromCfg, when: time0, want: 0},
{cfg: fromCfg, when: time1, want: 1},
{cfg: fromCfg, when: time2, want: 2},
{cfg: fromCfg, when: time2_5, want: 2},
{cfg: fromCfg, when: time3, want: 3},
{cfg: fromCfg, when: time4, want: 4},
{cfg: fromCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), want: 4},
{cfg: boundedCfg, when: time.Date(2000, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
{cfg: boundedCfg, when: time0, want: 0},
{cfg: boundedCfg, when: time1, want: 1},
{cfg: boundedCfg, when: time2, want: 2},
{cfg: boundedCfg, when: time2_5, want: 2},
{cfg: boundedCfg, when: time3, want: 3},
{cfg: boundedCfg, when: time4, wantErr: true},
{cfg: boundedCfg, when: time.Date(2015, 9, 19, 11, 00, 00, 00, time.UTC), wantErr: true},
}
for _, test := range tests {
tlc, err := NewTemporalLogClient(test.cfg, nil)
if err != nil {
t.Errorf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", test.cfg, err)
continue
}
got, err := tlc.IndexByDate(test.when)
if err != nil {
if !test.wantErr {
t.Errorf("NewTemporalLogClient(%+v).idxByDate()=%d,%v; want %d,nil", test.cfg, got, err, test.want)
}
continue
}
if test.wantErr {
t.Errorf("NewTemporalLogClient(%+v).idxByDate(%v)=%d, nil; want _, 'no log found'", test.cfg, test.when, got)
}
if got != test.want {
t.Errorf("NewTemporalLogClient(%+v).idxByDate(%v)=%d, nil; want %d, nil", test.cfg, test.when, got, test.want)
}
}
}
func TestTemporalAddChain(t *testing.T) {
hs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ct/v1/add-chain":
data, _ := sctToJSON(testdata.TestCertProof)
w.Write(data)
case "/ct/v1/add-pre-chain":
data, _ := sctToJSON(testdata.TestPreCertProof)
w.Write(data)
default:
t.Fatalf("Incorrect URL path: %s", r.URL.Path)
}
}))
defer hs.Close()
cert, err := x509util.CertificateFromPEM(testdata.TestCertPEM)
if err != nil {
t.Fatalf("Failed to parse certificate from PEM: %v", err)
}
certChain := []ct.ASN1Cert{{Data: cert.Raw}}
precert, err := x509util.CertificateFromPEM(testdata.TestPreCertPEM)
if err != nil {
t.Fatalf("Failed to parse pre-certificate from PEM: %v", err)
}
issuer, err := x509util.CertificateFromPEM(testdata.CACertPEM)
if err != nil {
t.Fatalf("Failed to parse issuer certificate from PEM: %v", err)
}
precertChain := []ct.ASN1Cert{{Data: precert.Raw}, {Data: issuer.Raw}}
// Both have Not After = Jun 1 00:00:00 2022 GMT
ts1, _ := ptypes.TimestampProto(time.Date(2022, 5, 19, 11, 00, 00, 00, time.UTC))
ts2, _ := ptypes.TimestampProto(time.Date(2022, 6, 19, 11, 00, 00, 00, time.UTC))
p, _ := pem.Decode([]byte(testdata.LogPublicKeyPEM))
if p == nil {
t.Fatalf("Failed to parse public key from PEM: %v", err)
}
tests := []struct {
cfg configpb.TemporalLogConfig
wantErr bool
}{
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: ts2, PublicKeyDer: p.Bytes},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: ts1, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: ts1, NotAfterLimit: ts2, PublicKeyDer: p.Bytes},
},
},
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: nil, NotAfterLimit: ts1, PublicKeyDer: p.Bytes},
},
},
wantErr: true,
},
{
cfg: configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{Uri: hs.URL, NotAfterStart: ts2, NotAfterLimit: nil, PublicKeyDer: p.Bytes},
},
},
wantErr: true,
},
}
ctx := context.Background()
for _, test := range tests {
tlc, err := NewTemporalLogClient(test.cfg, nil)
if err != nil {
t.Errorf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", test.cfg, err)
continue
}
_, err = tlc.AddChain(ctx, certChain)
if err != nil {
if !test.wantErr {
t.Errorf("AddChain()=nil,%v; want sct,nil", err)
}
} else if test.wantErr {
t.Errorf("AddChain()=sct,nil; want nil,_")
}
_, err = tlc.AddPreChain(ctx, precertChain)
if err != nil {
if !test.wantErr {
t.Errorf("AddPreChain()=nil,%v; want sct,nil", err)
}
} else if test.wantErr {
t.Errorf("AddPreChain()=sct,nil; want nil,_")
}
}
}
func TestTemporalAddChainErrors(t *testing.T) {
hs := serveSCTAt(t, "/ct/v1/add-chain", testdata.TestCertProof)
defer hs.Close()
cfg := configpb.TemporalLogConfig{
Shard: []*configpb.LogShardConfig{
{
Uri: hs.URL,
NotAfterStart: nil,
NotAfterLimit: nil,
},
},
}
ctx := context.Background()
tlc, err := NewTemporalLogClient(cfg, nil)
if err != nil {
t.Fatalf("NewTemporalLogClient(%+v)=nil, %v; want _,nil", cfg, err)
}
_, err = tlc.AddChain(ctx, nil)
if err == nil {
t.Errorf("AddChain(nil)=sct,nil; want nil, 'missing chain'")
}
_, err = tlc.AddChain(ctx, []ct.ASN1Cert{{Data: []byte{0x01, 0x02}}})
if err == nil {
t.Errorf("AddChain(nil)=sct,nil; want nil, 'failed to parse'")
}
}