├── cmd ├── certspotter │ ├── .gitignore │ └── main.go ├── ctparsewatch │ ├── .gitignore │ └── main.go ├── helpers.go ├── log_state.go ├── submitct │ └── main.go ├── state.go └── common.go ├── ct ├── README ├── AUTHORS ├── signatures.go ├── client │ └── logclient.go ├── types.go ├── LICENSE └── serialization.go ├── NEWS ├── CONTRIBUTING ├── helpers_test.go ├── asn1.go ├── precerts.go ├── logs.go ├── asn1time_test.go ├── auditing.go ├── README ├── asn1time.go ├── scanner.go ├── identifiers.go ├── helpers.go ├── x509.go └── COPYING /cmd/certspotter/.gitignore: -------------------------------------------------------------------------------- 1 | /certspotter 2 | -------------------------------------------------------------------------------- /cmd/ctparsewatch/.gitignore: -------------------------------------------------------------------------------- 1 | /ctparsewatch 2 | -------------------------------------------------------------------------------- /ct/README: -------------------------------------------------------------------------------- 1 | The code in this directory is from https://github.com/google/certificate-transparency/tree/master/go 2 | See AUTHORS for the copyright holders, and LICENSE for the license. 3 | -------------------------------------------------------------------------------- /ct/AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of benchmark authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | # 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | # 9 | # Please keep the list sorted. 10 | 11 | Comodo CA Limited 12 | Ed Maste 13 | Fiaz Hossain 14 | Google Inc. 15 | Jeff Trawick 16 | Katriel Cohn-Gordon 17 | Mark Schloesser 18 | NORDUnet A/S 19 | Nicholas Galbreath 20 | Oliver Weidner 21 | Ruslan Kovalov 22 | Venafi, Inc. 23 | Vladimir Rutsky 24 | Ximin Luo 25 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | v0.4 (2017-04-03) 2 | * Add PuChuangSiDa 1 log 3 | * Remove Venafi log due to fork and disqualification from Chrome 4 | 5 | v0.3 (2017-02-20) 6 | * Revise -all_time flag (behavior change): 7 | - If -all_time is specified, scan the entirety of all logs, even 8 | existing logs 9 | - When a new log is added, scan it in its entirety even if -all_time 10 | is not specified 11 | * Add new logs: 12 | - Google Icarus 13 | - Google Skydiver 14 | - StartCom 15 | - WoSign 16 | * Overhaul log processing and auditing logic: 17 | - STHs are never deleted unless they can be verified 18 | - Multiple unverified STHs can be queued per log, laying groundwork 19 | for STH pollination support 20 | - New state directory layout; current state directories will be 21 | migrated, but migration will be removed in a future version 22 | - Persist condensed Merkle Tree state between runs, instead of 23 | reconstructing it from consistency proof every time 24 | * Use a lock file to prevent multiple instances of Cert Spotter from 25 | running concurrently (which could clobber the state directory). 26 | * Minor bug fixes and improvements 27 | 28 | v0.2 (2016-08-25) 29 | * Suppress duplicate identifiers in output. 30 | * Fix "EOF" error when running under Go 1.7. 31 | * Fix bug where hook script could fail silently. 32 | * Fix compilation under Go 1.5. 33 | 34 | v0.1 (2016-07-27) 35 | * Initial release. 36 | -------------------------------------------------------------------------------- /cmd/ctparsewatch/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package main 11 | 12 | import ( 13 | "flag" 14 | "os" 15 | 16 | "software.sslmate.com/src/certspotter" 17 | "software.sslmate.com/src/certspotter/cmd" 18 | "software.sslmate.com/src/certspotter/ct" 19 | ) 20 | 21 | func DefaultStateDir() string { 22 | if envVar := os.Getenv("CTPARSEWATCH_STATE_DIR"); envVar != "" { 23 | return envVar 24 | } else { 25 | return cmd.DefaultStateDir("ctparsewatch") 26 | } 27 | } 28 | 29 | var stateDir = flag.String("state_dir", DefaultStateDir(), "Directory for storing state") 30 | 31 | func processEntry(scanner *certspotter.Scanner, entry *ct.LogEntry) { 32 | info := certspotter.EntryInfo{ 33 | LogUri: scanner.LogUri, 34 | Entry: entry, 35 | IsPrecert: certspotter.IsPrecert(entry), 36 | FullChain: certspotter.GetFullChain(entry), 37 | } 38 | 39 | info.CertInfo, info.ParseError = certspotter.MakeCertInfoFromLogEntry(entry) 40 | if info.CertInfo != nil { 41 | info.Identifiers, info.IdentifiersParseError = info.CertInfo.ParseIdentifiers() 42 | } 43 | 44 | if info.HasParseErrors() { 45 | cmd.LogEntry(&info) 46 | } 47 | } 48 | 49 | func main() { 50 | flag.Parse() 51 | os.Exit(cmd.Main(*stateDir, processEntry)) 52 | } 53 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | All contributors to Cert Spotter must accept the Developer Certificate 2 | of Origin (DCO): 3 | 4 | Developer Certificate of Origin 5 | Version 1.1 6 | 7 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 8 | 660 York Street, Suite 102, 9 | San Francisco, CA 94110 USA 10 | 11 | Everyone is permitted to copy and distribute verbatim copies of this 12 | license document, but changing it is not allowed. 13 | 14 | 15 | Developer's Certificate of Origin 1.1 16 | 17 | By making a contribution to this project, I certify that: 18 | 19 | (a) The contribution was created in whole or in part by me and I 20 | have the right to submit it under the open source license 21 | indicated in the file; or 22 | 23 | (b) The contribution is based upon previous work that, to the best 24 | of my knowledge, is covered under an appropriate open source 25 | license and I have the right under that license to submit that 26 | work with modifications, whether created in whole or in part 27 | by me, under the same open source license (unless I am 28 | permitted to submit under a different license), as indicated 29 | in the file; or 30 | 31 | (c) The contribution was provided directly to me by some other 32 | person who certified (a), (b) or (c) and I have not modified 33 | it. 34 | 35 | (d) I understand and agree that this project and the contribution 36 | are public and that a record of the contribution (including all 37 | personal information I submit with it, including my sign-off) is 38 | maintained indefinitely and may be redistributed consistent with 39 | this project or the open source license(s) involved. 40 | 41 | To accept the DCO, add the following line to each commit message with 42 | your real name and email address (`git commit -s` will do this for you): 43 | 44 | Signed-off-by: Alex Smith 45 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "testing" 14 | ) 15 | 16 | func doWildcardTest(t *testing.T, dnsName string, wildcard string, expected bool) { 17 | if MatchesWildcard(dnsName, wildcard) != expected { 18 | t.Errorf("MatchesWildcard(%q, %q) != %v", dnsName, wildcard, expected) 19 | } 20 | } 21 | 22 | func TestMatchesWildcard(t *testing.T) { 23 | doWildcardTest(t, "", "", true) 24 | doWildcardTest(t, "example.com", "example.com", true) 25 | doWildcardTest(t, "example.org", "example.com", false) 26 | doWildcardTest(t, "example.com", "", false) 27 | doWildcardTest(t, "", "example.com", false) 28 | doWildcardTest(t, "", "*.example.com", false) 29 | doWildcardTest(t, "", "exam*ple.com", false) 30 | doWildcardTest(t, "", "exam*ple.co*m", false) 31 | doWildcardTest(t, "example.org", "example.com", false) 32 | doWildcardTest(t, "example.org", "*.example.com", false) 33 | doWildcardTest(t, "example.org", "exam*ple.com", false) 34 | doWildcardTest(t, "example.org", "exam*ple.co*m", false) 35 | doWildcardTest(t, "example.com", "*.example.com", false) 36 | doWildcardTest(t, "www.example.com", "*.example.com", true) 37 | doWildcardTest(t, "", "*", true) 38 | doWildcardTest(t, "", "****", true) 39 | doWildcardTest(t, "a", "****", true) 40 | doWildcardTest(t, "a", "*", true) 41 | doWildcardTest(t, "a", "****", true) 42 | doWildcardTest(t, "abcd", "****", true) 43 | doWildcardTest(t, "abcdef", "****", true) 44 | doWildcardTest(t, "www-example.com", "*-example.com", true) 45 | doWildcardTest(t, "www-example-www.com", "*-example-*.com", true) 46 | doWildcardTest(t, "examplecom", "example*", true) 47 | doWildcardTest(t, "example.com", "example*", false) 48 | doWildcardTest(t, "examplea.com", "example*", false) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package cmd 11 | 12 | import ( 13 | "crypto/sha256" 14 | "encoding/hex" 15 | "encoding/json" 16 | "io/ioutil" 17 | "os" 18 | 19 | "software.sslmate.com/src/certspotter/ct" 20 | ) 21 | 22 | func fileExists(path string) bool { 23 | _, err := os.Lstat(path) 24 | return err == nil 25 | } 26 | 27 | func writeFile(filename string, data []byte, perm os.FileMode) error { 28 | tempname := filename + ".new" 29 | if err := ioutil.WriteFile(tempname, data, perm); err != nil { 30 | return err 31 | } 32 | if err := os.Rename(tempname, filename); err != nil { 33 | os.Remove(tempname) 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | func writeJSONFile(filename string, obj interface{}, perm os.FileMode) error { 40 | tempname := filename + ".new" 41 | f, err := os.OpenFile(tempname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 42 | if err != nil { 43 | return err 44 | } 45 | if err := json.NewEncoder(f).Encode(obj); err != nil { 46 | f.Close() 47 | os.Remove(tempname) 48 | return err 49 | } 50 | if err := f.Close(); err != nil { 51 | os.Remove(tempname) 52 | return err 53 | } 54 | if err := os.Rename(tempname, filename); err != nil { 55 | os.Remove(tempname) 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func readJSONFile(filename string, obj interface{}) error { 62 | bytes, err := ioutil.ReadFile(filename) 63 | if err != nil { 64 | return err 65 | } 66 | if err = json.Unmarshal(bytes, obj); err != nil { 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | func readSTHFile(filename string) (*ct.SignedTreeHead, error) { 73 | sth := new(ct.SignedTreeHead) 74 | if err := readJSONFile(filename, sth); err != nil { 75 | return nil, err 76 | } 77 | return sth, nil 78 | } 79 | 80 | func sha256sum(data []byte) []byte { 81 | sum := sha256.Sum256(data) 82 | return sum[:] 83 | } 84 | 85 | func sha256hex(data []byte) string { 86 | return hex.EncodeToString(sha256sum(data)) 87 | } 88 | -------------------------------------------------------------------------------- /asn1.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "encoding/asn1" 15 | "encoding/binary" 16 | "errors" 17 | "unicode/utf8" 18 | ) 19 | 20 | func stringFromByteSlice(chars []byte) string { 21 | runes := make([]rune, len(chars)) 22 | for i, ch := range chars { 23 | runes[i] = rune(ch) 24 | } 25 | return string(runes) 26 | } 27 | 28 | func stringFromUint16Slice(chars []uint16) string { 29 | runes := make([]rune, len(chars)) 30 | for i, ch := range chars { 31 | runes[i] = rune(ch) 32 | } 33 | return string(runes) 34 | } 35 | 36 | func stringFromUint32Slice(chars []uint32) string { 37 | runes := make([]rune, len(chars)) 38 | for i, ch := range chars { 39 | runes[i] = rune(ch) 40 | } 41 | return string(runes) 42 | } 43 | 44 | func decodeASN1String(value *asn1.RawValue) (string, error) { 45 | if !value.IsCompound && value.Class == 0 { 46 | if value.Tag == 12 { 47 | // UTF8String 48 | if !utf8.Valid(value.Bytes) { 49 | return "", errors.New("Malformed UTF8String") 50 | } 51 | return string(value.Bytes), nil 52 | } else if value.Tag == 19 || value.Tag == 22 || value.Tag == 20 { 53 | // * PrintableString - subset of ASCII 54 | // * IA5String - ASCII 55 | // * TeletexString - 8 bit charset; not quite ISO-8859-1, but often treated as such 56 | 57 | // Don't enforce character set rules. Allow any 8 bit character, since 58 | // CAs routinely mess this up 59 | return stringFromByteSlice(value.Bytes), nil 60 | } else if value.Tag == 30 { 61 | // BMPString - Unicode, encoded in big-endian format using two octets 62 | runes := make([]uint16, len(value.Bytes)/2) 63 | if err := binary.Read(bytes.NewReader(value.Bytes), binary.BigEndian, runes); err != nil { 64 | return "", errors.New("Malformed BMPString: " + err.Error()) 65 | } 66 | return stringFromUint16Slice(runes), nil 67 | } else if value.Tag == 28 { 68 | // UniversalString - Unicode, encoded in big-endian format using four octets 69 | runes := make([]uint32, len(value.Bytes)/4) 70 | if err := binary.Read(bytes.NewReader(value.Bytes), binary.BigEndian, runes); err != nil { 71 | return "", errors.New("Malformed UniversalString: " + err.Error()) 72 | } 73 | return stringFromUint32Slice(runes), nil 74 | } 75 | } 76 | return "", errors.New("Not a string") 77 | } 78 | -------------------------------------------------------------------------------- /ct/signatures.go: -------------------------------------------------------------------------------- 1 | package ct 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "encoding/asn1" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | "log" 14 | "math/big" 15 | ) 16 | 17 | // PublicKeyFromPEM parses a PEM formatted block and returns the public key contained within and any remaining unread bytes, or an error. 18 | func PublicKeyFromPEM(b []byte) (crypto.PublicKey, SHA256Hash, []byte, error) { 19 | p, rest := pem.Decode(b) 20 | if p == nil { 21 | return nil, [sha256.Size]byte{}, rest, fmt.Errorf("no PEM block found in %s", string(b)) 22 | } 23 | k, err := x509.ParsePKIXPublicKey(p.Bytes) 24 | return k, sha256.Sum256(p.Bytes), rest, err 25 | } 26 | 27 | // SignatureVerifier can verify signatures on SCTs and STHs 28 | type SignatureVerifier struct { 29 | pubKey crypto.PublicKey 30 | } 31 | 32 | // NewSignatureVerifier creates a new SignatureVerifier using the passed in PublicKey. 33 | func NewSignatureVerifier(pk crypto.PublicKey) (*SignatureVerifier, error) { 34 | switch pkType := pk.(type) { 35 | case *rsa.PublicKey: 36 | case *ecdsa.PublicKey: 37 | default: 38 | return nil, fmt.Errorf("Unsupported public key type %v", pkType) 39 | } 40 | 41 | return &SignatureVerifier{ 42 | pubKey: pk, 43 | }, nil 44 | } 45 | 46 | // verifySignature verifies that the passed in signature over data was created by our PublicKey. 47 | // Currently, only SHA256 is supported as a HashAlgorithm, and only ECDSA and RSA signatures are supported. 48 | func (s SignatureVerifier) verifySignature(data []byte, sig DigitallySigned) error { 49 | if sig.HashAlgorithm != SHA256 { 50 | return fmt.Errorf("unsupported HashAlgorithm in signature: %v", sig.HashAlgorithm) 51 | } 52 | 53 | hasherType := crypto.SHA256 54 | hasher := hasherType.New() 55 | if _, err := hasher.Write(data); err != nil { 56 | return fmt.Errorf("failed to write to hasher: %v", err) 57 | } 58 | hash := hasher.Sum([]byte{}) 59 | 60 | switch sig.SignatureAlgorithm { 61 | case RSA: 62 | rsaKey, ok := s.pubKey.(*rsa.PublicKey) 63 | if !ok { 64 | return fmt.Errorf("cannot verify RSA signature with %T key", s.pubKey) 65 | } 66 | if err := rsa.VerifyPKCS1v15(rsaKey, hasherType, hash, sig.Signature); err != nil { 67 | return fmt.Errorf("failed to verify rsa signature: %v", err) 68 | } 69 | case ECDSA: 70 | ecdsaKey, ok := s.pubKey.(*ecdsa.PublicKey) 71 | if !ok { 72 | return fmt.Errorf("cannot verify ECDSA signature with %T key", s.pubKey) 73 | } 74 | var ecdsaSig struct { 75 | R, S *big.Int 76 | } 77 | rest, err := asn1.Unmarshal(sig.Signature, &ecdsaSig) 78 | if err != nil { 79 | return fmt.Errorf("failed to unmarshal ECDSA signature: %v", err) 80 | } 81 | if len(rest) != 0 { 82 | log.Printf("Garbage following signature %v", rest) 83 | } 84 | 85 | if !ecdsa.Verify(ecdsaKey, hash, ecdsaSig.R, ecdsaSig.S) { 86 | return errors.New("failed to verify ecdsa signature") 87 | } 88 | default: 89 | return fmt.Errorf("unsupported signature type %v", sig.SignatureAlgorithm) 90 | } 91 | return nil 92 | } 93 | 94 | // VerifySCTSignature verifies that the SCT's signature is valid for the given LogEntry 95 | func (s SignatureVerifier) VerifySCTSignature(sct SignedCertificateTimestamp, entry LogEntry) error { 96 | sctData, err := SerializeSCTSignatureInput(sct, entry) 97 | if err != nil { 98 | return err 99 | } 100 | return s.verifySignature(sctData, sct.Signature) 101 | } 102 | 103 | // VerifySTHSignature verifies that the STH's signature is valid. 104 | func (s SignatureVerifier) VerifySTHSignature(sth SignedTreeHead) error { 105 | sthData, err := SerializeSTHSignatureInput(sth) 106 | if err != nil { 107 | return err 108 | } 109 | return s.verifySignature(sthData, sth.TreeHeadSignature) 110 | } 111 | -------------------------------------------------------------------------------- /cmd/log_state.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package cmd 11 | 12 | import ( 13 | "crypto/sha256" 14 | "encoding/base64" 15 | "encoding/binary" 16 | "fmt" 17 | "os" 18 | "path/filepath" 19 | "strconv" 20 | "strings" 21 | 22 | "software.sslmate.com/src/certspotter" 23 | "software.sslmate.com/src/certspotter/ct" 24 | ) 25 | 26 | type LogState struct { 27 | path string 28 | } 29 | 30 | // generate a filename that uniquely identifies the STH (within the context of a particular log) 31 | func sthFilename(sth *ct.SignedTreeHead) string { 32 | hasher := sha256.New() 33 | switch sth.Version { 34 | case ct.V1: 35 | binary.Write(hasher, binary.LittleEndian, sth.Timestamp) 36 | binary.Write(hasher, binary.LittleEndian, sth.SHA256RootHash) 37 | default: 38 | panic(fmt.Sprintf("Unsupported STH version %d", sth.Version)) 39 | } 40 | // For 6962-bis, we will need to handle a variable-length root hash, and include the signature in the filename hash (since signatures must be deterministic) 41 | return strconv.FormatUint(sth.TreeSize, 10) + "-" + base64.RawURLEncoding.EncodeToString(hasher.Sum(nil)) + ".json" 42 | } 43 | 44 | func makeLogStateDir(logStatePath string) error { 45 | if err := os.Mkdir(logStatePath, 0777); err != nil && !os.IsExist(err) { 46 | return fmt.Errorf("%s: %s", logStatePath, err) 47 | } 48 | for _, subdir := range []string{"unverified_sths"} { 49 | path := filepath.Join(logStatePath, subdir) 50 | if err := os.Mkdir(path, 0777); err != nil && !os.IsExist(err) { 51 | return fmt.Errorf("%s: %s", path, err) 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | func OpenLogState(logStatePath string) (*LogState, error) { 58 | if err := makeLogStateDir(logStatePath); err != nil { 59 | return nil, fmt.Errorf("Error creating log state directory: %s", err) 60 | } 61 | return &LogState{path: logStatePath}, nil 62 | } 63 | 64 | func (logState *LogState) VerifiedSTHFilename() string { 65 | return filepath.Join(logState.path, "sth.json") 66 | } 67 | 68 | func (logState *LogState) GetVerifiedSTH() (*ct.SignedTreeHead, error) { 69 | sth, err := readSTHFile(logState.VerifiedSTHFilename()) 70 | if err != nil { 71 | if os.IsNotExist(err) { 72 | return nil, nil 73 | } else { 74 | return nil, err 75 | } 76 | } 77 | return sth, nil 78 | } 79 | 80 | func (logState *LogState) StoreVerifiedSTH(sth *ct.SignedTreeHead) error { 81 | return writeJSONFile(logState.VerifiedSTHFilename(), sth, 0666) 82 | } 83 | 84 | func (logState *LogState) GetUnverifiedSTHs() ([]*ct.SignedTreeHead, error) { 85 | dir, err := os.Open(filepath.Join(logState.path, "unverified_sths")) 86 | if err != nil { 87 | if os.IsNotExist(err) { 88 | return []*ct.SignedTreeHead{}, nil 89 | } else { 90 | return nil, err 91 | } 92 | } 93 | filenames, err := dir.Readdirnames(0) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | sths := make([]*ct.SignedTreeHead, 0, len(filenames)) 99 | for _, filename := range filenames { 100 | if !strings.HasPrefix(filename, ".") { 101 | sth, _ := readSTHFile(filepath.Join(dir.Name(), filename)) 102 | if sth != nil { 103 | sths = append(sths, sth) 104 | } 105 | } 106 | } 107 | return sths, nil 108 | } 109 | 110 | func (logState *LogState) UnverifiedSTHFilename(sth *ct.SignedTreeHead) string { 111 | return filepath.Join(logState.path, "unverified_sths", sthFilename(sth)) 112 | } 113 | 114 | func (logState *LogState) StoreUnverifiedSTH(sth *ct.SignedTreeHead) error { 115 | filename := logState.UnverifiedSTHFilename(sth) 116 | if fileExists(filename) { 117 | return nil 118 | } 119 | return writeJSONFile(filename, sth, 0666) 120 | } 121 | 122 | func (logState *LogState) RemoveUnverifiedSTH(sth *ct.SignedTreeHead) error { 123 | filename := logState.UnverifiedSTHFilename(sth) 124 | err := os.Remove(filepath.Join(filename)) 125 | if err != nil && !os.IsNotExist(err) { 126 | return err 127 | } 128 | return nil 129 | } 130 | 131 | func (logState *LogState) GetTree() (*certspotter.CollapsedMerkleTree, error) { 132 | tree := new(certspotter.CollapsedMerkleTree) 133 | if err := readJSONFile(filepath.Join(logState.path, "tree.json"), tree); err != nil { 134 | if os.IsNotExist(err) { 135 | return nil, nil 136 | } else { 137 | return nil, err 138 | } 139 | } 140 | return tree, nil 141 | } 142 | 143 | func (logState *LogState) StoreTree(tree *certspotter.CollapsedMerkleTree) error { 144 | return writeJSONFile(filepath.Join(logState.path, "tree.json"), tree, 0666) 145 | } 146 | -------------------------------------------------------------------------------- /precerts.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "encoding/asn1" 15 | "errors" 16 | "fmt" 17 | ) 18 | 19 | func bitStringEqual(a, b *asn1.BitString) bool { 20 | return a.BitLength == b.BitLength && bytes.Equal(a.Bytes, b.Bytes) 21 | } 22 | 23 | var ( 24 | oidExtensionAuthorityKeyId = []int{2, 5, 29, 35} 25 | oidExtensionSCT = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2} 26 | oidExtensionCTPoison = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} 27 | ) 28 | 29 | func ValidatePrecert(precertBytes []byte, tbsBytes []byte) error { 30 | precert, err := ParseCertificate(precertBytes) 31 | if err != nil { 32 | return errors.New("failed to parse pre-certificate: " + err.Error()) 33 | } 34 | precertTBS, err := precert.ParseTBSCertificate() 35 | if err != nil { 36 | return errors.New("failed to parse pre-certificate TBS: " + err.Error()) 37 | } 38 | tbs, err := ParseTBSCertificate(tbsBytes) 39 | if err != nil { 40 | return errors.New("failed to parse TBS: " + err.Error()) 41 | } 42 | 43 | // Everything must be equal except: 44 | // issuer 45 | // Authority Key Identifier extension (both must have it OR neither can have it) 46 | // CT poison extension (precert must have it, TBS must not have it) 47 | if precertTBS.Version != tbs.Version { 48 | return errors.New("version not equal") 49 | } 50 | if !bytes.Equal(precertTBS.SerialNumber.FullBytes, tbs.SerialNumber.FullBytes) { 51 | return errors.New("serial number not equal") 52 | } 53 | sameIssuer := bytes.Equal(precertTBS.Issuer.FullBytes, tbs.Issuer.FullBytes) 54 | if !bytes.Equal(precertTBS.SignatureAlgorithm.FullBytes, tbs.SignatureAlgorithm.FullBytes) { 55 | return errors.New("SignatureAlgorithm not equal") 56 | } 57 | if !bytes.Equal(precertTBS.Validity.FullBytes, tbs.Validity.FullBytes) { 58 | return errors.New("Validity not equal") 59 | } 60 | if !bytes.Equal(precertTBS.Subject.FullBytes, tbs.Subject.FullBytes) { 61 | return errors.New("Subject not equal") 62 | } 63 | if !bytes.Equal(precertTBS.PublicKey.FullBytes, tbs.PublicKey.FullBytes) { 64 | return errors.New("PublicKey not equal") 65 | } 66 | if !bitStringEqual(&precertTBS.UniqueId, &tbs.UniqueId) { 67 | return errors.New("UniqueId not equal") 68 | } 69 | if !bitStringEqual(&precertTBS.SubjectUniqueId, &tbs.SubjectUniqueId) { 70 | return errors.New("SubjectUniqueId not equal") 71 | } 72 | 73 | precertHasPoison := false 74 | tbsIndex := 0 75 | for precertIndex := range precertTBS.Extensions { 76 | precertExt := &precertTBS.Extensions[precertIndex] 77 | 78 | if precertExt.Id.Equal(oidExtensionCTPoison) { 79 | if !precertExt.Critical { 80 | return errors.New("pre-cert poison extension is not critical") 81 | } 82 | /* CAs can't even get this right, and Google's logs don't check. Fortunately, 83 | it's not that important. 84 | if !bytes.Equal(precertExt.Value, []byte{0x05, 0x00}) { 85 | return errors.New("pre-cert poison extension contains incorrect value") 86 | } 87 | */ 88 | precertHasPoison = true 89 | continue 90 | } 91 | 92 | if tbsIndex >= len(tbs.Extensions) { 93 | return errors.New("pre-cert contains extension not in TBS") 94 | } 95 | tbsExt := &tbs.Extensions[tbsIndex] 96 | 97 | if !precertExt.Id.Equal(tbsExt.Id) { 98 | return fmt.Errorf("pre-cert and TBS contain different extensions (%v vs %v)", precertExt.Id, tbsExt.Id) 99 | } 100 | if precertExt.Critical != tbsExt.Critical { 101 | return fmt.Errorf("pre-cert and TBS %v extension differs in criticality", precertExt.Id) 102 | } 103 | if !precertExt.Id.Equal(oidExtensionAuthorityKeyId) || sameIssuer { 104 | if !bytes.Equal(precertExt.Value, tbsExt.Value) { 105 | return fmt.Errorf("pre-cert and TBS %v extension differs in value", precertExt.Id) 106 | } 107 | } 108 | 109 | tbsIndex++ 110 | } 111 | if tbsIndex < len(tbs.Extensions) { 112 | return errors.New("TBS contains extension not in pre-cert") 113 | } 114 | if !precertHasPoison { 115 | return errors.New("pre-cert does not have poison extension") 116 | } 117 | 118 | return nil 119 | } 120 | func ReconstructPrecertTBS(tbs *TBSCertificate) (*TBSCertificate, error) { 121 | precertTBS := TBSCertificate{ 122 | Version: tbs.Version, 123 | SerialNumber: tbs.SerialNumber, 124 | SignatureAlgorithm: tbs.SignatureAlgorithm, 125 | Issuer: tbs.Issuer, 126 | Validity: tbs.Validity, 127 | Subject: tbs.Subject, 128 | PublicKey: tbs.PublicKey, 129 | UniqueId: tbs.UniqueId, 130 | SubjectUniqueId: tbs.SubjectUniqueId, 131 | Extensions: make([]Extension, 0, len(tbs.Extensions)), 132 | } 133 | 134 | for _, ext := range tbs.Extensions { 135 | switch { 136 | case ext.Id.Equal(oidExtensionSCT): 137 | default: 138 | precertTBS.Extensions = append(precertTBS.Extensions, ext) 139 | } 140 | } 141 | 142 | var err error 143 | precertTBS.Raw, err = asn1.Marshal(precertTBS) 144 | return &precertTBS, err 145 | } 146 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "crypto" 14 | "crypto/sha256" 15 | "crypto/x509" 16 | "encoding/base64" 17 | ) 18 | 19 | type LogInfoFile struct { 20 | Logs []LogInfo `json:"logs"` 21 | } 22 | type LogInfo struct { 23 | Description string `json:"description"` 24 | Key []byte `json:"key"` 25 | Url string `json:"url"` 26 | MMD int `json:"maximum_merge_delay"` 27 | } 28 | 29 | func (info *LogInfo) FullURI() string { 30 | return "https://" + info.Url 31 | } 32 | 33 | func (info *LogInfo) ParsedPublicKey() (crypto.PublicKey, error) { 34 | if info.Key != nil { 35 | return x509.ParsePKIXPublicKey(info.Key) 36 | } else { 37 | return nil, nil 38 | } 39 | } 40 | 41 | func (info *LogInfo) ID() []byte { 42 | sum := sha256.Sum256(info.Key) 43 | return sum[:] 44 | } 45 | 46 | var DefaultLogs = []LogInfo{ 47 | { 48 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA=="), 49 | Url: "ct.googleapis.com/pilot", 50 | MMD: 86400, 51 | }, 52 | { 53 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q=="), 54 | Url: "ct.googleapis.com/aviator", 55 | MMD: 86400, 56 | }, 57 | { 58 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A=="), 59 | Url: "ct1.digicert-ct.com/log", 60 | MMD: 86400, 61 | }, 62 | { 63 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg=="), 64 | Url: "ct.googleapis.com/rocketeer", 65 | MMD: 86400, 66 | }, 67 | { 68 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg=="), 69 | Url: "ct.ws.symantec.com", 70 | MMD: 86400, 71 | }, 72 | { 73 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ=="), 74 | Url: "vega.ws.symantec.com", 75 | MMD: 86400, 76 | }, 77 | { 78 | Key: mustDecodeBase64("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7UIYZopMgTTJWPp2IXhhuAf1l6a9zM7gBvntj5fLaFm9pVKhKYhVnno94XuXeN8EsDgiSIJIj66FpUGvai5samyetZhLocRuXhAiXXbDNyQ4KR51tVebtEq2zT0mT9liTtGwiksFQccyUsaVPhsHq9gJ2IKZdWauVA2Fm5x9h8B9xKn/L/2IaMpkIYtd967TNTP/dLPgixN1PLCLaypvurDGSVDsuWabA3FHKWL9z8wr7kBkbdpEhLlg2H+NAC+9nGKx+tQkuhZ/hWR65aX+CNUPy2OB9/u2rNPyDydb988LENXoUcMkQT0dU3aiYGkFAY0uZjD2vH97TM20xYtNQIDAQAB"), 79 | Url: "ctserver.cnnic.cn", 80 | MMD: 86400, 81 | }, 82 | { 83 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA=="), 84 | Url: "ct.googleapis.com/icarus", 85 | MMD: 86400, 86 | }, 87 | { 88 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA=="), 89 | Url: "ct.googleapis.com/skydiver", 90 | MMD: 86400, 91 | }, 92 | { 93 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESPNZ8/YFGNPbsu1Gfs/IEbVXsajWTOaft0oaFIZDqUiwy1o/PErK38SCFFWa+PeOQFXc9NKv6nV0+05/YIYuUQ=="), 94 | Url: "ct.startssl.com", 95 | MMD: 86400, 96 | }, 97 | { 98 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBGIey1my66PTTBmJxklIpMhRrQvAdPG+SvVyLpzmwai8IoCnNBrRhgwhbrpJIsO0VtwKAx+8TpFf1rzgkJgMQ=="), 99 | Url: "ctlog.wosign.com", 100 | MMD: 86400, 101 | }, 102 | { 103 | Key: mustDecodeBase64("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArM8vS3Cs8Q2Wv+gK/kSd1IwXncOaEBGEE+2M+Tdtg+QAb7FLwKaJx2GPmjS7VlLKA1ZQ7yR/S0npNYHd8OcX9XLSI8XjE3/Xjng1j0nemASKY6+tojlwlYRoS5Ez/kzhMhfC8mG4Oo05f9WVgj5WGVBFb8sIMw3VGUIIGkhCEPFow8NBE8sNHtsCtyR6UZZuvAjqaa9t75KYjlXzZeXonL4aR2AwfXqArVaDepPDrpMraiiKpl9jGQy+fHshY0E4t/fodnNrhcy8civBUtBbXTFOnSrzTZtkFJkmxnH4e/hE1eMjIPMK14tRPnKA0nh4NS1K50CZEZU01C9/+V81NwIDAQAB"), 104 | Url: "www.certificatetransparency.cn/ct", 105 | MMD: 86400, 106 | }, 107 | } 108 | 109 | // Logs which monitor certs from distrusted roots 110 | var UnderwaterLogs = []LogInfo{ 111 | { 112 | Description: "Google 'Submariner' log", 113 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOfifIGLUV1Voou9JLfA5LZreRLSUMOCeeic8q3Dw0fpRkGMWV0Gtq20fgHQweQJeLVmEByQj9p81uIW4QkWkTw=="), 114 | Url: "ct.googleapis.com/submariner", 115 | MMD: 86400, 116 | }, 117 | } 118 | 119 | // Logs which accept submissions from anyone 120 | var OpenLogs = []LogInfo{ 121 | { 122 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA=="), 123 | Url: "ct.googleapis.com/pilot", 124 | MMD: 86400, 125 | }, 126 | { 127 | Key: mustDecodeBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg=="), 128 | Url: "ct.googleapis.com/rocketeer", 129 | MMD: 86400, 130 | }, 131 | } 132 | 133 | func mustDecodeBase64(str string) []byte { 134 | bytes, err := base64.StdEncoding.DecodeString(str) 135 | if err != nil { 136 | panic("MustDecodeBase64: " + err.Error()) 137 | } 138 | return bytes 139 | } 140 | -------------------------------------------------------------------------------- /asn1time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "testing" 14 | "time" 15 | ) 16 | 17 | type timeTest struct { 18 | in string 19 | ok bool 20 | out time.Time 21 | } 22 | 23 | var utcTimeTests = []timeTest{ 24 | {"9502101525Z", true, time.Date(1995, time.February, 10, 15, 25, 0, 0, time.UTC)}, 25 | {"950210152542Z", true, time.Date(1995, time.February, 10, 15, 25, 42, 0, time.UTC)}, 26 | {"1502101525Z", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC)}, 27 | {"150210152542Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC)}, 28 | {"1502101525+1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10*3600))}, 29 | {"1502101525-1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1*(10*3600)))}, 30 | {"1502101525+1035", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10*3600+35*60))}, 31 | {"1502101525-1035", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1*(10*3600+35*60)))}, 32 | {"150210152542+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10*3600))}, 33 | {"150210152542-1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1*(10*3600)))}, 34 | {"150210152542+1035", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10*3600+35*60))}, 35 | {"150210152542-1035", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1*(10*3600+35*60)))}, 36 | {"1502101525", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC)}, 37 | {"150210152542", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC)}, 38 | {"", false, time.Time{}}, 39 | {"123", false, time.Time{}}, 40 | {"150210152542-10", false, time.Time{}}, 41 | {"150210152542F", false, time.Time{}}, 42 | {"150210152542ZF", false, time.Time{}}, 43 | } 44 | 45 | func TestUTCTime(t *testing.T) { 46 | for i, test := range utcTimeTests { 47 | ret, err := parseUTCTime([]byte(test.in)) 48 | if err != nil { 49 | if test.ok { 50 | t.Errorf("#%d: parseUTCTime(%q) failed with error %v", i, test.in, err) 51 | } 52 | continue 53 | } 54 | if !test.ok { 55 | t.Errorf("#%d: parseUTCTime(%q) succeeded, should have failed", i, test.in) 56 | continue 57 | } 58 | if !test.out.Equal(ret) { 59 | t.Errorf("#%d: parseUTCTime(%q) = %v, want %v", i, test.in, ret, test.out) 60 | } 61 | } 62 | } 63 | 64 | var generalizedTimeTests = []timeTest{ 65 | {"2015021015", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.UTC)}, 66 | {"201502101525", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC)}, 67 | {"20150210152542", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC)}, 68 | {"20150210152542.123", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.UTC)}, 69 | {"20150210152542.12", false, time.Time{}}, 70 | {"20150210152542.1", false, time.Time{}}, 71 | {"20150210152542.", false, time.Time{}}, 72 | 73 | {"2015021015Z", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.UTC)}, 74 | {"201502101525Z", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC)}, 75 | {"20150210152542Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC)}, 76 | {"20150210152542.123Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.UTC)}, 77 | {"20150210152542.12Z", false, time.Time{}}, 78 | {"20150210152542.1Z", false, time.Time{}}, 79 | {"20150210152542.Z", false, time.Time{}}, 80 | 81 | {"2015021015+1000", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.FixedZone("", 10*3600))}, 82 | {"201502101525+1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10*3600))}, 83 | {"20150210152542+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10*3600))}, 84 | {"20150210152542.123+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.FixedZone("", 10*3600))}, 85 | {"20150210152542.12+1000", false, time.Time{}}, 86 | {"20150210152542.1+1000", false, time.Time{}}, 87 | {"20150210152542.+1000", false, time.Time{}}, 88 | 89 | {"2015021015-0835", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.FixedZone("", -1*(8*3600+35*60)))}, 90 | {"201502101525-0835", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1*(8*3600+35*60)))}, 91 | {"20150210152542-0835", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1*(8*3600+35*60)))}, 92 | {"20150210152542.123-0835", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.FixedZone("", -1*(8*3600+35*60)))}, 93 | {"20150210152542.12-0835", false, time.Time{}}, 94 | {"20150210152542.1-0835", false, time.Time{}}, 95 | {"20150210152542.-0835", false, time.Time{}}, 96 | 97 | {"", false, time.Time{}}, 98 | {"123", false, time.Time{}}, 99 | {"2015021015+1000Z", false, time.Time{}}, 100 | {"2015021015x", false, time.Time{}}, 101 | {"201502101525Zf", false, time.Time{}}, 102 | } 103 | 104 | func TestGeneralizedTime(t *testing.T) { 105 | for i, test := range generalizedTimeTests { 106 | ret, err := parseGeneralizedTime([]byte(test.in)) 107 | if err != nil { 108 | if test.ok { 109 | t.Errorf("#%d: parseGeneralizedTime(%q) failed with error %v", i, test.in, err) 110 | } 111 | continue 112 | } 113 | if !test.ok { 114 | t.Errorf("#%d: parseGeneralizedTime(%q) succeeded, should have failed", i, test.in) 115 | continue 116 | } 117 | if !test.out.Equal(ret) { 118 | t.Errorf("#%d: parseGeneralizedTime(%q) = %v, want %v", i, test.in, ret, test.out) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /cmd/certspotter/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package main 11 | 12 | import ( 13 | "bufio" 14 | "flag" 15 | "fmt" 16 | "io" 17 | "os" 18 | "path/filepath" 19 | "strings" 20 | 21 | "golang.org/x/net/idna" 22 | 23 | "software.sslmate.com/src/certspotter" 24 | "software.sslmate.com/src/certspotter/cmd" 25 | "software.sslmate.com/src/certspotter/ct" 26 | ) 27 | 28 | func defaultStateDir() string { 29 | if envVar := os.Getenv("CERTSPOTTER_STATE_DIR"); envVar != "" { 30 | return envVar 31 | } else { 32 | return cmd.DefaultStateDir("certspotter") 33 | } 34 | } 35 | func defaultConfigDir() string { 36 | if envVar := os.Getenv("CERTSPOTTER_CONFIG_DIR"); envVar != "" { 37 | return envVar 38 | } else { 39 | return cmd.DefaultConfigDir("certspotter") 40 | } 41 | } 42 | 43 | func trimTrailingDots(value string) string { 44 | length := len(value) 45 | for length > 0 && value[length-1] == '.' { 46 | length-- 47 | } 48 | return value[0:length] 49 | } 50 | 51 | var stateDir = flag.String("state_dir", defaultStateDir(), "Directory for storing state") 52 | var watchlistFilename = flag.String("watchlist", filepath.Join(defaultConfigDir(), "watchlist"), "File containing identifiers to watch (- for stdin)") 53 | 54 | type watchlistItem struct { 55 | Domain []string 56 | AcceptSuffix bool 57 | } 58 | 59 | var watchlist []watchlistItem 60 | 61 | func parseWatchlistItem(str string) (watchlistItem, error) { 62 | if str == "." { // "." as in root zone (matches everything) 63 | return watchlistItem{ 64 | Domain: []string{}, 65 | AcceptSuffix: true, 66 | }, nil 67 | } else { 68 | acceptSuffix := false 69 | if strings.HasPrefix(str, ".") { 70 | acceptSuffix = true 71 | str = str[1:] 72 | } 73 | asciiDomain, err := idna.ToASCII(strings.ToLower(trimTrailingDots(str))) 74 | if err != nil { 75 | return watchlistItem{}, fmt.Errorf("Invalid domain `%s': %s", str, err) 76 | } 77 | return watchlistItem{ 78 | Domain: strings.Split(asciiDomain, "."), 79 | AcceptSuffix: acceptSuffix, 80 | }, nil 81 | } 82 | } 83 | 84 | func readWatchlist(reader io.Reader) ([]watchlistItem, error) { 85 | items := []watchlistItem{} 86 | scanner := bufio.NewScanner(reader) 87 | for scanner.Scan() { 88 | line := scanner.Text() 89 | if line == "" || strings.HasPrefix(line, "#") { 90 | continue 91 | } 92 | item, err := parseWatchlistItem(line) 93 | if err != nil { 94 | return nil, err 95 | } 96 | items = append(items, item) 97 | } 98 | return items, scanner.Err() 99 | } 100 | 101 | func dnsLabelMatches(certLabel string, watchLabel string) bool { 102 | // For fail-safe behavior, if a label was unparsable, it matches everything. 103 | // Similarly, redacted labels match everything, since the label _might_ be 104 | // for a name we're interested in. 105 | 106 | return certLabel == "*" || 107 | certLabel == "?" || 108 | certLabel == certspotter.UnparsableDNSLabelPlaceholder || 109 | certspotter.MatchesWildcard(watchLabel, certLabel) 110 | } 111 | 112 | func dnsNameMatches(dnsName []string, watchDomain []string, acceptSuffix bool) bool { 113 | for len(dnsName) > 0 && len(watchDomain) > 0 { 114 | certLabel := dnsName[len(dnsName)-1] 115 | watchLabel := watchDomain[len(watchDomain)-1] 116 | 117 | if !dnsLabelMatches(certLabel, watchLabel) { 118 | return false 119 | } 120 | 121 | dnsName = dnsName[:len(dnsName)-1] 122 | watchDomain = watchDomain[:len(watchDomain)-1] 123 | } 124 | 125 | return len(watchDomain) == 0 && (acceptSuffix || len(dnsName) == 0) 126 | } 127 | 128 | func dnsNameIsWatched(dnsName string) bool { 129 | labels := strings.Split(dnsName, ".") 130 | for _, item := range watchlist { 131 | if dnsNameMatches(labels, item.Domain, item.AcceptSuffix) { 132 | return true 133 | } 134 | } 135 | return false 136 | } 137 | 138 | func anyDnsNameIsWatched(dnsNames []string) bool { 139 | for _, dnsName := range dnsNames { 140 | if dnsNameIsWatched(dnsName) { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | 147 | func processEntry(scanner *certspotter.Scanner, entry *ct.LogEntry) { 148 | info := certspotter.EntryInfo{ 149 | LogUri: scanner.LogUri, 150 | Entry: entry, 151 | IsPrecert: certspotter.IsPrecert(entry), 152 | FullChain: certspotter.GetFullChain(entry), 153 | } 154 | 155 | info.CertInfo, info.ParseError = certspotter.MakeCertInfoFromLogEntry(entry) 156 | 157 | if info.CertInfo != nil { 158 | info.Identifiers, info.IdentifiersParseError = info.CertInfo.ParseIdentifiers() 159 | } 160 | 161 | // Fail safe behavior: if info.Identifiers is nil (which is caused by a 162 | // parse error), report the certificate because we can't say for sure it 163 | // doesn't match a domain we care about. We try very hard to make sure 164 | // parsing identifiers always succeeds, so false alarms should be rare. 165 | if info.Identifiers == nil || anyDnsNameIsWatched(info.Identifiers.DNSNames) { 166 | cmd.LogEntry(&info) 167 | } 168 | } 169 | 170 | func main() { 171 | flag.Parse() 172 | 173 | if *watchlistFilename == "-" { 174 | var err error 175 | watchlist, err = readWatchlist(os.Stdin) 176 | if err != nil { 177 | fmt.Fprintf(os.Stderr, "%s: (stdin): %s\n", os.Args[0], err) 178 | os.Exit(1) 179 | } 180 | } else { 181 | file, err := os.Open(*watchlistFilename) 182 | if err != nil { 183 | fmt.Fprintf(os.Stderr, "%s: %s: %s\n", os.Args[0], *watchlistFilename, err) 184 | os.Exit(1) 185 | } 186 | defer file.Close() 187 | watchlist, err = readWatchlist(file) 188 | if err != nil { 189 | fmt.Fprintf(os.Stderr, "%s: %s: %s\n", os.Args[0], *watchlistFilename, err) 190 | os.Exit(1) 191 | } 192 | } 193 | 194 | os.Exit(cmd.Main(*stateDir, processEntry)) 195 | } 196 | -------------------------------------------------------------------------------- /cmd/submitct/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package main 11 | 12 | import ( 13 | "software.sslmate.com/src/certspotter" 14 | "software.sslmate.com/src/certspotter/ct" 15 | "software.sslmate.com/src/certspotter/ct/client" 16 | 17 | "bytes" 18 | "crypto/sha256" 19 | "encoding/pem" 20 | "flag" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "os" 25 | "sync" 26 | "sync/atomic" 27 | "time" 28 | ) 29 | 30 | var verbose = flag.Bool("v", false, "Enable verbose output") 31 | 32 | type Certificate struct { 33 | Subject []byte 34 | Issuer []byte 35 | Raw []byte 36 | } 37 | 38 | func (cert *Certificate) Fingerprint() [32]byte { 39 | return sha256.Sum256(cert.Raw) 40 | } 41 | 42 | func (cert *Certificate) CommonName() string { 43 | subject, err := certspotter.ParseRDNSequence(cert.Subject) 44 | if err != nil { 45 | return "???" 46 | } 47 | cns, err := subject.ParseCNs() 48 | if err != nil || len(cns) == 0 { 49 | return "???" 50 | } 51 | return cns[0] 52 | } 53 | 54 | func parseCertificate(data []byte) (*Certificate, error) { 55 | crt, err := certspotter.ParseCertificate(data) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | tbs, err := crt.ParseTBSCertificate() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return &Certificate{ 66 | Subject: tbs.Subject.FullBytes, 67 | Issuer: tbs.Issuer.FullBytes, 68 | Raw: data, 69 | }, nil 70 | } 71 | 72 | type Chain []*Certificate 73 | 74 | func (c Chain) GetRawCerts() [][]byte { 75 | rawCerts := make([][]byte, len(c)) 76 | for i := range c { 77 | rawCerts[i] = c[i].Raw 78 | } 79 | return rawCerts 80 | } 81 | 82 | type CertificateBunch struct { 83 | byFingerprint map[[32]byte]*Certificate 84 | bySubject map[[32]byte]*Certificate 85 | } 86 | 87 | func MakeCertificateBunch() CertificateBunch { 88 | return CertificateBunch{ 89 | byFingerprint: make(map[[32]byte]*Certificate), 90 | bySubject: make(map[[32]byte]*Certificate), 91 | } 92 | } 93 | 94 | func (certs *CertificateBunch) Add(cert *Certificate) { 95 | certs.byFingerprint[cert.Fingerprint()] = cert 96 | certs.bySubject[sha256.Sum256(cert.Subject)] = cert 97 | } 98 | 99 | func (certs *CertificateBunch) FindBySubject(subject []byte) *Certificate { 100 | return certs.bySubject[sha256.Sum256(subject)] 101 | } 102 | 103 | type Log struct { 104 | info certspotter.LogInfo 105 | verify *ct.SignatureVerifier 106 | client *client.LogClient 107 | } 108 | 109 | func (ctlog *Log) SubmitChain(chain Chain) (*ct.SignedCertificateTimestamp, error) { 110 | rawCerts := chain.GetRawCerts() 111 | sct, err := ctlog.client.AddChain(rawCerts) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | entry := ct.LogEntry{ 117 | Leaf: ct.MerkleTreeLeaf{ 118 | Version: 0, 119 | LeafType: ct.TimestampedEntryLeafType, 120 | TimestampedEntry: ct.TimestampedEntry{ 121 | Timestamp: sct.Timestamp, 122 | EntryType: ct.X509LogEntryType, 123 | X509Entry: rawCerts[0], 124 | Extensions: sct.Extensions, 125 | }, 126 | }, 127 | } 128 | 129 | if err := ctlog.verify.VerifySCTSignature(*sct, entry); err != nil { 130 | return nil, fmt.Errorf("Bad SCT signature: %s", err) 131 | } 132 | return sct, nil 133 | } 134 | 135 | func buildChain(cert *Certificate, certs *CertificateBunch) Chain { 136 | chain := make([]*Certificate, 0) 137 | for len(chain) < 16 && cert != nil && !bytes.Equal(cert.Subject, cert.Issuer) { 138 | chain = append(chain, cert) 139 | cert = certs.FindBySubject(cert.Issuer) 140 | } 141 | return chain 142 | } 143 | 144 | func main() { 145 | flag.Parse() 146 | log.SetPrefix("submitct: ") 147 | 148 | certsPem, err := ioutil.ReadAll(os.Stdin) 149 | if err != nil { 150 | log.Fatalf("Error reading stdin: %s", err) 151 | } 152 | 153 | logs := make([]Log, 0, len(certspotter.OpenLogs)) 154 | for _, loginfo := range certspotter.OpenLogs { 155 | pubkey, err := loginfo.ParsedPublicKey() 156 | if err != nil { 157 | log.Fatalf("%s: Failed to parse log public key: %s", loginfo.Url, err) 158 | } 159 | verify, err := ct.NewSignatureVerifier(pubkey) 160 | if err != nil { 161 | log.Fatalf("%s: Failed to create signature verifier for log: %s", loginfo.Url, err) 162 | } 163 | logs = append(logs, Log{ 164 | info: loginfo, 165 | verify: verify, 166 | client: client.New(loginfo.FullURI()), 167 | }) 168 | } 169 | 170 | certs := MakeCertificateBunch() 171 | var parseErrors uint32 172 | var submitErrors uint32 173 | 174 | for len(certsPem) > 0 { 175 | var pemBlock *pem.Block 176 | pemBlock, certsPem = pem.Decode(certsPem) 177 | if pemBlock == nil { 178 | log.Fatalf("Invalid PEM read from stdin") 179 | } 180 | if pemBlock.Type != "CERTIFICATE" { 181 | log.Printf("Ignoring non-certificate read from stdin") 182 | continue 183 | } 184 | 185 | cert, err := parseCertificate(pemBlock.Bytes) 186 | if err != nil { 187 | log.Printf("Ignoring un-parseable certificate read from stdin: %s", err) 188 | parseErrors++ 189 | continue 190 | } 191 | certs.Add(cert) 192 | } 193 | 194 | wg := sync.WaitGroup{} 195 | for fingerprint, cert := range certs.byFingerprint { 196 | cn := cert.CommonName() 197 | chain := buildChain(cert, &certs) 198 | if len(chain) == 0 { 199 | continue 200 | } 201 | for _, ctlog := range logs { 202 | wg.Add(1) 203 | go func(fingerprint [32]byte, ctlog Log) { 204 | sct, err := ctlog.SubmitChain(chain) 205 | if err != nil { 206 | log.Printf("%x (%s): %s: Submission Error: %s", fingerprint, cn, ctlog.info.Url, err) 207 | atomic.AddUint32(&submitErrors, 1) 208 | } else if *verbose { 209 | timestamp := time.Unix(int64(sct.Timestamp)/1000, int64(sct.Timestamp%1000)*1000000) 210 | log.Printf("%x (%s): %s: Submitted at %s", fingerprint, cn, ctlog.info.Url, timestamp) 211 | } 212 | wg.Done() 213 | }(fingerprint, ctlog) 214 | } 215 | } 216 | wg.Wait() 217 | 218 | exitStatus := 0 219 | if parseErrors > 0 { 220 | log.Printf("%d certificates failed to parse and were ignored", parseErrors) 221 | exitStatus |= 4 222 | } 223 | if submitErrors > 0 { 224 | log.Printf("%d submission errors occurred", submitErrors) 225 | exitStatus |= 8 226 | } 227 | os.Exit(exitStatus) 228 | } 229 | -------------------------------------------------------------------------------- /auditing.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "crypto/sha256" 15 | "encoding/json" 16 | "errors" 17 | "software.sslmate.com/src/certspotter/ct" 18 | ) 19 | 20 | func reverseHashes(hashes []ct.MerkleTreeNode) { 21 | for i := 0; i < len(hashes)/2; i++ { 22 | j := len(hashes) - i - 1 23 | hashes[i], hashes[j] = hashes[j], hashes[i] 24 | } 25 | } 26 | 27 | func VerifyConsistencyProof(proof ct.ConsistencyProof, first *ct.SignedTreeHead, second *ct.SignedTreeHead) bool { 28 | // TODO: make sure every hash in proof is right length? otherwise input to hashChildren is ambiguous 29 | if second.TreeSize < first.TreeSize { 30 | // Can't be consistent if tree got smaller 31 | return false 32 | } 33 | if first.TreeSize == second.TreeSize { 34 | if !(bytes.Equal(first.SHA256RootHash[:], second.SHA256RootHash[:]) && len(proof) == 0) { 35 | return false 36 | } 37 | return true 38 | } 39 | if first.TreeSize == 0 { 40 | // The purpose of the consistency proof is to ensure the append-only 41 | // nature of the tree; i.e. that the first tree is a "prefix" of the 42 | // second tree. If the first tree is empty, then it's trivially a prefix 43 | // of the second tree, so no proof is needed. 44 | if len(proof) != 0 { 45 | return false 46 | } 47 | return true 48 | } 49 | // Guaranteed that 0 < first.TreeSize < second.TreeSize 50 | 51 | node := first.TreeSize - 1 52 | lastNode := second.TreeSize - 1 53 | 54 | // While we're the right child, everything is in both trees, so move one level up. 55 | for node%2 == 1 { 56 | node /= 2 57 | lastNode /= 2 58 | } 59 | 60 | var newHash ct.MerkleTreeNode 61 | var oldHash ct.MerkleTreeNode 62 | if node > 0 { 63 | if len(proof) == 0 { 64 | return false 65 | } 66 | newHash = proof[0] 67 | proof = proof[1:] 68 | } else { 69 | // The old tree was balanced, so we already know the first hash to use 70 | newHash = first.SHA256RootHash[:] 71 | } 72 | oldHash = newHash 73 | 74 | for node > 0 { 75 | if node%2 == 1 { 76 | // node is a right child; left sibling exists in both trees 77 | if len(proof) == 0 { 78 | return false 79 | } 80 | newHash = hashChildren(proof[0], newHash) 81 | oldHash = hashChildren(proof[0], oldHash) 82 | proof = proof[1:] 83 | } else if node < lastNode { 84 | // node is a left child; rigth sibling only exists in the new tree 85 | if len(proof) == 0 { 86 | return false 87 | } 88 | newHash = hashChildren(newHash, proof[0]) 89 | proof = proof[1:] 90 | } // else node == lastNode: node is a left child with no sibling in either tree 91 | node /= 2 92 | lastNode /= 2 93 | } 94 | 95 | if !bytes.Equal(oldHash, first.SHA256RootHash[:]) { 96 | return false 97 | } 98 | 99 | // If trees have different height, continue up the path to reach the new root 100 | for lastNode > 0 { 101 | if len(proof) == 0 { 102 | return false 103 | } 104 | newHash = hashChildren(newHash, proof[0]) 105 | proof = proof[1:] 106 | lastNode /= 2 107 | } 108 | 109 | if !bytes.Equal(newHash, second.SHA256RootHash[:]) { 110 | return false 111 | } 112 | 113 | return true 114 | } 115 | 116 | func hashNothing() ct.MerkleTreeNode { 117 | return sha256.New().Sum(nil) 118 | } 119 | 120 | func hashLeaf(leafBytes []byte) ct.MerkleTreeNode { 121 | hasher := sha256.New() 122 | hasher.Write([]byte{0x00}) 123 | hasher.Write(leafBytes) 124 | return hasher.Sum(nil) 125 | } 126 | 127 | func hashChildren(left ct.MerkleTreeNode, right ct.MerkleTreeNode) ct.MerkleTreeNode { 128 | hasher := sha256.New() 129 | hasher.Write([]byte{0x01}) 130 | hasher.Write(left) 131 | hasher.Write(right) 132 | return hasher.Sum(nil) 133 | } 134 | 135 | type CollapsedMerkleTree struct { 136 | nodes []ct.MerkleTreeNode 137 | size uint64 138 | } 139 | 140 | func calculateNumNodes(size uint64) int { 141 | numNodes := 0 142 | for size > 0 { 143 | numNodes += int(size & 1) 144 | size >>= 1 145 | } 146 | return numNodes 147 | } 148 | func EmptyCollapsedMerkleTree() *CollapsedMerkleTree { 149 | return &CollapsedMerkleTree{} 150 | } 151 | func NewCollapsedMerkleTree(nodes []ct.MerkleTreeNode, size uint64) (*CollapsedMerkleTree, error) { 152 | if len(nodes) != calculateNumNodes(size) { 153 | return nil, errors.New("NewCollapsedMerkleTree: nodes has incorrect size") 154 | } 155 | return &CollapsedMerkleTree{nodes: nodes, size: size}, nil 156 | } 157 | func CloneCollapsedMerkleTree(source *CollapsedMerkleTree) *CollapsedMerkleTree { 158 | nodes := make([]ct.MerkleTreeNode, len(source.nodes)) 159 | copy(nodes, source.nodes) 160 | return &CollapsedMerkleTree{nodes: nodes, size: source.size} 161 | } 162 | 163 | func (tree *CollapsedMerkleTree) Add(hash ct.MerkleTreeNode) { 164 | tree.nodes = append(tree.nodes, hash) 165 | tree.size++ 166 | size := tree.size 167 | for size%2 == 0 { 168 | left, right := tree.nodes[len(tree.nodes)-2], tree.nodes[len(tree.nodes)-1] 169 | tree.nodes = tree.nodes[:len(tree.nodes)-2] 170 | tree.nodes = append(tree.nodes, hashChildren(left, right)) 171 | size /= 2 172 | } 173 | } 174 | 175 | func (tree *CollapsedMerkleTree) CalculateRoot() ct.MerkleTreeNode { 176 | if len(tree.nodes) == 0 { 177 | return hashNothing() 178 | } 179 | i := len(tree.nodes) - 1 180 | hash := tree.nodes[i] 181 | for i > 0 { 182 | i -= 1 183 | hash = hashChildren(tree.nodes[i], hash) 184 | } 185 | return hash 186 | } 187 | 188 | func (tree *CollapsedMerkleTree) GetSize() uint64 { 189 | return tree.size 190 | } 191 | 192 | func (tree *CollapsedMerkleTree) MarshalJSON() ([]byte, error) { 193 | return json.Marshal(map[string]interface{}{ 194 | "nodes": tree.nodes, 195 | "size": tree.size, 196 | }) 197 | } 198 | 199 | func (tree *CollapsedMerkleTree) UnmarshalJSON(b []byte) error { 200 | var rawTree struct { 201 | Nodes []ct.MerkleTreeNode `json:"nodes"` 202 | Size uint64 `json:"size"` 203 | } 204 | if err := json.Unmarshal(b, &rawTree); err != nil { 205 | return errors.New("Failed to unmarshal CollapsedMerkleTree: " + err.Error()) 206 | } 207 | if len(rawTree.Nodes) != calculateNumNodes(rawTree.Size) { 208 | return errors.New("Failed to unmarshal CollapsedMerkleTree: nodes has incorrect length") 209 | } 210 | tree.size = rawTree.Size 211 | tree.nodes = rawTree.Nodes 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Cert Spotter is a Certificate Transparency log monitor from SSLMate that 2 | alerts you when a SSL/TLS certificate is issued for one of your domains. 3 | Cert Spotter is easier than other open source CT monitors, since it does 4 | not require a database. It's also more robust, since it uses a special 5 | certificate parser that ensures it won't miss certificates. 6 | 7 | Cert Spotter is also available as a hosted service by SSLMate that 8 | requires zero setup and provides an easy web dashboard to centrally 9 | manage your certificates. Visit 10 | to sign up. 11 | 12 | You can use Cert Spotter to detect: 13 | 14 | * Certificates issued to attackers who have compromised a certificate 15 | authority and want to impersonate your site. 16 | 17 | * Certificates issued to attackers who are using your infrastructure 18 | to serve malware. 19 | 20 | * Certificates issued in violation of your corporate policy 21 | or outside of your centralized certificate procurement process. 22 | 23 | * Certificates issued to your infrastructure providers without your 24 | consent. 25 | 26 | 27 | USING CERT SPOTTER 28 | 29 | The easiest way to use Cert Spotter is to sign up for an account at 30 | . If you want to run Cert Spotter on 31 | your own server, follow these instructions. 32 | 33 | Cert Spotter requires Go version 1.5 or higher. 34 | 35 | 1. Install Cert Spotter using go get: 36 | 37 | go get software.sslmate.com/src/certspotter/cmd/certspotter 38 | 39 | 2. Create a file called ~/.certspotter/watchlist listing the DNS names 40 | you want to monitor, one per line. To monitor an entire domain tree 41 | (including the domain itself and all sub-domains) prefix the domain 42 | name with a dot (e.g. ".example.com"). To monitor a single DNS name 43 | only, do not prefix the name with a dot. 44 | 45 | 3. Create a cron job to periodically run: 46 | 47 | certspotter 48 | 49 | When Cert Spotter detects a certificate for a name on your watchlist, 50 | it writes a report to standard out, which the Cron daemon emails 51 | to you. Make sure you are able to receive emails sent by Cron. 52 | 53 | Cert Spotter also saves a copy of matching certificates in 54 | ~/.certspotter/certs. 55 | 56 | You can add and remove domains on your watchlist at any time. However, 57 | the certspotter command only notifies you of certificates that were 58 | logged since adding a domain to the watchlist, unless you specify the 59 | -all_time option, which requires scanning the entirety of every log 60 | and takes several hours to complete with a fast Internet connection. 61 | To examine preexisting certificates, it's better to use the Cert 62 | Spotter service , the Cert Spotter 63 | API , or a CT search engine such 64 | as . 65 | 66 | 67 | COMMAND LINE FLAGS 68 | 69 | -watchlist FILENAME 70 | File containing identifiers to watch, one per line, as described 71 | above (use - to read from stdin). Default: ~/.certspotter/watchlist 72 | -no_save 73 | Do not save a copy of matching certificates. 74 | -all_time 75 | Scan for certificates from all time, not just those added since 76 | the last run of Cert Spotter. Unless this option is specified, 77 | no certificates are scanned the first time Cert Spotter is run. 78 | -logs FILENAME 79 | JSON file containing logs to scan, in the format documented at 80 | . 81 | Default: use the logs trusted by Chromium. 82 | -state_dir PATH 83 | Directory for storing state. Default: ~/.certspotter 84 | -verbose 85 | Be verbose. 86 | 87 | 88 | WHAT CERTIFICATES ARE DETECTED BY CERT SPOTTER? 89 | 90 | Any certificate that is logged to a Certificate Transparency log trusted 91 | by Chromium will be detected by Cert Spotter. Currently, the following 92 | certificates are logged: 93 | 94 | * EV certificates 95 | 96 | * All certificates issued by the following CAs: 97 | 98 | * Let's Encrypt 99 | * StartCom 100 | * Symantec 101 | * WoSign 102 | 103 | * All DV certificates issued by GlobalSign . 104 | 105 | * Certificates that are detected when crawling web pages and doing 106 | Internet-wide scans. 107 | 108 | Starting from October 2017, all new certificates must be logged (and 109 | therefore detectable by Cert Spotter) to be trusted by Google Chrome. 110 | 111 | 112 | SECURITY 113 | 114 | Cert Spotter assumes an adversarial model in which an attacker produces 115 | a certificate that is accepted by at least some clients but goes 116 | undetected because of an encoding error that prevents CT monitors from 117 | understanding it. To defend against this attack, Cert Spotter uses a 118 | special certificate parser that keeps the certificate unparsed except 119 | for the identifiers. If one of the identifiers matches a domain on your 120 | watchlist, you will be notified, even if other parts of the certificate 121 | are unparsable. 122 | 123 | Cert Spotter takes special precautions to ensure identifiers are parsed 124 | correctly, and implements defenses against identifier-based attacks. 125 | For instance, if a DNS identifier contains a null byte, Cert Spotter 126 | interprets it as two identifiers: the complete identifier, and the 127 | identifier formed by truncating at the first null byte. For example, a 128 | certificate for example.org\0.example.com will alert the owners of both 129 | example.org and example.com. This defends against null prefix attacks 130 | . 131 | 132 | SSLMate continuously monitors CT logs to make sure every certificate's 133 | identifiers can be successfully parsed, and will release updates to 134 | Cert Spotter as necessary to fix parsing failures. 135 | 136 | Cert Spotter understands wildcard and redacted DNS names, and will alert 137 | you if a wildcard or redacted certificate might match an identifier on 138 | your watchlist. For example, a watchlist entry for sub.example.com would 139 | match certificates for *.example.com or ?.example.com. 140 | 141 | Cert Spotter is not just a log monitor, but also a log auditor which 142 | checks that the log is obeying its append-only property. A future 143 | release of Cert Spotter will support gossiping with other log monitors 144 | to ensure the log is presenting a single view. 145 | -------------------------------------------------------------------------------- /cmd/state.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package cmd 11 | 12 | import ( 13 | "bytes" 14 | "encoding/base64" 15 | "encoding/pem" 16 | "fmt" 17 | "io/ioutil" 18 | "log" 19 | "os" 20 | "path/filepath" 21 | "strconv" 22 | "strings" 23 | 24 | "software.sslmate.com/src/certspotter" 25 | "software.sslmate.com/src/certspotter/ct" 26 | ) 27 | 28 | type State struct { 29 | path string 30 | } 31 | 32 | func legacySTHFilename(logInfo *certspotter.LogInfo) string { 33 | return strings.Replace(strings.Replace(logInfo.FullURI(), "://", "_", 1), "/", "_", -1) 34 | } 35 | 36 | func readVersionFile(statePath string) (int, error) { 37 | versionFilePath := filepath.Join(statePath, "version") 38 | versionBytes, err := ioutil.ReadFile(versionFilePath) 39 | if err == nil { 40 | version, err := strconv.Atoi(string(bytes.TrimSpace(versionBytes))) 41 | if err != nil { 42 | return -1, fmt.Errorf("%s: contains invalid integer: %s", versionFilePath, err) 43 | } 44 | if version < 0 { 45 | return -1, fmt.Errorf("%s: contains negative integer", versionFilePath) 46 | } 47 | return version, nil 48 | } else if os.IsNotExist(err) { 49 | if fileExists(filepath.Join(statePath, "sths")) { 50 | // Original version of certspotter had no version file. 51 | // Infer version 0 if "sths" directory is present. 52 | return 0, nil 53 | } 54 | return -1, nil 55 | } else { 56 | return -1, fmt.Errorf("%s: %s", versionFilePath, err) 57 | } 58 | } 59 | 60 | func writeVersionFile(statePath string) error { 61 | version := 1 62 | versionString := fmt.Sprintf("%d\n", version) 63 | versionFilePath := filepath.Join(statePath, "version") 64 | if err := ioutil.WriteFile(versionFilePath, []byte(versionString), 0666); err != nil { 65 | return fmt.Errorf("%s: %s\n", versionFilePath, err) 66 | } 67 | return nil 68 | } 69 | 70 | func makeStateDir(statePath string) error { 71 | if err := os.Mkdir(statePath, 0777); err != nil && !os.IsExist(err) { 72 | return fmt.Errorf("%s: %s", statePath, err) 73 | } 74 | for _, subdir := range []string{"certs", "logs"} { 75 | path := filepath.Join(statePath, subdir) 76 | if err := os.Mkdir(path, 0777); err != nil && !os.IsExist(err) { 77 | return fmt.Errorf("%s: %s", path, err) 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func OpenState(statePath string) (*State, error) { 84 | version, err := readVersionFile(statePath) 85 | if err != nil { 86 | return nil, fmt.Errorf("Error reading version file: %s", err) 87 | } 88 | 89 | if version < 1 { 90 | if err := makeStateDir(statePath); err != nil { 91 | return nil, fmt.Errorf("Error creating state directory: %s", err) 92 | } 93 | if version == 0 { 94 | log.Printf("Migrating state directory (%s) to new layout...", statePath) 95 | if err := os.Rename(filepath.Join(statePath, "sths"), filepath.Join(statePath, "legacy_sths")); err != nil { 96 | return nil, fmt.Errorf("Error migrating STHs directory: %s", err) 97 | } 98 | for _, subdir := range []string{"evidence", "legacy_sths"} { 99 | os.Remove(filepath.Join(statePath, subdir)) 100 | } 101 | if err := ioutil.WriteFile(filepath.Join(statePath, "once"), []byte{}, 0666); err != nil { 102 | return nil, fmt.Errorf("Error creating once file: %s", err) 103 | } 104 | } 105 | if err := writeVersionFile(statePath); err != nil { 106 | return nil, fmt.Errorf("Error writing version file: %s", err) 107 | } 108 | } else if version > 1 { 109 | return nil, fmt.Errorf("%s was created by a newer version of Cert Spotter; please remove this directory or upgrade Cert Spotter", statePath) 110 | } 111 | 112 | return &State{path: statePath}, nil 113 | } 114 | 115 | func (state *State) IsFirstRun() bool { 116 | return !fileExists(filepath.Join(state.path, "once")) 117 | } 118 | 119 | func (state *State) WriteOnceFile() error { 120 | if err := ioutil.WriteFile(filepath.Join(state.path, "once"), []byte{}, 0666); err != nil { 121 | return fmt.Errorf("Error writing once file: %s", err) 122 | } 123 | return nil 124 | } 125 | 126 | func (state *State) SaveCert(isPrecert bool, certs [][]byte) (bool, string, error) { 127 | if len(certs) == 0 { 128 | return false, "", fmt.Errorf("Cannot write an empty certificate chain") 129 | } 130 | 131 | fingerprint := sha256hex(certs[0]) 132 | prefixPath := filepath.Join(state.path, "certs", fingerprint[0:2]) 133 | var filenameSuffix string 134 | if isPrecert { 135 | filenameSuffix = ".precert.pem" 136 | } else { 137 | filenameSuffix = ".cert.pem" 138 | } 139 | if err := os.Mkdir(prefixPath, 0777); err != nil && !os.IsExist(err) { 140 | return false, "", fmt.Errorf("Failed to create prefix directory %s: %s", prefixPath, err) 141 | } 142 | path := filepath.Join(prefixPath, fingerprint+filenameSuffix) 143 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) 144 | if err != nil { 145 | if os.IsExist(err) { 146 | return true, path, nil 147 | } else { 148 | return false, path, fmt.Errorf("Failed to open %s for writing: %s", path, err) 149 | } 150 | } 151 | for _, cert := range certs { 152 | if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil { 153 | file.Close() 154 | return false, path, fmt.Errorf("Error writing to %s: %s", path, err) 155 | } 156 | } 157 | if err := file.Close(); err != nil { 158 | return false, path, fmt.Errorf("Error writing to %s: %s", path, err) 159 | } 160 | 161 | return false, path, nil 162 | } 163 | 164 | func (state *State) OpenLogState(logInfo *certspotter.LogInfo) (*LogState, error) { 165 | return OpenLogState(filepath.Join(state.path, "logs", base64.RawURLEncoding.EncodeToString(logInfo.ID()))) 166 | } 167 | 168 | func (state *State) GetLegacySTH(logInfo *certspotter.LogInfo) (*ct.SignedTreeHead, error) { 169 | sth, err := readSTHFile(filepath.Join(state.path, "legacy_sths", legacySTHFilename(logInfo))) 170 | if err != nil { 171 | if os.IsNotExist(err) { 172 | return nil, nil 173 | } else { 174 | return nil, err 175 | } 176 | } 177 | return sth, nil 178 | } 179 | func (state *State) RemoveLegacySTH(logInfo *certspotter.LogInfo) error { 180 | err := os.Remove(filepath.Join(state.path, "legacy_sths", legacySTHFilename(logInfo))) 181 | os.Remove(filepath.Join(state.path, "legacy_sths")) 182 | return err 183 | } 184 | func (state *State) LockFilename() string { 185 | return filepath.Join(state.path, "lock") 186 | } 187 | func (state *State) Lock() (bool, error) { 188 | file, err := os.OpenFile(state.LockFilename(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) 189 | if err != nil { 190 | if os.IsExist(err) { 191 | return false, nil 192 | } else { 193 | return false, err 194 | } 195 | } 196 | if _, err := fmt.Fprintf(file, "%d\n", os.Getpid()); err != nil { 197 | file.Close() 198 | os.Remove(state.LockFilename()) 199 | return false, err 200 | } 201 | if err := file.Close(); err != nil { 202 | os.Remove(state.LockFilename()) 203 | return false, err 204 | } 205 | return true, nil 206 | } 207 | func (state *State) Unlock() error { 208 | return os.Remove(state.LockFilename()) 209 | } 210 | func (state *State) LockingPid() int { 211 | pidBytes, err := ioutil.ReadFile(state.LockFilename()) 212 | if err != nil { 213 | return 0 214 | } 215 | pid, err := strconv.Atoi(string(bytes.TrimSpace(pidBytes))) 216 | if err != nil { 217 | return 0 218 | } 219 | return pid 220 | } 221 | -------------------------------------------------------------------------------- /asn1time.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "encoding/asn1" 14 | "errors" 15 | "strconv" 16 | "time" 17 | "unicode" 18 | ) 19 | 20 | const ( 21 | tagUTCTime = 23 22 | tagGeneralizedTime = 24 23 | ) 24 | 25 | func isDigit(b byte) bool { 26 | return unicode.IsDigit(rune(b)) 27 | } 28 | 29 | func bytesToInt(bytes []byte) (int, error) { 30 | return strconv.Atoi(string(bytes)) 31 | } 32 | 33 | func parseUTCTime(bytes []byte) (time.Time, error) { 34 | var err error 35 | var year, month, day int 36 | var hour, min, sec int 37 | var tz *time.Location 38 | 39 | // YYMMDDhhmm 40 | if len(bytes) < 10 { 41 | return time.Time{}, errors.New("UTCTime is too short") 42 | } 43 | year, err = bytesToInt(bytes[0:2]) 44 | if err != nil { 45 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 46 | } 47 | 48 | month, err = bytesToInt(bytes[2:4]) 49 | if err != nil { 50 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 51 | } 52 | 53 | day, err = bytesToInt(bytes[4:6]) 54 | if err != nil { 55 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 56 | } 57 | 58 | hour, err = bytesToInt(bytes[6:8]) 59 | if err != nil { 60 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 61 | } 62 | 63 | min, err = bytesToInt(bytes[8:10]) 64 | if err != nil { 65 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 66 | } 67 | 68 | bytes = bytes[10:] 69 | 70 | // (optional) ss 71 | if len(bytes) >= 2 && isDigit(bytes[0]) { 72 | sec, err = bytesToInt(bytes[0:2]) 73 | if err != nil { 74 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 75 | } 76 | bytes = bytes[2:] 77 | } 78 | 79 | // timezone (required but allow it to be omitted, since this is a common error) 80 | if len(bytes) >= 1 { 81 | if bytes[0] == 'Z' { 82 | tz = time.UTC 83 | bytes = bytes[1:] 84 | } else if bytes[0] == '+' { 85 | // +hhmm 86 | if len(bytes) < 5 { 87 | return time.Time{}, errors.New("UTCTime positive timezone offset is too short") 88 | } 89 | tzHour, err := bytesToInt(bytes[1:3]) 90 | if err != nil { 91 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 92 | } 93 | 94 | tzMin, err := bytesToInt(bytes[3:5]) 95 | if err != nil { 96 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 97 | } 98 | 99 | tz = time.FixedZone("", tzHour*3600+tzMin*60) 100 | bytes = bytes[5:] 101 | } else if bytes[0] == '-' { 102 | // -hhmm 103 | if len(bytes) < 5 { 104 | return time.Time{}, errors.New("UTCTime negative timezone offset is too short") 105 | } 106 | tzHour, err := bytesToInt(bytes[1:3]) 107 | if err != nil { 108 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 109 | } 110 | 111 | tzMin, err := bytesToInt(bytes[3:5]) 112 | if err != nil { 113 | return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) 114 | } 115 | 116 | tz = time.FixedZone("", -1*(tzHour*3600+tzMin*60)) 117 | bytes = bytes[5:] 118 | } 119 | } else { 120 | tz = time.UTC 121 | } 122 | 123 | if len(bytes) > 0 { 124 | return time.Time{}, errors.New("UTCTime has trailing garbage") 125 | } 126 | 127 | // https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 128 | if year >= 50 { 129 | year = 1900 + year 130 | } else { 131 | year = 2000 + year 132 | } 133 | 134 | return time.Date(year, time.Month(month), day, hour, min, sec, 0, tz), nil 135 | } 136 | 137 | func parseGeneralizedTime(bytes []byte) (time.Time, error) { 138 | var err error 139 | var year, month, day int 140 | var hour, min, sec, ms int 141 | var tz *time.Location 142 | 143 | // YYYYMMDDHH 144 | if len(bytes) < 10 { 145 | return time.Time{}, errors.New("GeneralizedTime is too short") 146 | } 147 | year, err = bytesToInt(bytes[0:4]) 148 | if err != nil { 149 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 150 | } 151 | 152 | month, err = bytesToInt(bytes[4:6]) 153 | if err != nil { 154 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 155 | } 156 | 157 | day, err = bytesToInt(bytes[6:8]) 158 | if err != nil { 159 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 160 | } 161 | 162 | hour, err = bytesToInt(bytes[8:10]) 163 | if err != nil { 164 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 165 | } 166 | 167 | bytes = bytes[10:] 168 | 169 | // (optional) MM 170 | if len(bytes) >= 2 && isDigit(bytes[0]) { 171 | min, err = bytesToInt(bytes[0:2]) 172 | if err != nil { 173 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 174 | } 175 | bytes = bytes[2:] 176 | // (optional) SS 177 | if len(bytes) >= 2 && isDigit(bytes[0]) { 178 | sec, err = bytesToInt(bytes[0:2]) 179 | if err != nil { 180 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 181 | } 182 | bytes = bytes[2:] 183 | // (optional) .fff 184 | if len(bytes) >= 1 && bytes[0] == '.' { 185 | if len(bytes) < 4 { 186 | return time.Time{}, errors.New("GeneralizedTime fractional seconds is too short") 187 | } 188 | ms, err = bytesToInt(bytes[1:4]) 189 | if err != nil { 190 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 191 | } 192 | bytes = bytes[4:] 193 | } 194 | } 195 | } 196 | 197 | // timezone (Z or +hhmm or -hhmm or nothing) 198 | if len(bytes) >= 1 { 199 | if bytes[0] == 'Z' { 200 | bytes = bytes[1:] 201 | tz = time.UTC 202 | } else if bytes[0] == '+' { 203 | // +hhmm 204 | if len(bytes) < 5 { 205 | return time.Time{}, errors.New("GeneralizedTime positive timezone offset is too short") 206 | } 207 | tzHour, err := bytesToInt(bytes[1:3]) 208 | if err != nil { 209 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 210 | } 211 | 212 | tzMin, err := bytesToInt(bytes[3:5]) 213 | if err != nil { 214 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 215 | } 216 | 217 | tz = time.FixedZone("", tzHour*3600+tzMin*60) 218 | bytes = bytes[5:] 219 | } else if bytes[0] == '-' { 220 | // -hhmm 221 | if len(bytes) < 5 { 222 | return time.Time{}, errors.New("GeneralizedTime negative timezone offset is too short") 223 | } 224 | tzHour, err := bytesToInt(bytes[1:3]) 225 | if err != nil { 226 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 227 | } 228 | 229 | tzMin, err := bytesToInt(bytes[3:5]) 230 | if err != nil { 231 | return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) 232 | } 233 | 234 | tz = time.FixedZone("", -1*(tzHour*3600+tzMin*60)) 235 | bytes = bytes[5:] 236 | } 237 | } else { 238 | tz = time.UTC 239 | } 240 | 241 | if len(bytes) > 0 { 242 | return time.Time{}, errors.New("GeneralizedTime has trailing garbage") 243 | } 244 | 245 | return time.Date(year, time.Month(month), day, hour, min, sec, ms*1000*1000, tz), nil 246 | } 247 | 248 | func decodeASN1Time(value *asn1.RawValue) (time.Time, error) { 249 | if !value.IsCompound && value.Class == 0 { 250 | if value.Tag == tagUTCTime { 251 | return parseUTCTime(value.Bytes) 252 | } else if value.Tag == tagGeneralizedTime { 253 | return parseGeneralizedTime(value.Bytes) 254 | } 255 | } 256 | return time.Time{}, errors.New("Not a time value") 257 | } 258 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | // 10 | // This file contains code from https://github.com/google/certificate-transparency/tree/master/go 11 | // See ct/AUTHORS and ct/LICENSE for copyright and license information. 12 | 13 | package certspotter 14 | 15 | import ( 16 | // "container/list" 17 | "bytes" 18 | "crypto" 19 | "errors" 20 | "fmt" 21 | "log" 22 | "sync" 23 | "sync/atomic" 24 | "time" 25 | 26 | "software.sslmate.com/src/certspotter/ct" 27 | "software.sslmate.com/src/certspotter/ct/client" 28 | ) 29 | 30 | type ProcessCallback func(*Scanner, *ct.LogEntry) 31 | 32 | const ( 33 | FETCH_RETRIES = 10 34 | FETCH_RETRY_WAIT = 1 35 | ) 36 | 37 | // ScannerOptions holds configuration options for the Scanner 38 | type ScannerOptions struct { 39 | // Number of entries to request in one batch from the Log 40 | BatchSize int 41 | 42 | // Number of concurrent proecssors to run 43 | NumWorkers int 44 | 45 | // Don't print any status messages to stdout 46 | Quiet bool 47 | } 48 | 49 | // Creates a new ScannerOptions struct with sensible defaults 50 | func DefaultScannerOptions() *ScannerOptions { 51 | return &ScannerOptions{ 52 | BatchSize: 1000, 53 | NumWorkers: 1, 54 | Quiet: false, 55 | } 56 | } 57 | 58 | // Scanner is a tool to scan all the entries in a CT Log. 59 | type Scanner struct { 60 | // Base URI of CT log 61 | LogUri string 62 | 63 | // Public key of the log 64 | publicKey crypto.PublicKey 65 | LogId []byte 66 | 67 | // Client used to talk to the CT log instance 68 | logClient *client.LogClient 69 | 70 | // Configuration options for this Scanner instance 71 | opts ScannerOptions 72 | 73 | // Stats 74 | certsProcessed int64 75 | } 76 | 77 | // fetchRange represents a range of certs to fetch from a CT log 78 | type fetchRange struct { 79 | start int64 80 | end int64 81 | } 82 | 83 | // Worker function to process certs. 84 | // Accepts ct.LogEntries over the |entries| channel, and invokes processCert on them. 85 | // Returns true over the |done| channel when the |entries| channel is closed. 86 | func (s *Scanner) processerJob(id int, entries <-chan ct.LogEntry, processCert ProcessCallback, wg *sync.WaitGroup) { 87 | for entry := range entries { 88 | atomic.AddInt64(&s.certsProcessed, 1) 89 | processCert(s, &entry) 90 | } 91 | wg.Done() 92 | } 93 | 94 | func (s *Scanner) fetch(r fetchRange, entries chan<- ct.LogEntry, tree *CollapsedMerkleTree) error { 95 | success := false 96 | retries := FETCH_RETRIES 97 | retryWait := FETCH_RETRY_WAIT 98 | for !success { 99 | s.Log(fmt.Sprintf("Fetching entries %d to %d", r.start, r.end)) 100 | logEntries, err := s.logClient.GetEntries(r.start, r.end) 101 | if err != nil { 102 | if retries == 0 { 103 | s.Warn(fmt.Sprintf("Problem fetching entries %d to %d from log: %s", r.start, r.end, err.Error())) 104 | return err 105 | } else { 106 | s.Log(fmt.Sprintf("Problem fetching entries %d to %d from log (will retry): %s", r.start, r.end, err.Error())) 107 | time.Sleep(time.Duration(retryWait) * time.Second) 108 | retries-- 109 | retryWait *= 2 110 | continue 111 | } 112 | } 113 | retries = FETCH_RETRIES 114 | retryWait = FETCH_RETRY_WAIT 115 | for _, logEntry := range logEntries { 116 | if tree != nil { 117 | tree.Add(hashLeaf(logEntry.LeafBytes)) 118 | } 119 | logEntry.Index = r.start 120 | entries <- logEntry 121 | r.start++ 122 | } 123 | if r.start > r.end { 124 | // Only complete if we actually got all the leaves we were 125 | // expecting -- Logs MAY return fewer than the number of 126 | // leaves requested. 127 | success = true 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | // Worker function for fetcher jobs. 134 | // Accepts cert ranges to fetch over the |ranges| channel, and if the fetch is 135 | // successful sends the individual LeafInputs out into the 136 | // |entries| channel for the processors to chew on. 137 | // Will retry failed attempts to retrieve ranges indefinitely. 138 | // Sends true over the |done| channel when the |ranges| channel is closed. 139 | /* disabled becuase error handling is broken 140 | func (s *Scanner) fetcherJob(id int, ranges <-chan fetchRange, entries chan<- ct.LogEntry, wg *sync.WaitGroup) { 141 | for r := range ranges { 142 | s.fetch(r, entries, nil) 143 | } 144 | wg.Done() 145 | } 146 | */ 147 | 148 | // Returns the smaller of |a| and |b| 149 | func min(a int64, b int64) int64 { 150 | if a < b { 151 | return a 152 | } else { 153 | return b 154 | } 155 | } 156 | 157 | // Returns the larger of |a| and |b| 158 | func max(a int64, b int64) int64 { 159 | if a > b { 160 | return a 161 | } else { 162 | return b 163 | } 164 | } 165 | 166 | // Pretty prints the passed in number of |seconds| into a more human readable 167 | // string. 168 | func humanTime(seconds int) string { 169 | nanos := time.Duration(seconds) * time.Second 170 | hours := int(nanos / (time.Hour)) 171 | nanos %= time.Hour 172 | minutes := int(nanos / time.Minute) 173 | nanos %= time.Minute 174 | seconds = int(nanos / time.Second) 175 | s := "" 176 | if hours > 0 { 177 | s += fmt.Sprintf("%d hours ", hours) 178 | } 179 | if minutes > 0 { 180 | s += fmt.Sprintf("%d minutes ", minutes) 181 | } 182 | if seconds > 0 { 183 | s += fmt.Sprintf("%d seconds ", seconds) 184 | } 185 | return s 186 | } 187 | 188 | func (s Scanner) Log(msg string) { 189 | if !s.opts.Quiet { 190 | log.Print(msg) 191 | } 192 | } 193 | 194 | func (s Scanner) Warn(msg string) { 195 | log.Print(msg) 196 | } 197 | 198 | func (s *Scanner) GetSTH() (*ct.SignedTreeHead, error) { 199 | latestSth, err := s.logClient.GetSTH() 200 | if err != nil { 201 | return nil, err 202 | } 203 | if s.publicKey != nil { 204 | verifier, err := ct.NewSignatureVerifier(s.publicKey) 205 | if err != nil { 206 | return nil, err 207 | } 208 | if err := verifier.VerifySTHSignature(*latestSth); err != nil { 209 | return nil, errors.New("STH signature is invalid: " + err.Error()) 210 | } 211 | } 212 | copy(latestSth.LogID[:], s.LogId) 213 | return latestSth, nil 214 | } 215 | 216 | func (s *Scanner) CheckConsistency(first *ct.SignedTreeHead, second *ct.SignedTreeHead) (bool, error) { 217 | var proof ct.ConsistencyProof 218 | 219 | if first.TreeSize > second.TreeSize { 220 | // No way this can be valid 221 | return false, nil 222 | } else if first.TreeSize == second.TreeSize { 223 | // The proof *should* be empty, so don't bother contacting the server. 224 | // This is necessary because the digicert server returns a 400 error if first==second. 225 | proof = []ct.MerkleTreeNode{} 226 | } else { 227 | var err error 228 | proof, err = s.logClient.GetConsistencyProof(int64(first.TreeSize), int64(second.TreeSize)) 229 | if err != nil { 230 | return false, err 231 | } 232 | } 233 | 234 | return VerifyConsistencyProof(proof, first, second), nil 235 | } 236 | 237 | func (s *Scanner) MakeCollapsedMerkleTree(sth *ct.SignedTreeHead) (*CollapsedMerkleTree, error) { 238 | if sth.TreeSize == 0 { 239 | return &CollapsedMerkleTree{}, nil 240 | } 241 | 242 | entries, err := s.logClient.GetEntries(int64(sth.TreeSize-1), int64(sth.TreeSize-1)) 243 | if err != nil { 244 | return nil, err 245 | } 246 | if len(entries) == 0 { 247 | return nil, fmt.Errorf("Log did not return entry %d", sth.TreeSize-1) 248 | } 249 | leafHash := hashLeaf(entries[0].LeafBytes) 250 | 251 | var tree *CollapsedMerkleTree 252 | if sth.TreeSize > 1 { 253 | auditPath, _, err := s.logClient.GetAuditProof(leafHash, sth.TreeSize) 254 | if err != nil { 255 | return nil, err 256 | } 257 | reverseHashes(auditPath) 258 | tree, err = NewCollapsedMerkleTree(auditPath, sth.TreeSize-1) 259 | if err != nil { 260 | return nil, fmt.Errorf("Error returned bad audit proof for %x to %d", leafHash, sth.TreeSize) 261 | } 262 | } else { 263 | tree = EmptyCollapsedMerkleTree() 264 | } 265 | 266 | tree.Add(leafHash) 267 | if !bytes.Equal(tree.CalculateRoot(), sth.SHA256RootHash[:]) { 268 | return nil, fmt.Errorf("Calculated root hash does not match signed tree head at size %d", sth.TreeSize) 269 | } 270 | 271 | return tree, nil 272 | } 273 | 274 | func (s *Scanner) Scan(startIndex int64, endIndex int64, processCert ProcessCallback, tree *CollapsedMerkleTree) error { 275 | s.Log("Starting scan...") 276 | 277 | s.certsProcessed = 0 278 | startTime := time.Now() 279 | /* TODO: only launch ticker goroutine if in verbose mode; kill the goroutine when the scanner finishes 280 | ticker := time.NewTicker(time.Second) 281 | go func() { 282 | for range ticker.C { 283 | throughput := float64(s.certsProcessed) / time.Since(startTime).Seconds() 284 | remainingCerts := int64(endIndex) - int64(startIndex) - s.certsProcessed 285 | remainingSeconds := int(float64(remainingCerts) / throughput) 286 | remainingString := humanTime(remainingSeconds) 287 | s.Log(fmt.Sprintf("Processed: %d certs (to index %d). Throughput: %3.2f ETA: %s", s.certsProcessed, 288 | startIndex+int64(s.certsProcessed), throughput, remainingString)) 289 | } 290 | }() 291 | */ 292 | 293 | // Start processor workers 294 | jobs := make(chan ct.LogEntry, 100) 295 | var processorWG sync.WaitGroup 296 | for w := 0; w < s.opts.NumWorkers; w++ { 297 | processorWG.Add(1) 298 | go s.processerJob(w, jobs, processCert, &processorWG) 299 | } 300 | 301 | for start := startIndex; start < int64(endIndex); { 302 | end := min(start+int64(s.opts.BatchSize), int64(endIndex)) - 1 303 | if err := s.fetch(fetchRange{start, end}, jobs, tree); err != nil { 304 | return err 305 | } 306 | start = end + 1 307 | } 308 | close(jobs) 309 | processorWG.Wait() 310 | s.Log(fmt.Sprintf("Completed %d certs in %s", s.certsProcessed, humanTime(int(time.Since(startTime).Seconds())))) 311 | 312 | return nil 313 | } 314 | 315 | // Creates a new Scanner instance using |client| to talk to the log, and taking 316 | // configuration options from |opts|. 317 | func NewScanner(logUri string, logId []byte, publicKey crypto.PublicKey, opts *ScannerOptions) *Scanner { 318 | var scanner Scanner 319 | scanner.LogUri = logUri 320 | scanner.LogId = logId 321 | scanner.publicKey = publicKey 322 | scanner.logClient = client.New(logUri) 323 | scanner.opts = *opts 324 | return &scanner 325 | } 326 | -------------------------------------------------------------------------------- /identifiers.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "golang.org/x/net/idna" 15 | "net" 16 | "strings" 17 | "unicode/utf8" 18 | ) 19 | 20 | const UnparsableDNSLabelPlaceholder = "" 21 | 22 | /* 23 | const ( 24 | IdentifierSourceSubjectCN = iota 25 | IdentifierSourceDNSName 26 | IdentifierSourceIPAddr 27 | ) 28 | type IdentifierSource int 29 | 30 | type UnknownIdentifier struct { 31 | Source IdentifierSource 32 | Value []byte 33 | } 34 | */ 35 | 36 | type Identifiers struct { 37 | DNSNames []string // stored as ASCII, with IDNs in Punycode 38 | IPAddrs []net.IP 39 | //Unknowns []UnknownIdentifier 40 | } 41 | 42 | func NewIdentifiers() *Identifiers { 43 | return &Identifiers{ 44 | DNSNames: []string{}, 45 | IPAddrs: []net.IP{}, 46 | //Unknowns: []UnknownIdentifier{}, 47 | } 48 | } 49 | 50 | func parseIPAddrString(str string) net.IP { 51 | return net.ParseIP(str) 52 | } 53 | 54 | func isASCIIString(value []byte) bool { 55 | for _, b := range value { 56 | if b > 127 { 57 | return false 58 | } 59 | } 60 | return true 61 | } 62 | func isUTF8String(value []byte) bool { 63 | return utf8.Valid(value) 64 | } 65 | func latin1ToUTF8(value []byte) string { 66 | runes := make([]rune, len(value)) 67 | for i, b := range value { 68 | runes[i] = rune(b) 69 | } 70 | return string(runes) 71 | } 72 | 73 | // Make sure the DNS label doesn't have any weird characters that 74 | // could cause trouble during later processing. 75 | func isSaneDNSLabelChar(ch rune) bool { 76 | return ch == '\t' || (ch >= 32 && ch <= 126) 77 | } 78 | func isSaneDNSLabel(label string) bool { 79 | for _, ch := range label { 80 | if !isSaneDNSLabelChar(ch) { 81 | return false 82 | } 83 | } 84 | return true 85 | } 86 | 87 | func trimHttpPrefixString(value string) string { 88 | if strings.HasPrefix(value, "http://") { 89 | return value[7:] 90 | } else if strings.HasPrefix(value, "https://") { 91 | return value[8:] 92 | } else { 93 | return value 94 | } 95 | } 96 | 97 | func trimHttpPrefixBytes(value []byte) []byte { 98 | if bytes.HasPrefix(value, []byte("http://")) { 99 | return value[7:] 100 | } else if bytes.HasPrefix(value, []byte("https://")) { 101 | return value[8:] 102 | } else { 103 | return value 104 | } 105 | } 106 | 107 | func trimTrailingDots(value string) string { 108 | length := len(value) 109 | for length > 0 && value[length-1] == '.' { 110 | length-- 111 | } 112 | return value[0:length] 113 | } 114 | 115 | // Try to canonicalize/sanitize the DNS name: 116 | // 1. Trim leading and trailing whitespace 117 | // 2. Trim trailing dots 118 | // 3. Convert to lower case 119 | // 4. Replace totally nonsensical labels (e.g. having non-printable characters) with a placeholder 120 | func sanitizeDNSName(value string) string { 121 | value = strings.ToLower(trimTrailingDots(strings.TrimSpace(value))) 122 | labels := strings.Split(value, ".") 123 | for i, label := range labels { 124 | if !isSaneDNSLabel(label) { 125 | labels[i] = UnparsableDNSLabelPlaceholder 126 | } 127 | } 128 | return strings.Join(labels, ".") 129 | } 130 | 131 | // Like sanitizeDNSName, but labels that are Unicode are converted to Punycode. 132 | func sanitizeUnicodeDNSName(value string) string { 133 | value = strings.ToLower(trimTrailingDots(strings.TrimSpace(value))) 134 | labels := strings.Split(value, ".") 135 | for i, label := range labels { 136 | if asciiLabel, err := idna.ToASCII(label); err == nil && isSaneDNSLabel(asciiLabel) { 137 | labels[i] = asciiLabel 138 | } else { 139 | labels[i] = UnparsableDNSLabelPlaceholder 140 | } 141 | } 142 | return strings.Join(labels, ".") 143 | } 144 | 145 | func (ids *Identifiers) appendDNSName(dnsName string) { 146 | if dnsName != "" && !ids.hasDNSName(dnsName) { 147 | ids.DNSNames = append(ids.DNSNames, dnsName) 148 | } 149 | } 150 | func (ids *Identifiers) appendIPAddress(ipaddr net.IP) { 151 | if !ids.hasIPAddress(ipaddr) { 152 | ids.IPAddrs = append(ids.IPAddrs, ipaddr) 153 | } 154 | } 155 | 156 | func (ids *Identifiers) hasDNSName(target string) bool { 157 | for _, value := range ids.DNSNames { 158 | if value == target { 159 | return true 160 | } 161 | } 162 | return false 163 | } 164 | func (ids *Identifiers) hasIPAddress(target net.IP) bool { 165 | for _, value := range ids.IPAddrs { 166 | if value.Equal(target) { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | 173 | func (ids *Identifiers) addDnsSANfinal(value []byte) { 174 | if ipaddr := parseIPAddrString(string(value)); ipaddr != nil { 175 | // Stupid CAs put IP addresses in DNS SANs because stupid Microsoft 176 | // used to not support IP address SANs. Since there's no way for an IP 177 | // address to also be a valid DNS name, just treat it like an IP address 178 | // and not try to process it as a DNS name. 179 | ids.appendIPAddress(ipaddr) 180 | } else if isASCIIString(value) { 181 | ids.appendDNSName(sanitizeDNSName(string(value))) 182 | } else { 183 | // DNS SANs are supposed to be IA5Strings (i.e. ASCII) but CAs can't follow 184 | // simple rules. Unfortunately, we have no idea what the encoding really is 185 | // in this case, so interpret it as both UTF-8 (if it's valid UTF-8) 186 | // and Latin-1. 187 | if isUTF8String(value) { 188 | ids.appendDNSName(sanitizeUnicodeDNSName(string(value))) 189 | } 190 | ids.appendDNSName(sanitizeUnicodeDNSName(latin1ToUTF8(value))) 191 | } 192 | } 193 | 194 | func (ids *Identifiers) addDnsSANnonull(value []byte) { 195 | if slashIndex := bytes.IndexByte(value, '/'); slashIndex != -1 { 196 | // If the value contains a slash, then this might be a URL, 197 | // so process the part of the value up to the first slash, 198 | // which should be the domain. Even though no client should 199 | // ever successfully validate such a DNS name, the domain owner 200 | // might still want to know about it. 201 | ids.addDnsSANfinal(value[0:slashIndex]) 202 | } 203 | ids.addDnsSANfinal(value) 204 | } 205 | 206 | func (ids *Identifiers) AddDnsSAN(value []byte) { 207 | // Trim http:// and https:// prefixes, which are all too common in the wild, 208 | // so http://example.com becomes just example.com. Even though clients 209 | // should never successfully validate a DNS name like http://example.com, 210 | // the owner of example.com might still want to know about it. 211 | value = trimHttpPrefixBytes(value) 212 | 213 | if nullIndex := bytes.IndexByte(value, 0); nullIndex != -1 { 214 | // If the value contains a null byte, process the part of 215 | // the value up to the first null byte in addition to the 216 | // complete value, in case this certificate is an attempt to 217 | // fake out validators that only compare up to the first null. 218 | ids.addDnsSANnonull(value[0:nullIndex]) 219 | } 220 | ids.addDnsSANnonull(value) 221 | } 222 | 223 | func (ids *Identifiers) addCNfinal(value string) { 224 | if ipaddr := parseIPAddrString(value); ipaddr != nil { 225 | ids.appendIPAddress(ipaddr) 226 | } else if !strings.ContainsRune(value, ' ') { 227 | // If the CN contains a space it's clearly not a DNS name, so ignore it. 228 | ids.appendDNSName(sanitizeUnicodeDNSName(value)) 229 | } 230 | } 231 | 232 | func (ids *Identifiers) addCNnonull(value string) { 233 | if slashIndex := strings.IndexRune(value, '/'); slashIndex != -1 { 234 | // If the value contains a slash, then this might be a URL, 235 | // so process the part of the value up to the first slash, 236 | // which should be the domain. Even though no client should 237 | // ever successfully validate such a DNS name, the domain owner 238 | // might still want to know about it. 239 | ids.addCNfinal(value[0:slashIndex]) 240 | } 241 | ids.addCNfinal(value) 242 | } 243 | 244 | func (ids *Identifiers) AddCN(value string) { 245 | // Trim http:// and https:// prefixes, which are all too common in the wild, 246 | // so http://example.com becomes just example.com. Even though clients 247 | // should never successfully validate a DNS name like http://example.com, 248 | // the owner of example.com might still want to know about it. 249 | value = trimHttpPrefixString(value) 250 | 251 | if nullIndex := strings.IndexRune(value, 0); nullIndex != -1 { 252 | // If the value contains a null byte, process the part of 253 | // the value up to the first null byte in addition to the 254 | // complete value, in case this certificate is an attempt to 255 | // fake out validators that only compare up to the first null. 256 | ids.addCNnonull(value[0:nullIndex]) 257 | } 258 | ids.addCNnonull(value) 259 | } 260 | 261 | func (ids *Identifiers) AddIPAddress(value net.IP) { 262 | ids.appendIPAddress(value) 263 | } 264 | 265 | func (ids *Identifiers) dnsNamesString(sep string) string { 266 | return strings.Join(ids.DNSNames, sep) 267 | } 268 | 269 | func (ids *Identifiers) ipAddrsString(sep string) string { 270 | str := "" 271 | for _, ipAddr := range ids.IPAddrs { 272 | if str != "" { 273 | str += sep 274 | } 275 | str += ipAddr.String() 276 | } 277 | return str 278 | } 279 | 280 | func (cert *CertInfo) ParseIdentifiers() (*Identifiers, error) { 281 | ids := NewIdentifiers() 282 | 283 | if cert.SubjectParseError != nil { 284 | return nil, cert.SubjectParseError 285 | } 286 | cns, err := cert.Subject.ParseCNs() 287 | if err != nil { 288 | return nil, err 289 | } 290 | for _, cn := range cns { 291 | ids.AddCN(cn) 292 | } 293 | 294 | if cert.SANsParseError != nil { 295 | return nil, cert.SANsParseError 296 | } 297 | for _, san := range cert.SANs { 298 | switch san.Type { 299 | case sanDNSName: 300 | ids.AddDnsSAN(san.Value) 301 | case sanIPAddress: 302 | if len(san.Value) == 4 || len(san.Value) == 16 { 303 | ids.AddIPAddress(net.IP(san.Value)) 304 | } 305 | // TODO: decide what to do with IP addresses with an invalid length. 306 | // The two encoding errors I've observed in CT logs are: 307 | // 1. encoding the IP address as a string 308 | // 2. a value of 0x00000000FFFFFF00 (WTF?) 309 | // IP addresses aren't a high priority so just ignore invalid ones for now. 310 | // Hopefully no clients out there are dumb enough to process IP address 311 | // SANs encoded as strings... 312 | } 313 | } 314 | 315 | return ids, nil 316 | } 317 | -------------------------------------------------------------------------------- /ct/client/logclient.go: -------------------------------------------------------------------------------- 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 4 | package client 5 | 6 | import ( 7 | "bytes" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "net/http" 16 | "net/url" 17 | "time" 18 | 19 | "github.com/mreiferson/go-httpclient" 20 | "software.sslmate.com/src/certspotter/ct" 21 | ) 22 | 23 | // URI paths for CT Log endpoints 24 | const ( 25 | GetSTHPath = "/ct/v1/get-sth" 26 | GetEntriesPath = "/ct/v1/get-entries" 27 | GetSTHConsistencyPath = "/ct/v1/get-sth-consistency" 28 | GetProofByHashPath = "/ct/v1/get-proof-by-hash" 29 | AddChainPath = "/ct/v1/add-chain" 30 | ) 31 | 32 | // LogClient represents a client for a given CT Log instance 33 | type LogClient struct { 34 | uri string // the base URI of the log. e.g. http://ct.googleapis/pilot 35 | httpClient *http.Client // used to interact with the log via HTTP 36 | } 37 | 38 | ////////////////////////////////////////////////////////////////////////////////// 39 | // JSON structures follow. 40 | // These represent the structures returned by the CT Log server. 41 | ////////////////////////////////////////////////////////////////////////////////// 42 | 43 | // getSTHResponse respresents the JSON response to the get-sth CT method 44 | type getSTHResponse struct { 45 | TreeSize uint64 `json:"tree_size"` // Number of certs in the current tree 46 | Timestamp uint64 `json:"timestamp"` // Time that the tree was created 47 | SHA256RootHash []byte `json:"sha256_root_hash"` // Root hash of the tree 48 | TreeHeadSignature []byte `json:"tree_head_signature"` // Log signature for this STH 49 | } 50 | 51 | // base64LeafEntry respresents a Base64 encoded leaf entry 52 | type base64LeafEntry struct { 53 | LeafInput []byte `json:"leaf_input"` 54 | ExtraData []byte `json:"extra_data"` 55 | } 56 | 57 | // getEntriesReponse respresents the JSON response to the CT get-entries method 58 | type getEntriesResponse struct { 59 | Entries []base64LeafEntry `json:"entries"` // the list of returned entries 60 | } 61 | 62 | // getConsistencyProofResponse represents the JSON response to the CT get-consistency-proof method 63 | type getConsistencyProofResponse struct { 64 | Consistency [][]byte `json:"consistency"` 65 | } 66 | 67 | // getAuditProofResponse represents the JSON response to the CT get-proof-by-hash method 68 | type getAuditProofResponse struct { 69 | LeafIndex uint64 `json:"leaf_index"` 70 | AuditPath [][]byte `json:"audit_path"` 71 | } 72 | 73 | type addChainRequest struct { 74 | Chain [][]byte `json:"chain"` 75 | } 76 | 77 | type addChainResponse struct { 78 | SCTVersion uint8 `json:"sct_version"` 79 | ID []byte `json:"id"` 80 | Timestamp uint64 `json:"timestamp"` 81 | Extensions []byte `json:"extensions"` 82 | Signature []byte `json:"signature"` 83 | } 84 | 85 | // New constructs a new LogClient instance. 86 | // |uri| is the base URI of the CT log instance to interact with, e.g. 87 | // http://ct.googleapis.com/pilot 88 | func New(uri string) *LogClient { 89 | var c LogClient 90 | c.uri = uri 91 | transport := &httpclient.Transport{ 92 | ConnectTimeout: 10 * time.Second, 93 | RequestTimeout: 60 * time.Second, 94 | ResponseHeaderTimeout: 30 * time.Second, 95 | MaxIdleConnsPerHost: 10, 96 | DisableKeepAlives: false, 97 | } 98 | c.httpClient = &http.Client{Transport: transport} 99 | return &c 100 | } 101 | 102 | // Makes a HTTP call to |uri|, and attempts to parse the response as a JSON 103 | // representation of the structure in |res|. 104 | // Returns a non-nil |error| if there was a problem. 105 | func (c *LogClient) fetchAndParse(uri string, respBody interface{}) error { 106 | req, err := http.NewRequest("GET", uri, nil) 107 | if err != nil { 108 | return fmt.Errorf("GET %s: Sending request failed: %s", uri, err) 109 | } 110 | return c.doAndParse(req, respBody) 111 | } 112 | 113 | func (c *LogClient) postAndParse(uri string, body interface{}, respBody interface{}) error { 114 | bodyReader, bodyWriter := io.Pipe() 115 | go func() { 116 | json.NewEncoder(bodyWriter).Encode(body) 117 | bodyWriter.Close() 118 | }() 119 | req, err := http.NewRequest("POST", uri, bodyReader) 120 | if err != nil { 121 | return fmt.Errorf("POST %s: Sending request failed: %s", uri, err) 122 | } 123 | req.Header.Set("Content-Type", "application/json") 124 | return c.doAndParse(req, respBody) 125 | } 126 | 127 | func (c *LogClient) doAndParse(req *http.Request, respBody interface{}) error { 128 | // req.Header.Set("Keep-Alive", "timeout=15, max=100") 129 | resp, err := c.httpClient.Do(req) 130 | var respBodyBytes []byte 131 | if resp != nil { 132 | respBodyBytes, err = ioutil.ReadAll(resp.Body) 133 | resp.Body.Close() 134 | if err != nil { 135 | return fmt.Errorf("%s %s: Reading response failed: %s", req.Method, req.URL, err) 136 | } 137 | } 138 | if err != nil { 139 | return err 140 | } 141 | if resp.StatusCode/100 != 2 { 142 | return fmt.Errorf("%s %s: %s (%s)", req.Method, req.URL, resp.Status, string(respBodyBytes)) 143 | } 144 | if err = json.Unmarshal(respBodyBytes, &respBody); err != nil { 145 | return fmt.Errorf("%s %s: Parsing response JSON failed: %s", req.Method, req.URL, err) 146 | } 147 | return nil 148 | } 149 | 150 | // GetSTH retrieves the current STH from the log. 151 | // Returns a populated SignedTreeHead, or a non-nil error. 152 | func (c *LogClient) GetSTH() (sth *ct.SignedTreeHead, err error) { 153 | var resp getSTHResponse 154 | if err = c.fetchAndParse(c.uri+GetSTHPath, &resp); err != nil { 155 | return 156 | } 157 | sth = &ct.SignedTreeHead{ 158 | TreeSize: resp.TreeSize, 159 | Timestamp: resp.Timestamp, 160 | } 161 | 162 | if len(resp.SHA256RootHash) != sha256.Size { 163 | return nil, fmt.Errorf("STH returned by server has invalid sha256_root_hash (expected length %d got %d)", sha256.Size, len(resp.SHA256RootHash)) 164 | } 165 | copy(sth.SHA256RootHash[:], resp.SHA256RootHash) 166 | 167 | ds, err := ct.UnmarshalDigitallySigned(bytes.NewReader(resp.TreeHeadSignature)) 168 | if err != nil { 169 | return nil, err 170 | } 171 | // TODO(alcutter): Verify signature 172 | sth.TreeHeadSignature = *ds 173 | return 174 | } 175 | 176 | // GetEntries attempts to retrieve the entries in the sequence [|start|, |end|] from the CT 177 | // log server. (see section 4.6.) 178 | // Returns a slice of LeafInputs or a non-nil error. 179 | func (c *LogClient) GetEntries(start, end int64) ([]ct.LogEntry, error) { 180 | if end < 0 { 181 | return nil, errors.New("GetEntries: end should be >= 0") 182 | } 183 | if end < start { 184 | return nil, errors.New("GetEntries: start should be <= end") 185 | } 186 | var resp getEntriesResponse 187 | err := c.fetchAndParse(fmt.Sprintf("%s%s?start=%d&end=%d", c.uri, GetEntriesPath, start, end), &resp) 188 | if err != nil { 189 | return nil, err 190 | } 191 | entries := make([]ct.LogEntry, len(resp.Entries)) 192 | for index, entry := range resp.Entries { 193 | leaf, err := ct.ReadMerkleTreeLeaf(bytes.NewBuffer(entry.LeafInput)) 194 | if err != nil { 195 | return nil, fmt.Errorf("Reading Merkle Tree Leaf at index %d failed: %s", start+int64(index), err) 196 | } 197 | entries[index].LeafBytes = entry.LeafInput 198 | entries[index].Leaf = *leaf 199 | 200 | var chain []ct.ASN1Cert 201 | switch leaf.TimestampedEntry.EntryType { 202 | case ct.X509LogEntryType: 203 | chain, err = ct.UnmarshalX509ChainArray(entry.ExtraData) 204 | 205 | case ct.PrecertLogEntryType: 206 | chain, err = ct.UnmarshalPrecertChainArray(entry.ExtraData) 207 | 208 | default: 209 | return nil, fmt.Errorf("Unknown entry type at index %d: %v", start+int64(index), leaf.TimestampedEntry.EntryType) 210 | } 211 | if err != nil { 212 | return nil, fmt.Errorf("Parsing entry of type %d at index %d failed: %s", leaf.TimestampedEntry.EntryType, start+int64(index), err) 213 | } 214 | entries[index].Chain = chain 215 | entries[index].Index = start + int64(index) 216 | } 217 | return entries, nil 218 | } 219 | 220 | // GetConsistencyProof retrieves a Merkle Consistency Proof between two STHs (|first| and |second|) 221 | // from the log. Returns a slice of MerkleTreeNodes (a ct.ConsistencyProof) or a non-nil error. 222 | func (c *LogClient) GetConsistencyProof(first, second int64) (ct.ConsistencyProof, error) { 223 | if second < 0 { 224 | return nil, errors.New("GetConsistencyProof: second should be >= 0") 225 | } 226 | if second < first { 227 | return nil, errors.New("GetConsistencyProof: first should be <= second") 228 | } 229 | var resp getConsistencyProofResponse 230 | err := c.fetchAndParse(fmt.Sprintf("%s%s?first=%d&second=%d", c.uri, GetSTHConsistencyPath, first, second), &resp) 231 | if err != nil { 232 | return nil, err 233 | } 234 | nodes := make([]ct.MerkleTreeNode, len(resp.Consistency)) 235 | for index, nodeBytes := range resp.Consistency { 236 | nodes[index] = nodeBytes 237 | } 238 | return nodes, nil 239 | } 240 | 241 | // GetAuditProof retrieves a Merkle Audit Proof (aka Inclusion Proof) for the given 242 | // |hash| based on the STH at |treeSize| from the log. Returns a slice of MerkleTreeNodes 243 | // and the index of the leaf. 244 | func (c *LogClient) GetAuditProof(hash ct.MerkleTreeNode, treeSize uint64) (ct.AuditPath, uint64, error) { 245 | var resp getAuditProofResponse 246 | err := c.fetchAndParse(fmt.Sprintf("%s%s?hash=%s&tree_size=%d", c.uri, GetProofByHashPath, url.QueryEscape(base64.StdEncoding.EncodeToString(hash)), treeSize), &resp) 247 | if err != nil { 248 | return nil, 0, err 249 | } 250 | path := make([]ct.MerkleTreeNode, len(resp.AuditPath)) 251 | for index, nodeBytes := range resp.AuditPath { 252 | path[index] = nodeBytes 253 | } 254 | return path, resp.LeafIndex, nil 255 | } 256 | 257 | func (c *LogClient) AddChain(chain [][]byte) (*ct.SignedCertificateTimestamp, error) { 258 | req := addChainRequest{Chain: chain} 259 | 260 | var resp addChainResponse 261 | if err := c.postAndParse(c.uri+AddChainPath, &req, &resp); err != nil { 262 | return nil, err 263 | } 264 | 265 | sct := &ct.SignedCertificateTimestamp{ 266 | SCTVersion: ct.Version(resp.SCTVersion), 267 | Timestamp: resp.Timestamp, 268 | Extensions: resp.Extensions, 269 | } 270 | 271 | if len(resp.ID) != sha256.Size { 272 | return nil, fmt.Errorf("SCT returned by server has invalid id (expected length %d got %d)", sha256.Size, len(resp.ID)) 273 | } 274 | copy(sct.LogID[:], resp.ID) 275 | 276 | ds, err := ct.UnmarshalDigitallySigned(bytes.NewReader(resp.Signature)) 277 | if err != nil { 278 | return nil, err 279 | } 280 | sct.Signature = *ds 281 | return sct, nil 282 | } 283 | -------------------------------------------------------------------------------- /ct/types.go: -------------------------------------------------------------------------------- 1 | package ct 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | ) 10 | 11 | const ( 12 | issuerKeyHashLength = 32 13 | ) 14 | 15 | /////////////////////////////////////////////////////////////////////////////// 16 | // The following structures represent those outlined in the RFC6962 document: 17 | /////////////////////////////////////////////////////////////////////////////// 18 | 19 | // LogEntryType represents the LogEntryType enum from section 3.1 of the RFC: 20 | // enum { x509_entry(0), precert_entry(1), (65535) } LogEntryType; 21 | type LogEntryType uint16 22 | 23 | func (e LogEntryType) String() string { 24 | switch e { 25 | case X509LogEntryType: 26 | return "X509LogEntryType" 27 | case PrecertLogEntryType: 28 | return "PrecertLogEntryType" 29 | } 30 | panic(fmt.Sprintf("No string defined for LogEntryType constant value %d", e)) 31 | } 32 | 33 | // LogEntryType constants, see section 3.1 of RFC6962. 34 | const ( 35 | X509LogEntryType LogEntryType = 0 36 | PrecertLogEntryType LogEntryType = 1 37 | ) 38 | 39 | // MerkleLeafType represents the MerkleLeafType enum from section 3.4 of the 40 | // RFC: enum { timestamped_entry(0), (255) } MerkleLeafType; 41 | type MerkleLeafType uint8 42 | 43 | func (m MerkleLeafType) String() string { 44 | switch m { 45 | case TimestampedEntryLeafType: 46 | return "TimestampedEntryLeafType" 47 | default: 48 | return fmt.Sprintf("UnknownLeafType(%d)", m) 49 | } 50 | } 51 | 52 | // MerkleLeafType constants, see section 3.4 of the RFC. 53 | const ( 54 | TimestampedEntryLeafType MerkleLeafType = 0 // Entry type for an SCT 55 | ) 56 | 57 | // Version represents the Version enum from section 3.2 of the RFC: 58 | // enum { v1(0), (255) } Version; 59 | type Version uint8 60 | 61 | func (v Version) String() string { 62 | switch v { 63 | case V1: 64 | return "V1" 65 | default: 66 | return fmt.Sprintf("UnknownVersion(%d)", v) 67 | } 68 | } 69 | 70 | // CT Version constants, see section 3.2 of the RFC. 71 | const ( 72 | V1 Version = 0 73 | ) 74 | 75 | // SignatureType differentiates STH signatures from SCT signatures, see RFC 76 | // section 3.2 77 | type SignatureType uint8 78 | 79 | func (st SignatureType) String() string { 80 | switch st { 81 | case CertificateTimestampSignatureType: 82 | return "CertificateTimestamp" 83 | case TreeHashSignatureType: 84 | return "TreeHash" 85 | default: 86 | return fmt.Sprintf("UnknownSignatureType(%d)", st) 87 | } 88 | } 89 | 90 | // SignatureType constants, see RFC section 3.2 91 | const ( 92 | CertificateTimestampSignatureType SignatureType = 0 93 | TreeHashSignatureType SignatureType = 1 94 | ) 95 | 96 | // ASN1Cert type for holding the raw DER bytes of an ASN.1 Certificate 97 | // (section 3.1) 98 | type ASN1Cert []byte 99 | 100 | // PreCert represents a Precertificate (section 3.2) 101 | type PreCert struct { 102 | IssuerKeyHash [issuerKeyHashLength]byte 103 | TBSCertificate []byte 104 | } 105 | 106 | // CTExtensions is a representation of the raw bytes of any CtExtension 107 | // structure (see section 3.2) 108 | type CTExtensions []byte 109 | 110 | // MerkleTreeNode represents an internal node in the CT tree 111 | type MerkleTreeNode []byte 112 | 113 | // ConsistencyProof represents a CT consistency proof (see sections 2.1.2 and 114 | // 4.4) 115 | type ConsistencyProof []MerkleTreeNode 116 | 117 | // AuditPath represents a CT inclusion proof (see sections 2.1.1 and 4.5) 118 | type AuditPath []MerkleTreeNode 119 | 120 | // LeafInput represents a serialized MerkleTreeLeaf structure 121 | type LeafInput []byte 122 | 123 | // HashAlgorithm from the DigitallySigned struct 124 | type HashAlgorithm byte 125 | 126 | // HashAlgorithm constants 127 | const ( 128 | None HashAlgorithm = 0 129 | MD5 HashAlgorithm = 1 130 | SHA1 HashAlgorithm = 2 131 | SHA224 HashAlgorithm = 3 132 | SHA256 HashAlgorithm = 4 133 | SHA384 HashAlgorithm = 5 134 | SHA512 HashAlgorithm = 6 135 | ) 136 | 137 | func (h HashAlgorithm) String() string { 138 | switch h { 139 | case None: 140 | return "None" 141 | case MD5: 142 | return "MD5" 143 | case SHA1: 144 | return "SHA1" 145 | case SHA224: 146 | return "SHA224" 147 | case SHA256: 148 | return "SHA256" 149 | case SHA384: 150 | return "SHA384" 151 | case SHA512: 152 | return "SHA512" 153 | default: 154 | return fmt.Sprintf("UNKNOWN(%d)", h) 155 | } 156 | } 157 | 158 | // SignatureAlgorithm from the the DigitallySigned struct 159 | type SignatureAlgorithm byte 160 | 161 | // SignatureAlgorithm constants 162 | const ( 163 | Anonymous SignatureAlgorithm = 0 164 | RSA SignatureAlgorithm = 1 165 | DSA SignatureAlgorithm = 2 166 | ECDSA SignatureAlgorithm = 3 167 | ) 168 | 169 | func (s SignatureAlgorithm) String() string { 170 | switch s { 171 | case Anonymous: 172 | return "Anonymous" 173 | case RSA: 174 | return "RSA" 175 | case DSA: 176 | return "DSA" 177 | case ECDSA: 178 | return "ECDSA" 179 | default: 180 | return fmt.Sprintf("UNKNOWN(%d)", s) 181 | } 182 | } 183 | 184 | // DigitallySigned represents an RFC5246 DigitallySigned structure 185 | type DigitallySigned struct { 186 | HashAlgorithm HashAlgorithm 187 | SignatureAlgorithm SignatureAlgorithm 188 | Signature []byte 189 | } 190 | 191 | // FromBase64String populates the DigitallySigned structure from the base64 data passed in. 192 | // Returns an error if the base64 data is invalid. 193 | func (d *DigitallySigned) FromBase64String(b64 string) error { 194 | raw, err := base64.StdEncoding.DecodeString(b64) 195 | if err != nil { 196 | return fmt.Errorf("failed to unbase64 DigitallySigned: %v", err) 197 | } 198 | ds, err := UnmarshalDigitallySigned(bytes.NewReader(raw)) 199 | if err != nil { 200 | return fmt.Errorf("failed to unmarshal DigitallySigned: %v", err) 201 | } 202 | *d = *ds 203 | return nil 204 | } 205 | 206 | // Base64String returns the base64 representation of the DigitallySigned struct. 207 | func (d DigitallySigned) Base64String() (string, error) { 208 | b, err := MarshalDigitallySigned(d) 209 | if err != nil { 210 | return "", err 211 | } 212 | return base64.StdEncoding.EncodeToString(b), nil 213 | } 214 | 215 | // MarshalJSON implements the json.Marshaller interface. 216 | func (d DigitallySigned) MarshalJSON() ([]byte, error) { 217 | b64, err := d.Base64String() 218 | if err != nil { 219 | return []byte{}, err 220 | } 221 | return []byte(`"` + b64 + `"`), nil 222 | } 223 | 224 | // UnmarshalJSON implements the json.Unmarshaler interface. 225 | func (d *DigitallySigned) UnmarshalJSON(b []byte) error { 226 | var content string 227 | if err := json.Unmarshal(b, &content); err != nil { 228 | return fmt.Errorf("failed to unmarshal DigitallySigned: %v", err) 229 | } 230 | return d.FromBase64String(content) 231 | } 232 | 233 | // LogEntry represents the contents of an entry in a CT log, see section 3.1. 234 | type LogEntry struct { 235 | Index int64 236 | Leaf MerkleTreeLeaf 237 | Chain []ASN1Cert 238 | LeafBytes []byte 239 | } 240 | 241 | // SHA256Hash represents the output from the SHA256 hash function. 242 | type SHA256Hash [sha256.Size]byte 243 | 244 | // FromBase64String populates the SHA256 struct with the contents of the base64 data passed in. 245 | func (s *SHA256Hash) FromBase64String(b64 string) error { 246 | bs, err := base64.StdEncoding.DecodeString(b64) 247 | if err != nil { 248 | return fmt.Errorf("failed to unbase64 LogID: %v", err) 249 | } 250 | if len(bs) != sha256.Size { 251 | return fmt.Errorf("invalid SHA256 length, expected 32 but got %d", len(bs)) 252 | } 253 | copy(s[:], bs) 254 | return nil 255 | } 256 | 257 | // Base64String returns the base64 representation of this SHA256Hash. 258 | func (s SHA256Hash) Base64String() string { 259 | return base64.StdEncoding.EncodeToString(s[:]) 260 | } 261 | 262 | // MarshalJSON implements the json.Marshaller interface for SHA256Hash. 263 | func (s SHA256Hash) MarshalJSON() ([]byte, error) { 264 | return []byte(`"` + s.Base64String() + `"`), nil 265 | } 266 | 267 | // UnmarshalJSON implements the json.Unmarshaller interface. 268 | func (s *SHA256Hash) UnmarshalJSON(b []byte) error { 269 | var content string 270 | if err := json.Unmarshal(b, &content); err != nil { 271 | return fmt.Errorf("failed to unmarshal SHA256Hash: %v", err) 272 | } 273 | return s.FromBase64String(content) 274 | } 275 | 276 | // SignedTreeHead represents the structure returned by the get-sth CT method 277 | // after base64 decoding. See sections 3.5 and 4.3 in the RFC) 278 | type SignedTreeHead struct { 279 | Version Version `json:"sth_version"` // The version of the protocol to which the STH conforms 280 | TreeSize uint64 `json:"tree_size"` // The number of entries in the new tree 281 | Timestamp uint64 `json:"timestamp"` // The time at which the STH was created 282 | SHA256RootHash SHA256Hash `json:"sha256_root_hash"` // The root hash of the log's Merkle tree 283 | TreeHeadSignature DigitallySigned `json:"tree_head_signature"` // The Log's signature for this STH (see RFC section 3.5) 284 | LogID SHA256Hash `json:"log_id"` // The SHA256 hash of the log's public key 285 | } 286 | 287 | // SignedCertificateTimestamp represents the structure returned by the 288 | // add-chain and add-pre-chain methods after base64 decoding. (see RFC sections 289 | // 3.2 ,4.1 and 4.2) 290 | type SignedCertificateTimestamp struct { 291 | SCTVersion Version // The version of the protocol to which the SCT conforms 292 | LogID SHA256Hash // the SHA-256 hash of the log's public key, calculated over 293 | // the DER encoding of the key represented as SubjectPublicKeyInfo. 294 | Timestamp uint64 // Timestamp (in ms since unix epoc) at which the SCT was issued 295 | Extensions CTExtensions // For future extensions to the protocol 296 | Signature DigitallySigned // The Log's signature for this SCT 297 | } 298 | 299 | func (s SignedCertificateTimestamp) String() string { 300 | return fmt.Sprintf("{Version:%d LogId:%s Timestamp:%d Extensions:'%s' Signature:%v}", s.SCTVersion, 301 | base64.StdEncoding.EncodeToString(s.LogID[:]), 302 | s.Timestamp, 303 | s.Extensions, 304 | s.Signature) 305 | } 306 | 307 | // TimestampedEntry is part of the MerkleTreeLeaf structure. 308 | // See RFC section 3.4 309 | type TimestampedEntry struct { 310 | Timestamp uint64 311 | EntryType LogEntryType 312 | X509Entry ASN1Cert 313 | PrecertEntry PreCert 314 | Extensions CTExtensions 315 | } 316 | 317 | // MerkleTreeLeaf represents the deserialized sructure of the hash input for the 318 | // leaves of a log's Merkle tree. See RFC section 3.4 319 | type MerkleTreeLeaf struct { 320 | Version Version // the version of the protocol to which the MerkleTreeLeaf corresponds 321 | LeafType MerkleLeafType // The type of the leaf input, currently only TimestampedEntry can exist 322 | TimestampedEntry TimestampedEntry // The entry data itself 323 | } 324 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "crypto/sha256" 15 | "encoding/hex" 16 | "encoding/json" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "math/big" 21 | "os" 22 | "os/exec" 23 | "strconv" 24 | "strings" 25 | "time" 26 | 27 | "software.sslmate.com/src/certspotter/ct" 28 | ) 29 | 30 | func ReadSTHFile(path string) (*ct.SignedTreeHead, error) { 31 | content, err := ioutil.ReadFile(path) 32 | if err != nil { 33 | if os.IsNotExist(err) { 34 | return nil, nil 35 | } 36 | return nil, err 37 | } 38 | 39 | var sth ct.SignedTreeHead 40 | if err := json.Unmarshal(content, &sth); err != nil { 41 | return nil, err 42 | } 43 | 44 | return &sth, nil 45 | } 46 | 47 | func WriteSTHFile(path string, sth *ct.SignedTreeHead) error { 48 | sthJson, err := json.MarshalIndent(sth, "", "\t") 49 | if err != nil { 50 | return err 51 | } 52 | sthJson = append(sthJson, byte('\n')) 53 | return ioutil.WriteFile(path, sthJson, 0666) 54 | } 55 | 56 | func WriteProofFile(path string, proof ct.ConsistencyProof) error { 57 | proofJson, err := json.MarshalIndent(proof, "", "\t") 58 | if err != nil { 59 | return err 60 | } 61 | proofJson = append(proofJson, byte('\n')) 62 | return ioutil.WriteFile(path, proofJson, 0666) 63 | } 64 | 65 | func IsPrecert(entry *ct.LogEntry) bool { 66 | return entry.Leaf.TimestampedEntry.EntryType == ct.PrecertLogEntryType 67 | } 68 | 69 | func GetFullChain(entry *ct.LogEntry) [][]byte { 70 | certs := make([][]byte, 0, len(entry.Chain)+1) 71 | 72 | if entry.Leaf.TimestampedEntry.EntryType == ct.X509LogEntryType { 73 | certs = append(certs, entry.Leaf.TimestampedEntry.X509Entry) 74 | } 75 | for _, cert := range entry.Chain { 76 | certs = append(certs, cert) 77 | } 78 | 79 | return certs 80 | } 81 | 82 | func formatSerialNumber(serial *big.Int) string { 83 | if serial != nil { 84 | return fmt.Sprintf("%x", serial) 85 | } else { 86 | return "" 87 | } 88 | } 89 | 90 | func sha256sum(data []byte) []byte { 91 | sum := sha256.Sum256(data) 92 | return sum[:] 93 | } 94 | 95 | func sha256hex(data []byte) string { 96 | return hex.EncodeToString(sha256sum(data)) 97 | } 98 | 99 | type EntryInfo struct { 100 | LogUri string 101 | Entry *ct.LogEntry 102 | IsPrecert bool 103 | FullChain [][]byte // first entry is logged X509 cert or pre-cert 104 | CertInfo *CertInfo 105 | ParseError error // set iff CertInfo is nil 106 | Identifiers *Identifiers 107 | IdentifiersParseError error 108 | Filename string 109 | } 110 | 111 | type CertInfo struct { 112 | TBS *TBSCertificate 113 | 114 | Subject RDNSequence 115 | SubjectParseError error 116 | Issuer RDNSequence 117 | IssuerParseError error 118 | SANs []SubjectAltName 119 | SANsParseError error 120 | SerialNumber *big.Int 121 | SerialNumberParseError error 122 | Validity *CertValidity 123 | ValidityParseError error 124 | IsCA *bool 125 | IsCAParseError error 126 | } 127 | 128 | func MakeCertInfoFromTBS(tbs *TBSCertificate) *CertInfo { 129 | info := &CertInfo{TBS: tbs} 130 | 131 | info.Subject, info.SubjectParseError = tbs.ParseSubject() 132 | info.Issuer, info.IssuerParseError = tbs.ParseIssuer() 133 | info.SANs, info.SANsParseError = tbs.ParseSubjectAltNames() 134 | info.SerialNumber, info.SerialNumberParseError = tbs.ParseSerialNumber() 135 | info.Validity, info.ValidityParseError = tbs.ParseValidity() 136 | info.IsCA, info.IsCAParseError = tbs.ParseBasicConstraints() 137 | 138 | return info 139 | } 140 | 141 | func MakeCertInfoFromRawTBS(tbsBytes []byte) (*CertInfo, error) { 142 | tbs, err := ParseTBSCertificate(tbsBytes) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return MakeCertInfoFromTBS(tbs), nil 147 | } 148 | 149 | func MakeCertInfoFromRawCert(certBytes []byte) (*CertInfo, error) { 150 | cert, err := ParseCertificate(certBytes) 151 | if err != nil { 152 | return nil, err 153 | } 154 | return MakeCertInfoFromRawTBS(cert.GetRawTBSCertificate()) 155 | } 156 | 157 | func MakeCertInfoFromLogEntry(entry *ct.LogEntry) (*CertInfo, error) { 158 | switch entry.Leaf.TimestampedEntry.EntryType { 159 | case ct.X509LogEntryType: 160 | return MakeCertInfoFromRawCert(entry.Leaf.TimestampedEntry.X509Entry) 161 | 162 | case ct.PrecertLogEntryType: 163 | return MakeCertInfoFromRawTBS(entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate) 164 | 165 | default: 166 | return nil, fmt.Errorf("MakeCertInfoFromCTEntry: unknown CT entry type (neither X509 nor precert)") 167 | } 168 | } 169 | 170 | func (info *CertInfo) NotBefore() *time.Time { 171 | if info.ValidityParseError == nil { 172 | return &info.Validity.NotBefore 173 | } else { 174 | return nil 175 | } 176 | } 177 | 178 | func (info *CertInfo) NotAfter() *time.Time { 179 | if info.ValidityParseError == nil { 180 | return &info.Validity.NotAfter 181 | } else { 182 | return nil 183 | } 184 | } 185 | 186 | func (info *CertInfo) PubkeyHash() string { 187 | return sha256hex(info.TBS.GetRawPublicKey()) 188 | } 189 | 190 | func (info *CertInfo) PubkeyHashBytes() []byte { 191 | return sha256sum(info.TBS.GetRawPublicKey()) 192 | } 193 | 194 | func (info *CertInfo) Environ() []string { 195 | env := make([]string, 0, 10) 196 | 197 | env = append(env, "PUBKEY_HASH="+info.PubkeyHash()) 198 | 199 | if info.SerialNumberParseError != nil { 200 | env = append(env, "SERIAL_PARSE_ERROR="+info.SerialNumberParseError.Error()) 201 | } else { 202 | env = append(env, "SERIAL="+formatSerialNumber(info.SerialNumber)) 203 | } 204 | 205 | if info.ValidityParseError != nil { 206 | env = append(env, "VALIDITY_PARSE_ERROR="+info.ValidityParseError.Error()) 207 | } else { 208 | env = append(env, "NOT_BEFORE="+info.Validity.NotBefore.String()) 209 | env = append(env, "NOT_BEFORE_UNIXTIME="+strconv.FormatInt(info.Validity.NotBefore.Unix(), 10)) 210 | env = append(env, "NOT_AFTER="+info.Validity.NotAfter.String()) 211 | env = append(env, "NOT_AFTER_UNIXTIME="+strconv.FormatInt(info.Validity.NotAfter.Unix(), 10)) 212 | } 213 | 214 | if info.SubjectParseError != nil { 215 | env = append(env, "SUBJECT_PARSE_ERROR="+info.SubjectParseError.Error()) 216 | } else { 217 | env = append(env, "SUBJECT_DN="+info.Subject.String()) 218 | } 219 | 220 | if info.IssuerParseError != nil { 221 | env = append(env, "ISSUER_PARSE_ERROR="+info.IssuerParseError.Error()) 222 | } else { 223 | env = append(env, "ISSUER_DN="+info.Issuer.String()) 224 | } 225 | 226 | // TODO: include SANs in environment 227 | 228 | return env 229 | } 230 | 231 | func (info *EntryInfo) HasParseErrors() bool { 232 | return info.ParseError != nil || 233 | info.IdentifiersParseError != nil || 234 | info.CertInfo.SubjectParseError != nil || 235 | info.CertInfo.IssuerParseError != nil || 236 | info.CertInfo.SANsParseError != nil || 237 | info.CertInfo.SerialNumberParseError != nil || 238 | info.CertInfo.ValidityParseError != nil || 239 | info.CertInfo.IsCAParseError != nil 240 | } 241 | 242 | func (info *EntryInfo) Fingerprint() string { 243 | if len(info.FullChain) > 0 { 244 | return sha256hex(info.FullChain[0]) 245 | } else { 246 | return "" 247 | } 248 | } 249 | 250 | func (info *EntryInfo) FingerprintBytes() []byte { 251 | if len(info.FullChain) > 0 { 252 | return sha256sum(info.FullChain[0]) 253 | } else { 254 | return []byte{} 255 | } 256 | } 257 | 258 | func (info *EntryInfo) typeString() string { 259 | if info.IsPrecert { 260 | return "precert" 261 | } else { 262 | return "cert" 263 | } 264 | } 265 | 266 | func (info *EntryInfo) typeFriendlyString() string { 267 | if info.IsPrecert { 268 | return "Pre-certificate" 269 | } else { 270 | return "Certificate" 271 | } 272 | } 273 | 274 | func yesnoString(value bool) string { 275 | if value { 276 | return "yes" 277 | } else { 278 | return "no" 279 | } 280 | } 281 | 282 | func (info *EntryInfo) Environ() []string { 283 | env := []string{ 284 | "FINGERPRINT=" + info.Fingerprint(), 285 | "CERT_TYPE=" + info.typeString(), 286 | "CERT_PARSEABLE=" + yesnoString(info.ParseError == nil), 287 | "LOG_URI=" + info.LogUri, 288 | "ENTRY_INDEX=" + strconv.FormatInt(info.Entry.Index, 10), 289 | } 290 | 291 | if info.Filename != "" { 292 | env = append(env, "CERT_FILENAME="+info.Filename) 293 | } 294 | if info.ParseError != nil { 295 | env = append(env, "PARSE_ERROR="+info.ParseError.Error()) 296 | } else if info.CertInfo != nil { 297 | certEnv := info.CertInfo.Environ() 298 | env = append(env, certEnv...) 299 | } 300 | if info.IdentifiersParseError != nil { 301 | env = append(env, "IDENTIFIERS_PARSE_ERROR="+info.IdentifiersParseError.Error()) 302 | } else if info.Identifiers != nil { 303 | env = append(env, "DNS_NAMES="+info.Identifiers.dnsNamesString(",")) 304 | env = append(env, "IP_ADDRESSES="+info.Identifiers.ipAddrsString(",")) 305 | } 306 | 307 | return env 308 | } 309 | 310 | func writeField(out io.Writer, name string, value interface{}, err error) { 311 | if err == nil { 312 | fmt.Fprintf(out, "\t%13s = %s\n", name, value) 313 | } else { 314 | fmt.Fprintf(out, "\t%13s = *** UNKNOWN (%s) ***\n", name, err) 315 | } 316 | } 317 | 318 | func (info *EntryInfo) Write(out io.Writer) { 319 | fingerprint := info.Fingerprint() 320 | fmt.Fprintf(out, "%s:\n", fingerprint) 321 | if info.IdentifiersParseError != nil { 322 | writeField(out, "Identifiers", nil, info.IdentifiersParseError) 323 | } else if info.Identifiers != nil { 324 | for _, dnsName := range info.Identifiers.DNSNames { 325 | writeField(out, "DNS Name", dnsName, nil) 326 | } 327 | for _, ipaddr := range info.Identifiers.IPAddrs { 328 | writeField(out, "IP Address", ipaddr, nil) 329 | } 330 | } 331 | if info.ParseError != nil { 332 | writeField(out, "Parse Error", "*** "+info.ParseError.Error()+" ***", nil) 333 | } else if info.CertInfo != nil { 334 | writeField(out, "Pubkey", info.CertInfo.PubkeyHash(), nil) 335 | writeField(out, "Issuer", info.CertInfo.Issuer, info.CertInfo.IssuerParseError) 336 | writeField(out, "Not Before", info.CertInfo.NotBefore(), info.CertInfo.ValidityParseError) 337 | writeField(out, "Not After", info.CertInfo.NotAfter(), info.CertInfo.ValidityParseError) 338 | } 339 | writeField(out, "Log Entry", fmt.Sprintf("%d @ %s (%s)", info.Entry.Index, info.LogUri, info.typeFriendlyString()), nil) 340 | writeField(out, "crt.sh", "https://crt.sh/?sha256="+fingerprint, nil) 341 | if info.Filename != "" { 342 | writeField(out, "Filename", info.Filename, nil) 343 | } 344 | } 345 | 346 | func (info *EntryInfo) InvokeHookScript(command string) error { 347 | cmd := exec.Command(command) 348 | cmd.Env = os.Environ() 349 | infoEnv := info.Environ() 350 | cmd.Env = append(cmd.Env, infoEnv...) 351 | stderrBuffer := bytes.Buffer{} 352 | cmd.Stderr = &stderrBuffer 353 | if err := cmd.Run(); err != nil { 354 | if _, isExitError := err.(*exec.ExitError); isExitError { 355 | return fmt.Errorf("Script failed: %s: %s", command, strings.TrimSpace(stderrBuffer.String())) 356 | } else { 357 | return fmt.Errorf("Failed to execute script: %s: %s", command, err) 358 | } 359 | } 360 | return nil 361 | } 362 | 363 | func MatchesWildcard(dnsName string, pattern string) bool { 364 | for len(pattern) > 0 { 365 | if pattern[0] == '*' { 366 | if len(dnsName) > 0 && dnsName[0] != '.' && MatchesWildcard(dnsName[1:], pattern) { 367 | return true 368 | } 369 | pattern = pattern[1:] 370 | } else { 371 | if len(dnsName) == 0 || pattern[0] != dnsName[0] { 372 | return false 373 | } 374 | pattern = pattern[1:] 375 | dnsName = dnsName[1:] 376 | } 377 | } 378 | return len(dnsName) == 0 379 | } 380 | -------------------------------------------------------------------------------- /ct/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /x509.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package certspotter 11 | 12 | import ( 13 | "bytes" 14 | "encoding/asn1" 15 | "errors" 16 | "fmt" 17 | "math/big" 18 | "net" 19 | "time" 20 | ) 21 | 22 | var ( 23 | oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} 24 | oidExtensionBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19} 25 | oidCountry = asn1.ObjectIdentifier{2, 5, 4, 6} 26 | oidOrganization = asn1.ObjectIdentifier{2, 5, 4, 10} 27 | oidOrganizationalUnit = asn1.ObjectIdentifier{2, 5, 4, 11} 28 | oidCommonName = asn1.ObjectIdentifier{2, 5, 4, 3} 29 | oidSerialNumber = asn1.ObjectIdentifier{2, 5, 4, 5} 30 | oidLocality = asn1.ObjectIdentifier{2, 5, 4, 7} 31 | oidProvince = asn1.ObjectIdentifier{2, 5, 4, 8} 32 | oidStreetAddress = asn1.ObjectIdentifier{2, 5, 4, 9} 33 | oidPostalCode = asn1.ObjectIdentifier{2, 5, 4, 17} 34 | ) 35 | 36 | type CertValidity struct { 37 | NotBefore time.Time 38 | NotAfter time.Time 39 | } 40 | 41 | type basicConstraints struct { 42 | IsCA bool `asn1:"optional"` 43 | MaxPathLen int `asn1:"optional,default:-1"` 44 | } 45 | 46 | type Extension struct { 47 | Id asn1.ObjectIdentifier 48 | Critical bool `asn1:"optional"` 49 | Value []byte 50 | } 51 | 52 | const ( 53 | sanOtherName = 0 54 | sanRfc822Name = 1 55 | sanDNSName = 2 56 | sanX400Address = 3 57 | sanDirectoryName = 4 58 | sanEdiPartyName = 5 59 | sanURI = 6 60 | sanIPAddress = 7 61 | sanRegisteredID = 8 62 | ) 63 | 64 | type SubjectAltName struct { 65 | Type int 66 | Value []byte 67 | } 68 | 69 | type RDNSequence []RelativeDistinguishedNameSET 70 | type RelativeDistinguishedNameSET []AttributeTypeAndValue 71 | type AttributeTypeAndValue struct { 72 | Type asn1.ObjectIdentifier 73 | Value asn1.RawValue 74 | } 75 | 76 | func ParseRDNSequence(rdnsBytes []byte) (RDNSequence, error) { 77 | var rdns RDNSequence 78 | if rest, err := asn1.Unmarshal(rdnsBytes, &rdns); err != nil { 79 | return nil, errors.New("failed to parse RDNSequence: " + err.Error()) 80 | } else if len(rest) != 0 { 81 | return nil, fmt.Errorf("trailing data after RDNSequence: %v", rest) // XXX: too strict? 82 | } 83 | return rdns, nil 84 | } 85 | 86 | type TBSCertificate struct { 87 | Raw asn1.RawContent 88 | 89 | Version int `asn1:"optional,explicit,default:1,tag:0"` 90 | SerialNumber asn1.RawValue 91 | SignatureAlgorithm asn1.RawValue 92 | Issuer asn1.RawValue 93 | Validity asn1.RawValue 94 | Subject asn1.RawValue 95 | PublicKey asn1.RawValue 96 | UniqueId asn1.BitString `asn1:"optional,tag:1"` 97 | SubjectUniqueId asn1.BitString `asn1:"optional,tag:2"` 98 | Extensions []Extension `asn1:"optional,explicit,tag:3"` 99 | } 100 | 101 | type Certificate struct { 102 | Raw asn1.RawContent 103 | 104 | TBSCertificate asn1.RawValue 105 | SignatureAlgorithm asn1.RawValue 106 | SignatureValue asn1.RawValue 107 | } 108 | 109 | func (rdns RDNSequence) ParseCNs() ([]string, error) { 110 | var cns []string 111 | 112 | for _, rdn := range rdns { 113 | if len(rdn) == 0 { 114 | continue 115 | } 116 | atv := rdn[0] 117 | if atv.Type.Equal(oidCommonName) { 118 | cnString, err := decodeASN1String(&atv.Value) 119 | if err != nil { 120 | return nil, errors.New("Error decoding CN: " + err.Error()) 121 | } 122 | cns = append(cns, cnString) 123 | } 124 | } 125 | 126 | return cns, nil 127 | } 128 | 129 | func rdnLabel(oid asn1.ObjectIdentifier) string { 130 | switch { 131 | case oid.Equal(oidCountry): 132 | return "C" 133 | case oid.Equal(oidOrganization): 134 | return "O" 135 | case oid.Equal(oidOrganizationalUnit): 136 | return "OU" 137 | case oid.Equal(oidCommonName): 138 | return "CN" 139 | case oid.Equal(oidSerialNumber): 140 | return "serialNumber" 141 | case oid.Equal(oidLocality): 142 | return "L" 143 | case oid.Equal(oidProvince): 144 | return "ST" 145 | case oid.Equal(oidStreetAddress): 146 | return "street" 147 | case oid.Equal(oidPostalCode): 148 | return "postalCode" 149 | } 150 | return oid.String() 151 | } 152 | 153 | func (rdns RDNSequence) String() string { 154 | var buf bytes.Buffer 155 | 156 | for _, rdn := range rdns { 157 | if len(rdn) == 0 { 158 | continue 159 | } 160 | atv := rdn[0] 161 | 162 | if buf.Len() != 0 { 163 | buf.WriteString(", ") 164 | } 165 | buf.WriteString(rdnLabel(atv.Type)) 166 | buf.WriteString("=") 167 | valueString, err := decodeASN1String(&atv.Value) 168 | if err == nil { 169 | buf.WriteString(valueString) // TODO: escape non-printable characters, '\', and ',' 170 | } else { 171 | fmt.Fprintf(&buf, "%v", atv.Value.FullBytes) 172 | } 173 | } 174 | 175 | return buf.String() 176 | } 177 | 178 | func (san SubjectAltName) String() string { 179 | switch san.Type { 180 | case sanDNSName: 181 | return "DNS:" + string(san.Value) // TODO: escape non-printable characters, '\', and ',' 182 | case sanIPAddress: 183 | if len(san.Value) == 4 || len(san.Value) == 16 { 184 | return "IP:" + net.IP(san.Value).String() 185 | } else { 186 | return fmt.Sprintf("IP:%v", san.Value) 187 | } 188 | default: 189 | // TODO: support other types of SANs 190 | return fmt.Sprintf("%d:%v", san.Type, san.Value) 191 | } 192 | } 193 | 194 | func ParseTBSCertificate(tbsBytes []byte) (*TBSCertificate, error) { 195 | var tbs TBSCertificate 196 | if rest, err := asn1.Unmarshal(tbsBytes, &tbs); err != nil { 197 | return nil, errors.New("failed to parse TBS: " + err.Error()) 198 | } else if len(rest) > 0 { 199 | return nil, fmt.Errorf("trailing data after TBS: %v", rest) // XXX: too strict? 200 | } 201 | return &tbs, nil 202 | } 203 | 204 | func (tbs *TBSCertificate) ParseValidity() (*CertValidity, error) { 205 | var rawValidity struct { 206 | NotBefore asn1.RawValue 207 | NotAfter asn1.RawValue 208 | } 209 | if rest, err := asn1.Unmarshal(tbs.Validity.FullBytes, &rawValidity); err != nil { 210 | return nil, errors.New("failed to parse validity: " + err.Error()) 211 | } else if len(rest) > 0 { 212 | return nil, fmt.Errorf("trailing data after validity: %v", rest) 213 | } 214 | 215 | var validity CertValidity 216 | var err error 217 | if validity.NotBefore, err = decodeASN1Time(&rawValidity.NotBefore); err != nil { 218 | return nil, errors.New("failed to decode notBefore time: " + err.Error()) 219 | } 220 | if validity.NotAfter, err = decodeASN1Time(&rawValidity.NotAfter); err != nil { 221 | return nil, errors.New("failed to decode notAfter time: " + err.Error()) 222 | } 223 | 224 | return &validity, nil 225 | } 226 | 227 | func (tbs *TBSCertificate) ParseBasicConstraints() (*bool, error) { 228 | isCA := false 229 | isNotCA := false 230 | 231 | // Some certs in the wild have multiple BasicConstraints extensions (is there anything 232 | // that CAs haven't screwed up???), so we process all of them and only choke if they 233 | // are contradictory (which has not been observed...yet). 234 | for _, ext := range tbs.GetExtension(oidExtensionBasicConstraints) { 235 | var constraints basicConstraints 236 | if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil { 237 | return nil, errors.New("failed to parse Basic Constraints: " + err.Error()) 238 | } else if len(rest) > 0 { 239 | return nil, fmt.Errorf("trailing data after Basic Constraints: %v", rest) 240 | } 241 | 242 | if constraints.IsCA { 243 | isCA = true 244 | } else { 245 | isNotCA = true 246 | } 247 | } 248 | 249 | if !isCA && !isNotCA { 250 | return nil, nil 251 | } else if isCA && !isNotCA { 252 | trueValue := true 253 | return &trueValue, nil 254 | } else if !isCA && isNotCA { 255 | falseValue := false 256 | return &falseValue, nil 257 | } else { 258 | return nil, fmt.Errorf("Certificate has more than one Basic Constraints extension and they are contradictory") 259 | } 260 | } 261 | 262 | func (tbs *TBSCertificate) ParseSerialNumber() (*big.Int, error) { 263 | serialNumber := big.NewInt(0) 264 | if rest, err := asn1.Unmarshal(tbs.SerialNumber.FullBytes, &serialNumber); err != nil { 265 | return nil, errors.New("failed to parse serial number: " + err.Error()) 266 | } else if len(rest) > 0 { 267 | return nil, fmt.Errorf("trailing data after serial number: %v", rest) 268 | } 269 | return serialNumber, nil 270 | } 271 | 272 | func (tbs *TBSCertificate) GetRawPublicKey() []byte { 273 | return tbs.PublicKey.FullBytes 274 | } 275 | 276 | func (tbs *TBSCertificate) GetRawSubject() []byte { 277 | return tbs.Subject.FullBytes 278 | } 279 | 280 | func (tbs *TBSCertificate) GetRawIssuer() []byte { 281 | return tbs.Issuer.FullBytes 282 | } 283 | 284 | func (tbs *TBSCertificate) ParseSubject() (RDNSequence, error) { 285 | subject, err := ParseRDNSequence(tbs.GetRawSubject()) 286 | if err != nil { 287 | return nil, errors.New("failed to parse certificate subject: " + err.Error()) 288 | } 289 | return subject, nil 290 | } 291 | 292 | func (tbs *TBSCertificate) ParseIssuer() (RDNSequence, error) { 293 | issuer, err := ParseRDNSequence(tbs.GetRawIssuer()) 294 | if err != nil { 295 | return nil, errors.New("failed to parse certificate issuer: " + err.Error()) 296 | } 297 | return issuer, nil 298 | } 299 | 300 | func (tbs *TBSCertificate) ParseSubjectCommonNames() ([]string, error) { 301 | subject, err := tbs.ParseSubject() 302 | if err != nil { 303 | return nil, err 304 | } 305 | cns, err := subject.ParseCNs() 306 | if err != nil { 307 | return nil, errors.New("failed to process certificate subject: " + err.Error()) 308 | } 309 | 310 | return cns, nil 311 | } 312 | 313 | func (tbs *TBSCertificate) ParseSubjectAltNames() ([]SubjectAltName, error) { 314 | sans := []SubjectAltName{} 315 | 316 | for _, sanExt := range tbs.GetExtension(oidExtensionSubjectAltName) { 317 | var err error 318 | sans, err = parseSANExtension(sans, sanExt.Value) 319 | if err != nil { 320 | return nil, err 321 | } 322 | } 323 | 324 | return sans, nil 325 | } 326 | 327 | func (tbs *TBSCertificate) GetExtension(id asn1.ObjectIdentifier) []Extension { 328 | var exts []Extension 329 | for _, ext := range tbs.Extensions { 330 | if ext.Id.Equal(id) { 331 | exts = append(exts, ext) 332 | } 333 | } 334 | return exts 335 | } 336 | 337 | func ParseCertificate(certBytes []byte) (*Certificate, error) { 338 | var cert Certificate 339 | if rest, err := asn1.Unmarshal(certBytes, &cert); err != nil { 340 | return nil, errors.New("failed to parse certificate: " + err.Error()) 341 | } else if len(rest) > 0 { 342 | return nil, fmt.Errorf("trailing data after certificate: %v", rest) // XXX: too strict? 343 | } 344 | return &cert, nil 345 | } 346 | 347 | func (cert *Certificate) GetRawTBSCertificate() []byte { 348 | return cert.TBSCertificate.FullBytes 349 | } 350 | 351 | func (cert *Certificate) ParseTBSCertificate() (*TBSCertificate, error) { 352 | return ParseTBSCertificate(cert.GetRawTBSCertificate()) 353 | } 354 | 355 | func parseSANExtension(sans []SubjectAltName, value []byte) ([]SubjectAltName, error) { 356 | var seq asn1.RawValue 357 | if rest, err := asn1.Unmarshal(value, &seq); err != nil { 358 | return nil, errors.New("failed to parse subjectAltName extension: " + err.Error()) 359 | } else if len(rest) != 0 { 360 | // Don't complain if the SAN is followed by exactly one zero byte, 361 | // which is a common error. 362 | if !(len(rest) == 1 && rest[0] == 0) { 363 | return nil, fmt.Errorf("trailing data in subjectAltName extension: %v", rest) // XXX: too strict? 364 | } 365 | } 366 | if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 { 367 | return nil, errors.New("failed to parse subjectAltName extension: bad SAN sequence") // XXX: too strict? 368 | } 369 | 370 | rest := seq.Bytes 371 | for len(rest) > 0 { 372 | var val asn1.RawValue 373 | var err error 374 | rest, err = asn1.Unmarshal(rest, &val) 375 | if err != nil { 376 | return nil, errors.New("failed to parse subjectAltName extension item: " + err.Error()) 377 | } 378 | sans = append(sans, SubjectAltName{Type: val.Tag, Value: val.Bytes}) 379 | } 380 | 381 | return sans, nil 382 | } 383 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2017 Opsmate, Inc. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla 4 | // Public License, v. 2.0. If a copy of the MPL was not distributed 5 | // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This software is distributed WITHOUT A WARRANTY OF ANY KIND. 8 | // See the Mozilla Public License for details. 9 | 10 | package cmd 11 | 12 | import ( 13 | "bytes" 14 | "flag" 15 | "fmt" 16 | "log" 17 | "os" 18 | "os/user" 19 | "path/filepath" 20 | "sync" 21 | 22 | "software.sslmate.com/src/certspotter" 23 | "software.sslmate.com/src/certspotter/ct" 24 | ) 25 | 26 | var batchSize = flag.Int("batch_size", 1000, "Max number of entries to request at per call to get-entries (advanced)") 27 | var numWorkers = flag.Int("num_workers", 2, "Number of concurrent matchers (advanced)") 28 | var script = flag.String("script", "", "Script to execute when a matching certificate is found") 29 | var logsFilename = flag.String("logs", "", "JSON file containing log information") 30 | var underwater = flag.Bool("underwater", false, "Monitor certificates from distrusted CAs instead of trusted CAs") 31 | var noSave = flag.Bool("no_save", false, "Do not save a copy of matching certificates") 32 | var verbose = flag.Bool("verbose", false, "Be verbose") 33 | var allTime = flag.Bool("all_time", false, "Scan certs from all time, not just since last scan") 34 | var state *State 35 | 36 | var printMutex sync.Mutex 37 | 38 | func homedir() string { 39 | home := os.Getenv("HOME") 40 | if home != "" { 41 | return home 42 | } 43 | user, err := user.Current() 44 | if err == nil { 45 | return user.HomeDir 46 | } 47 | panic("Unable to determine home directory") 48 | } 49 | 50 | func DefaultStateDir(programName string) string { 51 | return filepath.Join(homedir(), "."+programName) 52 | } 53 | 54 | func DefaultConfigDir(programName string) string { 55 | return filepath.Join(homedir(), "."+programName) 56 | } 57 | 58 | func LogEntry(info *certspotter.EntryInfo) { 59 | if !*noSave { 60 | var alreadyPresent bool 61 | var err error 62 | alreadyPresent, info.Filename, err = state.SaveCert(info.IsPrecert, info.FullChain) 63 | if err != nil { 64 | log.Print(err) 65 | } 66 | if alreadyPresent { 67 | return 68 | } 69 | } 70 | 71 | if *script != "" { 72 | if err := info.InvokeHookScript(*script); err != nil { 73 | log.Print(err) 74 | } 75 | } else { 76 | printMutex.Lock() 77 | info.Write(os.Stdout) 78 | fmt.Fprintf(os.Stdout, "\n") 79 | printMutex.Unlock() 80 | } 81 | } 82 | 83 | func loadLogList() ([]certspotter.LogInfo, error) { 84 | if *logsFilename != "" { 85 | var logFileObj certspotter.LogInfoFile 86 | if err := readJSONFile(*logsFilename, &logFileObj); err != nil { 87 | return nil, fmt.Errorf("Error reading logs file: %s: %s", *logsFilename, err) 88 | } 89 | return logFileObj.Logs, nil 90 | } else if *underwater { 91 | return certspotter.UnderwaterLogs, nil 92 | } else { 93 | return certspotter.DefaultLogs, nil 94 | } 95 | } 96 | 97 | type logHandle struct { 98 | scanner *certspotter.Scanner 99 | state *LogState 100 | tree *certspotter.CollapsedMerkleTree 101 | verifiedSTH *ct.SignedTreeHead 102 | } 103 | 104 | func makeLogHandle(logInfo *certspotter.LogInfo) (*logHandle, error) { 105 | ctlog := new(logHandle) 106 | 107 | logKey, err := logInfo.ParsedPublicKey() 108 | if err != nil { 109 | return nil, fmt.Errorf("Bad public key: %s", err) 110 | } 111 | ctlog.scanner = certspotter.NewScanner(logInfo.FullURI(), logInfo.ID(), logKey, &certspotter.ScannerOptions{ 112 | BatchSize: *batchSize, 113 | NumWorkers: *numWorkers, 114 | Quiet: !*verbose, 115 | }) 116 | 117 | ctlog.state, err = state.OpenLogState(logInfo) 118 | if err != nil { 119 | return nil, fmt.Errorf("Error opening state directory: %s", err) 120 | } 121 | ctlog.tree, err = ctlog.state.GetTree() 122 | if err != nil { 123 | return nil, fmt.Errorf("Error loading tree: %s", err) 124 | } 125 | ctlog.verifiedSTH, err = ctlog.state.GetVerifiedSTH() 126 | if err != nil { 127 | return nil, fmt.Errorf("Error loading verified STH: %s", err) 128 | } 129 | 130 | if ctlog.tree == nil && ctlog.verifiedSTH == nil { // This branch can be removed eventually 131 | legacySTH, err := state.GetLegacySTH(logInfo) 132 | if err != nil { 133 | return nil, fmt.Errorf("Error loading legacy STH: %s", err) 134 | } 135 | if legacySTH != nil { 136 | log.Printf("Initializing log state from legacy state directory") 137 | ctlog.tree, err = ctlog.scanner.MakeCollapsedMerkleTree(legacySTH) 138 | if err != nil { 139 | return nil, fmt.Errorf("Error reconstructing Merkle Tree for legacy STH: %s", err) 140 | } 141 | if err := ctlog.state.StoreTree(ctlog.tree); err != nil { 142 | return nil, fmt.Errorf("Error storing tree: %s", err) 143 | } 144 | if err := ctlog.state.StoreVerifiedSTH(legacySTH); err != nil { 145 | return nil, fmt.Errorf("Error storing verified STH: %s", err) 146 | } 147 | state.RemoveLegacySTH(logInfo) 148 | } 149 | } 150 | 151 | return ctlog, nil 152 | } 153 | 154 | func (ctlog *logHandle) refresh() error { 155 | if *verbose { 156 | log.Printf("Retrieving latest STH from log") 157 | } 158 | latestSTH, err := ctlog.scanner.GetSTH() 159 | if err != nil { 160 | return fmt.Errorf("Error retrieving STH from log: %s", err) 161 | } 162 | if ctlog.verifiedSTH == nil { 163 | if *verbose { 164 | log.Printf("No existing STH is known; presuming latest STH (%d) is valid", latestSTH.TreeSize) 165 | } 166 | ctlog.verifiedSTH = latestSTH 167 | if err := ctlog.state.StoreVerifiedSTH(ctlog.verifiedSTH); err != nil { 168 | return fmt.Errorf("Error storing verified STH: %s", err) 169 | } 170 | } else { 171 | if err := ctlog.state.StoreUnverifiedSTH(latestSTH); err != nil { 172 | return fmt.Errorf("Error storing unverified STH: %s", err) 173 | } 174 | } 175 | return nil 176 | } 177 | 178 | func (ctlog *logHandle) audit() error { 179 | sths, err := ctlog.state.GetUnverifiedSTHs() 180 | if err != nil { 181 | return fmt.Errorf("Error loading unverified STHs: %s", err) 182 | } 183 | 184 | for _, sth := range sths { 185 | if *verbose { 186 | log.Printf("Verifying consistency between STH %d (%x) and STH %d (%x)", sth.TreeSize, sth.SHA256RootHash, ctlog.verifiedSTH.TreeSize, ctlog.verifiedSTH.SHA256RootHash) 187 | } 188 | if sth.TreeSize > ctlog.verifiedSTH.TreeSize { 189 | isValid, err := ctlog.scanner.CheckConsistency(ctlog.verifiedSTH, sth) 190 | if err != nil { 191 | return fmt.Errorf("Error fetching consistency proof between %d and %d (if this error persists, it should be construed as misbehavior by the log): %s", ctlog.verifiedSTH.TreeSize, sth.TreeSize, err) 192 | } 193 | if !isValid { 194 | return fmt.Errorf("Log has misbehaved: STH in '%s' is not consistent with STH in '%s'", ctlog.state.VerifiedSTHFilename(), ctlog.state.UnverifiedSTHFilename(sth)) 195 | } 196 | if *verbose { 197 | log.Printf("STH %d (%x) is now the latest verified STH", sth.TreeSize, sth.SHA256RootHash) 198 | } 199 | ctlog.verifiedSTH = sth 200 | if err := ctlog.state.StoreVerifiedSTH(ctlog.verifiedSTH); err != nil { 201 | return fmt.Errorf("Error storing verified STH: %s", err) 202 | } 203 | } else if sth.TreeSize < ctlog.verifiedSTH.TreeSize { 204 | isValid, err := ctlog.scanner.CheckConsistency(sth, ctlog.verifiedSTH) 205 | if err != nil { 206 | return fmt.Errorf("Error fetching consistency proof between %d and %d (if this error persists, it should be construed as misbehavior by the log): %s", ctlog.verifiedSTH.TreeSize, sth.TreeSize, err) 207 | } 208 | if !isValid { 209 | return fmt.Errorf("Log has misbehaved: STH in '%s' is not consistent with STH in '%s'", ctlog.state.VerifiedSTHFilename(), ctlog.state.UnverifiedSTHFilename(sth)) 210 | } 211 | } else { 212 | if !bytes.Equal(sth.SHA256RootHash[:], ctlog.verifiedSTH.SHA256RootHash[:]) { 213 | return fmt.Errorf("Log has misbehaved: STH in '%s' is not consistent with STH in '%s'", ctlog.state.VerifiedSTHFilename(), ctlog.state.UnverifiedSTHFilename(sth)) 214 | } 215 | } 216 | if err := ctlog.state.RemoveUnverifiedSTH(sth); err != nil { 217 | return fmt.Errorf("Error removing redundant STH: %s", err) 218 | } 219 | } 220 | 221 | return nil 222 | } 223 | 224 | func (ctlog *logHandle) scan(processCallback certspotter.ProcessCallback) error { 225 | startIndex := int64(ctlog.tree.GetSize()) 226 | endIndex := int64(ctlog.verifiedSTH.TreeSize) 227 | 228 | if endIndex > startIndex { 229 | tree := certspotter.CloneCollapsedMerkleTree(ctlog.tree) 230 | 231 | if err := ctlog.scanner.Scan(startIndex, endIndex, processCallback, tree); err != nil { 232 | return fmt.Errorf("Error scanning log (if this error persists, it should be construed as misbehavior by the log): %s", err) 233 | } 234 | 235 | rootHash := tree.CalculateRoot() 236 | if !bytes.Equal(rootHash, ctlog.verifiedSTH.SHA256RootHash[:]) { 237 | return fmt.Errorf("Log has misbehaved: log entries at tree size %d do not correspond to signed tree root", ctlog.verifiedSTH.TreeSize) 238 | } 239 | 240 | ctlog.tree = tree 241 | if err := ctlog.state.StoreTree(ctlog.tree); err != nil { 242 | return fmt.Errorf("Error storing tree: %s", err) 243 | } 244 | } 245 | 246 | return nil 247 | } 248 | 249 | func processLog(logInfo *certspotter.LogInfo, processCallback certspotter.ProcessCallback) int { 250 | log.SetPrefix(os.Args[0] + ": " + logInfo.Url + ": ") 251 | 252 | ctlog, err := makeLogHandle(logInfo) 253 | if err != nil { 254 | log.Printf("%s\n", err) 255 | return 1 256 | } 257 | 258 | if err := ctlog.refresh(); err != nil { 259 | log.Printf("%s\n", err) 260 | return 1 261 | } 262 | 263 | if err := ctlog.audit(); err != nil { 264 | log.Printf("%s\n", err) 265 | return 1 266 | } 267 | 268 | if *allTime { 269 | ctlog.tree = certspotter.EmptyCollapsedMerkleTree() 270 | if *verbose { 271 | log.Printf("Scanning all %d entries in the log because -all_time option specified", ctlog.verifiedSTH.TreeSize) 272 | } 273 | } else if ctlog.tree != nil { 274 | if *verbose { 275 | log.Printf("Existing log; scanning %d new entries since previous scan", ctlog.verifiedSTH.TreeSize-ctlog.tree.GetSize()) 276 | } 277 | } else if state.IsFirstRun() { 278 | ctlog.tree, err = ctlog.scanner.MakeCollapsedMerkleTree(ctlog.verifiedSTH) 279 | if err != nil { 280 | log.Printf("Error reconstructing Merkle Tree: %s", err) 281 | return 1 282 | } 283 | if *verbose { 284 | log.Printf("First run of Cert Spotter; not scanning %d existing entries because -all_time option not specified", ctlog.verifiedSTH.TreeSize) 285 | } 286 | } else { 287 | ctlog.tree = certspotter.EmptyCollapsedMerkleTree() 288 | if *verbose { 289 | log.Printf("New log; scanning all %d entries in the log", ctlog.verifiedSTH.TreeSize) 290 | } 291 | } 292 | if err := ctlog.state.StoreTree(ctlog.tree); err != nil { 293 | log.Printf("Error storing tree: %s\n", err) 294 | return 1 295 | } 296 | 297 | if err := ctlog.scan(processCallback); err != nil { 298 | log.Printf("%s\n", err) 299 | return 1 300 | } 301 | 302 | if *verbose { 303 | log.Printf("Final log size = %d, final root hash = %x", ctlog.verifiedSTH.TreeSize, ctlog.verifiedSTH.SHA256RootHash) 304 | } 305 | 306 | return 0 307 | } 308 | 309 | func Main(statePath string, processCallback certspotter.ProcessCallback) int { 310 | var err error 311 | 312 | logs, err := loadLogList() 313 | if err != nil { 314 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 315 | return 1 316 | } 317 | 318 | state, err = OpenState(statePath) 319 | if err != nil { 320 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 321 | return 1 322 | } 323 | locked, err := state.Lock() 324 | if err != nil { 325 | fmt.Fprintf(os.Stderr, "%s: Error locking state directory: %s\n", os.Args[0], err) 326 | return 1 327 | } 328 | if !locked { 329 | var otherPidInfo string 330 | if otherPid := state.LockingPid(); otherPid != 0 { 331 | otherPidInfo = fmt.Sprintf(" (as process ID %d)", otherPid) 332 | } 333 | fmt.Fprintf(os.Stderr, "%s: Another instance of %s is already running%s; remove the file %s if this is not the case\n", os.Args[0], os.Args[0], otherPidInfo, state.LockFilename()) 334 | return 1 335 | } 336 | 337 | exitCode := 0 338 | for i := range logs { 339 | exitCode |= processLog(&logs[i], processCallback) 340 | } 341 | 342 | if state.IsFirstRun() && exitCode == 0 { 343 | if err := state.WriteOnceFile(); err != nil { 344 | fmt.Fprintf(os.Stderr, "%s: Error writing once file: %s\n", os.Args[0], err) 345 | exitCode |= 1 346 | } 347 | } 348 | 349 | if err := state.Unlock(); err != nil { 350 | fmt.Fprintf(os.Stderr, "%s: Error unlocking state directory: %s\n", os.Args[0], err) 351 | exitCode |= 1 352 | } 353 | 354 | return exitCode 355 | } 356 | -------------------------------------------------------------------------------- /ct/serialization.go: -------------------------------------------------------------------------------- 1 | package ct 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "crypto" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "io" 11 | ) 12 | 13 | // Variable size structure prefix-header byte lengths 14 | const ( 15 | CertificateLengthBytes = 3 16 | PreCertificateLengthBytes = 3 17 | ExtensionsLengthBytes = 2 18 | CertificateChainLengthBytes = 3 19 | SignatureLengthBytes = 2 20 | ) 21 | 22 | // Max lengths 23 | const ( 24 | MaxCertificateLength = (1 << 24) - 1 25 | MaxExtensionsLength = (1 << 16) - 1 26 | ) 27 | 28 | func writeUint(w io.Writer, value uint64, numBytes int) error { 29 | buf := make([]uint8, numBytes) 30 | for i := 0; i < numBytes; i++ { 31 | buf[numBytes-i-1] = uint8(value & 0xff) 32 | value >>= 8 33 | } 34 | if value != 0 { 35 | return errors.New("numBytes was insufficiently large to represent value") 36 | } 37 | if _, err := w.Write(buf); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func writeVarBytes(w io.Writer, value []byte, numLenBytes int) error { 44 | if err := writeUint(w, uint64(len(value)), numLenBytes); err != nil { 45 | return err 46 | } 47 | if _, err := w.Write(value); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func readUint(r io.Reader, numBytes int) (uint64, error) { 54 | var l uint64 55 | for i := 0; i < numBytes; i++ { 56 | l <<= 8 57 | var t uint8 58 | if err := binary.Read(r, binary.BigEndian, &t); err != nil { 59 | return 0, err 60 | } 61 | l |= uint64(t) 62 | } 63 | return l, nil 64 | } 65 | 66 | // Reads a variable length array of bytes from |r|. |numLenBytes| specifies the 67 | // number of (BigEndian) prefix-bytes which contain the length of the actual 68 | // array data bytes that follow. 69 | // Allocates an array to hold the contents and returns a slice view into it if 70 | // the read was successful, or an error otherwise. 71 | func readVarBytes(r io.Reader, numLenBytes int) ([]byte, error) { 72 | switch { 73 | case numLenBytes > 8: 74 | return nil, fmt.Errorf("numLenBytes too large (%d)", numLenBytes) 75 | case numLenBytes == 0: 76 | return nil, errors.New("numLenBytes should be > 0") 77 | } 78 | l, err := readUint(r, numLenBytes) 79 | if err != nil { 80 | return nil, err 81 | } 82 | data := make([]byte, l) 83 | if n, err := io.ReadFull(r, data); err != nil { 84 | if err == io.EOF || err == io.ErrUnexpectedEOF { 85 | return nil, fmt.Errorf("short read: expected %d but got %d", l, n) 86 | } 87 | return nil, err 88 | } 89 | return data, nil 90 | } 91 | 92 | // Reads a list of ASN1Cert types from |r| 93 | func readASN1CertList(r io.Reader, totalLenBytes int, elementLenBytes int) ([]ASN1Cert, error) { 94 | listBytes, err := readVarBytes(r, totalLenBytes) 95 | if err != nil { 96 | return []ASN1Cert{}, err 97 | } 98 | list := list.New() 99 | listReader := bytes.NewReader(listBytes) 100 | var entry []byte 101 | for err == nil { 102 | entry, err = readVarBytes(listReader, elementLenBytes) 103 | if err != nil { 104 | if err != io.EOF { 105 | return []ASN1Cert{}, err 106 | } 107 | } else { 108 | list.PushBack(entry) 109 | } 110 | } 111 | ret := make([]ASN1Cert, list.Len()) 112 | i := 0 113 | for e := list.Front(); e != nil; e = e.Next() { 114 | ret[i] = e.Value.([]byte) 115 | i++ 116 | } 117 | return ret, nil 118 | } 119 | 120 | // ReadTimestampedEntryInto parses the byte-stream representation of a 121 | // TimestampedEntry from |r| and populates the struct |t| with the data. See 122 | // RFC section 3.4 for details on the format. 123 | // Returns a non-nil error if there was a problem. 124 | func ReadTimestampedEntryInto(r io.Reader, t *TimestampedEntry) error { 125 | var err error 126 | if err = binary.Read(r, binary.BigEndian, &t.Timestamp); err != nil { 127 | return err 128 | } 129 | if err = binary.Read(r, binary.BigEndian, &t.EntryType); err != nil { 130 | return err 131 | } 132 | switch t.EntryType { 133 | case X509LogEntryType: 134 | if t.X509Entry, err = readVarBytes(r, CertificateLengthBytes); err != nil { 135 | return err 136 | } 137 | case PrecertLogEntryType: 138 | if err := binary.Read(r, binary.BigEndian, &t.PrecertEntry.IssuerKeyHash); err != nil { 139 | return err 140 | } 141 | if t.PrecertEntry.TBSCertificate, err = readVarBytes(r, PreCertificateLengthBytes); err != nil { 142 | return err 143 | } 144 | default: 145 | return fmt.Errorf("unknown EntryType: %d", t.EntryType) 146 | } 147 | t.Extensions, err = readVarBytes(r, ExtensionsLengthBytes) 148 | return nil 149 | } 150 | 151 | // ReadMerkleTreeLeaf parses the byte-stream representation of a MerkleTreeLeaf 152 | // and returns a pointer to a new MerkleTreeLeaf structure containing the 153 | // parsed data. 154 | // See RFC section 3.4 for details on the format. 155 | // Returns a pointer to a new MerkleTreeLeaf or non-nil error if there was a 156 | // problem 157 | func ReadMerkleTreeLeaf(r io.Reader) (*MerkleTreeLeaf, error) { 158 | var m MerkleTreeLeaf 159 | if err := binary.Read(r, binary.BigEndian, &m.Version); err != nil { 160 | return nil, err 161 | } 162 | if m.Version != V1 { 163 | return nil, fmt.Errorf("unknown Version %d", m.Version) 164 | } 165 | if err := binary.Read(r, binary.BigEndian, &m.LeafType); err != nil { 166 | return nil, err 167 | } 168 | if m.LeafType != TimestampedEntryLeafType { 169 | return nil, fmt.Errorf("unknown LeafType %d", m.LeafType) 170 | } 171 | if err := ReadTimestampedEntryInto(r, &m.TimestampedEntry); err != nil { 172 | return nil, err 173 | } 174 | return &m, nil 175 | } 176 | 177 | // UnmarshalX509ChainArray unmarshalls the contents of the "chain:" entry in a 178 | // GetEntries response in the case where the entry refers to an X509 leaf. 179 | func UnmarshalX509ChainArray(b []byte) ([]ASN1Cert, error) { 180 | return readASN1CertList(bytes.NewReader(b), CertificateChainLengthBytes, CertificateLengthBytes) 181 | } 182 | 183 | // UnmarshalPrecertChainArray unmarshalls the contents of the "chain:" entry in 184 | // a GetEntries response in the case where the entry refers to a Precertificate 185 | // leaf. 186 | func UnmarshalPrecertChainArray(b []byte) ([]ASN1Cert, error) { 187 | var chain []ASN1Cert 188 | 189 | reader := bytes.NewReader(b) 190 | // read the pre-cert entry: 191 | precert, err := readVarBytes(reader, CertificateLengthBytes) 192 | if err != nil { 193 | return chain, err 194 | } 195 | chain = append(chain, precert) 196 | // and then read and return the chain up to the root: 197 | remainingChain, err := readASN1CertList(reader, CertificateChainLengthBytes, CertificateLengthBytes) 198 | if err != nil { 199 | return chain, err 200 | } 201 | chain = append(chain, remainingChain...) 202 | return chain, nil 203 | } 204 | 205 | // UnmarshalDigitallySigned reconstructs a DigitallySigned structure from a Reader 206 | func UnmarshalDigitallySigned(r io.Reader) (*DigitallySigned, error) { 207 | var h byte 208 | if err := binary.Read(r, binary.BigEndian, &h); err != nil { 209 | return nil, fmt.Errorf("failed to read HashAlgorithm: %v", err) 210 | } 211 | 212 | var s byte 213 | if err := binary.Read(r, binary.BigEndian, &s); err != nil { 214 | return nil, fmt.Errorf("failed to read SignatureAlgorithm: %v", err) 215 | } 216 | 217 | sig, err := readVarBytes(r, SignatureLengthBytes) 218 | if err != nil { 219 | return nil, fmt.Errorf("failed to read Signature bytes: %v", err) 220 | } 221 | 222 | return &DigitallySigned{ 223 | HashAlgorithm: HashAlgorithm(h), 224 | SignatureAlgorithm: SignatureAlgorithm(s), 225 | Signature: sig, 226 | }, nil 227 | } 228 | 229 | // MarshalDigitallySigned marshalls a DigitallySigned structure into a byte array 230 | func MarshalDigitallySigned(ds DigitallySigned) ([]byte, error) { 231 | var b bytes.Buffer 232 | if err := b.WriteByte(byte(ds.HashAlgorithm)); err != nil { 233 | return nil, fmt.Errorf("failed to write HashAlgorithm: %v", err) 234 | } 235 | if err := b.WriteByte(byte(ds.SignatureAlgorithm)); err != nil { 236 | return nil, fmt.Errorf("failed to write SignatureAlgorithm: %v", err) 237 | } 238 | if err := writeVarBytes(&b, ds.Signature, SignatureLengthBytes); err != nil { 239 | return nil, fmt.Errorf("failed to write HashAlgorithm: %v", err) 240 | } 241 | return b.Bytes(), nil 242 | } 243 | 244 | func checkCertificateFormat(cert ASN1Cert) error { 245 | if len(cert) == 0 { 246 | return errors.New("certificate is zero length") 247 | } 248 | if len(cert) > MaxCertificateLength { 249 | return errors.New("certificate too large") 250 | } 251 | return nil 252 | } 253 | 254 | func checkExtensionsFormat(ext CTExtensions) error { 255 | if len(ext) > MaxExtensionsLength { 256 | return errors.New("extensions too large") 257 | } 258 | return nil 259 | } 260 | 261 | func serializeV1CertSCTSignatureInput(timestamp uint64, cert ASN1Cert, ext CTExtensions) ([]byte, error) { 262 | if err := checkCertificateFormat(cert); err != nil { 263 | return nil, err 264 | } 265 | if err := checkExtensionsFormat(ext); err != nil { 266 | return nil, err 267 | } 268 | var buf bytes.Buffer 269 | if err := binary.Write(&buf, binary.BigEndian, V1); err != nil { 270 | return nil, err 271 | } 272 | if err := binary.Write(&buf, binary.BigEndian, CertificateTimestampSignatureType); err != nil { 273 | return nil, err 274 | } 275 | if err := binary.Write(&buf, binary.BigEndian, timestamp); err != nil { 276 | return nil, err 277 | } 278 | if err := binary.Write(&buf, binary.BigEndian, X509LogEntryType); err != nil { 279 | return nil, err 280 | } 281 | if err := writeVarBytes(&buf, cert, CertificateLengthBytes); err != nil { 282 | return nil, err 283 | } 284 | if err := writeVarBytes(&buf, ext, ExtensionsLengthBytes); err != nil { 285 | return nil, err 286 | } 287 | return buf.Bytes(), nil 288 | } 289 | 290 | func serializeV1PrecertSCTSignatureInput(timestamp uint64, issuerKeyHash [issuerKeyHashLength]byte, tbs []byte, ext CTExtensions) ([]byte, error) { 291 | if err := checkCertificateFormat(tbs); err != nil { 292 | return nil, err 293 | } 294 | if err := checkExtensionsFormat(ext); err != nil { 295 | return nil, err 296 | } 297 | var buf bytes.Buffer 298 | if err := binary.Write(&buf, binary.BigEndian, V1); err != nil { 299 | return nil, err 300 | } 301 | if err := binary.Write(&buf, binary.BigEndian, CertificateTimestampSignatureType); err != nil { 302 | return nil, err 303 | } 304 | if err := binary.Write(&buf, binary.BigEndian, timestamp); err != nil { 305 | return nil, err 306 | } 307 | if err := binary.Write(&buf, binary.BigEndian, PrecertLogEntryType); err != nil { 308 | return nil, err 309 | } 310 | if _, err := buf.Write(issuerKeyHash[:]); err != nil { 311 | return nil, err 312 | } 313 | if err := writeVarBytes(&buf, tbs, CertificateLengthBytes); err != nil { 314 | return nil, err 315 | } 316 | if err := writeVarBytes(&buf, ext, ExtensionsLengthBytes); err != nil { 317 | return nil, err 318 | } 319 | return buf.Bytes(), nil 320 | } 321 | 322 | func serializeV1SCTSignatureInput(sct SignedCertificateTimestamp, entry LogEntry) ([]byte, error) { 323 | if sct.SCTVersion != V1 { 324 | return nil, fmt.Errorf("unsupported SCT version, expected V1, but got %s", sct.SCTVersion) 325 | } 326 | if entry.Leaf.LeafType != TimestampedEntryLeafType { 327 | return nil, fmt.Errorf("Unsupported leaf type %s", entry.Leaf.LeafType) 328 | } 329 | switch entry.Leaf.TimestampedEntry.EntryType { 330 | case X509LogEntryType: 331 | return serializeV1CertSCTSignatureInput(sct.Timestamp, entry.Leaf.TimestampedEntry.X509Entry, entry.Leaf.TimestampedEntry.Extensions) 332 | case PrecertLogEntryType: 333 | return serializeV1PrecertSCTSignatureInput(sct.Timestamp, entry.Leaf.TimestampedEntry.PrecertEntry.IssuerKeyHash, 334 | entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate, 335 | entry.Leaf.TimestampedEntry.Extensions) 336 | default: 337 | return nil, fmt.Errorf("unknown TimestampedEntryLeafType %s", entry.Leaf.TimestampedEntry.EntryType) 338 | } 339 | } 340 | 341 | // SerializeSCTSignatureInput serializes the passed in sct and log entry into 342 | // the correct format for signing. 343 | func SerializeSCTSignatureInput(sct SignedCertificateTimestamp, entry LogEntry) ([]byte, error) { 344 | switch sct.SCTVersion { 345 | case V1: 346 | return serializeV1SCTSignatureInput(sct, entry) 347 | default: 348 | return nil, fmt.Errorf("unknown SCT version %d", sct.SCTVersion) 349 | } 350 | } 351 | 352 | func serializeV1SCT(sct SignedCertificateTimestamp) ([]byte, error) { 353 | if err := checkExtensionsFormat(sct.Extensions); err != nil { 354 | return nil, err 355 | } 356 | var buf bytes.Buffer 357 | if err := binary.Write(&buf, binary.BigEndian, V1); err != nil { 358 | return nil, err 359 | } 360 | if err := binary.Write(&buf, binary.BigEndian, sct.LogID); err != nil { 361 | return nil, err 362 | } 363 | if err := binary.Write(&buf, binary.BigEndian, sct.Timestamp); err != nil { 364 | return nil, err 365 | } 366 | if err := writeVarBytes(&buf, sct.Extensions, ExtensionsLengthBytes); err != nil { 367 | return nil, err 368 | } 369 | sig, err := MarshalDigitallySigned(sct.Signature) 370 | if err != nil { 371 | return nil, err 372 | } 373 | if err := binary.Write(&buf, binary.BigEndian, sig); err != nil { 374 | return nil, err 375 | } 376 | return buf.Bytes(), nil 377 | } 378 | 379 | // SerializeSCT serializes the passed in sct into the format specified 380 | // by RFC6962 section 3.2 381 | func SerializeSCT(sct SignedCertificateTimestamp) ([]byte, error) { 382 | switch sct.SCTVersion { 383 | case V1: 384 | return serializeV1SCT(sct) 385 | default: 386 | return nil, fmt.Errorf("unknown SCT version %d", sct.SCTVersion) 387 | } 388 | } 389 | 390 | func deserializeSCTV1(r io.Reader, sct *SignedCertificateTimestamp) error { 391 | if err := binary.Read(r, binary.BigEndian, &sct.LogID); err != nil { 392 | return err 393 | } 394 | if err := binary.Read(r, binary.BigEndian, &sct.Timestamp); err != nil { 395 | return err 396 | } 397 | ext, err := readVarBytes(r, ExtensionsLengthBytes) 398 | if err != nil { 399 | return err 400 | } 401 | sct.Extensions = ext 402 | ds, err := UnmarshalDigitallySigned(r) 403 | if err != nil { 404 | return err 405 | } 406 | sct.Signature = *ds 407 | return nil 408 | } 409 | 410 | func DeserializeSCT(r io.Reader) (*SignedCertificateTimestamp, error) { 411 | var sct SignedCertificateTimestamp 412 | if err := binary.Read(r, binary.BigEndian, &sct.SCTVersion); err != nil { 413 | return nil, err 414 | } 415 | switch sct.SCTVersion { 416 | case V1: 417 | return &sct, deserializeSCTV1(r, &sct) 418 | default: 419 | return nil, fmt.Errorf("unknown SCT version %d", sct.SCTVersion) 420 | } 421 | } 422 | 423 | func serializeV1STHSignatureInput(sth SignedTreeHead) ([]byte, error) { 424 | if sth.Version != V1 { 425 | return nil, fmt.Errorf("invalid STH version %d", sth.Version) 426 | } 427 | if sth.TreeSize < 0 { 428 | return nil, fmt.Errorf("invalid tree size %d", sth.TreeSize) 429 | } 430 | if len(sth.SHA256RootHash) != crypto.SHA256.Size() { 431 | return nil, fmt.Errorf("invalid TreeHash length, got %d expected %d", len(sth.SHA256RootHash), crypto.SHA256.Size()) 432 | } 433 | 434 | var buf bytes.Buffer 435 | if err := binary.Write(&buf, binary.BigEndian, V1); err != nil { 436 | return nil, err 437 | } 438 | if err := binary.Write(&buf, binary.BigEndian, TreeHashSignatureType); err != nil { 439 | return nil, err 440 | } 441 | if err := binary.Write(&buf, binary.BigEndian, sth.Timestamp); err != nil { 442 | return nil, err 443 | } 444 | if err := binary.Write(&buf, binary.BigEndian, sth.TreeSize); err != nil { 445 | return nil, err 446 | } 447 | if err := binary.Write(&buf, binary.BigEndian, sth.SHA256RootHash); err != nil { 448 | return nil, err 449 | } 450 | return buf.Bytes(), nil 451 | } 452 | 453 | // SerializeSTHSignatureInput serializes the passed in sth into the correct 454 | // format for signing. 455 | func SerializeSTHSignatureInput(sth SignedTreeHead) ([]byte, error) { 456 | switch sth.Version { 457 | case V1: 458 | return serializeV1STHSignatureInput(sth) 459 | default: 460 | return nil, fmt.Errorf("unsupported STH version %d", sth.Version) 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------