...

Source file src/github.com/google/certificate-transparency-go/loglist3/loglist3.go

Documentation: github.com/google/certificate-transparency-go/loglist3

     1  // Copyright 2022 Google LLC. 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 loglist3 allows parsing and searching of the master CT Log list.
    16  // It expects the log list to conform to the v3 schema.
    17  package loglist3
    18  
    19  import (
    20  	"bytes"
    21  	"crypto"
    22  	"crypto/ecdsa"
    23  	"crypto/rsa"
    24  	"crypto/sha256"
    25  	"encoding/base64"
    26  	"encoding/hex"
    27  	"encoding/json"
    28  	"fmt"
    29  	"regexp"
    30  	"strings"
    31  	"time"
    32  	"unicode"
    33  
    34  	"github.com/google/certificate-transparency-go/tls"
    35  )
    36  
    37  const (
    38  	// LogListURL has the master URL for Google Chrome's log list.
    39  	LogListURL = "https://www.gstatic.com/ct/log_list/v3/log_list.json"
    40  	// LogListSignatureURL has the URL for the signature over Google Chrome's log list.
    41  	LogListSignatureURL = "https://www.gstatic.com/ct/log_list/v3/log_list.sig"
    42  	// AllLogListURL has the URL for the list of all known logs (which isn't signed).
    43  	AllLogListURL = "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json"
    44  )
    45  
    46  // Manually mapped from https://www.gstatic.com/ct/log_list/v3/log_list_schema.json
    47  
    48  // LogList holds a collection of CT logs, grouped by operator.
    49  type LogList struct {
    50  	// IsAllLogs is set to true if the list contains all known logs, not
    51  	// only usable ones.
    52  	IsAllLogs bool `json:"is_all_logs,omitempty"`
    53  	// Version is the version of the log list.
    54  	Version string `json:"version,omitempty"`
    55  	// LogListTimestamp is the time at which the log list was published.
    56  	LogListTimestamp time.Time `json:"log_list_timestamp,omitempty"`
    57  	// Operators is a list of CT log operators and the logs they operate.
    58  	Operators []*Operator `json:"operators"`
    59  }
    60  
    61  // Operator holds a collection of CT logs run by the same organisation.
    62  // It also provides information about that organisation, e.g. contact details.
    63  type Operator struct {
    64  	// Name is the name of the CT log operator.
    65  	Name string `json:"name"`
    66  	// Email lists the email addresses that can be used to contact this log
    67  	// operator.
    68  	Email []string `json:"email"`
    69  	// Logs is a list of CT logs run by this operator.
    70  	Logs []*Log `json:"logs"`
    71  }
    72  
    73  // Log describes a single CT log.
    74  type Log struct {
    75  	// Description is a human-readable string that describes the log.
    76  	Description string `json:"description,omitempty"`
    77  	// LogID is the SHA-256 hash of the log's public key.
    78  	LogID []byte `json:"log_id"`
    79  	// Key is the public key with which signatures can be verified.
    80  	Key []byte `json:"key"`
    81  	// URL is the address of the HTTPS API.
    82  	URL string `json:"url"`
    83  	// DNS is the address of the DNS API.
    84  	DNS string `json:"dns,omitempty"`
    85  	// MMD is the Maximum Merge Delay, in seconds. All submitted
    86  	// certificates must be incorporated into the log within this time.
    87  	MMD int32 `json:"mmd"`
    88  	// PreviousOperators is a list of previous operators and the timestamp
    89  	// of when they stopped running the log.
    90  	PreviousOperators []*PreviousOperator `json:"previous_operators,omitempty"`
    91  	// State is the current state of the log, from the perspective of the
    92  	// log list distributor.
    93  	State *LogStates `json:"state,omitempty"`
    94  	// TemporalInterval, if set, indicates that this log only accepts
    95  	// certificates with a NotAfter date in this time range.
    96  	TemporalInterval *TemporalInterval `json:"temporal_interval,omitempty"`
    97  	// Type indicates the purpose of this log, e.g. "test" or "prod".
    98  	Type string `json:"log_type,omitempty"`
    99  }
   100  
   101  // PreviousOperator holds information about a log operator and the time at which
   102  // they stopped running a log.
   103  type PreviousOperator struct {
   104  	// Name is the name of the CT log operator.
   105  	Name string `json:"name"`
   106  	// EndTime is the time at which the operator stopped running a log.
   107  	EndTime time.Time `json:"end_time"`
   108  }
   109  
   110  // TemporalInterval is a time range.
   111  type TemporalInterval struct {
   112  	// StartInclusive is the beginning of the time range.
   113  	StartInclusive time.Time `json:"start_inclusive"`
   114  	// EndExclusive is just after the end of the time range.
   115  	EndExclusive time.Time `json:"end_exclusive"`
   116  }
   117  
   118  // LogStatus indicates Log status.
   119  type LogStatus int
   120  
   121  // LogStatus values
   122  const (
   123  	UndefinedLogStatus LogStatus = iota
   124  	PendingLogStatus
   125  	QualifiedLogStatus
   126  	UsableLogStatus
   127  	ReadOnlyLogStatus
   128  	RetiredLogStatus
   129  	RejectedLogStatus
   130  )
   131  
   132  //go:generate stringer -type=LogStatus
   133  
   134  // LogStates are the states that a CT log can be in, from the perspective of a
   135  // user agent. Only one should be set - this is the current state.
   136  type LogStates struct {
   137  	// Pending indicates that the log is in the "pending" state.
   138  	Pending *LogState `json:"pending,omitempty"`
   139  	// Qualified indicates that the log is in the "qualified" state.
   140  	Qualified *LogState `json:"qualified,omitempty"`
   141  	// Usable indicates that the log is in the "usable" state.
   142  	Usable *LogState `json:"usable,omitempty"`
   143  	// ReadOnly indicates that the log is in the "readonly" state.
   144  	ReadOnly *ReadOnlyLogState `json:"readonly,omitempty"`
   145  	// Retired indicates that the log is in the "retired" state.
   146  	Retired *LogState `json:"retired,omitempty"`
   147  	// Rejected indicates that the log is in the "rejected" state.
   148  	Rejected *LogState `json:"rejected,omitempty"`
   149  }
   150  
   151  // LogState contains details on the current state of a CT log.
   152  type LogState struct {
   153  	// Timestamp is the time when the state began.
   154  	Timestamp time.Time `json:"timestamp"`
   155  }
   156  
   157  // ReadOnlyLogState contains details on the current state of a read-only CT log.
   158  type ReadOnlyLogState struct {
   159  	LogState
   160  	// FinalTreeHead is the root hash and tree size at which the CT log was
   161  	// made read-only. This should never change while the log is read-only.
   162  	FinalTreeHead TreeHead `json:"final_tree_head"`
   163  }
   164  
   165  // TreeHead is the root hash and tree size of a CT log.
   166  type TreeHead struct {
   167  	// SHA256RootHash is the root hash of the CT log's Merkle tree.
   168  	SHA256RootHash []byte `json:"sha256_root_hash"`
   169  	// TreeSize is the size of the CT log's Merkle tree.
   170  	TreeSize int64 `json:"tree_size"`
   171  }
   172  
   173  // LogStatus method returns Log-status enum value for descriptive struct.
   174  func (ls *LogStates) LogStatus() LogStatus {
   175  	switch {
   176  	case ls == nil:
   177  		return UndefinedLogStatus
   178  	case ls.Pending != nil:
   179  		return PendingLogStatus
   180  	case ls.Qualified != nil:
   181  		return QualifiedLogStatus
   182  	case ls.Usable != nil:
   183  		return UsableLogStatus
   184  	case ls.ReadOnly != nil:
   185  		return ReadOnlyLogStatus
   186  	case ls.Retired != nil:
   187  		return RetiredLogStatus
   188  	case ls.Rejected != nil:
   189  		return RejectedLogStatus
   190  	default:
   191  		return UndefinedLogStatus
   192  	}
   193  }
   194  
   195  // String method returns printable name of the state.
   196  func (ls *LogStates) String() string {
   197  	return ls.LogStatus().String()
   198  }
   199  
   200  // Active picks the set-up state. If multiple states are set (not expected) picks one of them.
   201  func (ls *LogStates) Active() (*LogState, *ReadOnlyLogState) {
   202  	if ls == nil {
   203  		return nil, nil
   204  	}
   205  	switch {
   206  	case ls.Pending != nil:
   207  		return ls.Pending, nil
   208  	case ls.Qualified != nil:
   209  		return ls.Qualified, nil
   210  	case ls.Usable != nil:
   211  		return ls.Usable, nil
   212  	case ls.ReadOnly != nil:
   213  		return nil, ls.ReadOnly
   214  	case ls.Retired != nil:
   215  		return ls.Retired, nil
   216  	case ls.Rejected != nil:
   217  		return ls.Rejected, nil
   218  	default:
   219  		return nil, nil
   220  	}
   221  }
   222  
   223  // GoogleOperated returns whether Operator is considered to be Google.
   224  func (op *Operator) GoogleOperated() bool {
   225  	for _, email := range op.Email {
   226  		if strings.Contains(email, "google-ct-logs@googlegroups") {
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  // NewFromJSON creates a LogList from JSON encoded data.
   234  func NewFromJSON(llData []byte) (*LogList, error) {
   235  	var ll LogList
   236  	if err := json.Unmarshal(llData, &ll); err != nil {
   237  		return nil, fmt.Errorf("failed to parse log list: %v", err)
   238  	}
   239  	return &ll, nil
   240  }
   241  
   242  // NewFromSignedJSON creates a LogList from JSON encoded data, checking a
   243  // signature along the way. The signature data should be provided as the
   244  // raw signature data.
   245  func NewFromSignedJSON(llData, rawSig []byte, pubKey crypto.PublicKey) (*LogList, error) {
   246  	var sigAlgo tls.SignatureAlgorithm
   247  	switch pkType := pubKey.(type) {
   248  	case *rsa.PublicKey:
   249  		sigAlgo = tls.RSA
   250  	case *ecdsa.PublicKey:
   251  		sigAlgo = tls.ECDSA
   252  	default:
   253  		return nil, fmt.Errorf("unsupported public key type %v", pkType)
   254  	}
   255  	tlsSig := tls.DigitallySigned{
   256  		Algorithm: tls.SignatureAndHashAlgorithm{
   257  			Hash:      tls.SHA256,
   258  			Signature: sigAlgo,
   259  		},
   260  		Signature: rawSig,
   261  	}
   262  	if err := tls.VerifySignature(pubKey, llData, tlsSig); err != nil {
   263  		return nil, fmt.Errorf("failed to verify signature: %v", err)
   264  	}
   265  	return NewFromJSON(llData)
   266  }
   267  
   268  // FindLogByName returns all logs whose names contain the given string.
   269  func (ll *LogList) FindLogByName(name string) []*Log {
   270  	name = strings.ToLower(name)
   271  	var results []*Log
   272  	for _, op := range ll.Operators {
   273  		for _, log := range op.Logs {
   274  			if strings.Contains(strings.ToLower(log.Description), name) {
   275  				results = append(results, log)
   276  			}
   277  		}
   278  	}
   279  	return results
   280  }
   281  
   282  // FindLogByURL finds the log with the given URL.
   283  func (ll *LogList) FindLogByURL(url string) *Log {
   284  	for _, op := range ll.Operators {
   285  		for _, log := range op.Logs {
   286  			// Don't count trailing slashes
   287  			if strings.TrimRight(log.URL, "/") == strings.TrimRight(url, "/") {
   288  				return log
   289  			}
   290  		}
   291  	}
   292  	return nil
   293  }
   294  
   295  // FindLogByKeyHash finds the log with the given key hash.
   296  func (ll *LogList) FindLogByKeyHash(keyhash [sha256.Size]byte) *Log {
   297  	for _, op := range ll.Operators {
   298  		for _, log := range op.Logs {
   299  			if bytes.Equal(log.LogID, keyhash[:]) {
   300  				return log
   301  			}
   302  		}
   303  	}
   304  	return nil
   305  }
   306  
   307  // FindLogByKeyHashPrefix finds all logs whose key hash starts with the prefix.
   308  func (ll *LogList) FindLogByKeyHashPrefix(prefix string) []*Log {
   309  	var results []*Log
   310  	for _, op := range ll.Operators {
   311  		for _, log := range op.Logs {
   312  			hh := hex.EncodeToString(log.LogID[:])
   313  			if strings.HasPrefix(hh, prefix) {
   314  				results = append(results, log)
   315  			}
   316  		}
   317  	}
   318  	return results
   319  }
   320  
   321  // FindLogByKey finds the log with the given DER-encoded key.
   322  func (ll *LogList) FindLogByKey(key []byte) *Log {
   323  	for _, op := range ll.Operators {
   324  		for _, log := range op.Logs {
   325  			if bytes.Equal(log.Key[:], key) {
   326  				return log
   327  			}
   328  		}
   329  	}
   330  	return nil
   331  }
   332  
   333  var hexDigits = regexp.MustCompile("^[0-9a-fA-F]+$")
   334  
   335  // FuzzyFindLog tries to find logs that match the given unspecified input,
   336  // whose format is unspecified.  This generally returns a single log, but
   337  // if text input that matches multiple log descriptions is provided, then
   338  // multiple logs may be returned.
   339  func (ll *LogList) FuzzyFindLog(input string) []*Log {
   340  	input = strings.Trim(input, " \t")
   341  	if logs := ll.FindLogByName(input); len(logs) > 0 {
   342  		return logs
   343  	}
   344  	if log := ll.FindLogByURL(input); log != nil {
   345  		return []*Log{log}
   346  	}
   347  	// Try assuming the input is binary data of some form.  First base64:
   348  	if data, err := base64.StdEncoding.DecodeString(input); err == nil {
   349  		if len(data) == sha256.Size {
   350  			var hash [sha256.Size]byte
   351  			copy(hash[:], data)
   352  			if log := ll.FindLogByKeyHash(hash); log != nil {
   353  				return []*Log{log}
   354  			}
   355  		}
   356  		if log := ll.FindLogByKey(data); log != nil {
   357  			return []*Log{log}
   358  		}
   359  	}
   360  	// Now hex, but strip all internal whitespace first.
   361  	input = stripInternalSpace(input)
   362  	if data, err := hex.DecodeString(input); err == nil {
   363  		if len(data) == sha256.Size {
   364  			var hash [sha256.Size]byte
   365  			copy(hash[:], data)
   366  			if log := ll.FindLogByKeyHash(hash); log != nil {
   367  				return []*Log{log}
   368  			}
   369  		}
   370  		if log := ll.FindLogByKey(data); log != nil {
   371  			return []*Log{log}
   372  		}
   373  	}
   374  	// Finally, allow hex strings with an odd number of digits.
   375  	if hexDigits.MatchString(input) {
   376  		if logs := ll.FindLogByKeyHashPrefix(input); len(logs) > 0 {
   377  			return logs
   378  		}
   379  	}
   380  
   381  	return nil
   382  }
   383  
   384  func stripInternalSpace(input string) string {
   385  	return strings.Map(func(r rune) rune {
   386  		if !unicode.IsSpace(r) {
   387  			return r
   388  		}
   389  		return -1
   390  	}, input)
   391  }
   392  

View as plain text