├── .gitignore ├── README.md ├── go.mod ├── storage ├── mysql │ ├── root_store.sql │ ├── root_store.go │ ├── testdb │ │ └── testdb.go │ └── root_store_test.go ├── testonly │ └── roots.go ├── print │ └── print.go └── storage.go ├── .golangci.yaml ├── incident ├── mysql │ ├── incident.sql │ ├── incident.go │ └── incident_test.go ├── testonly │ └── fake.go └── incident.go ├── errors └── errors.go ├── CONTRIBUTING.md ├── scripts ├── check_license.sh ├── presubmit.sh └── resetmondb.sh ├── .travis.yml ├── testonly └── testonly.go ├── certgen ├── testdata │ ├── test_ca.key │ └── test_ca.pem ├── certgen.go └── certgen_test.go ├── rootsanalyzer ├── rootset_id.go ├── rootset_id_test.go ├── rootsanalyzer.go └── rootsanalyzer_test.go ├── apicall ├── api_call_test.go └── api_call.go ├── interval ├── interval.go └── interval_test.go ├── ctlog ├── ctlog_test.go └── ctlog.go ├── rootsgetter └── rootsgetter.go ├── client ├── errors.go └── client.go ├── collector ├── collector.go └── datacollector │ └── datacollector.go ├── sthgetter ├── sthgetter.go └── sthgetter_test.go ├── certsubmitter ├── certsubmitter_test.go └── certsubmitter.go ├── LICENSE └── testdata └── testdata.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.swp 3 | *.exe 4 | /coverage.txt 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certificate Transparency Log Monitor - Discontinued 2 | 3 | This repository is currently archived and not being updated since Monologue is being developed internally. 4 | 5 | Once Monologue development comes back to GitHub, it will be under the https://github.com/GoogleChrome organization. 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/monologue 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 8 | github.com/google/certificate-transparency-go v1.0.22-0.20200117142630-6f5c98d767a3 9 | github.com/google/go-cmp v0.4.0 10 | github.com/google/trillian v1.3.3 11 | github.com/kylelemons/godebug v1.1.0 12 | ) 13 | -------------------------------------------------------------------------------- /storage/mysql/root_store.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS Roots( 2 | ID BINARY(32), 3 | DER MEDIUMBLOB, 4 | PRIMARY KEY(ID) 5 | ); 6 | 7 | CREATE TABLE IF NOT EXISTS RootSets( 8 | RootSetID Binary(32), 9 | RootID Binary(32), 10 | PRIMARY KEY(RootSetID, RootID) 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS RootSetObservations( 14 | LogName VARCHAR(128), 15 | RootSetID Binary(32), 16 | ReceivedAt DATETIME, 17 | PRIMARY KEY(LogName, RootSetID, ReceivedAt) 18 | ); 19 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 90s 3 | 4 | linters-settings: 5 | gocyclo: 6 | min-complexity: 25 7 | depguard: 8 | list-type: blacklist 9 | packages: 10 | - golang.org/x/net/context 11 | - github.com/gogo/protobuf/proto 12 | 13 | linters: 14 | disable-all: true 15 | enable: 16 | - gocyclo 17 | - gofmt 18 | - goimports 19 | - golint 20 | - megacheck 21 | - misspell 22 | - govet 23 | - depguard 24 | - deadcode 25 | - ineffassign 26 | - varcheck 27 | 28 | issues: 29 | exclude-rules: 30 | - path: _test\.go 31 | linters: 32 | - gocyclo 33 | -------------------------------------------------------------------------------- /incident/mysql/incident.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS Incidents( 2 | Id SERIAL, 3 | Timestamp DATETIME, 4 | Source VARCHAR(128), 5 | BaseURL VARCHAR(512), 6 | Summary VARCHAR(2048), 7 | IsViolation BOOLEAN, 8 | FullURL VARCHAR(512), 9 | Details TEXT, 10 | -- OwningId indicates that an incident is considered a sub-incident of the owning incident. 11 | OwningId BIGINT UNSIGNED NULL, 12 | PRIMARY KEY(Id), 13 | FOREIGN KEY(OwningId) REFERENCES Incidents(Id) 14 | ); 15 | 16 | CREATE INDEX TimestampIndex ON Incidents(Timestamp); 17 | CREATE INDEX SourceIndex ON Incidents(Source); 18 | CREATE INDEX BaseURLIndex ON Incidents(BaseURL); 19 | # Indexing the whole Summary field exceeds the 3K key limit on multi-byte 20 | # character sets. 21 | CREATE INDEX SummaryIndex ON Incidents(Summary(512)); 22 | CREATE INDEX FullURLIndex ON Incidents(FullURL); 23 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package errors contains error types used by multiple Monologue packages. 16 | package errors 17 | 18 | // SignatureVerificationError indicates that a signature did not validate. 19 | type SignatureVerificationError struct { 20 | Err error 21 | } 22 | 23 | func (e *SignatureVerificationError) Error() string { 24 | return e.Err.Error() 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /scripts/check_license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Checks that source files (.go and .proto) have the Apache License header. 4 | # Automatically skips generated files. 5 | set -eu 6 | 7 | check_license() { 8 | local path="$1" 9 | 10 | if head -1 "$path" | grep -iq 'generated by'; then 11 | return 0 12 | fi 13 | 14 | # Look for "Apache License" on the file header 15 | if ! head -10 "$path" | grep -q 'Apache License'; then 16 | # Format: $path:$line:$message 17 | echo "$path:10:license header not found" 18 | return 1 19 | fi 20 | } 21 | 22 | main() { 23 | if [[ $# -lt 1 ]]; then 24 | echo "Usage: $0 " 25 | exit 1 26 | fi 27 | 28 | local code=0 29 | while [[ $# -gt 0 ]]; do 30 | local path="$1" 31 | if [[ -d "$path" ]]; then 32 | for f in "$path"/*.{go,proto}; do 33 | if [[ ! -f "$f" ]]; then 34 | continue # Empty glob 35 | fi 36 | check_license "$f" || code=1 37 | done 38 | else 39 | check_license "$path" || code=1 40 | fi 41 | shift 42 | done 43 | exit $code 44 | } 45 | 46 | main "$@" 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | version: ~> 1.0 2 | language: go 3 | go_import_path: github.com/google/monologue 4 | go: 5 | - 1.13 6 | 7 | services: 8 | - mysql 9 | 10 | cache: 11 | directories: 12 | - $GOPATH/pkg/mod 13 | 14 | env: 15 | global: 16 | - GO111MODULE=on 17 | 18 | jobs: 19 | include: 20 | - name: go mod tidy 21 | install: skip 22 | before_script: go mod tidy -v 23 | script: git diff --exit-code -- go.mod go.sum 24 | - name: test with coverage 25 | env: 26 | - WITH_COVERAGE=true 27 | after_success: 28 | # Upload coverage info as per https://docs.codecov.io/docs/about-the-codecov-bash-uploader 29 | - bash <(curl -s https://codecov.io/bash) 30 | - name: test with race detection 31 | env: 32 | - GOFLAGS='-race' 33 | 34 | install: 35 | # Install golangci-lint as per https://github.com/golangci/golangci-lint#install 36 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0 37 | 38 | script: 39 | - ./scripts/presubmit.sh ${WITH_COVERAGE:+--coverage} 40 | 41 | -------------------------------------------------------------------------------- /testonly/testonly.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testonly contains resources used during testing by multiple of the 16 | // monologue packages. 17 | package testonly 18 | 19 | import ( 20 | "encoding/base64" 21 | "fmt" 22 | 23 | "github.com/google/certificate-transparency-go/x509" 24 | "github.com/google/certificate-transparency-go/x509util" 25 | ) 26 | 27 | func MustB64Decode(b64 string) []byte { 28 | b, err := base64.StdEncoding.DecodeString(b64) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return b 33 | } 34 | 35 | func MustCreateChain(pemChain []string) []*x509.Certificate { 36 | var chain []*x509.Certificate 37 | for _, pc := range pemChain { 38 | cert, err := x509util.CertificateFromPEM([]byte(pc)) 39 | if err != nil { 40 | panic(fmt.Errorf("unable to parse from PEM to *x509.Certificate: %s", err)) 41 | } 42 | chain = append(chain, cert) 43 | } 44 | return chain 45 | } 46 | -------------------------------------------------------------------------------- /storage/testonly/roots.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testonly contains fakes for use in tests that interact with storage. 16 | package testonly 17 | 18 | import ( 19 | "context" 20 | 21 | "github.com/google/certificate-transparency-go/x509" 22 | "github.com/google/monologue/ctlog" 23 | "github.com/google/monologue/storage" 24 | ) 25 | 26 | // FakeRootsReader returns preset values in order to fulfill the storage.RootsReader interface. 27 | type FakeRootsReader struct { 28 | // RootSetChan is returned by WatchRoots. 29 | RootSetChan chan storage.RootSetID 30 | // RootCerts maps RootSetIDs to sets of certificates. It is used by ReadRoots. 31 | RootSetCerts map[storage.RootSetID][]*x509.Certificate 32 | } 33 | 34 | // WatchRoots returns FakeRootsReader.RootSetChan. 35 | func (f *FakeRootsReader) WatchRoots(ctx context.Context, l *ctlog.Log) (<-chan storage.RootSetID, error) { 36 | return f.RootSetChan, nil 37 | } 38 | 39 | // ReadRoots returns FakeRootsReader.RootSetCerts[rootSet]. 40 | func (f *FakeRootsReader) ReadRoots(ctx context.Context, rootSet storage.RootSetID) ([]*x509.Certificate, error) { 41 | return f.RootSetCerts[rootSet], nil 42 | } 43 | -------------------------------------------------------------------------------- /certgen/testdata/test_ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAz0uQLcHrCa3sPTySQC9ypxEQdkYBhJMACItPTVU1G7EWXZWv 3 | 2Yfh9M7ZLtF2jZQA7gicP4s494ZEf/Lz8XcoqUpzVnqxqK6QoYaKhMkIspXnoinD 4 | fydK3523FGYEBs8JG8TGdrsVECFPiByjgg6MERE0k4Pma0hvq+wiuzByYa2LgpQi 5 | CHi46FT0D62zhBnnz2yHgytWz3JphlqPilUI7y10KYxK/3Wlx3ibc/E8ZNz8ce51 6 | 9FkZ1C9LIR3wqzbJ2416wJdXU9BFlGoU8qkGuMu599naRvQZEEJKmD0I+BkrS4U9 7 | ZN338trTBNViNNOX3CHAN+AUARO2uWODn9VOMQIDAQABAoIBABIQZKUIJAI47MA9 8 | WgVL3TLf0s5rtVAnOzaDZUq45SLfoLJRC/zp0W/y4owo5qrZvecASxVifXlccvWv 9 | Z0CggKOYAEsF8Bth8uqQfduqZR6i34eLGiUneGfaJ40YgrtRKVsJH75S+TfpnzOe 10 | Q0VfHSeTl0BPmzG4IzsZHcGwLBeEzs3Y2SVDnD/EdxzKeJE+st/O6qaR7RtXipt3 11 | XiM6LbTMpscHYlozOjOldoHzquLiVlMLgkawCqKZdISyULUnPXLKEIr7cwOShJxA 12 | p8N0C5ECBaIY0hsCNtmlmzA40fgdyi7VHzCuwk/KDUMc5U4DiBIbXESsXFiU9P6W 13 | RpqpO00CgYEA9JOyGsXzqBTLdrf/hfeIGUA87kqaRTM3MCbzBjj5lAx57FuMlbMW 14 | l3VOtG24rLj2tSR4rVX1F1sg69FH4RU0P/TbjxZzfq1l203zRVn/ovPV0VxIBa8L 15 | 4Fb+7mAnopiePmaQJxX3QhrzKtaN/mtxkPFeBgsHgfO8XoAfeKBloKsCgYEA2Poa 16 | 3iZKXaQI2FPMmTu8nDJ+3+8pONIgsITsu0WZRjkQhKLp/tTLhCxvdxNkUYSqvZSm 17 | oR5PJrVMRXxEFw0GUmfQp/nAPlvYuSJSl0pQlsda09kYV/DIRGJ1wHUL1+tkaM9K 18 | +OlIkLP7sRf+ThhUFRkj0R/c9mtqunUYmSSmJJMCgYEAv4F2kCg35YCC4G6qkceE 19 | teDq1NtteInlyLp2yISMSDSMs1nr9rvIEMg13GmdlQEvjB8GrxMDWkpsuLmiQshL 20 | pVihIqU9wg8cFmAvADq8RGgRIGHVsz2hWwRDkXuRDWvbXJkKftBgT06y4PQnMvQG 21 | dKvKJ4kzU7n54TUJb3aInQkCgYB3TLztHFyTNBfw6dJ7Bk9kCpecBOHDbjWvGMcZ 22 | COGHrOSaKQFDrjjgSZZYAH+OGsXOSyd7OzEF9XUkV3kQu4aYVKBzW20KXHHDCU4k 23 | BSZeYOpdxOYPpEJY51IbKcADTlf9EM0GIy5U8cEX1DttMQ86MoJw/3X8v38FWlDW 24 | E0KOpQKBgFKB2tsfjTOsy2W3JEMxYVOKH/TFwhuYrQ2LR1jPfGegev9w24hxE0ke 25 | XKR/IZ+p8Z5l1HMRa/9aJhB38BXECgSS8dwUYRndUZmHtvQ4ZDw+ddsDga3X7BSy 26 | w/mH8nujTFL1x2LYcKSiOGCOsins70KeBvfvfmiBWur2PEK+EKTx 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootsanalyzer/rootset_id.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rootsanalyzer 16 | 17 | import ( 18 | "crypto/sha256" 19 | "fmt" 20 | "sort" 21 | "strings" 22 | 23 | "github.com/google/certificate-transparency-go/x509" 24 | "github.com/google/monologue/storage" 25 | ) 26 | 27 | // GenerateCertID returns SHA-256 of certificate's DER representation. 28 | func GenerateCertID(root *x509.Certificate) ([32]byte, error) { 29 | if root == nil { 30 | return [32]byte{}, fmt.Errorf("unable to generate root-ID for nil") 31 | } 32 | return sha256.Sum256(root.Raw), nil 33 | } 34 | 35 | // GenerateSetID returns ID for a set of root-certificates. 36 | // Order or multi-entries of a same certificate do not influence the ID. 37 | func GenerateSetID(roots []*x509.Certificate) (storage.RootSetID, error) { 38 | var dedupRootIDs []string 39 | rootIDSet := make(map[[32]byte]bool) 40 | for _, r := range roots { 41 | certID, err := GenerateCertID(r) 42 | if err != nil { 43 | return "", fmt.Errorf("unable to generate ID for certificate %q: %s", r.Subject, err) 44 | } 45 | if !(rootIDSet[certID]) { 46 | rootIDSet[certID] = true 47 | dedupRootIDs = append(dedupRootIDs, string(certID[:])) 48 | } 49 | } 50 | 51 | // sort deduplicated roots IDs 52 | sort.Strings(dedupRootIDs) 53 | 54 | // concatenate roots IDs 55 | concat := strings.Join(dedupRootIDs[:], "") 56 | concatHash := sha256.Sum256([]byte(concat)) 57 | return storage.RootSetID(string(concatHash[:])), nil 58 | } 59 | -------------------------------------------------------------------------------- /storage/print/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package print provides a concrete implementation of the storage interfaces 16 | // needed by the CT monitor, which simply prints everything that is passed to it 17 | // to be 'stored'. 18 | // 19 | // This package is only intended to be a handy tool used during development, and 20 | // will likely be deleted in the not-so-distant future. Don't rely on it. 21 | package print 22 | 23 | import ( 24 | "context" 25 | "time" 26 | 27 | "github.com/golang/glog" 28 | ct "github.com/google/certificate-transparency-go" 29 | "github.com/google/certificate-transparency-go/x509" 30 | "github.com/google/monologue/apicall" 31 | "github.com/google/monologue/ctlog" 32 | ) 33 | 34 | // Storage implements the storage interfaces needed by the CT monitor. 35 | type Storage struct{} 36 | 37 | // WriteAPICall simply prints the API Call passed to it. 38 | func (s *Storage) WriteAPICall(ctx context.Context, l *ctlog.Log, apiCall *apicall.APICall) error { 39 | glog.Infof("%s: %s", l.Name, apiCall.String()) 40 | return nil 41 | } 42 | 43 | // WriteSTH simply prints the STH and errors passed to it. 44 | func (s *Storage) WriteSTH(ctx context.Context, l *ctlog.Log, sth *ct.SignedTreeHead, receivedAt time.Time, errs []error) error { 45 | glog.Infof("%s:\n\tSTH: %s\n\tVerification errors: %s", l.Name, sth, errs) 46 | return nil 47 | } 48 | 49 | // WriteRoots simply prints the number of certificates passed to it. 50 | func (s *Storage) WriteRoots(ctx context.Context, l *ctlog.Log, certs []*x509.Certificate, receivedAt time.Time) error { 51 | glog.Infof("%s at %s: %d root certificates", l.Name, receivedAt, len(certs)) 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /apicall/api_call_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apicall 16 | 17 | import ( 18 | "net/http" 19 | "testing" 20 | "time" 21 | 22 | ct "github.com/google/certificate-transparency-go" 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/google/monologue/client" 25 | ) 26 | 27 | func TestNew(t *testing.T) { 28 | pilotGetSTH := "https://ct.googleapis.com/pilot/ct/v1/get-sth" 29 | 30 | tests := []struct { 31 | name string 32 | endpoint ct.APIEndpoint 33 | httpData *client.HTTPData 34 | err error 35 | want *APICall 36 | }{ 37 | { 38 | name: "nil httpData", 39 | endpoint: ct.GetSTHStr, 40 | err: &client.NilResponseError{URL: pilotGetSTH}, 41 | want: &APICall{ 42 | Endpoint: ct.GetSTHStr, 43 | Err: &client.NilResponseError{URL: pilotGetSTH}, 44 | }, 45 | }, 46 | { 47 | name: "no error", 48 | endpoint: ct.GetSTHStr, 49 | httpData: &client.HTTPData{ 50 | Timing: client.Timing{ 51 | Start: time.Date(2018, time.August, 21, 14, 12, 0, 0, time.UTC), 52 | End: time.Date(2018, time.August, 21, 14, 14, 0, 0, time.UTC), 53 | }, 54 | Response: &http.Response{StatusCode: http.StatusOK}, 55 | Body: []byte("some bytes"), 56 | }, 57 | want: &APICall{ 58 | Start: time.Date(2018, time.August, 21, 14, 12, 0, 0, time.UTC), 59 | End: time.Date(2018, time.August, 21, 14, 14, 0, 0, time.UTC), 60 | Endpoint: ct.GetSTHStr, 61 | Response: &http.Response{StatusCode: http.StatusOK}, 62 | Body: []byte("some bytes"), 63 | }, 64 | }, 65 | } 66 | 67 | for _, test := range tests { 68 | t.Run(test.name, func(t *testing.T) { 69 | got := New(test.endpoint, test.httpData, test.err) 70 | if diff := cmp.Diff(got, test.want); diff != "" { 71 | t.Errorf("CreateAPICall(): diff: (-got +want)\n%s", diff) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /incident/testonly/fake.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testonly contains fakes for use in tests that interact with incident reporting. 16 | package testonly 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | ) 22 | 23 | // Report contains all of the information submitted when creating an incident report. 24 | type Report struct { 25 | BaseURL string 26 | Summary string 27 | FullURL string 28 | Details string 29 | } 30 | 31 | // FakeReporter sends incident reports to its Reports channel. 32 | type FakeReporter struct { 33 | Updates chan Report 34 | Violations chan Report 35 | } 36 | 37 | // LogUpdate sends an incident report to the FakeReporter's Updates channel. 38 | func (f *FakeReporter) LogUpdate(ctx context.Context, baseURL, summary, fullURL, details string) { 39 | f.Updates <- Report{ 40 | BaseURL: baseURL, 41 | Summary: summary, 42 | FullURL: fullURL, 43 | Details: details, 44 | } 45 | } 46 | 47 | // LogUpdatef sends an incident report to the FakeReporter's Updates channel, formatting parameters along the way. 48 | func (f *FakeReporter) LogUpdatef(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 49 | f.LogUpdate(ctx, baseURL, summary, fullURL, fmt.Sprintf(detailsFmt, args...)) 50 | } 51 | 52 | // LogViolation sends an incident report to the FakeReporter's Violations channel. 53 | func (f *FakeReporter) LogViolation(ctx context.Context, baseURL, summary, fullURL, details string) { 54 | f.Violations <- Report{ 55 | BaseURL: baseURL, 56 | Summary: summary, 57 | FullURL: fullURL, 58 | Details: details, 59 | } 60 | } 61 | 62 | // LogViolationf sends an incident report to the FakeReporter's Violations channel, formatting parameters along the way. 63 | func (f *FakeReporter) LogViolationf(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 64 | f.LogViolation(ctx, baseURL, summary, fullURL, fmt.Sprintf(detailsFmt, args...)) 65 | } 66 | -------------------------------------------------------------------------------- /apicall/api_call.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package apicall provides types and functions directly relating to the api 16 | // calls made by the monitor to a Log. 17 | package apicall 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | "time" 24 | 25 | ct "github.com/google/certificate-transparency-go" 26 | "github.com/google/monologue/client" 27 | ) 28 | 29 | // maxBodyStringLen is the length beyond which APICall.Body will be truncated when converted to a string by APICall.String(). 30 | const maxBodyStringLen = 1000 31 | 32 | // APICall contains the details of a call to one of the API endpoints of a CT 33 | // Log. 34 | type APICall struct { 35 | Start time.Time 36 | End time.Time 37 | Endpoint ct.APIEndpoint 38 | Response *http.Response 39 | Body []byte 40 | Err error 41 | } 42 | 43 | func (ac APICall) String() string { 44 | var responseBody = string(ac.Body) 45 | 46 | if len(ac.Body) > maxBodyStringLen { 47 | responseBody = fmt.Sprintf("%s [truncated next %d bytes]", ac.Body[:maxBodyStringLen], len(ac.Body)-maxBodyStringLen) 48 | } 49 | 50 | lines := []string{ 51 | "APICall {", 52 | fmt.Sprintf("\tStart: %s", ac.Start), 53 | fmt.Sprintf("\tEnd: %s", ac.End), 54 | fmt.Sprintf("\tEndpoint: %s", ac.Endpoint), 55 | fmt.Sprintf("\tResponse body: %s", responseBody), 56 | fmt.Sprintf("\tResponse: %v", ac.Response), 57 | fmt.Sprintf("\tErr: %v", ac.Err), 58 | "}", 59 | } 60 | return strings.Join(lines, "\n") 61 | } 62 | 63 | // New populates and returns an APICall struct using the given APIendpoint, 64 | // HTTPData and error, all of which should relate to the same single call to a 65 | // CT API endpoint. 66 | func New(ep ct.APIEndpoint, httpData *client.HTTPData, err error) *APICall { 67 | apiCall := &APICall{Endpoint: ep, Err: err} 68 | if httpData != nil { 69 | apiCall.Start = httpData.Timing.Start 70 | apiCall.End = httpData.Timing.End 71 | apiCall.Response = httpData.Response 72 | apiCall.Body = httpData.Body 73 | } 74 | return apiCall 75 | } 76 | -------------------------------------------------------------------------------- /storage/mysql/root_store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package mysql provides a MySQL based implementation of Monologue storage. 16 | package mysql 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/google/certificate-transparency-go/x509" 25 | "github.com/google/monologue/ctlog" 26 | "github.com/google/monologue/rootsanalyzer" 27 | "github.com/google/monologue/storage" 28 | ) 29 | 30 | // RootStore implements storage.RootsWriter interface. 31 | type rootStore struct { 32 | rootDB *sql.DB 33 | } 34 | 35 | // NewRootStore builds an RootStore instance that records root certificates in a MySQL database. 36 | func NewRootStore(ctx context.Context, db *sql.DB) storage.RootsWriter { 37 | return &rootStore{rootDB: db} 38 | } 39 | 40 | func (rs *rootStore) WriteRoots(ctx context.Context, l *ctlog.Log, roots []*x509.Certificate, receivedAt time.Time) error { 41 | rootSetID, err := rootsanalyzer.GenerateSetID(roots) 42 | if err != nil { 43 | return fmt.Errorf("unable to generate RootSetID: %s", err) 44 | } 45 | rootSetIDBytes := []byte(rootSetID) 46 | 47 | for _, r := range roots { 48 | rootID, err := rootsanalyzer.GenerateCertID(r) 49 | if err != nil { 50 | return fmt.Errorf("WriteRoots: %s", err) 51 | } 52 | 53 | if _, err = rs.rootDB.ExecContext(ctx, "INSERT INTO Roots(ID, DER) VALUES (?, ?) ON DUPLICATE KEY UPDATE ID=ID;", rootID[:], r.Raw); err != nil { 54 | return fmt.Errorf("WriteRoots: %s", err) 55 | } 56 | 57 | if _, err = rs.rootDB.ExecContext(ctx, "INSERT INTO RootSets(RootSetID, RootID) VALUES (?, ?) ON DUPLICATE KEY UPDATE RootSetID=RootSetID;", rootSetIDBytes, rootID[:]); err != nil { 58 | return fmt.Errorf("WriteRoots: %s", err) 59 | } 60 | } 61 | 62 | if _, err = rs.rootDB.ExecContext(ctx, "INSERT INTO RootSetObservations(LogName, RootSetID, ReceivedAt) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE RootSetID=RootSetID;", l.Name, rootSetIDBytes, receivedAt); err != nil { 63 | return fmt.Errorf("WriteRoots: %s", err) 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /interval/interval.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package interval provides a struct representing a time interval. 16 | package interval 17 | 18 | import ( 19 | "math/rand" 20 | "time" 21 | ) 22 | 23 | // Interval represents the interval [Start, End). 24 | type Interval struct { 25 | Start, End time.Time 26 | } 27 | 28 | // RandomSecond returns a random second-precision time that falls within the 29 | // Interval. 30 | // 31 | // If there is no second-precision time that falls in the Interval, the zero 32 | // value time.Time will be returned. 33 | // For example: 34 | // Start = 2019-03-25 00:00:00.1 +0000 UTC 35 | // End = 2019-03-25 00:00:00.9 +0000 UTC 36 | // 37 | // If Interval.End == Interval.Start, the zero value time.Time will be returned. 38 | // This is because an Interval represents the interval 39 | // [Interval.Start, Interval.End), so if Interval.Start and Interval.End are 40 | // equal, the interval is invalid. 41 | // 42 | // If Interval.End < Interval.Start, the zero value time.Time will be returned. 43 | // 44 | // If the Interval is nil, the zero value time.Time will be returned. 45 | func (i *Interval) RandomSecond() time.Time { 46 | if i == nil { 47 | return time.Time{} 48 | } 49 | 50 | if !i.Start.Before(i.End) { 51 | return time.Time{} 52 | } 53 | 54 | // Set start to be the first second boundary >= i.Start. 55 | start := i.Start.Unix() 56 | if i.Start.Nanosecond() != 0 { 57 | start++ 58 | } 59 | 60 | // Set end to be the first second boundary >= i.End. 61 | end := i.End.Unix() 62 | if i.End.Nanosecond() != 0 { 63 | end++ 64 | } 65 | 66 | delta := end - start 67 | 68 | // If delta == 0 there are no second boundaries in [i.Start, i.End). Return 69 | // the zero time. 70 | // 71 | // For example: 72 | // Start = 2019-03-25 00:00:00.1 +0000 UTC 73 | // End = 2019-03-25 00:00:00.9 +0000 UTC 74 | if delta == 0 { 75 | return time.Time{} 76 | } 77 | 78 | // Otherwise, there is at least one second boundary in [i.Start, i.End). 79 | // Randomly choose one of them, and return it. 80 | rand.Seed(time.Now().UnixNano()) 81 | return time.Unix(start+rand.Int63n(delta), 0) 82 | } 83 | -------------------------------------------------------------------------------- /ctlog/ctlog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ctlog 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/google/monologue/interval" 22 | "github.com/kylelemons/godebug/pretty" 23 | ) 24 | 25 | func TestNewTemporalInterval(t *testing.T) { 26 | tests := []struct { 27 | desc string 28 | interval *interval.Interval 29 | want *interval.Interval 30 | }{ 31 | { 32 | desc: "nil interval", 33 | interval: nil, 34 | want: nil, 35 | }, 36 | { 37 | desc: "strip nanos", 38 | interval: &interval.Interval{ 39 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 1, time.UTC), 40 | End: time.Date(2019, time.March, 25, 23, 59, 59, 999999999, time.UTC), 41 | }, 42 | want: &interval.Interval{ 43 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 44 | End: time.Date(2019, time.March, 25, 23, 59, 59, 0, time.UTC), 45 | }, 46 | }, 47 | { 48 | desc: "no nanos", 49 | interval: &interval.Interval{ 50 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 51 | End: time.Date(2019, time.March, 25, 23, 59, 59, 0, time.UTC), 52 | }, 53 | want: &interval.Interval{ 54 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 55 | End: time.Date(2019, time.March, 25, 23, 59, 59, 0, time.UTC), 56 | }, 57 | }, 58 | } 59 | 60 | url := "https://ct.googleapis.com/pilot" 61 | name := "google_pilot" 62 | b64PubKey := "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==" 63 | mmd := 24 * time.Hour 64 | 65 | for _, test := range tests { 66 | t.Run(test.desc, func(t *testing.T) { 67 | got, err := New(url, name, b64PubKey, mmd, test.interval) 68 | if err != nil { 69 | t.Fatalf("New(%q, %q, %q, %s, %+v) = _, %s, want no error", url, name, b64PubKey, mmd, test.interval, err) 70 | } 71 | if diff := pretty.Compare(test.want, got.TemporalInterval); diff != "" { 72 | t.Fatalf("New(%q, %q, %q, %s, %+v) returned TemporalInterval diff (-want +got):\n%s", url, name, b64PubKey, mmd, test.interval, diff) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ctlog/ctlog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package ctlog contains data structures and methods to do with CT Log metadata 16 | // that is needed by Monologue. 17 | // 18 | // TODO(katjoyce): Try to come up with a better package name. 19 | package ctlog 20 | 21 | import ( 22 | "crypto" 23 | "fmt" 24 | "time" 25 | 26 | ct "github.com/google/certificate-transparency-go" 27 | "github.com/google/certificate-transparency-go/logid" 28 | "github.com/google/monologue/interval" 29 | ) 30 | 31 | // Log contains metadata about a CT Log that is needed by Monologue. 32 | type Log struct { 33 | Name string 34 | URL string 35 | PublicKey crypto.PublicKey 36 | LogID logid.LogID 37 | MMD time.Duration 38 | 39 | // TemporalInterval represents the interval in which a certificate's 40 | // NotAfter field must fall to be accepted by the Log (as specified by the 41 | // Log Operators). 42 | // TemporalInterval.Start and TemporalInterval.End are both to second 43 | // precision. Any smaller units may be ignored/discarded. 44 | TemporalInterval *interval.Interval 45 | } 46 | 47 | // New creates a Log structure, populating the fields appropriately. 48 | // 49 | // If the Log is not a temporal shard, interval should be nil. 50 | // 51 | // TODO(katjoyce): replace this implementation with something less hacky that 52 | // takes log details from a log list struct based on the new Log list JSON 53 | // schema. 54 | func New(url, name, b64PubKey string, mmd time.Duration, i *interval.Interval) (*Log, error) { 55 | pk, err := ct.PublicKeyFromB64(b64PubKey) 56 | if err != nil { 57 | return nil, fmt.Errorf("ct.PublicKeyFromB64(%s): %s", b64PubKey, err) 58 | } 59 | 60 | logID, err := logid.FromPubKeyB64(b64PubKey) 61 | if err != nil { 62 | return nil, fmt.Errorf("logid.FromPubKeyB64(%s): %s", b64PubKey, err) 63 | } 64 | 65 | var ti *interval.Interval 66 | if i != nil { 67 | ti = &interval.Interval{ 68 | Start: i.Start.Truncate(time.Second), 69 | End: i.End.Truncate(time.Second), 70 | } 71 | } 72 | 73 | return &Log{ 74 | Name: name, 75 | URL: url, 76 | PublicKey: pk, 77 | LogID: logID, 78 | MMD: mmd, 79 | TemporalInterval: ti, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package storage provides the storage interfaces required by the various 16 | // pieces of the CT monitor. 17 | package storage 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | ct "github.com/google/certificate-transparency-go" 24 | "github.com/google/certificate-transparency-go/x509" 25 | "github.com/google/monologue/apicall" 26 | "github.com/google/monologue/ctlog" 27 | ) 28 | 29 | // APICallWriter is an interface for storing individual calls to CT API 30 | // endpoints. 31 | type APICallWriter interface { 32 | WriteAPICall(ctx context.Context, l *ctlog.Log, apiCall *apicall.APICall) error 33 | } 34 | 35 | // STHWriter is an interface for storing STHs received from a CT Log. 36 | type STHWriter interface { 37 | WriteSTH(ctx context.Context, l *ctlog.Log, sth *ct.SignedTreeHead, receivedAt time.Time, errs []error) error 38 | } 39 | 40 | // RootsWriter is an interface for storing root certificates retrieved from a CT get-roots call. 41 | type RootsWriter interface { 42 | // WriteRoots stores the fact that the given roots were received from a particular CT Log at the specified time. 43 | // It will remove any duplicate certificates from roots before storing them. 44 | WriteRoots(ctx context.Context, l *ctlog.Log, roots []*x509.Certificate, receivedAt time.Time) error 45 | } 46 | 47 | // RootSetID uniquely identifies a specific set of certificates, regardless of their order. 48 | type RootSetID string 49 | 50 | // RootsReader is an interface for reading root certificates retrieved from an earlier CT get-roots call. 51 | type RootsReader interface { 52 | // WatchRoots monitors storage for get-roots responses and communicates their content to the caller. 53 | // A unique, deterministic identifier is generated for any set of root certificates (a RootSetID); 54 | // this will be sent over the channel and can be used to lookup further information about that set. 55 | // WatchRoots will immediately send the latest RootSetID when it is first called. 56 | WatchRoots(ctx context.Context, l *ctlog.Log) (<-chan RootSetID, error) 57 | 58 | // ReadRoots returns the root certificates that make up a particular RootSet, 59 | // i.e. the set of certificates returned by a CT get-roots call. 60 | ReadRoots(ctx context.Context, rootSet RootSetID) ([]*x509.Certificate, error) 61 | } 62 | -------------------------------------------------------------------------------- /scripts/presubmit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Presubmit checks for Monologue. 4 | # 5 | # Checks for lint errors, spelling, licensing, correct builds / tests and so on. 6 | # Flags may be specified to allow suppressing of checks or automatic fixes, try 7 | # `scripts/presubmit.sh --help` for details. 8 | # 9 | # Globals: 10 | # GO_TEST_TIMEOUT: timeout for 'go test'. Optional (defaults to 5m). 11 | set -eu 12 | 13 | check_pkg() { 14 | local cmd="$1" 15 | local pkg="$2" 16 | check_cmd "$cmd" "try running 'go get -u $pkg'" 17 | } 18 | 19 | check_cmd() { 20 | local cmd="$1" 21 | local msg="$2" 22 | if ! type -p "${cmd}" > /dev/null; then 23 | echo "${cmd} not found, ${msg}" 24 | return 1 25 | fi 26 | } 27 | 28 | usage() { 29 | echo "$0 [--coverage] [--fix] [--no-build] [--no-linters]" 30 | } 31 | 32 | main() { 33 | local coverage=0 34 | local fix=0 35 | local run_build=1 36 | local run_lint=1 37 | while [[ $# -gt 0 ]]; do 38 | case "$1" in 39 | --coverage) 40 | coverage=1 41 | ;; 42 | --fix) 43 | fix=1 44 | ;; 45 | --help) 46 | usage 47 | exit 0 48 | ;; 49 | --no-build) 50 | run_build=0 51 | ;; 52 | --no-linters) 53 | run_lint=0 54 | ;; 55 | *) 56 | usage 57 | exit 1 58 | ;; 59 | esac 60 | shift 1 61 | done 62 | 63 | cd "$(dirname "$0")" # at scripts/ 64 | cd .. # at top level 65 | 66 | go_srcs="$(find . -name '*.go' | \ 67 | grep -v vendor/ | \ 68 | grep -v mock_ | \ 69 | grep -v .pb.go | \ 70 | grep -v .pb.gw.go | \ 71 | grep -v _string.go | \ 72 | tr '\n' ' ')" 73 | 74 | if [[ "$fix" -eq 1 ]]; then 75 | check_pkg goimports golang.org/x/tools/cmd/goimports || exit 1 76 | 77 | echo 'running gofmt' 78 | gofmt -s -w ${go_srcs} 79 | echo 'running goimports' 80 | goimports -w ${go_srcs} 81 | fi 82 | 83 | if [[ "${run_build}" -eq 1 ]]; then 84 | echo 'running go build' 85 | go build ./... 86 | 87 | # Install test deps so that individual test runs below can reuse them. 88 | echo 'installing test deps' 89 | go test -i ./... 90 | 91 | local coverflags="" 92 | if [[ ${coverage} -eq 1 ]]; then 93 | coverflags="-covermode=atomic -coverprofile=coverage.txt" 94 | fi 95 | 96 | echo 'running go test' 97 | go test \ 98 | -short \ 99 | -timeout=${GO_TEST_TIMEOUT:-5m} \ 100 | ${coverflags} \ 101 | ./... 102 | fi 103 | 104 | if [[ "${run_lint}" -eq 1 ]]; then 105 | check_cmd golangci-lint \ 106 | 'have you installed github.com/golangci/golangci-lint?' || exit 1 107 | 108 | echo 'running golangci-lint' 109 | golangci-lint run 110 | echo 'checking license headers' 111 | ./scripts/check_license.sh ${go_srcs} 112 | fi 113 | } 114 | 115 | main "$@" 116 | -------------------------------------------------------------------------------- /rootsgetter/rootsgetter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package rootsgetter periodically queries a Log for its set of acceptable root certificates and stores them. 16 | package rootsgetter 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/golang/glog" 24 | ct "github.com/google/certificate-transparency-go" 25 | "github.com/google/certificate-transparency-go/schedule" 26 | "github.com/google/certificate-transparency-go/x509" 27 | "github.com/google/monologue/apicall" 28 | "github.com/google/monologue/client" 29 | "github.com/google/monologue/ctlog" 30 | "github.com/google/monologue/storage" 31 | ) 32 | 33 | const logStr = "Roots Getter" 34 | 35 | // Storage interface required by Roots Getter. 36 | type Storage interface { 37 | storage.APICallWriter 38 | storage.RootsWriter 39 | } 40 | 41 | // Run starts a Roots Getter, which periodically queries a Log for its set of acceptable root certificates and stores them. 42 | func Run(ctx context.Context, lc *client.LogClient, st Storage, l *ctlog.Log, period time.Duration) { 43 | glog.Infof("%s: %s: started with period %v", l.URL, logStr, period) 44 | 45 | schedule.Every(ctx, period, func(ctx context.Context) { 46 | roots, receivedAt, err := getRoots(ctx, lc, st, l) 47 | if err != nil { 48 | glog.Errorf("%s: %s: %s", l.URL, logStr, err) 49 | return 50 | } 51 | if err := st.WriteRoots(ctx, l, roots, receivedAt); err != nil { 52 | glog.Errorf("%s: %s: %s", l.URL, logStr, err) 53 | } 54 | }) 55 | 56 | glog.Infof("%s: %s: stopped", l.URL, logStr) 57 | } 58 | 59 | func getRoots(ctx context.Context, lc *client.LogClient, st storage.APICallWriter, l *ctlog.Log) ([]*x509.Certificate, time.Time, error) { 60 | glog.Infof("%s: %s: getting roots...", l.URL, logStr) 61 | roots, httpData, getErr := lc.GetRoots() 62 | 63 | // Store get-roots API call. 64 | apiCall := apicall.New(ct.GetRootsStr, httpData, getErr) 65 | glog.Infof("%s: %s: writing API Call...", l.URL, logStr) 66 | if err := st.WriteAPICall(ctx, l, apiCall); err != nil { 67 | return nil, httpData.Timing.End, fmt.Errorf("error writing API Call %s: %s", apiCall, err) 68 | } 69 | 70 | if getErr != nil { 71 | return nil, httpData.Timing.End, fmt.Errorf("error getting roots: %s", getErr) 72 | } 73 | 74 | glog.Infof("%s: %s: response: %d certificates", l.URL, logStr, len(roots)) 75 | return roots, httpData.Timing.End, nil 76 | } 77 | -------------------------------------------------------------------------------- /incident/incident.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package incident provides access to functionality for recording and 16 | // classifying compliance incidents. 17 | package incident 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/golang/glog" 24 | ) 25 | 26 | // Reporter describes a mechanism for recording compliance incidents. 27 | type Reporter interface { 28 | // LogUpdate records a non-violation incident with the given parameters. 29 | // The baseURL, summary and category fields should be stable across 30 | // multiple similar incidents to allow aggregation. Information that 31 | // varies between instances of the 'same' incident should be included in 32 | // the fullURL or details field. 33 | LogUpdate(ctx context.Context, baseURL, summary, fullURL, details string) 34 | LogUpdatef(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) 35 | 36 | // LogViolation records a RFC/Policy violation incident with the given parameters. 37 | LogViolation(ctx context.Context, baseURL, summary, fullURL, details string) 38 | LogViolationf(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) 39 | } 40 | 41 | // LoggingReporter implements the Reporter interface by simply emitting 42 | // log messages. 43 | type LoggingReporter struct { 44 | } 45 | 46 | // LogUpdate emits a log message for the incident details. 47 | func (l *LoggingReporter) LogUpdate(ctx context.Context, baseURL, summary, fullURL, details string) { 48 | glog.Infof("%s: %s (%s)\n %s", baseURL, summary, fullURL, details) 49 | } 50 | 51 | // LogViolation emits a log message for the incident details. 52 | func (l *LoggingReporter) LogViolation(ctx context.Context, baseURL, summary, fullURL, details string) { 53 | glog.Errorf("%s: %s (%s)\n %s", baseURL, summary, fullURL, details) 54 | } 55 | 56 | // LogUpdatef emits a log message for the incident details, formatting parameters along the way. 57 | func (l *LoggingReporter) LogUpdatef(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 58 | glog.Infof("%s: %s (%s)\n %s", baseURL, summary, fullURL, fmt.Sprintf(detailsFmt, args...)) 59 | } 60 | 61 | // LogUpdatef emits a log message for the incident details, formatting parameters along the way. 62 | func (l *LoggingReporter) LogViolationf(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 63 | glog.Errorf("%s: %s (%s)\n %s", baseURL, summary, fullURL, fmt.Sprintf(detailsFmt, args...)) 64 | } 65 | -------------------------------------------------------------------------------- /client/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | ) 21 | 22 | const maxBodyStringLen = 100 23 | 24 | // GetError for if http.Client.Get() fails. 25 | type GetError struct { 26 | URL string 27 | Err error 28 | } 29 | 30 | func (e *GetError) Error() string { 31 | return fmt.Sprintf("GET %s: %v", e.URL, e.Err) 32 | } 33 | 34 | // PostError for if http.Client.Post() fails. 35 | type PostError struct { 36 | URL string 37 | ContentType string 38 | Body []byte 39 | Err error 40 | } 41 | 42 | func (e *PostError) Error() string { 43 | body := string(e.Body) 44 | if len(e.Body) > maxBodyStringLen { 45 | body = fmt.Sprintf("%s... [truncated next %d bytes]", e.Body[:maxBodyStringLen], len(e.Body)-maxBodyStringLen) 46 | } 47 | return fmt.Sprintf("POST %s (content type: %s, body: %s): %v", e.URL, contentType, body, e.Err) 48 | } 49 | 50 | func (e *PostError) VerboseError() string { 51 | return fmt.Sprintf("POST %s (content type: %s, body: %s): %v", e.URL, contentType, e.Body, e.Err) 52 | } 53 | 54 | // NilResponseError for if http.Client.Get() returns a nil response, but no 55 | // error. 56 | type NilResponseError struct { 57 | URL string 58 | } 59 | 60 | func (e *NilResponseError) Error() string { 61 | return fmt.Sprintf("nil response from %s", e.URL) 62 | } 63 | 64 | // BodyReadError for if reading the body of an http.Response fails. 65 | type BodyReadError struct { 66 | URL string 67 | Err error 68 | } 69 | 70 | func (e *BodyReadError) Error() string { 71 | return fmt.Sprintf("error reading body from %s: %s", e.URL, e.Err) 72 | } 73 | 74 | // HTTPStatusError for if the status code of an HTTP response is not 200. 75 | type HTTPStatusError struct { 76 | StatusCode int 77 | } 78 | 79 | func (e *HTTPStatusError) Error() string { 80 | return fmt.Sprintf("HTTP status code: %d, want 200", e.StatusCode) 81 | } 82 | 83 | // JSONParseError for if JSON fails to parse. 84 | type JSONParseError struct { 85 | Data []byte 86 | Err error 87 | } 88 | 89 | func (e *JSONParseError) Error() string { 90 | return fmt.Sprintf("json.Unmarshal(): %s", e.Err) 91 | } 92 | 93 | // ResponseToStructError for if conversion from response type to ct type fails. 94 | type ResponseToStructError struct { 95 | From reflect.Type 96 | To reflect.Type 97 | Err error 98 | } 99 | 100 | func (e *ResponseToStructError) Error() string { 101 | return fmt.Sprintf("converting %v to %v: %s", e.From, e.To, e.Err) 102 | } 103 | -------------------------------------------------------------------------------- /incident/mysql/incident.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package mysql provides a MySQL based implementation of incident management. 16 | package mysql 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/golang/glog" 25 | "github.com/google/monologue/incident" 26 | ) 27 | 28 | type mysqlReporter struct { 29 | db *sql.DB 30 | stmt *sql.Stmt 31 | source string 32 | } 33 | 34 | // NewMySQLReporter builds an incident.Reporter instance that records incidents 35 | // in a MySQL database, all of which will be marked as emanating from the given 36 | // source. 37 | func NewMySQLReporter(ctx context.Context, db *sql.DB, source string) (incident.Reporter, error) { 38 | stmt, err := db.PrepareContext(ctx, "INSERT INTO Incidents(Timestamp, Source, BaseURL, Summary, IsViolation, FullURL, Details) VALUES (?, ?, ?, ?, ?, ?, ?);") 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to prepare context for %q: %v", source, err) 41 | } 42 | return &mysqlReporter{db: db, source: source, stmt: stmt}, nil 43 | } 44 | 45 | // LogUpdate records an incident with the given details. 46 | func (m *mysqlReporter) LogUpdate(ctx context.Context, baseURL, summary, fullURL, details string) { 47 | now := time.Now() 48 | glog.Errorf("[%s] %s: %s (url=%s)\n %s", now, baseURL, summary, fullURL, details) 49 | if _, err := m.stmt.ExecContext(ctx, now, m.source, baseURL, summary, false /* isViolation */, fullURL, details); err != nil { 50 | glog.Errorf("failed to insert incident for %q: %v", m.source, err) 51 | } 52 | } 53 | 54 | // LogViolation records an incident with the given details. 55 | func (m *mysqlReporter) LogViolation(ctx context.Context, baseURL, summary, fullURL, details string) { 56 | now := time.Now() 57 | glog.Infof("[%s] %s: %s (url=%s)\n %s", now, baseURL, summary, fullURL, details) 58 | if _, err := m.stmt.ExecContext(ctx, now, m.source, baseURL, summary, true /* isViolation */, fullURL, details); err != nil { 59 | glog.Errorf("failed to insert incident for %q: %v", m.source, err) 60 | } 61 | } 62 | 63 | // LogUpdatef records an incident with the given details and formatting. 64 | func (m *mysqlReporter) LogUpdatef(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 65 | details := fmt.Sprintf(detailsFmt, args...) 66 | m.LogUpdate(ctx, baseURL, summary, fullURL, details) 67 | } 68 | 69 | // LogUpdatef records an incident with the given details and formatting. 70 | func (m *mysqlReporter) LogViolationf(ctx context.Context, baseURL, summary, fullURL, detailsFmt string, args ...interface{}) { 71 | details := fmt.Sprintf(detailsFmt, args...) 72 | m.LogViolation(ctx, baseURL, summary, fullURL, details) 73 | } 74 | -------------------------------------------------------------------------------- /scripts/resetmondb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | usage() { 6 | cat < /dev/stderr 28 | exit 1 29 | } 30 | 31 | collect_vars() { 32 | # set unset environment variables to defaults 33 | [ -z ${MYSQL_ROOT_USER+x} ] && MYSQL_ROOT_USER="root" 34 | [ -z ${MYSQL_HOST+x} ] && MYSQL_HOST="localhost" 35 | [ -z ${MYSQL_PORT+x} ] && MYSQL_PORT="3306" 36 | [ -z ${MYSQL_DATABASE+x} ] && MYSQL_DATABASE="monologuetest" 37 | [ -z ${MYSQL_USER+x} ] && MYSQL_USER="monologuetest" 38 | [ -z ${MYSQL_PASSWORD+x} ] && MYSQL_PASSWORD="soliloquy" 39 | [ -z ${MYSQL_USER_HOST+x} ] && MYSQL_USER_HOST="localhost" 40 | FLAGS=() 41 | 42 | # handle flags 43 | FORCE=false 44 | VERBOSE=false 45 | while [[ $# -gt 0 ]]; do 46 | case "$1" in 47 | --force) FORCE=true ;; 48 | --verbose) VERBOSE=true ;; 49 | --help) usage; exit ;; 50 | *) FLAGS+=("$1") 51 | esac 52 | shift 1 53 | done 54 | 55 | FLAGS+=(-u "${MYSQL_ROOT_USER}") 56 | FLAGS+=(--host "${MYSQL_HOST}") 57 | FLAGS+=(--port "${MYSQL_PORT}") 58 | 59 | # Optionally print flags (before appending password) 60 | [[ ${VERBOSE} = 'true' ]] && echo "- Using MySQL Flags: ${FLAGS[@]}" 61 | 62 | # append password if supplied 63 | [ -z ${MYSQL_ROOT_PASSWORD+x} ] || FLAGS+=(-p"${MYSQL_ROOT_PASSWORD}") 64 | } 65 | 66 | main() { 67 | collect_vars "$@" 68 | 69 | readonly INCIDENT_PATH=$(go list -f '{{.Dir}}' github.com/google/monologue/incident/mysql) 70 | 71 | echo "Warning: about to destroy and reset database '${MYSQL_DATABASE}'" 72 | 73 | [[ ${FORCE} = true ]] || read -p "Are you sure? [Y/N]: " -n 1 -r 74 | echo # Print newline following the above prompt 75 | 76 | if [ -z ${REPLY+x} ] || [[ $REPLY =~ ^[Yy]$ ]] 77 | then 78 | echo "Resetting DB..." 79 | mysql "${FLAGS[@]}" -e "DROP DATABASE IF EXISTS ${MYSQL_DATABASE};" || \ 80 | die "Error: Failed to drop database '${MYSQL_DATABASE}'." 81 | mysql "${FLAGS[@]}" -e "CREATE DATABASE ${MYSQL_DATABASE};" || \ 82 | die "Error: Failed to create database '${MYSQL_DATABASE}'." 83 | mysql "${FLAGS[@]}" -e "CREATE USER IF NOT EXISTS ${MYSQL_USER}@'${MYSQL_USER_HOST}' IDENTIFIED BY '${MYSQL_PASSWORD}';" || \ 84 | die "Error: Failed to create user '${MYSQL_USER}@${MYSQL_USER_HOST}'." 85 | mysql "${FLAGS[@]}" -e "GRANT ALL ON ${MYSQL_DATABASE}.* TO ${MYSQL_USER}@'${MYSQL_USER_HOST}'" || \ 86 | die "Error: Failed to grant '${MYSQL_USER}' user all privileges on '${MYSQL_DATABASE}'." 87 | mysql "${FLAGS[@]}" -D ${MYSQL_DATABASE} < ${INCIDENT_PATH}/incident.sql || \ 88 | die "Error: Failed to create incident table in '${MYSQL_DATABASE}' database." 89 | echo "Reset Complete" 90 | fi 91 | } 92 | 93 | main "$@" 94 | -------------------------------------------------------------------------------- /storage/mysql/testdb/testdb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // testdb contains helper functions for testing mysql storage 16 | package testdb 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "database/sql" 22 | "fmt" 23 | "io/ioutil" 24 | "strings" 25 | "time" 26 | 27 | "github.com/golang/glog" 28 | 29 | _ "github.com/go-sql-driver/mysql" // Load MySQL driver 30 | ) 31 | 32 | var ( 33 | dataSource = "root@tcp(127.0.0.1)/" 34 | ) 35 | 36 | // MySQLAvailable indicates whether a default MySQL database is available. 37 | func MySQLAvailable() error { 38 | db, err := sql.Open("mysql", dataSource) 39 | if err != nil { 40 | return fmt.Errorf("sql.Open(): %v", err) 41 | } 42 | defer db.Close() 43 | if err := db.Ping(); err != nil { 44 | return fmt.Errorf("db.Ping(): %v", err) 45 | } 46 | return nil 47 | } 48 | 49 | // newEmptyDB creates a new, empty database. 50 | func newEmptyDB(ctx context.Context) (*sql.DB, error) { 51 | db, err := sql.Open("mysql", dataSource) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // Create a randomly-named database and then connect using the new name. 57 | name := fmt.Sprintf("mono_%v", time.Now().UnixNano()) 58 | 59 | stmt := fmt.Sprintf("CREATE DATABASE %v", name) 60 | if _, err := db.ExecContext(ctx, stmt); err != nil { 61 | return nil, fmt.Errorf("error running statement %q: %v", stmt, err) 62 | } 63 | db.Close() 64 | 65 | db, err = sql.Open("mysql", dataSource+name+"?parseTime=true") 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to open new database %q: %v", name, err) 68 | } 69 | return db, db.Ping() 70 | } 71 | 72 | // NewDB creates an empty database with the given schema. 73 | func New(ctx context.Context, schemaPath string) (*sql.DB, error) { 74 | db, err := newEmptyDB(ctx) 75 | if err != nil { 76 | return nil, fmt.Errorf("failed to create empty DB: %v", err) 77 | } 78 | 79 | sqlBytes, err := ioutil.ReadFile(schemaPath) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to read schema SQL: %v", err) 82 | } 83 | 84 | for _, stmt := range strings.Split(sanitize(string(sqlBytes)), ";") { 85 | stmt = strings.TrimSpace(stmt) 86 | if stmt == "" { 87 | continue 88 | } 89 | if _, err := db.ExecContext(ctx, stmt); err != nil { 90 | return nil, fmt.Errorf("error running statement %q: %v", stmt, err) 91 | } 92 | } 93 | return db, nil 94 | } 95 | 96 | func sanitize(script string) string { 97 | buf := &bytes.Buffer{} 98 | for _, line := range strings.Split(string(script), "\n") { 99 | line = strings.TrimSpace(line) 100 | if line == "" || line[0] == '#' || strings.Index(line, "--") == 0 { 101 | continue // skip empty lines and comments 102 | } 103 | buf.WriteString(line) 104 | buf.WriteString("\n") 105 | } 106 | return buf.String() 107 | } 108 | 109 | func Clean(ctx context.Context, testDB *sql.DB, name string) { 110 | if _, err := testDB.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s", name)); err != nil { 111 | glog.Exitf("Failed to delete rows in %s: %v", name, err) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /incident/mysql/incident_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mysql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "flag" 21 | "os" 22 | "testing" 23 | 24 | "github.com/golang/glog" 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/google/monologue/storage/mysql/testdb" 27 | 28 | _ "github.com/go-sql-driver/mysql" // Load MySQL driver 29 | ) 30 | 31 | type entry struct { 32 | BaseURL, Summary string 33 | IsViolation bool 34 | FullURL, Details string 35 | } 36 | 37 | func checkContents(ctx context.Context, testDB *sql.DB, t *testing.T, want []entry) { 38 | t.Helper() 39 | 40 | tx, err := testDB.BeginTx(ctx, nil /* opts */) 41 | if err != nil { 42 | t.Fatalf("failed to create transaction: %v", err) 43 | } 44 | defer tx.Commit() 45 | rows, err := tx.QueryContext(ctx, "SELECT BaseURL, Summary, IsViolation, FullURL, Details FROM Incidents;") 46 | if err != nil { 47 | t.Fatalf("failed to query rows: %v", err) 48 | } 49 | defer rows.Close() 50 | 51 | var got []entry 52 | for rows.Next() { 53 | var e entry 54 | if err := rows.Scan(&e.BaseURL, &e.Summary, &e.IsViolation, &e.FullURL, &e.Details); err != nil { 55 | t.Fatalf("failed to scan row: %v", err) 56 | } 57 | got = append(got, e) 58 | } 59 | if err := rows.Err(); err != nil { 60 | t.Errorf("incident table iteration failed: %v", err) 61 | } 62 | if diff := cmp.Diff(got, want); diff != "" { 63 | t.Errorf("incident table: diff (-got +want)\n%s", diff) 64 | } 65 | } 66 | 67 | func TestLogf(t *testing.T) { 68 | ctx := context.Background() 69 | testdb.Clean(ctx, testDB, "Incidents") 70 | 71 | checkContents(ctx, testDB, t, nil) 72 | 73 | reporter, err := NewMySQLReporter(ctx, testDB, "unittest") 74 | if err != nil { 75 | t.Fatalf("failed to build MySQLReporter: %v", err) 76 | } 77 | 78 | e := entry{BaseURL: "base", Summary: "summary", IsViolation: false, FullURL: "full", Details: "blah"} 79 | ev := entry{BaseURL: "base", Summary: "summary", IsViolation: true, FullURL: "full", Details: "blah"} 80 | 81 | reporter.LogUpdate(ctx, e.BaseURL, e.Summary, e.FullURL, e.Details) 82 | checkContents(ctx, testDB, t, []entry{e}) 83 | 84 | reporter.LogViolation(ctx, ev.BaseURL, ev.Summary, ev.FullURL, ev.Details) 85 | checkContents(ctx, testDB, t, []entry{e, ev}) 86 | 87 | reporter.LogUpdatef(ctx, e.BaseURL, e.Summary, e.FullURL, "%s", e.Details) 88 | checkContents(ctx, testDB, t, []entry{e, ev, e}) 89 | } 90 | 91 | func TestMain(m *testing.M) { 92 | flag.Parse() 93 | if err := testdb.MySQLAvailable(); err != nil { 94 | glog.Errorf("MySQL not available, skipping all MySQL storage tests: %v", err) 95 | return 96 | } 97 | ctx := context.Background() 98 | var err error 99 | testDB, err = testdb.New(ctx, incidentSQL) 100 | if err != nil { 101 | glog.Exitf("failed to create test database: %v", err) 102 | } 103 | defer testDB.Close() 104 | testdb.Clean(ctx, testDB, "Incidents") 105 | ec := m.Run() 106 | os.Exit(ec) 107 | } 108 | 109 | var ( 110 | testDB *sql.DB 111 | incidentSQL = "incident.sql" 112 | ) 113 | -------------------------------------------------------------------------------- /collector/collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package collector fetches the data needed to monitor a single Certificate 16 | // Transparency Log, which can then be used to check that it is adhering to RFC 17 | // 6962. 18 | package collector 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | "net/http" 25 | "sync" 26 | "time" 27 | 28 | ct "github.com/google/certificate-transparency-go" 29 | "github.com/google/monologue/certgen" 30 | "github.com/google/monologue/certsubmitter" 31 | "github.com/google/monologue/client" 32 | "github.com/google/monologue/ctlog" 33 | "github.com/google/monologue/rootsgetter" 34 | "github.com/google/monologue/sthgetter" 35 | "github.com/google/monologue/storage" 36 | ) 37 | 38 | // Config contains all of the configuration details for running the collector 39 | // for a particular Log. 40 | type Config struct { 41 | // Details of the Log to collect data from. 42 | Log *ctlog.Log 43 | // How regularly the monitor should get an STH from the Log. 44 | // To disable getting STHs, set to 0. 45 | GetSTHPeriod time.Duration 46 | // How regularly the monitor should get root certificates from the Log. 47 | // To disable getting roots, set to 0. 48 | GetRootsPeriod time.Duration 49 | // How regularly the monitor should submit a (pre-)certificate to the Log. 50 | // To disable (pre-)certificate submission, set to 0. 51 | AddChainPeriod time.Duration 52 | // The CA that issues certificates for submission to the Log. Must be set 53 | // if AddChainPeriod != 0. 54 | CA *certgen.CA 55 | } 56 | 57 | // Storage is an interface containing all of the storage methods required by 58 | // Monologue. It will therefore satisfy every interface needed by the various 59 | // modules that the collector runs (e.g. sthgetter, rootsgetter etc). 60 | type Storage interface { 61 | storage.APICallWriter 62 | storage.RootsWriter 63 | storage.STHWriter 64 | } 65 | 66 | // Run runs the collector on the Log specified in cfg, and stores the collected 67 | // data in st. Run doesn't return unless an error occurs or ctx expires. 68 | func Run(ctx context.Context, cfg *Config, cl *http.Client, st Storage) error { 69 | if cfg == nil { 70 | return errors.New("nil Config") 71 | } 72 | if cfg.Log == nil { 73 | return errors.New("no Log provided in Config") 74 | } 75 | 76 | lc := client.New(cfg.Log.URL, cl) 77 | 78 | sv, err := ct.NewSignatureVerifier(cfg.Log.PublicKey) 79 | if err != nil { 80 | return fmt.Errorf("couldn't create signature verifier: %s", err) 81 | } 82 | 83 | var wg sync.WaitGroup 84 | if cfg.GetSTHPeriod > 0 { 85 | wg.Add(1) 86 | go func() { 87 | sthgetter.Run(ctx, lc, sv, st, cfg.Log, cfg.GetSTHPeriod) 88 | wg.Done() 89 | }() 90 | } 91 | if cfg.GetRootsPeriod > 0 { 92 | wg.Add(1) 93 | go func() { 94 | rootsgetter.Run(ctx, lc, st, cfg.Log, cfg.GetRootsPeriod) 95 | wg.Done() 96 | }() 97 | } 98 | if cfg.AddChainPeriod > 0 { 99 | wg.Add(1) 100 | go func() { 101 | certsubmitter.Run(ctx, lc, cfg.CA, sv, st, cfg.Log, cfg.AddChainPeriod) 102 | wg.Done() 103 | }() 104 | } 105 | wg.Wait() 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /interval/interval_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package interval 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestRandomSecond(t *testing.T) { 23 | tests := []struct { 24 | desc string 25 | in *Interval 26 | wantZero bool 27 | }{ 28 | { 29 | desc: "nil", 30 | in: nil, 31 | wantZero: true, 32 | }, 33 | { 34 | desc: "day", 35 | in: &Interval{ 36 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 37 | End: time.Date(2019, time.March, 26, 0, 0, 0, 0, time.UTC), 38 | }, 39 | }, 40 | { 41 | desc: "second", 42 | in: &Interval{ 43 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 44 | End: time.Date(2019, time.March, 25, 0, 0, 1, 0, time.UTC), 45 | }, 46 | }, 47 | { 48 | desc: "one second boundary between", 49 | in: &Interval{ 50 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 999999999, time.UTC), 51 | End: time.Date(2019, time.March, 25, 0, 0, 1, 1, time.UTC), 52 | }, 53 | }, 54 | { 55 | desc: "one second boundary between, start on second boundary", 56 | in: &Interval{ 57 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 58 | End: time.Date(2019, time.March, 25, 0, 0, 0, 1, time.UTC), 59 | }, 60 | }, 61 | { 62 | desc: "no second boundaries between", 63 | in: &Interval{ 64 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 1, time.UTC), 65 | End: time.Date(2019, time.March, 25, 0, 0, 0, 999999999, time.UTC), 66 | }, 67 | wantZero: true, 68 | }, 69 | { 70 | desc: "no second boundaries between, end on second boundary", 71 | in: &Interval{ 72 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 1, time.UTC), 73 | End: time.Date(2019, time.March, 25, 0, 0, 1, 0, time.UTC), 74 | }, 75 | wantZero: true, 76 | }, 77 | { 78 | desc: "equal", 79 | in: &Interval{ 80 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 81 | End: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 82 | }, 83 | wantZero: true, 84 | }, 85 | { 86 | desc: "end just before start", 87 | in: &Interval{ 88 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 1, time.UTC), 89 | End: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 90 | }, 91 | wantZero: true, 92 | }, 93 | { 94 | desc: "end way before start", 95 | in: &Interval{ 96 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 97 | End: time.Date(2019, time.March, 24, 0, 0, 0, 0, time.UTC), 98 | }, 99 | wantZero: true, 100 | }, 101 | } 102 | 103 | for _, test := range tests { 104 | t.Run(test.desc, func(t *testing.T) { 105 | got := test.in.RandomSecond() 106 | 107 | if test.wantZero { 108 | if !got.IsZero() { 109 | t.Fatalf("%v.RandomSecond() = %s, want %s (the zero time)", test.in, got, time.Time{}) 110 | } 111 | return 112 | } 113 | 114 | if got.Before(test.in.Start) || !test.in.End.After(got) { 115 | t.Fatalf("%v.RandomSecond() = %s, want between [%s, %s)", test.in, got, test.in.Start, test.in.End) 116 | } 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /rootsanalyzer/rootset_id_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rootsanalyzer 16 | 17 | import ( 18 | "encoding/hex" 19 | "testing" 20 | 21 | "github.com/google/certificate-transparency-go/x509" 22 | "github.com/google/certificate-transparency-go/x509util" 23 | "github.com/google/monologue/storage" 24 | "github.com/google/monologue/testdata" 25 | ) 26 | 27 | func TestCertIDOnNil(t *testing.T) { 28 | if _, gotErr := GenerateCertID(nil); gotErr == nil { 29 | t.Errorf("Error expected, got nil") 30 | } 31 | } 32 | 33 | func mustHexCode(s string, t *testing.T) []byte { 34 | t.Helper() 35 | decoded, err := hex.DecodeString(s) 36 | if err != nil { 37 | t.Errorf("Unexpected error while preparing testdata: %s", err) 38 | } 39 | return decoded 40 | } 41 | 42 | func TestCertIDOnValid(t *testing.T) { 43 | decoded := mustHexCode("86d8219c7e2b6009e37eb14356268489b81379e076e8f372e3dde8c162a34134", t) 44 | var wantID [32]byte 45 | copy(wantID[:], decoded[:32]) 46 | cert, _ := x509util.CertificateFromPEM([]byte(testdata.RootCertPEM)) 47 | 48 | gotCertID, gotErr := GenerateCertID(cert) 49 | if gotErr != nil { 50 | t.Errorf("Nil error expected, got %v", gotErr) 51 | } 52 | if gotCertID != wantID { 53 | t.Errorf("Got cert-ID %x for root certificate, want %x", string(gotCertID[:]), wantID) 54 | } 55 | } 56 | 57 | func TestGenerateSetID(t *testing.T) { 58 | cert, _ := x509util.CertificateFromPEM([]byte(testdata.RootCertPEM)) 59 | cert2, _ := x509util.CertificateFromPEM([]byte(testdata.IntermediateCertPEM)) 60 | 61 | tests := []struct { 62 | desc string 63 | roots []*x509.Certificate 64 | wantSetID storage.RootSetID 65 | }{ 66 | { 67 | desc: "SingleRoot", 68 | roots: []*x509.Certificate{cert}, 69 | wantSetID: storage.RootSetID(mustHexCode("35d1cd6dbd84a37a5884351d1d0d197d2e9048709b1442391cdfac69f8371272", t)), 70 | }, 71 | { 72 | desc: "DedupRoot", 73 | roots: []*x509.Certificate{cert, cert, cert}, 74 | wantSetID: storage.RootSetID(mustHexCode("35d1cd6dbd84a37a5884351d1d0d197d2e9048709b1442391cdfac69f8371272", t)), 75 | }, 76 | { 77 | desc: "TwoCerts", 78 | roots: []*x509.Certificate{cert, cert2}, 79 | wantSetID: storage.RootSetID(mustHexCode("be6b3e0736f965cf707eb773709027a7250de5e32910f09370146d1318d6df04", t)), 80 | }, 81 | { 82 | desc: "TwoCertsSort", 83 | roots: []*x509.Certificate{cert2, cert}, 84 | wantSetID: storage.RootSetID(mustHexCode("be6b3e0736f965cf707eb773709027a7250de5e32910f09370146d1318d6df04", t)), 85 | }, 86 | { 87 | desc: "TwoCertsSortDedup", 88 | roots: []*x509.Certificate{cert2, cert, cert, cert2}, 89 | wantSetID: storage.RootSetID(mustHexCode("be6b3e0736f965cf707eb773709027a7250de5e32910f09370146d1318d6df04", t)), 90 | }, 91 | } 92 | 93 | for _, test := range tests { 94 | t.Run(test.desc, func(t *testing.T) { 95 | gotSetID, gotErr := GenerateSetID(test.roots) 96 | if gotErr != nil { 97 | t.Fatalf("Nil error expected, got %v", gotErr) 98 | } 99 | if gotSetID != test.wantSetID { 100 | t.Errorf("Got cert-ID %x for root certificate, want %x", string(gotSetID), test.wantSetID) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /certgen/testdata/test_ca.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 67:94:a1:b8:a0:54:22:26:c8:c4:a4:46:c4:42:9a:95:32:b6:02:39 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: C = GB, ST = Test State, L = Test Locality, O = Test Organisation, OU = Test Organizational Unit Name, CN = Test CA 8 | Validity 9 | Not Before: Sep 10 10:11:57 2019 GMT 10 | Not After : Oct 10 10:11:57 2019 GMT 11 | Subject: C = GB, ST = Test State, L = Test Locality, O = Test Organisation, OU = Test Organizational Unit Name, CN = Test CA 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:cf:4b:90:2d:c1:eb:09:ad:ec:3d:3c:92:40:2f: 17 | 72:a7:11:10:76:46:01:84:93:00:08:8b:4f:4d:55: 18 | 35:1b:b1:16:5d:95:af:d9:87:e1:f4:ce:d9:2e:d1: 19 | 76:8d:94:00:ee:08:9c:3f:8b:38:f7:86:44:7f:f2: 20 | f3:f1:77:28:a9:4a:73:56:7a:b1:a8:ae:90:a1:86: 21 | 8a:84:c9:08:b2:95:e7:a2:29:c3:7f:27:4a:df:9d: 22 | b7:14:66:04:06:cf:09:1b:c4:c6:76:bb:15:10:21: 23 | 4f:88:1c:a3:82:0e:8c:11:11:34:93:83:e6:6b:48: 24 | 6f:ab:ec:22:bb:30:72:61:ad:8b:82:94:22:08:78: 25 | b8:e8:54:f4:0f:ad:b3:84:19:e7:cf:6c:87:83:2b: 26 | 56:cf:72:69:86:5a:8f:8a:55:08:ef:2d:74:29:8c: 27 | 4a:ff:75:a5:c7:78:9b:73:f1:3c:64:dc:fc:71:ee: 28 | 75:f4:59:19:d4:2f:4b:21:1d:f0:ab:36:c9:db:8d: 29 | 7a:c0:97:57:53:d0:45:94:6a:14:f2:a9:06:b8:cb: 30 | b9:f7:d9:da:46:f4:19:10:42:4a:98:3d:08:f8:19: 31 | 2b:4b:85:3d:64:dd:f7:f2:da:d3:04:d5:62:34:d3: 32 | 97:dc:21:c0:37:e0:14:01:13:b6:b9:63:83:9f:d5: 33 | 4e:31 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Subject Key Identifier: 37 | 49:B3:00:00:FF:4C:2B:12:34:1A:8C:A8:2F:07:28:6E:50:AD:4F:1D 38 | X509v3 Authority Key Identifier: 39 | keyid:49:B3:00:00:FF:4C:2B:12:34:1A:8C:A8:2F:07:28:6E:50:AD:4F:1D 40 | 41 | X509v3 Basic Constraints: critical 42 | CA:TRUE 43 | Signature Algorithm: sha256WithRSAEncryption 44 | 87:e2:b2:dd:5a:89:74:46:e7:e3:f1:e0:99:f8:de:95:dd:00: 45 | f9:77:b4:2b:d0:b2:14:e9:80:bb:62:50:99:fc:20:9d:7b:e1: 46 | e7:2a:51:2a:92:78:90:2c:22:14:86:d4:20:ad:4c:b2:17:72: 47 | 3a:48:42:f0:59:34:de:f4:97:45:82:cc:be:bd:f9:2c:98:9e: 48 | 21:0c:4c:77:65:d4:d7:ee:ff:c3:aa:0c:6c:fa:15:9f:6f:b0: 49 | 7b:f5:c9:89:a3:a5:0f:e5:3b:26:27:41:55:11:e8:63:64:2e: 50 | 98:6f:bf:77:7d:d7:8f:9d:03:c1:ab:16:fc:66:74:21:de:ad: 51 | 9b:32:9f:13:40:76:ed:f0:40:0a:52:2e:a9:2c:8f:46:33:90: 52 | ee:84:e1:30:ef:ad:74:bd:46:44:c3:26:78:ce:e1:08:cc:31: 53 | 92:ae:a9:36:7a:0c:b6:30:62:b1:2f:ae:ed:a3:56:9d:33:6c: 54 | 80:f5:89:21:5b:9e:56:f8:13:98:e9:75:6c:94:46:30:48:76: 55 | dd:94:a2:da:24:47:c9:c7:3d:24:47:ff:ee:f1:9d:d7:22:c0: 56 | 85:d0:59:5b:3f:db:2f:c6:8d:5f:30:e4:75:e0:55:7f:1a:41: 57 | 28:a9:17:98:11:0e:99:21:9a:98:94:ed:58:1e:39:17:c9:4f: 58 | b9:d2:56:94 59 | 60 | -----BEGIN CERTIFICATE----- 61 | MIIEAzCCAuugAwIBAgIUZ5ShuKBUIibIxKRGxEKalTK2AjkwDQYJKoZIhvcNAQEL 62 | BQAwgZAxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApUZXN0IFN0YXRlMRYwFAYDVQQH 63 | DA1UZXN0IExvY2FsaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXNhdGlvbjEmMCQG 64 | A1UECwwdVGVzdCBPcmdhbml6YXRpb25hbCBVbml0IE5hbWUxEDAOBgNVBAMMB1Rl 65 | c3QgQ0EwHhcNMTkwOTEwMTAxMTU3WhcNMTkxMDEwMTAxMTU3WjCBkDELMAkGA1UE 66 | BhMCR0IxEzARBgNVBAgMClRlc3QgU3RhdGUxFjAUBgNVBAcMDVRlc3QgTG9jYWxp 67 | dHkxGjAYBgNVBAoMEVRlc3QgT3JnYW5pc2F0aW9uMSYwJAYDVQQLDB1UZXN0IE9y 68 | Z2FuaXphdGlvbmFsIFVuaXQgTmFtZTEQMA4GA1UEAwwHVGVzdCBDQTCCASIwDQYJ 69 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM9LkC3B6wmt7D08kkAvcqcREHZGAYST 70 | AAiLT01VNRuxFl2Vr9mH4fTO2S7Rdo2UAO4InD+LOPeGRH/y8/F3KKlKc1Z6saiu 71 | kKGGioTJCLKV56Ipw38nSt+dtxRmBAbPCRvExna7FRAhT4gco4IOjBERNJOD5mtI 72 | b6vsIrswcmGti4KUIgh4uOhU9A+ts4QZ589sh4MrVs9yaYZaj4pVCO8tdCmMSv91 73 | pcd4m3PxPGTc/HHudfRZGdQvSyEd8Ks2yduNesCXV1PQRZRqFPKpBrjLuffZ2kb0 74 | GRBCSpg9CPgZK0uFPWTd9/La0wTVYjTTl9whwDfgFAETtrljg5/VTjECAwEAAaNT 75 | MFEwHQYDVR0OBBYEFEmzAAD/TCsSNBqMqC8HKG5QrU8dMB8GA1UdIwQYMBaAFEmz 76 | AAD/TCsSNBqMqC8HKG5QrU8dMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL 77 | BQADggEBAIfist1aiXRG5+Px4Jn43pXdAPl3tCvQshTpgLtiUJn8IJ174ecqUSqS 78 | eJAsIhSG1CCtTLIXcjpIQvBZNN70l0WCzL69+SyYniEMTHdl1Nfu/8OqDGz6FZ9v 79 | sHv1yYmjpQ/lOyYnQVUR6GNkLphvv3d914+dA8GrFvxmdCHerZsynxNAdu3wQApS 80 | Lqksj0YzkO6E4TDvrXS9RkTDJnjO4QjMMZKuqTZ6DLYwYrEvru2jVp0zbID1iSFb 81 | nlb4E5jpdWyURjBIdt2UotokR8nHPSRH/+7xndciwIXQWVs/2y/GjV8w5HXgVX8a 82 | QSipF5gRDpkhmpiU7VgeORfJT7nSVpQ= 83 | -----END CERTIFICATE----- 84 | -------------------------------------------------------------------------------- /collector/datacollector/datacollector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Datacollector runs a tool to collect the data needed to monitor Certificate 16 | // Transparency Logs, which can then be used to check that they are adhering to 17 | // RFC 6962. 18 | package main 19 | 20 | import ( 21 | "context" 22 | "flag" 23 | "fmt" 24 | "io/ioutil" 25 | "net/http" 26 | "time" 27 | 28 | "github.com/golang/glog" 29 | "github.com/google/certificate-transparency-go/x509" 30 | "github.com/google/certificate-transparency-go/x509util" 31 | "github.com/google/monologue/certgen" 32 | "github.com/google/monologue/collector" 33 | "github.com/google/monologue/ctlog" 34 | "github.com/google/monologue/storage/print" 35 | "github.com/google/trillian/crypto/keys/pem" 36 | ) 37 | 38 | var ( 39 | getRootsPeriod = flag.Duration("get_roots_period", 0, "How regularly the monitor should get root certificates from the Log") 40 | getSTHPeriod = flag.Duration("get_sth_period", 0, "How regularly the monitor should get an STH from the Log") 41 | addChainPeriod = flag.Duration("add_chain_period", 0, "How regularly the monitor should submit a (pre-)certificate to the Log") 42 | // TODO(katjoyce): Change to read from log_list.json or all_logs_list.json to get Log details. 43 | // TODO(katjoyce): Add ability to run against multiple Logs. 44 | logURL = flag.String("log_url", "", "The URL of the Log to monitor, e.g. https://ct.googleapis.com/pilot/") 45 | logName = flag.String("log_name", "", "A short, snappy, canonical name for the Log to monitor, e.g. google_pilot") 46 | b64PubKey = flag.String("public_key", "", "The base64-encoded public key of the Log to monitor") 47 | mmd = flag.Duration("mmd", 24*time.Hour, "The Maximum Merge Delay for the Log") 48 | 49 | signingCertFile = flag.String("signing_cert", "", "Path to the certificate containing the public key that corresponds to the signing key. Only needed if add_chain_period is not 0") 50 | signingKeyFile = flag.String("signing_key", "", "Path to the private key for signing certificates to submit to the Log. Only needed if add_chain_period is not 0") 51 | ) 52 | 53 | func main() { 54 | flag.Parse() 55 | if *logURL == "" { 56 | glog.Exit("No Log URL provided.") 57 | } 58 | if *logName == "" { 59 | glog.Exit("No Log name provided.") 60 | } 61 | if *b64PubKey == "" { 62 | glog.Exit("No public key provided.") 63 | } 64 | 65 | ctx := context.Background() 66 | l, err := ctlog.New(*logURL, *logName, *b64PubKey, *mmd, nil) 67 | if err != nil { 68 | glog.Exitf("Unable to obtain Log metadata: %s", err) 69 | } 70 | 71 | var ca *certgen.CA 72 | if *addChainPeriod > 0 { 73 | var err error 74 | if ca, err = setupCA(l, *signingCertFile, *signingKeyFile); err != nil { 75 | glog.Exitf("Unable to create CA: %s", err) 76 | } 77 | } 78 | 79 | cfg := &collector.Config{ 80 | Log: l, 81 | GetSTHPeriod: *getSTHPeriod, 82 | GetRootsPeriod: *getRootsPeriod, 83 | AddChainPeriod: *addChainPeriod, 84 | CA: ca, 85 | } 86 | 87 | if err := collector.Run(ctx, cfg, &http.Client{}, &print.Storage{}); err != nil { 88 | glog.Exit(err) 89 | } 90 | } 91 | 92 | func setupCA(ctl *ctlog.Log, signingCertFile, signingKeyFile string) (*certgen.CA, error) { 93 | // TODO(katjoyce): Add support for other key encodings and 94 | // generally improve key management here. 95 | signingCertPEM, err := ioutil.ReadFile(signingCertFile) 96 | if err != nil { 97 | return nil, fmt.Errorf("Error reading signing cert from %s: %s", signingCertFile, err) 98 | } 99 | signingCert, err := x509util.CertificateFromPEM(signingCertPEM) 100 | if err != nil { 101 | return nil, fmt.Errorf("Error parsing signing cert: %s", err) 102 | } 103 | 104 | signingKeyPEM, err := ioutil.ReadFile(signingKeyFile) 105 | if err != nil { 106 | return nil, fmt.Errorf("Error reading signing key from %s: %s", signingKeyFile, err) 107 | } 108 | signingKey, err := pem.UnmarshalPrivateKey(string(signingKeyPEM), "") 109 | if err != nil { 110 | return nil, fmt.Errorf("Error parsing signing key: %s", err) 111 | } 112 | 113 | certConfig := certgen.CertificateConfig{ 114 | SubjectCommonName: "flowers-to-the-world.com", 115 | SubjectOrganization: "Google", 116 | SubjectOrganizationalUnit: "Certificate Transparency", 117 | SubjectLocality: "London", 118 | SubjectCountry: "GB", 119 | SignatureAlgorithm: x509.SHA256WithRSA, 120 | DNSPrefix: ctl.Name, 121 | } 122 | 123 | return &certgen.CA{ 124 | SigningCert: signingCert, 125 | SigningKey: signingKey, 126 | CertConfig: certConfig, 127 | }, nil 128 | } 129 | -------------------------------------------------------------------------------- /sthgetter/sthgetter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package sthgetter periodically gets an STH from a Log, checks that each one 16 | // meets per-STH requirements defined in RFC 6962, and stores them. 17 | package sthgetter 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strings" 23 | "time" 24 | 25 | "github.com/golang/glog" 26 | ct "github.com/google/certificate-transparency-go" 27 | "github.com/google/certificate-transparency-go/schedule" 28 | "github.com/google/monologue/apicall" 29 | "github.com/google/monologue/client" 30 | "github.com/google/monologue/ctlog" 31 | "github.com/google/monologue/errors" 32 | "github.com/google/monologue/storage" 33 | ) 34 | 35 | const logStr = "STH Getter" 36 | 37 | // APICallSTHWriter represents a type that can store API Calls and store STHs. 38 | type APICallSTHWriter interface { 39 | storage.APICallWriter 40 | storage.STHWriter 41 | } 42 | 43 | // Run runs an STH Getter, which periodically gets an STH from a Log, checks 44 | // that each one meets per-STH requirements defined in RFC 6962, and stores 45 | // them. 46 | func Run(ctx context.Context, lc *client.LogClient, sv *ct.SignatureVerifier, st APICallSTHWriter, l *ctlog.Log, period time.Duration) { 47 | glog.Infof("%s: %s: started with period %v", l.URL, logStr, period) 48 | 49 | schedule.Every(ctx, period, func(ctx context.Context) { 50 | getCheckStoreSTH(ctx, lc, sv, st, l) 51 | }) 52 | 53 | glog.Infof("%s: %s: stopped", l.URL, logStr) 54 | } 55 | 56 | func getCheckStoreSTH(ctx context.Context, lc *client.LogClient, sv *ct.SignatureVerifier, st APICallSTHWriter, l *ctlog.Log) { 57 | // Get STH from Log. 58 | glog.Infof("%s: %s: getting STH...", l.URL, logStr) 59 | sth, httpData, getErr := lc.GetSTH() 60 | if getErr != nil { 61 | glog.Errorf("%s: %s: error getting STH: %s", l.URL, logStr, getErr) 62 | } 63 | if len(httpData.Body) > 0 { 64 | glog.Infof("%s: %s: response: %s", l.URL, logStr, httpData.Body) 65 | } 66 | 67 | // Store get-sth API call. 68 | apiCall := apicall.New(ct.GetSTHStr, httpData, getErr) 69 | glog.Infof("%s: %s: writing API Call...", l.URL, logStr) 70 | if err := st.WriteAPICall(ctx, l, apiCall); err != nil { 71 | glog.Errorf("%s: %s: error writing API Call %s: %s", l.URL, logStr, apiCall, err) 72 | } 73 | 74 | if sth == nil { 75 | return 76 | } 77 | 78 | // Verify the STH. 79 | receivedAt := apiCall.End 80 | errs := checkSTH(sth, receivedAt, sv, l) 81 | if len(errs) != 0 { 82 | var b strings.Builder 83 | fmt.Fprintf(&b, "STH verification errors for STH %v:", sth) 84 | for _, e := range errs { 85 | fmt.Fprintf(&b, "\n\t%T: %v,", e, e) 86 | } 87 | glog.Infof("%s: %s: %s", l.URL, logStr, b.String()) 88 | } 89 | 90 | // Store STH & associated errors. 91 | glog.Infof("%s: %s: writing STH...", l.URL, logStr) 92 | if err := st.WriteSTH(ctx, l, sth, receivedAt, errs); err != nil { 93 | glog.Infof("%s: %s: error writing STH %s and associated errors: %s", l.URL, logStr, sth, err) 94 | } 95 | } 96 | 97 | func checkSTH(sth *ct.SignedTreeHead, receivedAt time.Time, sv *ct.SignatureVerifier, l *ctlog.Log) []error { 98 | var errs []error 99 | // Check that the STH signature verifies. 100 | glog.Infof("%s: %s: verifying STH signature...", l.URL, logStr) 101 | if err := sv.VerifySTHSignature(*sth); err != nil { 102 | errs = append(errs, &errors.SignatureVerificationError{Err: err}) 103 | glog.Warningf("%s: %s: STH signature verification failed", l.URL, logStr) 104 | } 105 | 106 | // Check STH is not older than the MMD of the Log. 107 | if err := checkSTHTimestamp(sth, receivedAt, l.MMD); err != nil { 108 | errs = append(errs, &OldTimestampError{Err: err}) 109 | glog.Warningf("%s: %s: STH timestamp verification failed", l.URL, logStr) 110 | } 111 | 112 | // TODO(katjoyce): Implement other checks on the STH: 113 | // - Check that the root hash isn't different from any previously known root 114 | // hashes for this tree size. 115 | // - If tree size is 0, Check that the root hash is the SHA-256 hash of the 116 | // empty string. 117 | // - Check that the root hash is the right length? Question because client 118 | // code already checks this when converting ct.GetSTHResponse to 119 | // ct.SignedTreeHead. 120 | 121 | return errs 122 | } 123 | 124 | // checkSTHTimestamp checks that the STH was "no older than the Maximum Merge 125 | // Delay" (RFC 6962 section 3.5) when it was received. 126 | func checkSTHTimestamp(sth *ct.SignedTreeHead, receivedAt time.Time, mmd time.Duration) error { 127 | sthTimestamp := time.Unix(0, int64(sth.Timestamp*uint64(time.Millisecond))) 128 | // If the timestamp of the STH is more than the mmd before the time the STH 129 | // was received, report an error. 130 | if sthTimestamp.Before(receivedAt.Add(-mmd)) { 131 | return fmt.Errorf("STH timestamp %v is more than %v before %v", sthTimestamp, mmd, receivedAt) 132 | } 133 | return nil 134 | } 135 | 136 | // OldTimestampError indicates that an STH was older than the MMD of the Log. 137 | type OldTimestampError struct { 138 | Err error 139 | } 140 | 141 | func (e *OldTimestampError) Error() string { 142 | return e.Err.Error() 143 | } 144 | -------------------------------------------------------------------------------- /rootsanalyzer/rootsanalyzer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package rootsanalyzer reports on changes in the set of root certificates 16 | // returned by a CT Log's get-roots endpoint. 17 | package rootsanalyzer 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "crypto/sha256" 23 | "net/url" 24 | "path" 25 | "sort" 26 | "strings" 27 | "text/template" 28 | 29 | "github.com/golang/glog" 30 | ct "github.com/google/certificate-transparency-go" 31 | "github.com/google/certificate-transparency-go/x509" 32 | "github.com/google/monologue/ctlog" 33 | "github.com/google/monologue/incident" 34 | "github.com/google/monologue/storage" 35 | ) 36 | 37 | const logStr = "Roots Analyzer" 38 | 39 | var ( 40 | incidentTemplate = template.Must(template.New("roots_incident").Funcs(template.FuncMap{ 41 | "sha256": sha256.Sum256, 42 | }).Parse(`The root certificates accepted by {{ .Log.Name }} ({{ .Log.URL }}) have changed. 43 | {{ if gt (len .AddedCerts) 0 }} 44 | Certificates added ({{ len .AddedCerts }}): 45 | {{ range .AddedCerts }}{{ .Subject }} (SHA256: {{ sha256 .Raw | printf "%X" }}) 46 | {{ end }}{{ end }}{{ if gt (len .RemovedCerts) 0 }} 47 | Certificates removed ({{ len .RemovedCerts }}): 48 | {{ range .RemovedCerts }}{{ .Subject }} (SHA256: {{ sha256 .Raw | printf "%X" }}) 49 | {{ end }}{{ end }}`)) 50 | ) 51 | 52 | type incidentTemplateArgs struct { 53 | Log *ctlog.Log 54 | AddedCerts []*x509.Certificate 55 | RemovedCerts []*x509.Certificate 56 | } 57 | 58 | // Run starts a Roots Analyzer, which watches a CT log's root certificates and creates incident reports for changes to them. 59 | func Run(ctx context.Context, st storage.RootsReader, rep incident.Reporter, l *ctlog.Log) { 60 | rootSetChan, err := st.WatchRoots(ctx, l) 61 | if err != nil { 62 | glog.Errorf("%s: %s: storage.RootsReader.WatchRoots() = %q", l.URL, logStr, err) 63 | return 64 | } 65 | 66 | var lastRootSetID storage.RootSetID 67 | for { 68 | select { 69 | case <-ctx.Done(): 70 | return 71 | case rootSetID := <-rootSetChan: 72 | if lastRootSetID != "" && lastRootSetID != rootSetID { 73 | // TODO(RJPercival): If the root set is flapping back to what it recently was, suppress sending an incident report 74 | // since it could just be the result of skew between log frontends. However, if it doesn't flap back to the new 75 | // root set again within a certain amount of time, then the suppressed report should be sent. 76 | oldRoots, err := st.ReadRoots(ctx, lastRootSetID) 77 | if err != nil { 78 | glog.Errorf("%s: %s: %s", l.URL, logStr, err) 79 | return 80 | } 81 | newRoots, err := st.ReadRoots(ctx, rootSetID) 82 | if err != nil { 83 | glog.Errorf("%s: %s: %s", l.URL, logStr, err) 84 | return 85 | } 86 | addedCerts, removedCerts := diffRootSets(oldRoots, newRoots) 87 | if err := reportChange(ctx, rep, l, addedCerts, removedCerts); err != nil { 88 | glog.Errorf("%s: %s: %s", l.URL, logStr, err) 89 | return 90 | } 91 | } 92 | lastRootSetID = rootSetID 93 | } 94 | } 95 | } 96 | 97 | // diffRootSets returns the certificates that have been added or removed in new, relative to old. 98 | // Neither old nor new are allowed to contain duplicates. 99 | func diffRootSets(old, new []*x509.Certificate) (added, removed []*x509.Certificate) { 100 | oldSet := make(map[string]*x509.Certificate, len(old)) 101 | for _, cert := range old { 102 | oldSet[string(cert.Raw)] = cert 103 | } 104 | // This algorithm assumes that there are no duplicates in new. 105 | // TODO(RJPercival): Support old and new containing duplicate certificates. 106 | for _, cert := range new { 107 | certDER := string(cert.Raw) 108 | if oldSet[certDER] != nil { 109 | // cert appears in both old and new - remove it from oldSet 110 | // so that, after the loop, oldSet contains only certs 111 | // that are in old but not new. 112 | delete(oldSet, certDER) 113 | } else { 114 | // cert is only in new. 115 | added = append(added, cert) 116 | } 117 | } 118 | for _, cert := range oldSet { 119 | removed = append(removed, cert) 120 | } 121 | return added, removed 122 | } 123 | 124 | // sortCerts sorts a slice of certificates first by their subject, then by their raw DER. 125 | func sortCerts(certs []*x509.Certificate) { 126 | sort.Slice(certs, func(i, j int) bool { 127 | if subj1, subj2 := certs[i].Subject.String(), certs[j].Subject.String(); subj1 != subj2 { 128 | return subj1 < subj2 129 | } 130 | return bytes.Compare(certs[i].Raw, certs[j].Raw) < 0 131 | }) 132 | } 133 | 134 | func reportChange(ctx context.Context, rep incident.Reporter, l *ctlog.Log, addedCerts, removedCerts []*x509.Certificate) error { 135 | fullURL := l.URL 136 | getRootsURL, err := url.Parse(l.URL) 137 | if err != nil { 138 | glog.Errorf("%s: %s: failed to parse CT Log URL: %v", l.URL, logStr, err) 139 | } else { 140 | getRootsURL.Path = path.Join(getRootsURL.Path, ct.GetRootsPath) 141 | fullURL = getRootsURL.String() 142 | } 143 | 144 | // Sort certs so that the report is deterministic - makes testing easier. 145 | sortCerts(addedCerts) 146 | sortCerts(removedCerts) 147 | 148 | var strBuilder strings.Builder 149 | if err := incidentTemplate.Execute(&strBuilder, incidentTemplateArgs{ 150 | Log: l, 151 | AddedCerts: addedCerts, 152 | RemovedCerts: removedCerts, 153 | }); err != nil { 154 | return err 155 | } 156 | rep.LogUpdate(ctx, l.URL, "Root certificates changed", fullURL, strBuilder.String()) 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /sthgetter/sthgetter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sthgetter 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | "time" 21 | 22 | ct "github.com/google/certificate-transparency-go" 23 | "github.com/google/monologue/ctlog" 24 | "github.com/google/monologue/errors" 25 | "github.com/google/monologue/testonly" 26 | ) 27 | 28 | var ( 29 | // A valid STH from the Google Pilot Log. 30 | validSTH = &ct.GetSTHResponse{ 31 | TreeSize: 580682455, 32 | Timestamp: 1554897886201, // 2019-04-10 12:04:46.201 UTC 33 | SHA256RootHash: testonly.MustB64Decode("VicMkhzrGNv+lNCwXRVHH0WniZuDg3IXhgPai5kyHdA="), 34 | TreeHeadSignature: testonly.MustB64Decode("BAMARzBFAiEAs0GiYnPT5ZQJ2LGLhLmIXZXSLg+N+CxTkJL75tECEqgCIBZzJGyzH9h+IL63XCvRlfTKhLvzSxVicrT30+rwTSU0"), 35 | } 36 | // The public key for the Google Pilot Log. 37 | b64PubKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==" 38 | // A time less than 24 hours after the timestamp of validSTH. If validSTH 39 | // was received at this time, it would not be considered 'too old'. 40 | validReceiveTime = time.Date(2019, time.April, 10, 15, 0, 0, 0, time.UTC) 41 | invalidReceiveTime = time.Date(2019, time.April, 11, 15, 0, 0, 0, time.UTC) 42 | ) 43 | 44 | func TestCheckSTH(t *testing.T) { 45 | // Create Log structure. 46 | l, err := ctlog.New("https://ct.googleapis.com", "google_pilot", b64PubKey, 24*time.Hour, nil) 47 | if err != nil { 48 | t.Fatalf("Unable to obtain Log metadata: %s", err) 49 | } 50 | 51 | // Create signature verifier. 52 | sv, err := ct.NewSignatureVerifier(l.PublicKey) 53 | if err != nil { 54 | t.Fatalf("Couldn't create signature verifier: %s", err) 55 | } 56 | 57 | tests := []struct { 58 | desc string 59 | sth *ct.GetSTHResponse 60 | receivedAt time.Time 61 | wantErrTypes []reflect.Type 62 | }{ 63 | { 64 | desc: "valid", 65 | sth: validSTH, 66 | receivedAt: validReceiveTime, 67 | }, 68 | { 69 | desc: "invalid signature", 70 | // STH with TreeSize modified (set to 0) so that signature will not 71 | // verify. 72 | sth: &ct.GetSTHResponse{ 73 | Timestamp: validSTH.Timestamp, 74 | SHA256RootHash: validSTH.SHA256RootHash, 75 | TreeHeadSignature: validSTH.TreeHeadSignature, 76 | }, 77 | receivedAt: validReceiveTime, 78 | wantErrTypes: []reflect.Type{ 79 | reflect.TypeOf(&errors.SignatureVerificationError{}), 80 | }, 81 | }, 82 | { 83 | desc: "old STH", 84 | sth: validSTH, 85 | receivedAt: invalidReceiveTime, 86 | wantErrTypes: []reflect.Type{ 87 | reflect.TypeOf(&OldTimestampError{}), 88 | }, 89 | }, 90 | { 91 | desc: "invalid sig and old STH", 92 | sth: &ct.GetSTHResponse{ 93 | Timestamp: validSTH.Timestamp, 94 | SHA256RootHash: validSTH.SHA256RootHash, 95 | TreeHeadSignature: validSTH.TreeHeadSignature, 96 | }, 97 | receivedAt: invalidReceiveTime, 98 | wantErrTypes: []reflect.Type{ 99 | reflect.TypeOf(&errors.SignatureVerificationError{}), 100 | reflect.TypeOf(&OldTimestampError{}), 101 | }, 102 | }, 103 | } 104 | 105 | for _, test := range tests { 106 | t.Run(test.desc, func(t *testing.T) { 107 | sth, err := test.sth.ToSignedTreeHead() 108 | if err != nil { 109 | t.Fatalf("error converting ct.GetSTHResponse to ct.SignedTreeHead: %s", err) 110 | } 111 | 112 | errs := checkSTH(sth, test.receivedAt, sv, l) 113 | if len(errs) != len(test.wantErrTypes) { 114 | t.Fatalf("checkSTH(%v, %v, _, _) = %v (%d errors), want errors of types %v (%d errors)", sth, test.receivedAt, errs, len(errs), test.wantErrTypes, len(test.wantErrTypes)) 115 | } 116 | 117 | // Slightly brittle test: Relies on the order of the returned slice 118 | // of errors from checkSTH(). 119 | for i, err := range errs { 120 | if got := reflect.TypeOf(err); got != test.wantErrTypes[i] { 121 | t.Errorf("The error at position %d is of type %v, want error of type %v", i, got, test.wantErrTypes[i]) 122 | } 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestCheckSTHTimestamp(t *testing.T) { 129 | tests := []struct { 130 | desc string 131 | sthTimestamp time.Time 132 | receivedAt time.Time 133 | mmd time.Duration 134 | wantErr bool 135 | }{ 136 | { 137 | desc: "STH less than MMD old", 138 | sthTimestamp: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), 139 | receivedAt: time.Date(2019, time.January, 1, 12, 0, 0, 0, time.UTC), 140 | mmd: 24 * time.Hour, 141 | }, 142 | { 143 | desc: "STH exactly MMD old", 144 | sthTimestamp: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), 145 | receivedAt: time.Date(2019, time.January, 2, 0, 0, 0, 0, time.UTC), 146 | mmd: 24 * time.Hour, 147 | }, 148 | { 149 | desc: "STH just greater than MMD old", 150 | sthTimestamp: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), 151 | receivedAt: time.Date(2019, time.January, 2, 0, 0, 0, 1, time.UTC), 152 | mmd: 24 * time.Hour, 153 | wantErr: true, 154 | }, 155 | { 156 | desc: "STH greater than MMD old", 157 | sthTimestamp: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), 158 | receivedAt: time.Date(2019, time.January, 2, 12, 0, 0, 0, time.UTC), 159 | mmd: 24 * time.Hour, 160 | wantErr: true, 161 | }, 162 | } 163 | 164 | for _, test := range tests { 165 | t.Run(test.desc, func(t *testing.T) { 166 | sth := &ct.SignedTreeHead{ 167 | Timestamp: uint64(test.sthTimestamp.UnixNano() / time.Millisecond.Nanoseconds()), 168 | } 169 | err := checkSTHTimestamp(sth, test.receivedAt, test.mmd) 170 | if gotErr := (err != nil); gotErr != test.wantErr { 171 | t.Fatalf("checkSTHTimestamp(%v, %v, %v) = %v, want err? %t", sth, test.receivedAt, test.mmd, err, test.wantErr) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /certsubmitter/certsubmitter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package certsubmitter 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | "time" 21 | 22 | ct "github.com/google/certificate-transparency-go" 23 | "github.com/google/monologue/ctlog" 24 | "github.com/google/monologue/errors" 25 | "github.com/google/monologue/interval" 26 | "github.com/google/monologue/testdata" 27 | "github.com/google/monologue/testonly" 28 | ) 29 | 30 | var ( 31 | url = "https://ct.googleapis.com/logs/xenon2019/" 32 | name = "google_xenon2019" 33 | pubKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/XyDwqzXL9i2GTjMYkqaEyiRL0Dy9sHq/BTebFdshbvCaXXEh6mjUK0Yy+AsDcI4MpzF1l7Kded2MD5zi420gA==" 34 | mmd = 24 * time.Hour 35 | tempInt = &interval.Interval{ 36 | Start: time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), 37 | End: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 38 | } 39 | ) 40 | 41 | func TestCheckSCT(t *testing.T) { 42 | tests := []struct { 43 | desc string 44 | sct *ct.AddChainResponse 45 | receivedAt time.Time 46 | wantErrTypes []reflect.Type 47 | }{ 48 | { 49 | desc: "SCT not v1", 50 | sct: &ct.AddChainResponse{ 51 | SCTVersion: 1, 52 | ID: testonly.MustB64Decode("CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA="), 53 | Timestamp: 1512556025588, 54 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="), 55 | }, 56 | receivedAt: time.Unix(1512559800, 0), 57 | wantErrTypes: []reflect.Type{ 58 | reflect.TypeOf(&SCTVersionError{}), 59 | reflect.TypeOf(&errors.SignatureVerificationError{}), // Version is covered by the SCT signature, so changing the version also invalidates the signature. 60 | }, 61 | }, 62 | { 63 | desc: "SCT contains wrong LogID", 64 | sct: &ct.AddChainResponse{ 65 | ID: testonly.MustB64Decode("B7dcG+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohw="), 66 | Timestamp: 1512556025588, 67 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="), 68 | }, 69 | receivedAt: time.Unix(1512559800, 0), 70 | wantErrTypes: []reflect.Type{ 71 | reflect.TypeOf(&SCTLogIDError{}), // Log ID is not covered by the SCT signature. 72 | }, 73 | }, 74 | { 75 | desc: "SCT contains extensions data", 76 | sct: &ct.AddChainResponse{ 77 | ID: testonly.MustB64Decode("CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA="), 78 | Timestamp: 1512556025588, 79 | Extensions: "data", 80 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="), 81 | }, 82 | receivedAt: time.Unix(1512559800, 0), 83 | wantErrTypes: []reflect.Type{ 84 | reflect.TypeOf(&SCTExtensionsError{}), 85 | reflect.TypeOf(&errors.SignatureVerificationError{}), // Extensions field is covered by the SCT signature, so changing the extensions data also invalidates the signature. 86 | 87 | }, 88 | }, 89 | { 90 | desc: "SCT signature doesn't verify", 91 | sct: &ct.AddChainResponse{ 92 | ID: testonly.MustB64Decode("CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA="), 93 | Timestamp: 1512556025588, 94 | Extensions: "", 95 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3D="), // Last alpha-numeric character Q has been changed to D, so signature is now invalid. 96 | }, 97 | receivedAt: time.Unix(1512559800, 0), 98 | wantErrTypes: []reflect.Type{ 99 | reflect.TypeOf(&errors.SignatureVerificationError{}), 100 | }, 101 | }, 102 | { 103 | desc: "SCT from future", 104 | sct: &ct.AddChainResponse{ 105 | ID: testonly.MustB64Decode("CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA="), 106 | Timestamp: 1512556025588, 107 | Extensions: "", 108 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="), 109 | }, 110 | receivedAt: time.Unix(1512552600, 0), 111 | wantErrTypes: []reflect.Type{ 112 | reflect.TypeOf(&SCTFromFutureError{}), 113 | }, 114 | }, 115 | { 116 | desc: "no errors", 117 | sct: &ct.AddChainResponse{ 118 | ID: testonly.MustB64Decode("CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA="), 119 | Timestamp: 1512556025588, 120 | Extensions: "", 121 | Signature: testonly.MustB64Decode("BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="), 122 | }, 123 | receivedAt: time.Unix(1512559800, 0), 124 | }, 125 | } 126 | 127 | ctl, err := ctlog.New(url, name, pubKey, mmd, tempInt) 128 | if err != nil { 129 | t.Fatalf("ctlog.New(%s, %s, %s, %s, %v) = _, %s", url, name, pubKey, mmd, tempInt, err) 130 | } 131 | 132 | chain := testonly.MustCreateChain([]string{testdata.LeafCertPEM, testdata.IntermediateCertPEM, testdata.RootCertPEM}) 133 | 134 | sv, err := ct.NewSignatureVerifier(ctl.PublicKey) 135 | if err != nil { 136 | t.Errorf("couldn't create signature verifier: %s", err) 137 | } 138 | 139 | for _, test := range tests { 140 | t.Run(test.desc, func(t *testing.T) { 141 | sct, err := test.sct.ToSignedCertificateTimestamp() 142 | if err != nil { 143 | t.Fatalf("error converting ct.AddChainResponse to ct.SignedCertificateTimestamp: %s", err) 144 | } 145 | 146 | errs := checkSCT(sct, chain, sv, ctl, test.receivedAt) 147 | if len(errs) != len(test.wantErrTypes) { 148 | t.Fatalf("checkSCT(%v) = %v (%d errors), want errors of types %v (%d errors)", sct, errs, len(errs), test.wantErrTypes, len(test.wantErrTypes)) 149 | } 150 | 151 | // Slightly brittle test: Relies on the order of the returned slice 152 | // of errors from checkSCT(). 153 | for i, err := range errs { 154 | if got := reflect.TypeOf(err); got != test.wantErrTypes[i] { 155 | t.Errorf("The error at position %d is of type %v, want error of type %v", i, got, test.wantErrTypes[i]) 156 | } 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /certsubmitter/certsubmitter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package certsubmitter periodically issues a certificate or pre-certificate 16 | // and submits it to a CT Log. 17 | package certsubmitter 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "strings" 24 | "time" 25 | 26 | "github.com/golang/glog" 27 | ct "github.com/google/certificate-transparency-go" 28 | "github.com/google/certificate-transparency-go/ctutil" 29 | "github.com/google/certificate-transparency-go/logid" 30 | "github.com/google/certificate-transparency-go/schedule" 31 | "github.com/google/certificate-transparency-go/x509" 32 | "github.com/google/monologue/apicall" 33 | "github.com/google/monologue/certgen" 34 | "github.com/google/monologue/client" 35 | "github.com/google/monologue/ctlog" 36 | "github.com/google/monologue/errors" 37 | "github.com/google/monologue/storage" 38 | ) 39 | 40 | const logStr = "Certificate Submitter" 41 | 42 | // Run runs a Certificate Submitter, which periodically issues a certificate or 43 | // pre-certificate, submits it to a CT Log, and checks and stores the SCT that 44 | // the Log returns. 45 | func Run(ctx context.Context, lc *client.LogClient, ca *certgen.CA, sv *ct.SignatureVerifier, st storage.APICallWriter, l *ctlog.Log, period time.Duration) { 46 | glog.Infof("%s: %s: started with period %v", l.URL, logStr, period) 47 | schedule.Every(ctx, period, func(ctx context.Context) { 48 | chain, sct, receivedAt, err := issueAndSubmit(ctx, lc, ca, st, l, false /* isPreSubmit */) 49 | if err != nil { 50 | return 51 | } 52 | 53 | // Verify the SCT. 54 | errs := checkSCT(sct, chain, sv, l, *receivedAt) 55 | 56 | // Log any errors found. 57 | if len(errs) != 0 { 58 | var b strings.Builder 59 | fmt.Fprintf(&b, "SCT verification errors for SCT %v:", sct) 60 | for _, e := range errs { 61 | fmt.Fprintf(&b, "\n\t%T: %v,", e, e) 62 | } 63 | glog.Infof("%s: %s: %s", l.URL, logStr, b.String()) 64 | } 65 | 66 | // TODO(katjoyce): Store the SCT & associated errors. 67 | }) 68 | 69 | glog.Infof("%s: %s: stopped", l.URL, logStr) 70 | } 71 | 72 | func issueAndSubmit(ctx context.Context, lc *client.LogClient, ca *certgen.CA, st storage.APICallWriter, l *ctlog.Log, isPreChain bool) ([]*x509.Certificate, *ct.SignedCertificateTimestamp, *time.Time, error) { 73 | prefix := "" 74 | if isPreChain { 75 | prefix = "pre-" 76 | } 77 | 78 | glog.Infof("%s: %s: issuing %scertificate chain...", l.URL, logStr, prefix) 79 | var chain []*x509.Certificate 80 | var err error 81 | if isPreChain { 82 | chain, err = ca.IssuePrecertificateChain() 83 | } else { 84 | chain, err = ca.IssueCertificateChain() 85 | } 86 | if err != nil { 87 | glog.Errorf("%s: %s: ca.Issue%sCertificateChain(): %s", l.URL, logStr, prefix, err) 88 | return nil, nil, nil, err 89 | } 90 | 91 | glog.Infof("%s: %s: adding %schain...", l.URL, logStr, prefix) 92 | var sct *ct.SignedCertificateTimestamp 93 | var httpData *client.HTTPData 94 | var addErr error 95 | if isPreChain { 96 | sct, httpData, addErr = lc.AddPreChain(chain) 97 | } else { 98 | sct, httpData, addErr = lc.AddChain(chain) 99 | } 100 | if len(httpData.Body) > 0 { 101 | glog.Infof("%s: %s: response: %s", l.URL, logStr, httpData.Body) 102 | } 103 | 104 | // Store add-(pre-)chain API call. 105 | apiCall := apicall.New(ct.AddChainStr, httpData, addErr) 106 | glog.Infof("%s: %s: writing API Call...", l.URL, logStr) 107 | if err := st.WriteAPICall(ctx, l, apiCall); err != nil { 108 | glog.Errorf("%s: %s: error writing API Call %s: %s", l.URL, logStr, apiCall, err) 109 | } 110 | return chain, sct, &httpData.Timing.End, nil 111 | } 112 | 113 | func checkSCT(sct *ct.SignedCertificateTimestamp, chain []*x509.Certificate, sv *ct.SignatureVerifier, l *ctlog.Log, receivedAt time.Time) []error { 114 | var errs []error 115 | 116 | // Check that the SCT is version 1. 117 | // 118 | // Section 4.1 of RFC 6962 says ‘a compliant v1 client implementation must 119 | // not expect this to be v1’, so it would not count as Log misbehaviour if 120 | // a Log issued an SCT with the version set to something else. However, 121 | // given the current state of the CT ecosystem it would be strange to see 122 | // such a thing, and worth noting if seen. 123 | // 124 | // TODO(katjoyce): Add a way to classify errors as 'misbehaviour' vs 125 | // 'unexpected behaviour worth noting'. 126 | if sct.SCTVersion != ct.V1 { 127 | errs = append(errs, &SCTVersionError{Got: sct.SCTVersion, Want: ct.V1}) 128 | } 129 | 130 | // Check that the Log ID is the ID of the Log that the SCT was received 131 | // from. 132 | if sct.LogID.KeyID != l.LogID { 133 | errs = append(errs, &SCTLogIDError{Got: sct.LogID.KeyID, Want: l.LogID}) 134 | } 135 | 136 | // Check that Extensions in the SCT is empty. 137 | // 138 | // Section 3.2 of RFC 6962 says '"extensions" are future extensions to this 139 | // protocol version (v1)', but then goes on to say 'Currently, no extensions 140 | // are specified' and Section 4.1 says 'Logs should set this to the empty 141 | // string'. This is another case where having data in this field may not 142 | // count as misbehaviour, but may be note worthy if seen. 143 | if sct.Extensions != nil && !bytes.Equal(sct.Extensions, []byte{}) { 144 | errs = append(errs, &SCTExtensionsError{Extensions: sct.Extensions}) 145 | } 146 | 147 | // Verify the signature of the SCT. 148 | if err := ctutil.VerifySCTWithVerifier(sv, chain, sct, false); err != nil { 149 | errs = append(errs, &errors.SignatureVerificationError{Err: err}) 150 | } 151 | 152 | // Verify SCT timestamp. 153 | sctTimestamp := ct.TimestampToTime(sct.Timestamp) 154 | // Consider only gaps bigger than 1 second as incidents. 155 | if sctTimestamp.Sub(receivedAt).Milliseconds() > 1000 { 156 | errs = append(errs, &SCTFromFutureError{ReceivedAt: receivedAt, Timestamp: sctTimestamp}) 157 | } 158 | 159 | // TODO(katjoyce): Implement other SCT checks. 160 | 161 | return errs 162 | } 163 | 164 | // SCTVersionError indicates that an SCT contained a version that was not as 165 | // expected. 166 | type SCTVersionError struct { 167 | Got ct.Version 168 | Want ct.Version 169 | } 170 | 171 | func (e *SCTVersionError) Error() string { 172 | return fmt.Sprintf("version is %v, want %v", e.Got, e.Want) 173 | } 174 | 175 | // SCTLogIDError indicates that an SCT contained the wrong Log ID. 176 | type SCTLogIDError struct { 177 | Got logid.LogID 178 | Want logid.LogID 179 | } 180 | 181 | func (e *SCTLogIDError) Error() string { 182 | return fmt.Sprintf("Log ID is %s, want %s", e.Got, e.Want) 183 | } 184 | 185 | // SCTExtensionsError indicates that an SCT contained unexpected data in its 186 | // extensions field. 187 | type SCTExtensionsError struct { 188 | Extensions ct.CTExtensions 189 | } 190 | 191 | func (e *SCTExtensionsError) Error() string { 192 | return fmt.Sprintf("unexpected extensions data: %v", e.Extensions) 193 | } 194 | 195 | // SCTFromFutureError indicates that an SCT timestamp is fresher than time when 196 | // it was received. 197 | type SCTFromFutureError struct { 198 | ReceivedAt time.Time 199 | Timestamp time.Time 200 | } 201 | 202 | func (e *SCTFromFutureError) Error() string { 203 | return fmt.Sprintf("SCT with Timestamp %s received at %s", e.Timestamp.Format(time.RFC3339), e.ReceivedAt.Format(time.RFC3339)) 204 | } 205 | -------------------------------------------------------------------------------- /certgen/certgen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package certgen generates (pre-)certificates and (pre-)certificate chains. 16 | package certgen 17 | 18 | import ( 19 | "crypto" 20 | crand "crypto/rand" 21 | "crypto/rsa" 22 | "fmt" 23 | "math" 24 | "math/big" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/google/certificate-transparency-go/x509" 30 | "github.com/google/certificate-transparency-go/x509/pkix" 31 | "github.com/google/monologue/interval" 32 | ) 33 | 34 | const ( 35 | keySizeBits = 2048 36 | certValidity = time.Hour * 24 37 | ) 38 | 39 | var timeNowUTC = func() time.Time { 40 | return time.Now().UTC() 41 | } 42 | 43 | // CertificateConfig contains details to be used to populate newly created leaf 44 | // certificates. 45 | type CertificateConfig struct { 46 | // Required fields 47 | // 48 | // What these are set to, including the zero values if left unset, is what 49 | // will appear in the leaf certificates. 50 | SubjectCommonName string 51 | SubjectOrganization string 52 | SubjectOrganizationalUnit string 53 | SubjectLocality string 54 | SubjectCountry string 55 | SignatureAlgorithm x509.SignatureAlgorithm 56 | 57 | // Optional fields 58 | 59 | // DNSPrefix is a prefix that will be used in conjunction with the 60 | // SubjectCommonName to create a more specific DNS SAN. 61 | DNSPrefix string 62 | // NotAfterInterval specifies an interval in which the NotAfter time of a 63 | // certificate must fall. 64 | // 65 | // For example, if a certificate is being generated to be submitted to a 66 | // temporal CT Log shard, then, in order to be accepted by the Log, its 67 | // NotAfter value must fall within the Log's temporal range, so this field 68 | // would be set to the temporal interval of the Log. However, if a 69 | // certificate is being generated to be submitted to a non-temporal CT Log, 70 | // this field should be left unset/set to nil. 71 | NotAfterInterval *interval.Interval 72 | } 73 | 74 | // CA is a Certificate Authority that issues certificates and certificate chains 75 | // using its SigningCert and SigningKey. 76 | type CA struct { 77 | SigningCert *x509.Certificate 78 | SigningKey crypto.Signer 79 | CertConfig CertificateConfig 80 | } 81 | 82 | // IssueCertificate creates a new leaf certificate, issued by the key specified 83 | // in the SigningCert and SigningKey fields of the CA, and configured using the 84 | // CertConfig in the CA. 85 | func (ca *CA) IssueCertificate() (*x509.Certificate, error) { 86 | return ca.issueCertificate(false /* Not a precertificate */) 87 | } 88 | 89 | // IssueCertificateChain creates a certificate chain, containing a new leaf 90 | // certificate (as created by IssueCertificate) and the certificate for the key 91 | // that signed it (stored in the SigningCert field of the CA). 92 | func (ca *CA) IssueCertificateChain() ([]*x509.Certificate, error) { 93 | leaf, err := ca.IssueCertificate() 94 | if err != nil { 95 | return nil, fmt.Errorf("error issuing leaf certificate: %s", err) 96 | } 97 | 98 | return []*x509.Certificate{leaf, ca.SigningCert}, nil 99 | } 100 | 101 | // IssuePrecertificate creates a new leaf precertificate, issued by the key 102 | // specified in the SigningCert and SigningKey fields of the CA, and configured 103 | // using the CertConfig in the CA. 104 | func (ca *CA) IssuePrecertificate() (*x509.Certificate, error) { 105 | return ca.issueCertificate(true /* precertificate */) 106 | } 107 | 108 | // IssuePrecertificateChain creates a certificate chain, containing a new leaf 109 | // precertificate (as created by IssuePrecertificate) and the certificate for 110 | // the key that signed it (stored in the SigningCert field of the CA). 111 | // 112 | // TODO(katjoyce): Add precert-signing-cert functionality. 113 | func (ca *CA) IssuePrecertificateChain() ([]*x509.Certificate, error) { 114 | leaf, err := ca.IssuePrecertificate() 115 | if err != nil { 116 | return nil, fmt.Errorf("error issuing leaf precertificate: %s", err) 117 | } 118 | 119 | return []*x509.Certificate{leaf, ca.SigningCert}, nil 120 | } 121 | 122 | func (ca *CA) issueCertificate(precert bool) (*x509.Certificate, error) { 123 | key, err := rsa.GenerateKey(crand.Reader, keySizeBits) 124 | if err != nil { 125 | return nil, fmt.Errorf("error generating key pair: %s", err) 126 | } 127 | 128 | template, err := leafTemplate(ca.CertConfig) 129 | if err != nil { 130 | return nil, fmt.Errorf("error creating leaf template: %s", err) 131 | } 132 | 133 | if precert { 134 | // Add the CT poison extension. 135 | poison := pkix.Extension{ 136 | Id: x509.OIDExtensionCTPoison, 137 | Critical: true, 138 | Value: []byte{0x05, 0x00}, // ASN.1 NULL 139 | } 140 | template.ExtraExtensions = append(template.ExtraExtensions, poison) 141 | } 142 | 143 | leafDER, err := x509.CreateCertificate(crand.Reader, template, ca.SigningCert, key.Public(), ca.SigningKey) 144 | if err != nil { 145 | return nil, fmt.Errorf("error creating leaf certificate: %s", err) 146 | } 147 | 148 | leaf, err := x509.ParseCertificate(leafDER) 149 | if err != nil { 150 | return nil, fmt.Errorf("error parsing leaf certificate DER: %s", err) 151 | } 152 | 153 | return leaf, nil 154 | } 155 | 156 | func leafTemplate(c CertificateConfig) (*x509.Certificate, error) { 157 | sn, err := randSerialNumber() 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | notAfter := timeNowUTC().Add(certValidity) 163 | if c.NotAfterInterval != nil { 164 | notAfter = c.NotAfterInterval.RandomSecond() 165 | } 166 | 167 | return &x509.Certificate{ 168 | SerialNumber: sn, 169 | Subject: pkix.Name{ 170 | Country: []string{c.SubjectCountry}, 171 | Organization: []string{c.SubjectOrganization}, 172 | OrganizationalUnit: []string{c.SubjectOrganizationalUnit}, 173 | Locality: []string{c.SubjectLocality}, 174 | CommonName: c.SubjectCommonName, 175 | }, 176 | NotBefore: notAfter.Add(-certValidity), 177 | NotAfter: notAfter, 178 | SignatureAlgorithm: c.SignatureAlgorithm, 179 | 180 | KeyUsage: x509.KeyUsageDigitalSignature, 181 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 182 | BasicConstraintsValid: true, 183 | IsCA: false, 184 | DNSNames: []string{c.SubjectCommonName, extendedDNSSAN(c.DNSPrefix, c.SubjectCommonName)}, 185 | }, nil 186 | } 187 | 188 | func randSerialNumber() (*big.Int, error) { 189 | i := big.NewInt(0) 190 | return crand.Int(crand.Reader, i.SetUint64(math.MaxUint64)) 191 | } 192 | 193 | // extendedDNSSAN creates a string to be used in the DNSNames SAN. The string 194 | // created is or the format ..... where the 195 | // time elements are based on the time now. For example, if 196 | // extendedDNSSAN(squirrel, example.com) was called at 2019-03-25 12:00 UTC, it 197 | // would return 12.25.03.2019.squirrel.example.com 198 | func extendedDNSSAN(prefix string, url string) string { 199 | now := timeNowUTC() 200 | dns := []string{ 201 | fmt.Sprintf("%02d", now.Hour()), 202 | fmt.Sprintf("%02d", now.Day()), 203 | fmt.Sprintf("%02d", int(now.Month())), 204 | strconv.Itoa(now.Year()), 205 | } 206 | if prefix != "" { 207 | dns = append(dns, prefix) 208 | } 209 | dns = append(dns, url) 210 | return strings.Join(dns, ".") 211 | } 212 | -------------------------------------------------------------------------------- /rootsanalyzer/rootsanalyzer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rootsanalyzer 16 | 17 | import ( 18 | "context" 19 | "encoding/base64" 20 | "testing" 21 | "time" 22 | 23 | "github.com/google/certificate-transparency-go/x509" 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/google/monologue/ctlog" 26 | "github.com/google/monologue/storage" 27 | 28 | itestonly "github.com/google/monologue/incident/testonly" 29 | stestonly "github.com/google/monologue/storage/testonly" 30 | ) 31 | 32 | var ( 33 | root1 = mustParseCert(`MIIH/jCCBeagAwIBAgIBADANBgkqhkiG9w0BAQUFADCB1DELMAkGA1UEBhMCQVQxDzANBgNVBAcTBlZpZW5uYTEQMA4GA1UECBMHQXVzdHJpYTE6MDgGA1UEChMxQVJHRSBEQVRFTiAtIEF1c3RyaWFuIFNvY2lldHkgZm9yIERhdGEgUHJvdGVjdGlvbjEqMCgGA1UECxMhR0xPQkFMVFJVU1QgQ2VydGlmaWNhdGlvbiBTZXJ2aWNlMRQwEgYDVQQDEwtHTE9CQUxUUlVTVDEkMCIGCSqGSIb3DQEJARYVaW5mb0BnbG9iYWx0cnVzdC5pbmZvMB4XDTA2MDgwNzE0MTIzNVoXDTM2MDkxODE0MTIzNVowgdQxCzAJBgNVBAYTAkFUMQ8wDQYDVQQHEwZWaWVubmExEDAOBgNVBAgTB0F1c3RyaWExOjA4BgNVBAoTMUFSR0UgREFURU4gLSBBdXN0cmlhbiBTb2NpZXR5IGZvciBEYXRhIFByb3RlY3Rpb24xKjAoBgNVBAsTIUdMT0JBTFRSVVNUIENlcnRpZmljYXRpb24gU2VydmljZTEUMBIGA1UEAxMLR0xPQkFMVFJVU1QxJDAiBgkqhkiG9w0BCQEWFWluZm9AZ2xvYmFsdHJ1c3QuaW5mbzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANISR+xfmOgNhhVJxN3snvFszVG2+5VPi8SQPVMzsdMTxUjipb/19AOED5x4cfaSl/FbWXUYPycLUS9caMeh6wDz9pU9acN+wqzECjZyelum0PcBeyjHKscyYO5ZuNcLJ92zRQUre2Snc1zokwKXaOz8hNue1NWBR8acwKyXyxnqh6UKo7h1JOdQJw2rFvlWXbGBARZ98+nhJPMIIbm6rF2ex0h5f2rK3zl3BG0bbjrNf85cSKwSPFnyas+ASOH2AGd4IOD9tWR7F5ez5SfdRWubYZkGvvLnnqRtiztrDIHutG+hvhoSQUuerQ75RrRa0QMAlBbAwPOs+3y8lsAp2PkzFomjDh2V2QPUIQzdVghJZciNqyEfVLuZvPFEW3sAGP0qGVjSBcnZKTYl/nfua1lUTwgUopkJRVetB94i/IccoO+ged0KfcB/NegMZk3jtWoWWXFb85CwUl6RAseoucIEb55PtAAt7AjsrkBu8CknIjm2zaCGELoLNex7Wg22ecP6x63B++vtK4QN6t7565pZM2zBKxKMuD7FNiM4GtZ3k5DWd3VqWBkXoRWObnYOo3PhXJVJ28EPlBTF1WIbmas41Wdu0qkZ4Vo6h2pIP5GW48bFJ2tXdDGY9j5xce1+3rBNLPPuj9t7aNcQRCmt7KtQWVKabGpyFE0WFFH3134fAgMBAAGjggHXMIIB0zAdBgNVHQ4EFgQUwAHV4HgfL3Q64+vAIVKmBO4my6QwggEBBgNVHSMEgfkwgfaAFMAB1eB4Hy90OuPrwCFSpgTuJsukoYHapIHXMIHUMQswCQYDVQQGEwJBVDEPMA0GA1UEBxMGVmllbm5hMRAwDgYDVQQIEwdBdXN0cmlhMTowOAYDVQQKEzFBUkdFIERBVEVOIC0gQXVzdHJpYW4gU29jaWV0eSBmb3IgRGF0YSBQcm90ZWN0aW9uMSowKAYDVQQLEyFHTE9CQUxUUlVTVCBDZXJ0aWZpY2F0aW9uIFNlcnZpY2UxFDASBgNVBAMTC0dMT0JBTFRSVVNUMSQwIgYJKoZIhvcNAQkBFhVpbmZvQGdsb2JhbHRydXN0LmluZm+CAQAwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAcYwEQYDVR0gBAowCDAGBgRVHSAAMD0GA1UdEQQ2MDSBFWluZm9AZ2xvYmFsdHJ1c3QuaW5mb4YbaHR0cDovL3d3dy5nbG9iYWx0cnVzdC5pbmZvMD0GA1UdEgQ2MDSBFWluZm9AZ2xvYmFsdHJ1c3QuaW5mb4YbaHR0cDovL3d3dy5nbG9iYWx0cnVzdC5pbmZvMA0GCSqGSIb3DQEBBQUAA4ICAQAVO4iDXg7ePvA+XdwtoUr6KKXWB6UkSM6eeeh5mlwkjlhyFEGFx0XuPChpOEmuIo27jAVtrmW7h7l+djsoY2rWbzMwiH5VBbq5FQOYHWLSzsAPbhyaNO7krx9i0ey0ec/PaZKKWP3Bx3YLXM1SNEhr5Qt/yTIS35gKFtkzVhaP30M/170/xR7FrSGshyya5BwfhQOsi8e3M2JJwfiqK05dhz52Uq5ZfjHhfLpSi1iQ14BGCzQ23u8RyVwiRsI8p39iBG/fPkiO6gs+CKwYGlLW8fbUYi8DuZrWPFN/VSbGNSshdLCJkFTkAYhcnIUqmmVeS1fygBzsZzSaRtwCdv5yN3IJsfAjj1izAn3ueA65PXMSLVWfF2Ovrtiuc7bHUGqFwdt9+5RZcMbDB2xWxbAH/E59kx25J8CwldXnfAW89w8Ks/RuFVdJG7UUAKQwK1r0Vli/djSiPf4BJvDduG3wpOe8IPZRCPbjN4lXNvb3L/7NuGS96tem0P94737hHB5Ufg80GYEQc9LjeAYXttJR+zV4dtp3gzdBPi1GqH6G3lb0ypCetK2wHkUYPDSIAofo8DaR6/LntdIEuS64XY0dmi4LFhnNdqSr+9Hio6LchH176lDq9bIEO4lSOrLDGU+5JrG8vCyy4YGms2G19EVgLyx1xcgtiEsmu3DuO38BLQ==`) 34 | root2 = mustParseCert(`MIIDyzCCArOgAwIBAgIDAOJIMA0GCSqGSIb3DQEBBQUAMIGLMQswCQYDVQQGEwJBVDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRgwFgYDVQQLDA9BLVRydXN0LVF1YWwtMDIxGDAWBgNVBAMMD0EtVHJ1c3QtUXVhbC0wMjAeFw0wNDEyMDIyMzAwMDBaFw0xNDEyMDIyMzAwMDBaMIGLMQswCQYDVQQGEwJBVDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRgwFgYDVQQLDA9BLVRydXN0LVF1YWwtMDIxGDAWBgNVBAMMD0EtVHJ1c3QtUXVhbC0wMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJaRq9eOsFm4Ab20Hq2Z/aH86gyWa48uSUjY6eQkguHYuszr3gdcSMYZggFHQgnhfLmfro/27l5rqKhWiDhWs+b+yZ1PNDhRPJy+86ycHMg9XJqErveULBSyZDdgjhSwOyrNibUir/fkf+4sKzP5jjytTKJXD/uCxY4fAd9TjMEVpN3umpIS0ijpYhclYDHvzzGU833z5Dwhq5D8bc9jp8YSAHFJ1xzIoO1jmn3jjyjdYPnY5harJtHQL73nDQnfbtTs5ThT9GQLulrMgLU4WeyAWWWEMWpfVZFMJOUkmoOEer6A8e5fIAeqdxdsC+JVqpZ4CAKel/Arrlj1gFA//jsCAwEAAaM2MDQwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIQj0rJKbBRc4wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBGyxFjUA2bPkXUSC2SfJ29tmrbiLKal+g6a9M8Xwd+Ejo+oYkNP6F4GfeDtAXpm7xb9Ly8lhdbHcpRhzCUQHJ1tBCiGdLgmhSx7TXjhhanKOdDgkdsC1T+++piuuYL72TDgUy2Sb1GHlJ1Nc6rvB4fpxSDAOHqGpUq9LWsc3tFkXqRqmQVtqtR77npKIFBioc62jTBwDMPX3hDJDR1DSPc6BnZliaNw2IHdiMQ0mBoYeRnFdq+TyDKsjmJOOQPLzzL/saaw6F891+gBjLFEFquDyR73lAPJS279R3csi8WWk4ZYUC/1V8H3Ktip/J6ac8eqhLCbmJ81Lo92JGHz/ot`) 35 | ) 36 | 37 | func mustParseCert(b64CertDER string) *x509.Certificate { 38 | certDER, err := base64.StdEncoding.DecodeString(b64CertDER) 39 | if err != nil { 40 | panic(err) 41 | } 42 | cert, err := x509.ParseCertificate(certDER) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return cert 47 | } 48 | 49 | func TestRootsChanged(t *testing.T) { 50 | rootSetID1 := storage.RootSetID("1") 51 | rootSetID2 := storage.RootSetID("2") 52 | 53 | l := &ctlog.Log{ 54 | Name: "testtube", 55 | URL: "https://ct.googleapis.com/testtube/", 56 | } 57 | 58 | tests := []struct { 59 | desc string 60 | rootSetIDs []storage.RootSetID 61 | rootSetCerts map[storage.RootSetID][]*x509.Certificate 62 | wantReport *itestonly.Report 63 | }{ 64 | { 65 | desc: "no changes", 66 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID1}, 67 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 68 | rootSetID1: {root1}, 69 | }, 70 | }, 71 | { 72 | desc: "1 cert added", 73 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID2}, 74 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 75 | rootSetID1: {root1}, 76 | rootSetID2: {root1, root2}, 77 | }, 78 | wantReport: &itestonly.Report{ 79 | Summary: "Root certificates changed", 80 | BaseURL: "https://ct.googleapis.com/testtube/", 81 | FullURL: "https://ct.googleapis.com/testtube/ct/v1/get-roots", 82 | Details: `The root certificates accepted by testtube (https://ct.googleapis.com/testtube/) have changed. 83 | 84 | Certificates added (1): 85 | CN=A-Trust-Qual-02,OU=A-Trust-Qual-02,O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH,C=AT (SHA256: 75C9D4361CB96E993ABD9620CF043BE9407A4633F202F0F4C0E17851CC6089CD) 86 | `, 87 | }, 88 | }, 89 | { 90 | desc: "2 certs added", 91 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID2}, 92 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 93 | rootSetID1: {}, 94 | rootSetID2: {root1, root2}, 95 | }, 96 | wantReport: &itestonly.Report{ 97 | Summary: "Root certificates changed", 98 | BaseURL: "https://ct.googleapis.com/testtube/", 99 | FullURL: "https://ct.googleapis.com/testtube/ct/v1/get-roots", 100 | Details: `The root certificates accepted by testtube (https://ct.googleapis.com/testtube/) have changed. 101 | 102 | Certificates added (2): 103 | CN=A-Trust-Qual-02,OU=A-Trust-Qual-02,O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH,C=AT (SHA256: 75C9D4361CB96E993ABD9620CF043BE9407A4633F202F0F4C0E17851CC6089CD) 104 | CN=GLOBALTRUST,OU=GLOBALTRUST Certification Service,O=ARGE DATEN - Austrian Society for Data Protection,L=Vienna,ST=Austria,C=AT (SHA256: 5E3571F33F45A7DF1537A68B5FFB9E036AF9D2F5BC4C9717130DC43D7175AAC7) 105 | `, 106 | }, 107 | }, 108 | { 109 | desc: "1 cert removed", 110 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID2}, 111 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 112 | rootSetID1: {root1, root2}, 113 | rootSetID2: {root1}, 114 | }, 115 | wantReport: &itestonly.Report{ 116 | Summary: "Root certificates changed", 117 | BaseURL: "https://ct.googleapis.com/testtube/", 118 | FullURL: "https://ct.googleapis.com/testtube/ct/v1/get-roots", 119 | Details: `The root certificates accepted by testtube (https://ct.googleapis.com/testtube/) have changed. 120 | 121 | Certificates removed (1): 122 | CN=A-Trust-Qual-02,OU=A-Trust-Qual-02,O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH,C=AT (SHA256: 75C9D4361CB96E993ABD9620CF043BE9407A4633F202F0F4C0E17851CC6089CD) 123 | `, 124 | }, 125 | }, 126 | { 127 | desc: "2 certs removed", 128 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID2}, 129 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 130 | rootSetID1: {root1, root2}, 131 | rootSetID2: {}, 132 | }, 133 | wantReport: &itestonly.Report{ 134 | Summary: "Root certificates changed", 135 | BaseURL: "https://ct.googleapis.com/testtube/", 136 | FullURL: "https://ct.googleapis.com/testtube/ct/v1/get-roots", 137 | Details: `The root certificates accepted by testtube (https://ct.googleapis.com/testtube/) have changed. 138 | 139 | Certificates removed (2): 140 | CN=A-Trust-Qual-02,OU=A-Trust-Qual-02,O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH,C=AT (SHA256: 75C9D4361CB96E993ABD9620CF043BE9407A4633F202F0F4C0E17851CC6089CD) 141 | CN=GLOBALTRUST,OU=GLOBALTRUST Certification Service,O=ARGE DATEN - Austrian Society for Data Protection,L=Vienna,ST=Austria,C=AT (SHA256: 5E3571F33F45A7DF1537A68B5FFB9E036AF9D2F5BC4C9717130DC43D7175AAC7) 142 | `, 143 | }, 144 | }, 145 | { 146 | desc: "1 cert added, 1 cert removed", 147 | rootSetIDs: []storage.RootSetID{rootSetID1, rootSetID2}, 148 | rootSetCerts: map[storage.RootSetID][]*x509.Certificate{ 149 | rootSetID1: {root1}, 150 | rootSetID2: {root2}, 151 | }, 152 | wantReport: &itestonly.Report{ 153 | Summary: "Root certificates changed", 154 | BaseURL: "https://ct.googleapis.com/testtube/", 155 | FullURL: "https://ct.googleapis.com/testtube/ct/v1/get-roots", 156 | Details: `The root certificates accepted by testtube (https://ct.googleapis.com/testtube/) have changed. 157 | 158 | Certificates added (1): 159 | CN=A-Trust-Qual-02,OU=A-Trust-Qual-02,O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH,C=AT (SHA256: 75C9D4361CB96E993ABD9620CF043BE9407A4633F202F0F4C0E17851CC6089CD) 160 | 161 | Certificates removed (1): 162 | CN=GLOBALTRUST,OU=GLOBALTRUST Certification Service,O=ARGE DATEN - Austrian Society for Data Protection,L=Vienna,ST=Austria,C=AT (SHA256: 5E3571F33F45A7DF1537A68B5FFB9E036AF9D2F5BC4C9717130DC43D7175AAC7) 163 | `, 164 | }, 165 | }, 166 | } 167 | 168 | for _, test := range tests { 169 | t.Run(test.desc, func(t *testing.T) { 170 | test := test 171 | t.Parallel() 172 | 173 | fakeStorage := &stestonly.FakeRootsReader{ 174 | RootSetChan: make(chan storage.RootSetID, len(test.rootSetIDs)), 175 | RootSetCerts: test.rootSetCerts, 176 | } 177 | fakeReporter := &itestonly.FakeReporter{ 178 | Updates: make(chan itestonly.Report, 1), 179 | } 180 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 181 | defer cancel() 182 | 183 | go Run(ctx, fakeStorage, fakeReporter, l) 184 | 185 | for _, id := range test.rootSetIDs { 186 | fakeStorage.RootSetChan <- id 187 | } 188 | 189 | select { 190 | case <-ctx.Done(): 191 | if test.wantReport != nil { 192 | t.Errorf("No incident report received before context expired") 193 | } 194 | case report := <-fakeReporter.Updates: 195 | if diff := cmp.Diff(&report, test.wantReport); diff != "" { 196 | t.Errorf("Incident report diff (-want +got):\n%s", diff) 197 | } 198 | } 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /certgen/certgen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package certgen 16 | 17 | import ( 18 | "bytes" 19 | "crypto" 20 | "fmt" 21 | "io/ioutil" 22 | "testing" 23 | "time" 24 | 25 | "github.com/google/certificate-transparency-go/x509" 26 | "github.com/google/certificate-transparency-go/x509util" 27 | "github.com/google/go-cmp/cmp" 28 | "github.com/google/monologue/interval" 29 | "github.com/google/trillian/crypto/keys/pem" 30 | ) 31 | 32 | const ( 33 | rootFile = "./testdata/test_ca.pem" 34 | rootKeyFile = "./testdata/test_ca.key" 35 | ) 36 | 37 | var certConfig = CertificateConfig{ 38 | SubjectCommonName: "test-leaf-certificate", 39 | SubjectOrganization: "Test Organisation", 40 | SubjectOrganizationalUnit: "Test Organisational Unit", 41 | SubjectLocality: "Test Locality", 42 | SubjectCountry: "GB", 43 | SignatureAlgorithm: x509.SHA256WithRSA, 44 | DNSPrefix: "test-log", 45 | } 46 | 47 | func rootAndKeySetup(rootFile, rootKeyFile string) (*x509.Certificate, crypto.Signer, error) { 48 | rootPEM, err := ioutil.ReadFile(rootFile) 49 | if err != nil { 50 | return nil, nil, fmt.Errorf("error reading root cert: %s", err) 51 | } 52 | root, err := x509util.CertificateFromPEM(rootPEM) 53 | if err != nil { 54 | return nil, nil, fmt.Errorf("error parsing root cert: %s", err) 55 | } 56 | 57 | rootKeyPEM, err := ioutil.ReadFile(rootKeyFile) 58 | if err != nil { 59 | return nil, nil, fmt.Errorf("error reading root key: %s", err) 60 | } 61 | rootKey, err := pem.UnmarshalPrivateKey(string(rootKeyPEM), "") 62 | if err != nil { 63 | return nil, nil, fmt.Errorf("error parsing root key: %s", err) 64 | } 65 | 66 | return root, rootKey, nil 67 | } 68 | 69 | func TestIssueCertificate(t *testing.T) { 70 | tests := []struct { 71 | desc string 72 | notAfterInterval *interval.Interval 73 | precert bool 74 | }{ 75 | { 76 | desc: "not temporal", 77 | }, 78 | { 79 | desc: "smallest temporal", 80 | notAfterInterval: &interval.Interval{ 81 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 82 | End: time.Date(2019, time.March, 25, 0, 0, 1, 0, time.UTC), 83 | }, 84 | }, 85 | { 86 | desc: "year temporal", 87 | notAfterInterval: &interval.Interval{ 88 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 89 | End: time.Date(2020, time.March, 25, 0, 0, 0, 0, time.UTC), 90 | }, 91 | }, 92 | { 93 | desc: "precert not temporal", 94 | precert: true, 95 | }, 96 | { 97 | desc: "precert smallest temporal", 98 | notAfterInterval: &interval.Interval{ 99 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 100 | End: time.Date(2019, time.March, 25, 0, 0, 1, 0, time.UTC), 101 | }, 102 | precert: true, 103 | }, 104 | { 105 | desc: "precert year temporal", 106 | notAfterInterval: &interval.Interval{ 107 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 108 | End: time.Date(2020, time.March, 25, 0, 0, 0, 0, time.UTC), 109 | }, 110 | precert: true, 111 | }, 112 | } 113 | 114 | root, rootKey, err := rootAndKeySetup(rootFile, rootKeyFile) 115 | if err != nil { 116 | t.Fatalf("root and key setup error: %s", err) 117 | } 118 | 119 | timeNowUTC = func() time.Time { 120 | return time.Date(2019, time.March, 25, 12, 0, 0, 0, time.UTC) 121 | } 122 | 123 | for _, test := range tests { 124 | t.Run(test.desc, func(t *testing.T) { 125 | cc := certConfig 126 | cc.NotAfterInterval = test.notAfterInterval 127 | ca := &CA{SigningCert: root, SigningKey: rootKey, CertConfig: cc} 128 | 129 | var cert *x509.Certificate 130 | var err error 131 | if test.precert { 132 | cert, err = ca.IssuePrecertificate() 133 | } else { 134 | cert, err = ca.IssueCertificate() 135 | } 136 | if err != nil { 137 | t.Fatalf("error creating (pre? %t)certificate: %s", test.precert, err) 138 | } 139 | 140 | // Check the fields that are set in the leaf template are present 141 | // and correct. 142 | 143 | if cert.SerialNumber == nil { 144 | t.Error("certificate Serial Number is nil") 145 | } 146 | 147 | // Check the Subject fields. 148 | if got, want := cert.Subject.Country, []string{cc.SubjectCountry}; !cmp.Equal(got, want) { 149 | t.Errorf("certificate Subject Country = %v, want %v", got, want) 150 | } 151 | if got, want := cert.Subject.Organization, []string{cc.SubjectOrganization}; !cmp.Equal(got, want) { 152 | t.Errorf("certificate Subject Organization = %v, want %v", got, want) 153 | } 154 | if got, want := cert.Subject.OrganizationalUnit, []string{cc.SubjectOrganizationalUnit}; !cmp.Equal(got, want) { 155 | t.Errorf("certificate Subject OrganizationalUnit = %v, want %v", got, want) 156 | } 157 | if got, want := cert.Subject.Locality, []string{cc.SubjectLocality}; !cmp.Equal(got, want) { 158 | t.Errorf("certificate Subject Locality = %v, want %v", got, want) 159 | } 160 | if got, want := cert.Subject.CommonName, cc.SubjectCommonName; got != want { 161 | t.Errorf("certificate Subject Common Name = %s, want %s", got, want) 162 | } 163 | 164 | // Check the validity period fields. 165 | if cert.NotBefore.IsZero() { 166 | t.Error("certificate NotBefore is the zero time") 167 | } 168 | if got, want := cert.NotBefore, cert.NotAfter.Add(-certValidity); got != want { 169 | t.Errorf("certificate NotBefore = %s, want %s (%s before Not After)", got, want, certValidity) 170 | } 171 | if cert.NotAfter.IsZero() { 172 | t.Error("certificate NotAfter is the zero time") 173 | } 174 | if cc.NotAfterInterval != nil { 175 | // Check that cert.NotAfter is in the NotAfterInterval [Start, End). 176 | if got := cert.NotAfter; got.Before(cc.NotAfterInterval.Start) || !cc.NotAfterInterval.End.After(got) { 177 | t.Errorf("certificate NotAfter = %s, should be between [%s, %s)", got, cc.NotAfterInterval.Start, cc.NotAfterInterval.End) 178 | } 179 | } 180 | 181 | // Check the extension fields. 182 | if got, want := cert.KeyUsage, x509.KeyUsageDigitalSignature; got != want { 183 | t.Errorf("certificate KeyUsage = %d, want %d", got, want) 184 | } 185 | if got, want := cert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}; !cmp.Equal(got, want) { 186 | t.Errorf("certificate ExtKeyUsage = %v, want %v", got, want) 187 | } 188 | if !cert.BasicConstraintsValid { 189 | t.Errorf("certificate BasicConstraintsValid = %t, want true", cert.BasicConstraintsValid) 190 | } 191 | if cert.IsCA { 192 | t.Errorf("certificate IsCA = %t, want false", cert.IsCA) 193 | } 194 | if gotPre, wantPre := cert.IsPrecertificate(), test.precert; gotPre != wantPre { 195 | t.Errorf("certificate type is wrong: got precertificate? %t, want precertificate? %t", gotPre, wantPre) 196 | } 197 | 198 | want := []string{cc.SubjectCommonName, extendedDNSSAN(cc.DNSPrefix, cc.SubjectCommonName)} 199 | if got := cert.DNSNames; !cmp.Equal(got, want) { 200 | t.Errorf("certificate DNSNames = %v, want %v", got, want) 201 | } 202 | 203 | // Check any other fields that should have been populated are 204 | // present and correct. 205 | 206 | if got, want := cert.PublicKeyAlgorithm, x509.RSA; got != want { 207 | t.Errorf("certificate PublicKeyAlgorithm = %s, want %s", got, want) 208 | } 209 | if cert.PublicKey == nil { 210 | t.Error("certificate Public Key is nil") 211 | } 212 | if got, want := cert.Issuer, root.Subject; !cmp.Equal(got, want) { 213 | t.Errorf("certificate Issuer = %v, want %v", got, want) 214 | } 215 | if got, want := cert.AuthorityKeyId, root.SubjectKeyId; !bytes.Equal(got, want) { 216 | t.Errorf("certificate AuthorityKeyId = %v, want %v", got, want) 217 | } 218 | 219 | // Check the signature algorithm. 220 | if got, want := cert.SignatureAlgorithm, cc.SignatureAlgorithm; got != want { 221 | t.Errorf("certificate SignatureAlgorithm = %s, want %s", got, want) 222 | } 223 | 224 | // Check the signature is valid. 225 | if err := cert.CheckSignatureFrom(root); err != nil { 226 | t.Errorf("certificate signature doesn't verify: %s", err) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestIssueCertificateChain(t *testing.T) { 233 | root, rootKey, err := rootAndKeySetup(rootFile, rootKeyFile) 234 | if err != nil { 235 | t.Fatalf("root and key setup error: %s", err) 236 | } 237 | cc := certConfig 238 | cc.NotAfterInterval = &interval.Interval{ 239 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 240 | End: time.Date(2020, time.March, 25, 0, 0, 0, 0, time.UTC), 241 | } 242 | ca := &CA{SigningCert: root, SigningKey: rootKey, CertConfig: cc} 243 | 244 | chain, err := ca.IssueCertificateChain() 245 | 246 | if err != nil { 247 | t.Fatalf("ca.IssueCertificateChain() = _, %q, want nil error", err) 248 | } 249 | 250 | if len(chain) != 2 { 251 | t.Fatalf("ca.IssueCertificateChain(): chain length = %d, want 2", len(chain)) 252 | } 253 | 254 | if !chain[1].Equal(ca.SigningCert) { 255 | t.Fatalf("ca.IssueCertificateChain(): root of chain (%v) is not equal to ca.SigningCert (%v)", chain[1], ca.SigningCert) 256 | } 257 | 258 | if err := chain[0].CheckSignatureFrom(chain[1]); err != nil { 259 | t.Errorf("ca.IssueCertificateChain(): leaf certificate signature doesn't verify against ca.SigningCert: %s", err) 260 | } 261 | } 262 | 263 | func TestIssuePrecertificateChain(t *testing.T) { 264 | root, rootKey, err := rootAndKeySetup(rootFile, rootKeyFile) 265 | if err != nil { 266 | t.Fatalf("root and key setup error: %s", err) 267 | } 268 | cc := certConfig 269 | cc.NotAfterInterval = &interval.Interval{ 270 | Start: time.Date(2019, time.March, 25, 0, 0, 0, 0, time.UTC), 271 | End: time.Date(2020, time.March, 25, 0, 0, 0, 0, time.UTC), 272 | } 273 | ca := &CA{SigningCert: root, SigningKey: rootKey, CertConfig: cc} 274 | 275 | chain, err := ca.IssuePrecertificateChain() 276 | 277 | if err != nil { 278 | t.Fatalf("ca.IssuePrecertificateChain() = _, %q, want nil error", err) 279 | } 280 | 281 | if len(chain) != 2 { 282 | t.Fatalf("ca.IssuePrecertificateChain(): chain length = %d, want 2", len(chain)) 283 | } 284 | 285 | if !chain[0].IsPrecertificate() { 286 | t.Fatal("ca.IssuePrecertificateChain(): leaf of chain is not a precertificate") 287 | } 288 | 289 | if !chain[1].Equal(ca.SigningCert) { 290 | t.Fatalf("ca.IssuePrecertificateChain(): root of chain (%v) is not equal to ca.SigningCert (%v)", chain[1], ca.SigningCert) 291 | } 292 | 293 | if err := chain[0].CheckSignatureFrom(chain[1]); err != nil { 294 | t.Errorf("ca.IssuePrecertificateChain(): leaf certificate signature doesn't verify against ca.SigningCert: %s", err) 295 | } 296 | } 297 | 298 | func TestExtendedDNSSAN(t *testing.T) { 299 | tests := []struct { 300 | desc string 301 | timeNow time.Time 302 | prefix string 303 | url string 304 | want string 305 | }{ 306 | { 307 | desc: "prefix", 308 | timeNow: time.Date(2019, time.March, 25, 12, 0, 0, 0, time.UTC), 309 | prefix: "squirrel", 310 | url: "example.com", 311 | want: "12.25.03.2019.squirrel.example.com", 312 | }, 313 | { 314 | desc: "empty prefix", 315 | timeNow: time.Date(2019, time.January, 25, 12, 0, 0, 0, time.UTC), 316 | prefix: "", 317 | url: "example.com", 318 | want: "12.25.01.2019.example.com", 319 | }, 320 | } 321 | 322 | for _, test := range tests { 323 | timeNowUTC = func() time.Time { 324 | return test.timeNow 325 | } 326 | if got := extendedDNSSAN(test.prefix, test.url); got != test.want { 327 | t.Errorf("%s: extendedDNSSAN(%s, %s) = %s, want %s", test.desc, test.prefix, test.url, got, test.want) 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /storage/mysql/root_store_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mysql 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "encoding/base64" 21 | "encoding/hex" 22 | "flag" 23 | "os" 24 | "testing" 25 | "time" 26 | 27 | "github.com/golang/glog" 28 | "github.com/google/certificate-transparency-go/x509" 29 | "github.com/google/go-cmp/cmp" 30 | "github.com/google/monologue/ctlog" 31 | "github.com/google/monologue/storage/mysql/testdb" 32 | 33 | _ "github.com/go-sql-driver/mysql" // Load MySQL driver 34 | ) 35 | 36 | func mustCreateNewLog(url, name, b64PubKey string) *ctlog.Log { 37 | l, err := ctlog.New(url, name, b64PubKey, 24*time.Hour, nil) 38 | if err != nil { 39 | glog.Fatalf("ctlog.New(%q, %q, %q) = _, %s", url, name, b64PubKey, err) 40 | } 41 | return l 42 | } 43 | 44 | var ( 45 | pilot = mustCreateNewLog("https://ct.googleapis.com/pilot", "pilot", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==") 46 | ) 47 | 48 | func mustParseCert(b64CertDER string, t *testing.T) *x509.Certificate { 49 | t.Helper() 50 | certDER, err := base64.StdEncoding.DecodeString(b64CertDER) 51 | if err != nil { 52 | t.Fatalf("Unexpected error while preparing testdata: %s", err) 53 | } 54 | cert, err := x509.ParseCertificate(certDER) 55 | if err != nil { 56 | t.Fatalf("Unexpected error while preparing testdata: %s", err) 57 | } 58 | return cert 59 | } 60 | 61 | func mustBytes32(in []byte, t *testing.T) [32]byte { 62 | t.Helper() 63 | if len(in) != 32 { 64 | t.Fatalf("mustBytes32 expects input of length 32, got %d", len(in)) 65 | } 66 | var out [32]byte 67 | copy(out[:], in) 68 | return out 69 | } 70 | 71 | func mustHexCode32(s string, t *testing.T) [32]byte { 72 | t.Helper() 73 | decoded, err := hex.DecodeString(s) 74 | if err != nil { 75 | t.Fatalf("Unexpected error while preparing testdata: %s", err) 76 | } 77 | return mustBytes32(decoded, t) 78 | } 79 | 80 | type rootEntry struct { 81 | RootID []byte 82 | SHA256DER []byte 83 | } 84 | 85 | type setEntry struct { 86 | RootSetID []byte 87 | RootID []byte 88 | } 89 | 90 | type obEntry struct { 91 | LogName string 92 | RootSetID []byte 93 | ReceivedAt time.Time 94 | } 95 | 96 | func checkContents(ctx context.Context, t *testing.T, want []rootEntry, wantSets []setEntry, wantObservations []obEntry) { 97 | t.Helper() 98 | 99 | tx, err := testDB.BeginTx(ctx, nil /* opts */) 100 | if err != nil { 101 | t.Fatalf("failed to create transaction: %v", err) 102 | } 103 | defer tx.Commit() 104 | 105 | // Roots 106 | rows, err := tx.QueryContext(ctx, "SELECT ID, DER FROM Roots") 107 | if err != nil { 108 | t.Fatalf("failed to query rows: %v", err) 109 | } 110 | defer rows.Close() 111 | 112 | var got []rootEntry 113 | for rows.Next() { 114 | var e rootEntry 115 | if err := rows.Scan(&e.RootID, &e.SHA256DER); err != nil { 116 | t.Fatalf("failed to scan row: %v", err) 117 | } 118 | got = append(got, e) 119 | } 120 | if err := rows.Err(); err != nil { 121 | t.Errorf("root table iteration failed: %v", err) 122 | } 123 | if diff := cmp.Diff(got, want); diff != "" { 124 | t.Errorf("root table: diff (-got +want)\n%s", diff) 125 | } 126 | 127 | // RootSets 128 | setrows, err := tx.QueryContext(ctx, "SELECT RootSetID, RootID FROM RootSets;") 129 | if err != nil { 130 | t.Fatalf("failed to query rows: %v", err) 131 | } 132 | defer setrows.Close() 133 | 134 | var gotS []setEntry 135 | for setrows.Next() { 136 | var e setEntry 137 | if err := setrows.Scan(&e.RootSetID, &e.RootID); err != nil { 138 | t.Fatalf("failed to scan row: %v", err) 139 | } 140 | gotS = append(gotS, e) 141 | } 142 | if err := rows.Err(); err != nil { 143 | t.Errorf("rootset table iteration failed: %v", err) 144 | } 145 | if diff := cmp.Diff(gotS, wantSets); diff != "" { 146 | t.Errorf("rootset table: diff (-got +want)\n%s", diff) 147 | } 148 | 149 | // RootSetObservations 150 | obrows, err := tx.QueryContext(ctx, "SELECT LogName, RootSetID, ReceivedAt FROM RootSetObservations;") 151 | if err != nil { 152 | t.Fatalf("failed to query rows: %v", err) 153 | } 154 | defer obrows.Close() 155 | 156 | var gotO []obEntry 157 | for obrows.Next() { 158 | var e obEntry 159 | if err := obrows.Scan(&e.LogName, &e.RootSetID, &e.ReceivedAt); err != nil { 160 | t.Fatalf("failed to scan row: %v", err) 161 | } 162 | gotO = append(gotO, e) 163 | } 164 | if err := obrows.Err(); err != nil { 165 | t.Errorf("RootSetObservations table iteration failed: %v", err) 166 | } 167 | if diff := cmp.Diff(gotO, wantObservations); diff != "" { 168 | t.Errorf("RootSetObservations table: diff (-got +want)\n%s", diff) 169 | } 170 | } 171 | 172 | func TestWriteRoots(t *testing.T) { 173 | root1 := mustParseCert("MIIFzTCCA7WgAwIBAgIJAJ7TzLHRLKJyMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoMDkdvb2dsZSBVSyBMdGQuMSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxITAfBgNVBAMMGE1lcmdlIERlbGF5IE1vbml0b3IgUm9vdDAeFw0xNDA3MTcxMjA1NDNaFw00MTEyMDIxMjA1NDNaMH0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoMDkdvb2dsZSBVSyBMdGQuMSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxITAfBgNVBAMMGE1lcmdlIERlbGF5IE1vbml0b3IgUm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKoWHPIgXtgaxWVIPNpCaj2y5Yj9t1ixe5PqjWhJXVNKAbpPbNHA/AoSivecBm3FTD9DfgW6J17mHb+cvbKSgYNzgTk5e2GJrnOP7yubYJpt2OCw0OILJD25NsApzcIiCvLA4aXkqkGgBq9FiVfisReNJxVu8MtxfhbVQCXZf0PpkW+yQPuF99V5Ri+grHbHYlaEN1C/HM3+t2yMR4hkd2RNXsMjViit9qCchIi/pQNt5xeQgVGmtYXyc92ftTMrmvduj7+pHq9DEYFt3ifFxE8v0GzCIE1xR/d7prFqKl/KRwAjYUcpU4vuazywcmRxODKuwWFVDrUBkGgCIVIjrMJWStH5i7WTSSTrVtOD/HWYvkXInZlSgcDvsNIG0pptJaEKSP4jUzI3nFymnoNZn6pnfdIII/XISpYSVeyl1IcdVMod8HdKoRew9CzW6f2n6KSKU5I8X5QEM1NUTmRLWmVi5c75/CvS/PzOMyMzXPf+fE2Dwbf4OcR5AZLTupqp8yCTqo7ny+cIBZ1TjcZjzKG4JTMaqDZ1Sg0T3mO/ZbbiBE3N8EHxoMWpw8OP50z1dtRRwj6qUZ2zLvngOb2EihlMO15BpVZC3Cg929c9Hdl65pUd4YrYnQBQB/rn6IvHo8zot8zElgOg22fHbViijUt3qnRggB40N30MXkYGwuJbAgMBAAGjUDBOMB0GA1UdDgQWBBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAfBgNVHSMEGDAWgBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQB3HP6jRXmpdSDYwkI9aOzQeJH4x/HDi/PNMOqdNje/xdNzUy7HZWVYvvSVBkZ1DG/ghcUtn/wJ5m6/orBn3ncnyzgdKyXbWLnCGX/V61PgIPQpuGo7HzegenYaZqWz7NeXxGaVo3/y1HxUEmvmvSiioQM1cifGtz9/aJsJtIkn5umlImenKKEV1Ly7R3Uz3Cjz/Ffac1o+xU+8NpkLF/67fkazJCCMH6dCWgy6SL3AOB6oKFIVJhw8SD8vptHaDbpJSRBxifMtcop/85XUNDCvO4zkvlB1vPZ9ZmYZQdyL43NA+PkoKy0qrdaQZZMq1Jdp+Lx/yeX255/zkkILp43jFyd44rZ+TfGEQN1WHlp4RMjvoGwOX1uGlfoGkRSgBRj7TBn514VYMbXu687RS4WY2v+kny3PUFv/ZBfYSyjoNZnU4Dce9kstgv+gaKMQRPcyL+4vZU7DV8nBIfNFilCXKMN/VnNBKtDV52qmtOsVghgai+QE09w15x7dg+44gIfWFHxNhvHKys+s4BBN8fSxAMLOsb5NGFHE8x58RAkmIYWHjyPM6zB5AUPw1b2A0sDtQmCqoxJZfZUKrzyLz8gS2aVujRYN13KklHQ3EKfkeKBG2KXVBe5rjMN/7Anf1MtXxsTY6O8qIuHZ5QlXhSYzE41yIlPlG6d7AGnTiBIgeg==", t) 174 | root1ID := mustHexCode32("86d8219c7e2b6009e37eb14356268489b81379e076e8f372e3dde8c162a34134", t) 175 | root2 := mustParseCert("MIIF9jCCA7CgAwIBAgICEAEwDQYJKoZIhvcNAQEFBQAwfTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xlIFVLIEx0ZC4xITAfBgNVBAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEhMB8GA1UEAwwYTWVyZ2UgRGVsYXkgTW9uaXRvciBSb290MB4XDTE0MDcxNzEyMjYzMFoXDTE5MDcxNjEyMjYzMFowfzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xlIFVLIEx0ZC4xITAfBgNVBAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEjMCEGA1UEAwwaTWVyZ2UgRGVsYXkgSW50ZXJtZWRpYXRlIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDB6HT+/5ru8wO7+mNFOIH6r43BwiwJZB2vQwOB8zvBV79sTIqNV7Grx5KFnSDyGRUJxZfEN7FGc96lr0vqFDlt1DbcYgVV15U+Dt4B9/+0Tz/3zeZO0kVjTg3wqvzpw6xetj2N4dlpysiFQZVAOp+dHUw9zu3xNR7dlFdDvFSrdFsgT7Uln+Pt9pXCz5C4hsSP9oC3RP7CaRtDRSQrMcNvMRi3J8XeXCXsGqMKTCRhxRGe9ruQ2Bbm5ExbmVW/ou00Fr9uSlPJL6+sDR8Li/PTW+DU9hygXSj8Zi36WI+6PuA4BHDAEt7Z5Ru/Hnol76dFeExJ0F6vjc7gUnNh7JExJgBelyz0uGORT4NhWC7SRWP/ngPFLoqcoyZMVsGGtOxSt+aVzkKuF+x64CVxMeHb9I8t3iQubpHqMEmIE1oVSCsF/AkTVTKLOeWG6N06SjoUy5fu9o+faXKMKR8hldLM5z1K6QhFsb/F+uBAuU/DWaKVEZgbmWautW06fF5I+OyoFeW+hrPTbmon4OLE3ubjDxKnyTa4yYytWSisojjfw5z58sUkbLu7KAy2+Z60m/0deAiVOQcsFkxwgzcXRt7bxN7By5Q5Bzrz8uYPjFBfBnlhqMU5RU/FNBFY7Mx4Uy8+OcMYfJQ5/A/4julXEx1HjfBj3VCyrT/noHDpBeOGiwIDAQABo1AwTjAdBgNVHQ4EFgQU6TwE4YAvwoQTLSZwnvL9Gs+q/sYwHwYDVR0jBBgwFoAU8197dUnjeEE5aiC2fGtMXMk9WEEwDAYDVR0TBAUwAwEB/zA7BgkqhkiG9w0BAQUTLlRoaXMgaXMgbm90IHRoZSBjZXJ0aWZpY2F0ZSB5b3UncmUgbG9va2luZyBmb3IDggIBAAhYy9VF8ukuCZBqw589VcE2B6ZRQ2OGxukPEodzoOs/RyXYrzX7f4gENrSB8s9HgBgl3lTxP49ZIL89kW51MUHeXlnS3rv8P8Imch8VoW07ekYY6gVRY58dLLe5+q0bfgcPI6boGXw9dUm7plU/1dtBnOOZR39qBIG5D1HJ0wfYLLBc+WeCihrOZSB8+GttFnkiRdzyS0wXn5EYRzbn4vy4Y6S1yJsKwvNoOQoQWUuVyFbiWcd1ZDFomM+HpoF9GFhfyXbWgdnVEO8q036K0OSfW9SZyex/6PQ7F9/7m30N/YMAwcU4nJ6gvkNw3L94vT78IwjSULhmu8oDHAxJ/3enpUINqh8bakRNNmZTl0wtF5wwCYce5siRQPyp798jvUuIxuuuuShvWPPPwh5IdPGC0ezWBYkZsDsY23t5W+nLJfxRZqlF744RM81gMSoyNPRknfYWZAfLtezIOOnhGMBd7nyJapmHZVrn40nNgWbmjTTeo7SuiSqfI4UFMnHoYLVCvjZQUDl087tvJog3WrKEh9pnUdDyw6NeeO/jCxnVeAjr6ixEU5kK2B65YonA+ZxQgPggkrxhI6NAxjphfzvErcEpjYiieKaT75NohhHTtO3tC20CPtn2wuqINkgxl1JbGxvOcKkMNAOwlNX0EqoRQbmWWrgxTFL3ct7/wQCM", t) 176 | root2ID := mustHexCode32("0ac607a81e0828b60dc88034cccafd982cddf95b3a0efd1f8cd59232e5fb754f", t) 177 | 178 | aprilTimestamp := time.Date(2019, time.April, 10, 15, 0, 0, 0, time.UTC) 179 | mayTimestamp := time.Date(2019, time.May, 10, 15, 0, 0, 0, time.UTC) 180 | juneTimestamp := time.Date(2019, time.June, 10, 15, 0, 0, 0, time.UTC) 181 | 182 | // ID for a set of only root1 183 | root1SetID := mustHexCode32("35d1cd6dbd84a37a5884351d1d0d197d2e9048709b1442391cdfac69f8371272", t) 184 | root1AndRoot2SetID := mustHexCode32("be6b3e0736f965cf707eb773709027a7250de5e32910f09370146d1318d6df04", t) 185 | 186 | e := rootEntry{RootID: root1ID[:], SHA256DER: root1.Raw} 187 | e2 := rootEntry{RootID: root2ID[:], SHA256DER: root2.Raw} 188 | 189 | s := setEntry{RootSetID: root1SetID[:], RootID: root1ID[:]} 190 | s1 := setEntry{RootSetID: root1AndRoot2SetID[:], RootID: root1ID[:]} 191 | s2 := setEntry{RootSetID: root1AndRoot2SetID[:], RootID: root2ID[:]} 192 | 193 | o := obEntry{LogName: "pilot", RootSetID: root1SetID[:], ReceivedAt: aprilTimestamp} 194 | oMay := obEntry{LogName: "pilot", RootSetID: root1SetID[:], ReceivedAt: mayTimestamp} 195 | o2May := obEntry{LogName: "pilot", RootSetID: root1AndRoot2SetID[:], ReceivedAt: mayTimestamp} 196 | o2June := obEntry{LogName: "pilot", RootSetID: root1AndRoot2SetID[:], ReceivedAt: juneTimestamp} 197 | 198 | type rootData struct { 199 | roots []*x509.Certificate 200 | receivedAt time.Time 201 | } 202 | 203 | tests := []struct { 204 | name string 205 | log *ctlog.Log 206 | rootsData []rootData 207 | wantErr bool 208 | wantRoots []rootEntry 209 | wantSets []setEntry 210 | wantObservations []obEntry 211 | }{ 212 | { 213 | name: "one root", 214 | log: pilot, 215 | rootsData: []rootData{ 216 | { 217 | roots: []*x509.Certificate{ 218 | root1, 219 | }, 220 | receivedAt: aprilTimestamp, 221 | }, 222 | }, 223 | wantRoots: []rootEntry{e}, 224 | wantSets: []setEntry{s}, 225 | wantObservations: []obEntry{o}, 226 | }, 227 | { 228 | name: "duplicated root", 229 | log: pilot, 230 | rootsData: []rootData{ 231 | { 232 | roots: []*x509.Certificate{ 233 | root1, 234 | }, 235 | receivedAt: aprilTimestamp, 236 | }, 237 | { 238 | roots: []*x509.Certificate{ 239 | root1, 240 | }, 241 | receivedAt: mayTimestamp, 242 | }, 243 | }, 244 | wantRoots: []rootEntry{e}, 245 | wantSets: []setEntry{s}, 246 | wantObservations: []obEntry{o, oMay}, 247 | }, 248 | { 249 | name: "root order", 250 | log: pilot, 251 | rootsData: []rootData{ 252 | { 253 | roots: []*x509.Certificate{ 254 | root1, 255 | }, 256 | receivedAt: aprilTimestamp, 257 | }, 258 | { 259 | roots: []*x509.Certificate{ 260 | root1, root2, 261 | }, 262 | receivedAt: mayTimestamp, 263 | }, 264 | { 265 | roots: []*x509.Certificate{ 266 | root2, root1, 267 | }, 268 | receivedAt: juneTimestamp, 269 | }, 270 | }, 271 | wantRoots: []rootEntry{e2, e}, 272 | wantSets: []setEntry{s, s2, s1}, 273 | wantObservations: []obEntry{o, o2May, o2June}, 274 | }, 275 | } 276 | 277 | for _, test := range tests { 278 | t.Run(test.name, func(t *testing.T) { 279 | ctx := context.Background() 280 | testdb.Clean(ctx, testDB, "Roots") 281 | testdb.Clean(ctx, testDB, "RootSets") 282 | testdb.Clean(ctx, testDB, "RootSetObservations") 283 | checkContents(ctx, t, nil, nil, nil) 284 | st := NewRootStore(ctx, testDB) 285 | 286 | var gotErr bool 287 | for _, rdt := range test.rootsData { 288 | if err := st.WriteRoots(ctx, test.log, rdt.roots, rdt.receivedAt); err != nil { 289 | if !test.wantErr { 290 | t.Fatalf("Storage.WriteRoots(ctx, %v, %v, %v) = %s, want nil", test.log, rdt.roots, rdt.receivedAt, err) 291 | } 292 | gotErr = true 293 | } 294 | } 295 | if !gotErr && test.wantErr { 296 | t.Fatal("Storage.WriteRoots() for all Root-sets produced no errors, wanted error") 297 | } 298 | checkContents(ctx, t, test.wantRoots, test.wantSets, test.wantObservations) 299 | }) 300 | } 301 | } 302 | 303 | func TestMain(m *testing.M) { 304 | flag.Parse() 305 | if err := testdb.MySQLAvailable(); err != nil { 306 | glog.Errorf("MySQL not available, skipping all MySQL storage tests: %v", err) 307 | return 308 | } 309 | ctx := context.Background() 310 | var err error 311 | testDB, err = testdb.New(ctx, rootStoreSQL) 312 | if err != nil { 313 | glog.Exitf("failed to create test database: %v", err) 314 | } 315 | defer testDB.Close() 316 | testdb.Clean(ctx, testDB, "Roots") 317 | ec := m.Run() 318 | os.Exit(ec) 319 | } 320 | 321 | var ( 322 | testDB *sql.DB 323 | rootStoreSQL = "root_store.sql" 324 | ) 325 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package client provides a Certificate Transparency (CT) Log Client that 16 | // prioritizes preserving and returning as much information about each http 17 | // request that is made, and detailed descriptions of any errors that occur 18 | // along the way. 19 | package client 20 | 21 | import ( 22 | "bytes" 23 | "encoding/base64" 24 | "encoding/json" 25 | "fmt" 26 | "io/ioutil" 27 | "net/http" 28 | "net/url" 29 | "reflect" 30 | "strconv" 31 | "strings" 32 | "time" 33 | 34 | ct "github.com/google/certificate-transparency-go" 35 | "github.com/google/certificate-transparency-go/x509" 36 | ) 37 | 38 | const contentType = "application/json" 39 | 40 | // LogClient is a client for a specific CT Log. 41 | // 42 | // Most of the LogClient methods return HTTPData structs and errors. 43 | // 44 | // A returned HTTPData struct contains: 45 | // - Timing: The time it took for the LogClient's HTTP client to send the 46 | // request and receive a response. 47 | // - Response: The http.Response returned by the LogClient's HTTP client, with 48 | // http.Response.Body already read and closed. 49 | // - Body: The body of the response received, read from the Body field in the 50 | // http.Response returned by the LogClient's HTTP client. 51 | // This HTTPData struct will always be returned containing at least the timing 52 | // of the request, even in the case where an error is returned too. 53 | // 54 | // If an error is returned it could be any of the following types, in addition 55 | // to any error types specified in the documentation specific to that method. 56 | // The type of error that is returned influences what the HTTPData struct 57 | // returned will contain: 58 | // - GetError 59 | // - HTTPData will contain only the timing of the request. 60 | // - PostError 61 | // - HTTPData will contain only the timing of the request. 62 | // - NilResponseError 63 | // - HTTPData will contain only the timing of the request. 64 | // - BodyReadError 65 | // - HTTPData will contain the timing of the request and the received 66 | // response. 67 | // - HTTPStatusError 68 | // - HTTPData will contain the timing of the request, the received 69 | // response, and the body of the response. 70 | // - JSONParseError 71 | // - HTTPData will contain the timing of the request, the received 72 | // response, and the body of the response. 73 | type LogClient struct { 74 | url string 75 | httpClient *http.Client 76 | } 77 | 78 | // New creates a new LogClient for monitoring the CT Log served at logURL. 79 | func New(logURL string, hc *http.Client) *LogClient { 80 | return &LogClient{url: logURL, httpClient: hc} 81 | } 82 | 83 | // buildURL builds a URL made up of a base URL, a path and a map of parameters. 84 | // 85 | // Example: 86 | // - Base URL: https://ct.googleapis.com/pilot/ 87 | // - Path: ct/v1/get-sth-consistency 88 | // - Params: map[string]string{"first":"15", "second":"20"} 89 | // Result: https://ct.googleapis.com/pilot/ct/v1/get-sth-consistency?first=15&second=20 90 | // 91 | // When concatenating baseURL, path and params, buildURL ensures that only one 92 | // "/" appears between the baseURL and the path, and that no "/" appears between 93 | // the result of concatenating the baseURL and path, and the params. 94 | // 95 | // Example: 96 | // - Base URL: https://ct.googleapis.com/pilot/ 97 | // - Path: /ct/v1/get-sth-consistency/ 98 | // - Params: map[string]string{"first":"15", "second":"20"} 99 | // Result: https://ct.googleapis.com/pilot/ct/v1/get-sth-consistency?first=15&second=20 100 | // 101 | // See the tests for further examples. 102 | func buildURL(baseURL, path string, params map[string]string) string { 103 | var withoutParams string 104 | if len(baseURL) > 0 && len(path) > 0 { 105 | // If we need to concatenate a non-empty baseURL and a non-empty path, 106 | // do it so that exactly one "/" will appear between the two. 107 | withoutParams = fmt.Sprintf("%s/%s", strings.TrimRight(baseURL, "/"), strings.TrimLeft(path, "/")) 108 | } else { 109 | // Otherwise, at least one of them is empty, so just concatenating will 110 | // result in the non-empty one (if there is one) remaining unaltered. 111 | withoutParams = fmt.Sprintf("%s%s", baseURL, path) 112 | } 113 | 114 | if len(params) == 0 { 115 | return withoutParams 116 | } 117 | 118 | // If there are parameters to be added to the URL, remove any trailing /'s 119 | // before adding the parameters. 120 | withoutParams = strings.TrimRight(withoutParams, "/") 121 | vals := url.Values{} 122 | for k, v := range params { 123 | vals.Add(k, v) 124 | } 125 | return fmt.Sprintf("%s?%s", withoutParams, vals.Encode()) 126 | } 127 | 128 | // HTTPData contains information about an HTTP request that was made. 129 | type HTTPData struct { 130 | Timing Timing 131 | Response *http.Response 132 | Body []byte 133 | } 134 | 135 | // Timing represents an interval of time. It can be used to represent when an 136 | // event started and ended. 137 | type Timing struct { 138 | Start time.Time 139 | End time.Time 140 | } 141 | 142 | // get makes an HTTP GET call to path on the server at lc.url, using the 143 | // parameters provided. 144 | func (lc *LogClient) get(path string, params map[string]string) (*HTTPData, error) { 145 | httpData := &HTTPData{Timing: Timing{}} 146 | 147 | fullURL := buildURL(lc.url, path, params) 148 | httpData.Timing.Start = time.Now().UTC() 149 | resp, err := lc.httpClient.Get(fullURL) 150 | httpData.Timing.End = time.Now().UTC() 151 | if err != nil { 152 | return httpData, &GetError{URL: fullURL, Err: err} 153 | } 154 | 155 | if resp == nil { 156 | return httpData, &NilResponseError{URL: fullURL} 157 | } 158 | httpData.Response = resp 159 | 160 | body, err := ioutil.ReadAll(resp.Body) 161 | resp.Body.Close() 162 | if err != nil { 163 | return httpData, &BodyReadError{URL: fullURL, Err: err} 164 | } 165 | httpData.Body = body 166 | 167 | if resp.StatusCode != http.StatusOK { 168 | return httpData, &HTTPStatusError{StatusCode: resp.StatusCode} 169 | } 170 | 171 | return httpData, nil 172 | } 173 | 174 | // getAndParse calls get() (see above) and then attempts to parse the JSON 175 | // response body into rsp. 176 | func (lc *LogClient) getAndParse(path string, params map[string]string, rsp interface{}) (*HTTPData, error) { 177 | httpData, err := lc.get(path, params) 178 | if err != nil { 179 | return httpData, err 180 | } 181 | if err = json.Unmarshal(httpData.Body, rsp); err != nil { 182 | return httpData, &JSONParseError{Data: httpData.Body, Err: err} 183 | } 184 | return httpData, nil 185 | } 186 | 187 | // GetSTH performs a get-sth request. 188 | // Returned is: 189 | // - a populated ct.SignedTreeHead, if no error is returned. 190 | // - an HTTPData struct (see above). 191 | // - an error, which could be any of the error types listed in the LogClient 192 | // documentation (see above), or a ResponseToStructError. 193 | func (lc *LogClient) GetSTH() (*ct.SignedTreeHead, *HTTPData, error) { 194 | var resp ct.GetSTHResponse 195 | httpData, err := lc.getAndParse(ct.GetSTHPath, nil, &resp) 196 | if err != nil { 197 | return nil, httpData, err 198 | } 199 | 200 | sth, err := resp.ToSignedTreeHead() 201 | if err != nil { 202 | return nil, httpData, &ResponseToStructError{From: reflect.TypeOf(resp), To: reflect.TypeOf(sth), Err: err} 203 | } 204 | 205 | return sth, httpData, nil 206 | } 207 | 208 | // GetRoots performs a get-roots request. 209 | // Returned is: 210 | // - a list of certificates, if no error is returned. 211 | // - the HTTPData struct returned by GetAndParse() (see above). 212 | // - an error, which could be any of the error types returned by 213 | // GetAndParse(), or a ResponseToStructError. 214 | func (lc *LogClient) GetRoots() ([]*x509.Certificate, *HTTPData, error) { 215 | var resp ct.GetRootsResponse 216 | httpData, err := lc.getAndParse(ct.GetRootsPath, nil, &resp) 217 | if err != nil { 218 | return nil, httpData, err 219 | } 220 | 221 | roots := make([]*x509.Certificate, len(resp.Certificates)) 222 | 223 | if resp.Certificates == nil { 224 | return nil, httpData, &ResponseToStructError{ 225 | From: reflect.TypeOf(resp), 226 | To: reflect.TypeOf(roots), 227 | Err: fmt.Errorf("no %q field in %q response", "certificates", ct.GetRootsStr), 228 | } 229 | } 230 | 231 | for i, certB64 := range resp.Certificates { 232 | roots[i], err = parseCertificate(certB64) 233 | if err != nil { 234 | return nil, httpData, &ResponseToStructError{ 235 | From: reflect.TypeOf(resp), 236 | To: reflect.TypeOf(roots), 237 | Err: fmt.Errorf("certificates[%d] is invalid: %s", i, err), 238 | } 239 | } 240 | } 241 | 242 | return roots, httpData, nil 243 | } 244 | 245 | func parseCertificate(b64 string) (*x509.Certificate, error) { 246 | certDER, err := base64.StdEncoding.DecodeString(b64) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | return x509.ParseCertificate(certDER) 252 | } 253 | 254 | // GetProofByHash performs a get-proof-by-hash request, with parameters hash and 255 | // treeSize. 256 | // Returned is: 257 | // - a GetProofByHashResponse struct, if no error is returned. 258 | // - the HTTPData struct returned by GetAndParse() (see above). 259 | // - an error, which could be any of the error types returned by 260 | // GetAndParse(). 261 | func (lc *LogClient) GetProofByHash(hash []byte, treeSize uint64) (*ct.GetProofByHashResponse, *HTTPData, error) { 262 | params := map[string]string{ 263 | "hash": base64.URLEncoding.EncodeToString(hash), 264 | "tree_size": strconv.FormatUint(treeSize, 10), 265 | } 266 | var resp ct.GetProofByHashResponse 267 | httpData, err := lc.getAndParse(ct.GetProofByHashPath, params, &resp) 268 | if err != nil { 269 | return nil, httpData, err 270 | } 271 | 272 | return &resp, httpData, nil 273 | } 274 | 275 | // post makes an HTTP POST call to path on the server at lc.url, sending the 276 | // body provided. 277 | func (lc *LogClient) post(path string, body []byte) (*HTTPData, error) { 278 | httpData := &HTTPData{Timing: Timing{}} 279 | 280 | fullURL := buildURL(lc.url, path, nil) 281 | httpData.Timing.Start = time.Now().UTC() 282 | resp, err := lc.httpClient.Post(fullURL, contentType, bytes.NewReader(body)) 283 | httpData.Timing.End = time.Now().UTC() 284 | if err != nil { 285 | return httpData, &PostError{URL: fullURL, ContentType: contentType, Body: body, Err: err} 286 | } 287 | 288 | // For the purposes of CT Logs, there should always be a response. 289 | if resp == nil { 290 | return httpData, &NilResponseError{URL: fullURL} 291 | } 292 | httpData.Response = resp 293 | 294 | rspBody, err := ioutil.ReadAll(resp.Body) 295 | resp.Body.Close() 296 | if err != nil { 297 | return httpData, &BodyReadError{URL: fullURL, Err: err} 298 | } 299 | httpData.Body = rspBody 300 | 301 | if resp.StatusCode != http.StatusOK { 302 | return httpData, &HTTPStatusError{StatusCode: resp.StatusCode} 303 | } 304 | 305 | return httpData, nil 306 | } 307 | 308 | // postAndParse calls post() (see above) and then attempts to parse the JSON 309 | // response body into rsp. 310 | func (lc *LogClient) postAndParse(path string, body []byte, rsp interface{}) (*HTTPData, error) { 311 | httpData, err := lc.post(path, body) 312 | if err != nil { 313 | return httpData, err 314 | } 315 | if err = json.Unmarshal(httpData.Body, rsp); err != nil { 316 | return httpData, &JSONParseError{Data: httpData.Body, Err: err} 317 | } 318 | return httpData, nil 319 | } 320 | 321 | func (lc *LogClient) addChain(path string, chain []*x509.Certificate) (*ct.SignedCertificateTimestamp, *HTTPData, error) { 322 | var req ct.AddChainRequest 323 | for _, cert := range chain { 324 | req.Chain = append(req.Chain, cert.Raw) 325 | } 326 | 327 | body, err := json.Marshal(req) 328 | if err != nil { 329 | return nil, nil, err 330 | } 331 | 332 | var resp ct.AddChainResponse 333 | httpData, err := lc.postAndParse(path, body, &resp) 334 | if err != nil { 335 | return nil, httpData, err 336 | } 337 | 338 | sct, err := resp.ToSignedCertificateTimestamp() 339 | if err != nil { 340 | return nil, httpData, &ResponseToStructError{From: reflect.TypeOf(resp), To: reflect.TypeOf(sct), Err: err} 341 | } 342 | 343 | return sct, httpData, nil 344 | } 345 | 346 | // AddChain performs an add-chain request, posting the provided certificate 347 | // chain to the CT Log hosted at LogClient.url. The first certificate in the 348 | // chain must be the end-entity certificate, with the second chaining to the 349 | // first and so on to the last, which should either be the root certificate or a 350 | // certificate that chains to a root certificate that is accepted by the Log. 351 | // Returned is: 352 | // - a populated ct.SignedCertificateTimestamp, if no error is returned. 353 | // - an HTTPData struct (may be non-nil even when err != nil, see above). 354 | // - an error, which could be an error from the Go standard library, any of 355 | // the error types listed in the LogClient documentation (see above), or a 356 | // ResponseToStructError. 357 | func (lc *LogClient) AddChain(chain []*x509.Certificate) (*ct.SignedCertificateTimestamp, *HTTPData, error) { 358 | return lc.addChain(ct.AddChainPath, chain) 359 | } 360 | 361 | // AddPreChain performs an add-pre-chain request, posting the provided 362 | // certificate chain to the CT Log hosted at LogClient.url. The first 363 | // certificate in the chain must be the end-entity pre-certificate, with the 364 | // second chaining to the first and so on to the last, which should either be 365 | // the root certificate or a certificate that chains to a root certificate that 366 | // is accepted by the Log. 367 | // Returned is: 368 | // - a populated ct.SignedCertificateTimestamp, if no error is returned. 369 | // - an HTTPData struct (may be non-nil even when err != nil, see above). 370 | // - an error, which could be an error from the Go standard library, any of 371 | // the error types listed in the LogClient documentation (see above), or a 372 | // ResponseToStructError. 373 | func (lc *LogClient) AddPreChain(chain []*x509.Certificate) (*ct.SignedCertificateTimestamp, *HTTPData, error) { 374 | return lc.addChain(ct.AddPreChainPath, chain) 375 | } 376 | -------------------------------------------------------------------------------- /testdata/testdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testdata holds data used for testing by multiple Monologue packages. 16 | package testdata 17 | 18 | var ( 19 | // Certificate: 20 | // Data: 21 | // Version: 3 (0x2) 22 | // Serial Number: 23 | // 9e:d3:cc:b1:d1:2c:a2:72 24 | // Signature Algorithm: sha1WithRSAEncryption 25 | // Issuer: C = GB, ST = London, O = Google UK Ltd., OU = Certificate Transparency, CN = Merge Delay Monitor Root 26 | // Validity 27 | // Not Before: Jul 17 12:05:43 2014 GMT 28 | // Not After : Dec 2 12:05:43 2041 GMT 29 | // Subject: C = GB, ST = London, O = Google UK Ltd., OU = Certificate Transparency, CN = Merge Delay Monitor Root 30 | // Subject Public Key Info: 31 | // Public Key Algorithm: rsaEncryption 32 | // RSA Public-Key: (4096 bit) 33 | // Modulus: 34 | // 00:aa:16:1c:f2:20:5e:d8:1a:c5:65:48:3c:da:42: 35 | // 6a:3d:b2:e5:88:fd:b7:58:b1:7b:93:ea:8d:68:49: 36 | // 5d:53:4a:01:ba:4f:6c:d1:c0:fc:0a:12:8a:f7:9c: 37 | // 06:6d:c5:4c:3f:43:7e:05:ba:27:5e:e6:1d:bf:9c: 38 | // bd:b2:92:81:83:73:81:39:39:7b:61:89:ae:73:8f: 39 | // ef:2b:9b:60:9a:6d:d8:e0:b0:d0:e2:0b:24:3d:b9: 40 | // 36:c0:29:cd:c2:22:0a:f2:c0:e1:a5:e4:aa:41:a0: 41 | // 06:af:45:89:57:e2:b1:17:8d:27:15:6e:f0:cb:71: 42 | // 7e:16:d5:40:25:d9:7f:43:e9:91:6f:b2:40:fb:85: 43 | // f7:d5:79:46:2f:a0:ac:76:c7:62:56:84:37:50:bf: 44 | // 1c:cd:fe:b7:6c:8c:47:88:64:77:64:4d:5e:c3:23: 45 | // 56:28:ad:f6:a0:9c:84:88:bf:a5:03:6d:e7:17:90: 46 | // 81:51:a6:b5:85:f2:73:dd:9f:b5:33:2b:9a:f7:6e: 47 | // 8f:bf:a9:1e:af:43:11:81:6d:de:27:c5:c4:4f:2f: 48 | // d0:6c:c2:20:4d:71:47:f7:7b:a6:b1:6a:2a:5f:ca: 49 | // 47:00:23:61:47:29:53:8b:ee:6b:3c:b0:72:64:71: 50 | // 38:32:ae:c1:61:55:0e:b5:01:90:68:02:21:52:23: 51 | // ac:c2:56:4a:d1:f9:8b:b5:93:49:24:eb:56:d3:83: 52 | // fc:75:98:be:45:c8:9d:99:52:81:c0:ef:b0:d2:06: 53 | // d2:9a:6d:25:a1:0a:48:fe:23:53:32:37:9c:5c:a6: 54 | // 9e:83:59:9f:aa:67:7d:d2:08:23:f5:c8:4a:96:12: 55 | // 55:ec:a5:d4:87:1d:54:ca:1d:f0:77:4a:a1:17:b0: 56 | // f4:2c:d6:e9:fd:a7:e8:a4:8a:53:92:3c:5f:94:04: 57 | // 33:53:54:4e:64:4b:5a:65:62:e5:ce:f9:fc:2b:d2: 58 | // fc:fc:ce:33:23:33:5c:f7:fe:7c:4d:83:c1:b7:f8: 59 | // 39:c4:79:01:92:d3:ba:9a:a9:f3:20:93:aa:8e:e7: 60 | // cb:e7:08:05:9d:53:8d:c6:63:cc:a1:b8:25:33:1a: 61 | // a8:36:75:4a:0d:13:de:63:bf:65:b6:e2:04:4d:cd: 62 | // f0:41:f1:a0:c5:a9:c3:c3:8f:e7:4c:f5:76:d4:51: 63 | // c2:3e:aa:51:9d:b3:2e:f9:e0:39:bd:84:8a:19:4c: 64 | // 3b:5e:41:a5:56:42:dc:28:3d:db:d7:3d:1d:d9:7a: 65 | // e6:95:1d:e1:8a:d8:9d:00:50:07:fa:e7:e8:8b:c7: 66 | // a3:cc:e8:b7:cc:c4:96:03:a0:db:67:c7:6d:58:a2: 67 | // 8d:4b:77:aa:74:60:80:1e:34:37:7d:0c:5e:46:06: 68 | // c2:e2:5b 69 | // Exponent: 65537 (0x10001) 70 | // X509v3 extensions: 71 | // X509v3 Subject Key Identifier: 72 | // F3:5F:7B:75:49:E3:78:41:39:6A:20:B6:7C:6B:4C:5C:C9:3D:58:41 73 | // X509v3 Authority Key Identifier: 74 | // keyid:F3:5F:7B:75:49:E3:78:41:39:6A:20:B6:7C:6B:4C:5C:C9:3D:58:41 75 | // 76 | // X509v3 Basic Constraints: 77 | // CA:TRUE 78 | // Signature Algorithm: sha1WithRSAEncryption 79 | // 77:1c:fe:a3:45:79:a9:75:20:d8:c2:42:3d:68:ec:d0:78:91: 80 | // f8:c7:f1:c3:8b:f3:cd:30:ea:9d:36:37:bf:c5:d3:73:53:2e: 81 | // c7:65:65:58:be:f4:95:06:46:75:0c:6f:e0:85:c5:2d:9f:fc: 82 | // 09:e6:6e:bf:a2:b0:67:de:77:27:cb:38:1d:2b:25:db:58:b9: 83 | // c2:19:7f:d5:eb:53:e0:20:f4:29:b8:6a:3b:1f:37:a0:7a:76: 84 | // 1a:66:a5:b3:ec:d7:97:c4:66:95:a3:7f:f2:d4:7c:54:12:6b: 85 | // e6:bd:28:a2:a1:03:35:72:27:c6:b7:3f:7f:68:9b:09:b4:89: 86 | // 27:e6:e9:a5:22:67:a7:28:a1:15:d4:bc:bb:47:75:33:dc:28: 87 | // f3:fc:57:da:73:5a:3e:c5:4f:bc:36:99:0b:17:fe:bb:7e:46: 88 | // b3:24:20:8c:1f:a7:42:5a:0c:ba:48:bd:c0:38:1e:a8:28:52: 89 | // 15:26:1c:3c:48:3f:2f:a6:d1:da:0d:ba:49:49:10:71:89:f3: 90 | // 2d:72:8a:7f:f3:95:d4:34:30:af:3b:8c:e4:be:50:75:bc:f6: 91 | // 7d:66:66:19:41:dc:8b:e3:73:40:f8:f9:28:2b:2d:2a:ad:d6: 92 | // 90:65:93:2a:d4:97:69:f8:bc:7f:c9:e5:f6:e7:9f:f3:92:42: 93 | // 0b:a7:8d:e3:17:27:78:e2:b6:7e:4d:f1:84:40:dd:56:1e:5a: 94 | // 78:44:c8:ef:a0:6c:0e:5f:5b:86:95:fa:06:91:14:a0:05:18: 95 | // fb:4c:19:f9:d7:85:58:31:b5:ee:eb:ce:d1:4b:85:98:da:ff: 96 | // a4:9f:2d:cf:50:5b:ff:64:17:d8:4b:28:e8:35:99:d4:e0:37: 97 | // 1e:f6:4b:2d:82:ff:a0:68:a3:10:44:f7:32:2f:ee:2f:65:4e: 98 | // c3:57:c9:c1:21:f3:45:8a:50:97:28:c3:7f:56:73:41:2a:d0: 99 | // d5:e7:6a:a6:b4:eb:15:82:18:1a:8b:e4:04:d3:dc:35:e7:1e: 100 | // dd:83:ee:38:80:87:d6:14:7c:4d:86:f1:ca:ca:cf:ac:e0:10: 101 | // 4d:f1:f4:b1:00:c2:ce:b1:be:4d:18:51:c4:f3:1e:7c:44:09: 102 | // 26:21:85:87:8f:23:cc:eb:30:79:01:43:f0:d5:bd:80:d2:c0: 103 | // ed:42:60:aa:a3:12:59:7d:95:0a:af:3c:8b:cf:c8:12:d9:a5: 104 | // 6e:8d:16:0d:d7:72:a4:94:74:37:10:a7:e4:78:a0:46:d8:a5: 105 | // d5:05:ee:6b:8c:c3:7f:ec:09:df:d4:cb:57:c6:c4:d8:e8:ef: 106 | // 2a:22:e1:d9:e5:09:57:85:26:33:13:8d:72:22:53:e5:1b:a7: 107 | // 7b:00:69:d3:88:12:20:7a 108 | RootCertPEM = `-----BEGIN CERTIFICATE----- 109 | MIIFzTCCA7WgAwIBAgIJAJ7TzLHRLKJyMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNV 110 | BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoMDkdvb2dsZSBVSyBMdGQu 111 | MSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxITAfBgNVBAMMGE1l 112 | cmdlIERlbGF5IE1vbml0b3IgUm9vdDAeFw0xNDA3MTcxMjA1NDNaFw00MTEyMDIx 113 | MjA1NDNaMH0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoM 114 | Dkdvb2dsZSBVSyBMdGQuMSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVu 115 | Y3kxITAfBgNVBAMMGE1lcmdlIERlbGF5IE1vbml0b3IgUm9vdDCCAiIwDQYJKoZI 116 | hvcNAQEBBQADggIPADCCAgoCggIBAKoWHPIgXtgaxWVIPNpCaj2y5Yj9t1ixe5Pq 117 | jWhJXVNKAbpPbNHA/AoSivecBm3FTD9DfgW6J17mHb+cvbKSgYNzgTk5e2GJrnOP 118 | 7yubYJpt2OCw0OILJD25NsApzcIiCvLA4aXkqkGgBq9FiVfisReNJxVu8MtxfhbV 119 | QCXZf0PpkW+yQPuF99V5Ri+grHbHYlaEN1C/HM3+t2yMR4hkd2RNXsMjViit9qCc 120 | hIi/pQNt5xeQgVGmtYXyc92ftTMrmvduj7+pHq9DEYFt3ifFxE8v0GzCIE1xR/d7 121 | prFqKl/KRwAjYUcpU4vuazywcmRxODKuwWFVDrUBkGgCIVIjrMJWStH5i7WTSSTr 122 | VtOD/HWYvkXInZlSgcDvsNIG0pptJaEKSP4jUzI3nFymnoNZn6pnfdIII/XISpYS 123 | Veyl1IcdVMod8HdKoRew9CzW6f2n6KSKU5I8X5QEM1NUTmRLWmVi5c75/CvS/PzO 124 | MyMzXPf+fE2Dwbf4OcR5AZLTupqp8yCTqo7ny+cIBZ1TjcZjzKG4JTMaqDZ1Sg0T 125 | 3mO/ZbbiBE3N8EHxoMWpw8OP50z1dtRRwj6qUZ2zLvngOb2EihlMO15BpVZC3Cg9 126 | 29c9Hdl65pUd4YrYnQBQB/rn6IvHo8zot8zElgOg22fHbViijUt3qnRggB40N30M 127 | XkYGwuJbAgMBAAGjUDBOMB0GA1UdDgQWBBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAf 128 | BgNVHSMEGDAWgBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAMBgNVHRMEBTADAQH/MA0G 129 | CSqGSIb3DQEBBQUAA4ICAQB3HP6jRXmpdSDYwkI9aOzQeJH4x/HDi/PNMOqdNje/ 130 | xdNzUy7HZWVYvvSVBkZ1DG/ghcUtn/wJ5m6/orBn3ncnyzgdKyXbWLnCGX/V61Pg 131 | IPQpuGo7HzegenYaZqWz7NeXxGaVo3/y1HxUEmvmvSiioQM1cifGtz9/aJsJtIkn 132 | 5umlImenKKEV1Ly7R3Uz3Cjz/Ffac1o+xU+8NpkLF/67fkazJCCMH6dCWgy6SL3A 133 | OB6oKFIVJhw8SD8vptHaDbpJSRBxifMtcop/85XUNDCvO4zkvlB1vPZ9ZmYZQdyL 134 | 43NA+PkoKy0qrdaQZZMq1Jdp+Lx/yeX255/zkkILp43jFyd44rZ+TfGEQN1WHlp4 135 | RMjvoGwOX1uGlfoGkRSgBRj7TBn514VYMbXu687RS4WY2v+kny3PUFv/ZBfYSyjo 136 | NZnU4Dce9kstgv+gaKMQRPcyL+4vZU7DV8nBIfNFilCXKMN/VnNBKtDV52qmtOsV 137 | ghgai+QE09w15x7dg+44gIfWFHxNhvHKys+s4BBN8fSxAMLOsb5NGFHE8x58RAkm 138 | IYWHjyPM6zB5AUPw1b2A0sDtQmCqoxJZfZUKrzyLz8gS2aVujRYN13KklHQ3EKfk 139 | eKBG2KXVBe5rjMN/7Anf1MtXxsTY6O8qIuHZ5QlXhSYzE41yIlPlG6d7AGnTiBIg 140 | eg== 141 | -----END CERTIFICATE-----` 142 | 143 | // Certificate: 144 | // Data: 145 | // Version: 3 (0x2) 146 | // Serial Number: 4097 (0x1001) 147 | // Signature Algorithm: sha1WithRSAEncryption 148 | // Issuer: C = GB, ST = London, O = Google UK Ltd., OU = Certificate Transparency, CN = Merge Delay Monitor Root 149 | // Validity 150 | // Not Before: Jul 17 12:26:30 2014 GMT 151 | // Not After : Jul 16 12:26:30 2019 GMT 152 | // Subject: C = GB, ST = London, O = Google UK Ltd., OU = Certificate Transparency, CN = Merge Delay Intermediate 1 153 | // Subject Public Key Info: 154 | // Public Key Algorithm: rsaEncryption 155 | // RSA Public-Key: (4096 bit) 156 | // Modulus: 157 | // 00:c1:e8:74:fe:ff:9a:ee:f3:03:bb:fa:63:45:38: 158 | // 81:fa:af:8d:c1:c2:2c:09:64:1d:af:43:03:81:f3: 159 | // 3b:c1:57:bf:6c:4c:8a:8d:57:b1:ab:c7:92:85:9d: 160 | // 20:f2:19:15:09:c5:97:c4:37:b1:46:73:de:a5:af: 161 | // 4b:ea:14:39:6d:d4:36:dc:62:05:55:d7:95:3e:0e: 162 | // de:01:f7:ff:b4:4f:3f:f7:cd:e6:4e:d2:45:63:4e: 163 | // 0d:f0:aa:fc:e9:c3:ac:5e:b6:3d:8d:e1:d9:69:ca: 164 | // c8:85:41:95:40:3a:9f:9d:1d:4c:3d:ce:ed:f1:35: 165 | // 1e:dd:94:57:43:bc:54:ab:74:5b:20:4f:b5:25:9f: 166 | // e3:ed:f6:95:c2:cf:90:b8:86:c4:8f:f6:80:b7:44: 167 | // fe:c2:69:1b:43:45:24:2b:31:c3:6f:31:18:b7:27: 168 | // c5:de:5c:25:ec:1a:a3:0a:4c:24:61:c5:11:9e:f6: 169 | // bb:90:d8:16:e6:e4:4c:5b:99:55:bf:a2:ed:34:16: 170 | // bf:6e:4a:53:c9:2f:af:ac:0d:1f:0b:8b:f3:d3:5b: 171 | // e0:d4:f6:1c:a0:5d:28:fc:66:2d:fa:58:8f:ba:3e: 172 | // e0:38:04:70:c0:12:de:d9:e5:1b:bf:1e:7a:25:ef: 173 | // a7:45:78:4c:49:d0:5e:af:8d:ce:e0:52:73:61:ec: 174 | // 91:31:26:00:5e:97:2c:f4:b8:63:91:4f:83:61:58: 175 | // 2e:d2:45:63:ff:9e:03:c5:2e:8a:9c:a3:26:4c:56: 176 | // c1:86:b4:ec:52:b7:e6:95:ce:42:ae:17:ec:7a:e0: 177 | // 25:71:31:e1:db:f4:8f:2d:de:24:2e:6e:91:ea:30: 178 | // 49:88:13:5a:15:48:2b:05:fc:09:13:55:32:8b:39: 179 | // e5:86:e8:dd:3a:4a:3a:14:cb:97:ee:f6:8f:9f:69: 180 | // 72:8c:29:1f:21:95:d2:cc:e7:3d:4a:e9:08:45:b1: 181 | // bf:c5:fa:e0:40:b9:4f:c3:59:a2:95:11:98:1b:99: 182 | // 66:ae:b5:6d:3a:7c:5e:48:f8:ec:a8:15:e5:be:86: 183 | // b3:d3:6e:6a:27:e0:e2:c4:de:e6:e3:0f:12:a7:c9: 184 | // 36:b8:c9:8c:ad:59:28:ac:a2:38:df:c3:9c:f9:f2: 185 | // c5:24:6c:bb:bb:28:0c:b6:f9:9e:b4:9b:fd:1d:78: 186 | // 08:95:39:07:2c:16:4c:70:83:37:17:46:de:db:c4: 187 | // de:c1:cb:94:39:07:3a:f3:f2:e6:0f:8c:50:5f:06: 188 | // 79:61:a8:c5:39:45:4f:c5:34:11:58:ec:cc:78:53: 189 | // 2f:3e:39:c3:18:7c:94:39:fc:0f:f8:8e:e9:57:13: 190 | // 1d:47:8d:f0:63:dd:50:b2:ad:3f:e7:a0:70:e9:05: 191 | // e3:86:8b 192 | // Exponent: 65537 (0x10001) 193 | // X509v3 extensions: 194 | // X509v3 Subject Key Identifier: 195 | // E9:3C:04:E1:80:2F:C2:84:13:2D:26:70:9E:F2:FD:1A:CF:AA:FE:C6 196 | // X509v3 Authority Key Identifier: 197 | // keyid:F3:5F:7B:75:49:E3:78:41:39:6A:20:B6:7C:6B:4C:5C:C9:3D:58:41 198 | // 199 | // X509v3 Basic Constraints: 200 | // CA:TRUE 201 | // Signature Algorithm: sha1WithRSAEncryption 202 | // 08:58:cb:d5:45:f2:e9:2e:09:90:6a:c3:9f:3d:55:c1:36:07: 203 | // a6:51:43:63:86:c6:e9:0f:12:87:73:a0:eb:3f:47:25:d8:af: 204 | // 35:fb:7f:88:04:36:b4:81:f2:cf:47:80:18:25:de:54:f1:3f: 205 | // 8f:59:20:bf:3d:91:6e:75:31:41:de:5e:59:d2:de:bb:fc:3f: 206 | // c2:26:72:1f:15:a1:6d:3b:7a:46:18:ea:05:51:63:9f:1d:2c: 207 | // b7:b9:fa:ad:1b:7e:07:0f:23:a6:e8:19:7c:3d:75:49:bb:a6: 208 | // 55:3f:d5:db:41:9c:e3:99:47:7f:6a:04:81:b9:0f:51:c9:d3: 209 | // 07:d8:2c:b0:5c:f9:67:82:8a:1a:ce:65:20:7c:f8:6b:6d:16: 210 | // 79:22:45:dc:f2:4b:4c:17:9f:91:18:47:36:e7:e2:fc:b8:63: 211 | // a4:b5:c8:9b:0a:c2:f3:68:39:0a:10:59:4b:95:c8:56:e2:59: 212 | // c7:75:64:31:68:98:cf:87:a6:81:7d:18:58:5f:c9:76:d6:81: 213 | // d9:d5:10:ef:2a:d3:7e:8a:d0:e4:9f:5b:d4:99:c9:ec:7f:e8: 214 | // f4:3b:17:df:fb:9b:7d:0d:fd:83:00:c1:c5:38:9c:9e:a0:be: 215 | // 43:70:dc:bf:78:bd:3e:fc:23:08:d2:50:b8:66:bb:ca:03:1c: 216 | // 0c:49:ff:77:a7:a5:42:0d:aa:1f:1b:6a:44:4d:36:66:53:97: 217 | // 4c:2d:17:9c:30:09:87:1e:e6:c8:91:40:fc:a9:ef:df:23:bd: 218 | // 4b:88:c6:eb:ae:b9:28:6f:58:f3:cf:c2:1e:48:74:f1:82:d1: 219 | // ec:d6:05:89:19:b0:3b:18:db:7b:79:5b:e9:cb:25:fc:51:66: 220 | // a9:45:ef:8e:11:33:cd:60:31:2a:32:34:f4:64:9d:f6:16:64: 221 | // 07:cb:b5:ec:c8:38:e9:e1:18:c0:5d:ee:7c:89:6a:99:87:65: 222 | // 5a:e7:e3:49:cd:81:66:e6:8d:34:de:a3:b4:ae:89:2a:9f:23: 223 | // 85:05:32:71:e8:60:b5:42:be:36:50:50:39:74:f3:bb:6f:26: 224 | // 88:37:5a:b2:84:87:da:67:51:d0:f2:c3:a3:5e:78:ef:e3:0b: 225 | // 19:d5:78:08:eb:ea:2c:44:53:99:0a:d8:1e:b9:62:89:c0:f9: 226 | // 9c:50:80:f8:20:92:bc:61:23:a3:40:c6:3a:61:7f:3b:c4:ad: 227 | // c1:29:8d:88:a2:78:a6:93:ef:93:68:86:11:d3:b4:ed:ed:0b: 228 | // 6d:02:3e:d9:f6:c2:ea:88:36:48:31:97:52:5b:1b:1b:ce:70: 229 | // a9:0c:34:03:b0:94:d5:f4:12:aa:11:41:b9:96:5a:b8:31:4c: 230 | // 52:f7:72:de:ff:c1:00:8c 231 | IntermediateCertPEM = `-----BEGIN CERTIFICATE----- 232 | MIIF9jCCA7CgAwIBAgICEAEwDQYJKoZIhvcNAQEFBQAwfTELMAkGA1UEBhMCR0Ix 233 | DzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xlIFVLIEx0ZC4xITAfBgNV 234 | BAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEhMB8GA1UEAwwYTWVyZ2UgRGVs 235 | YXkgTW9uaXRvciBSb290MB4XDTE0MDcxNzEyMjYzMFoXDTE5MDcxNjEyMjYzMFow 236 | fzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xl 237 | IFVLIEx0ZC4xITAfBgNVBAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEjMCEG 238 | A1UEAwwaTWVyZ2UgRGVsYXkgSW50ZXJtZWRpYXRlIDEwggIiMA0GCSqGSIb3DQEB 239 | AQUAA4ICDwAwggIKAoICAQDB6HT+/5ru8wO7+mNFOIH6r43BwiwJZB2vQwOB8zvB 240 | V79sTIqNV7Grx5KFnSDyGRUJxZfEN7FGc96lr0vqFDlt1DbcYgVV15U+Dt4B9/+0 241 | Tz/3zeZO0kVjTg3wqvzpw6xetj2N4dlpysiFQZVAOp+dHUw9zu3xNR7dlFdDvFSr 242 | dFsgT7Uln+Pt9pXCz5C4hsSP9oC3RP7CaRtDRSQrMcNvMRi3J8XeXCXsGqMKTCRh 243 | xRGe9ruQ2Bbm5ExbmVW/ou00Fr9uSlPJL6+sDR8Li/PTW+DU9hygXSj8Zi36WI+6 244 | PuA4BHDAEt7Z5Ru/Hnol76dFeExJ0F6vjc7gUnNh7JExJgBelyz0uGORT4NhWC7S 245 | RWP/ngPFLoqcoyZMVsGGtOxSt+aVzkKuF+x64CVxMeHb9I8t3iQubpHqMEmIE1oV 246 | SCsF/AkTVTKLOeWG6N06SjoUy5fu9o+faXKMKR8hldLM5z1K6QhFsb/F+uBAuU/D 247 | WaKVEZgbmWautW06fF5I+OyoFeW+hrPTbmon4OLE3ubjDxKnyTa4yYytWSisojjf 248 | w5z58sUkbLu7KAy2+Z60m/0deAiVOQcsFkxwgzcXRt7bxN7By5Q5Bzrz8uYPjFBf 249 | BnlhqMU5RU/FNBFY7Mx4Uy8+OcMYfJQ5/A/4julXEx1HjfBj3VCyrT/noHDpBeOG 250 | iwIDAQABo1AwTjAdBgNVHQ4EFgQU6TwE4YAvwoQTLSZwnvL9Gs+q/sYwHwYDVR0j 251 | BBgwFoAU8197dUnjeEE5aiC2fGtMXMk9WEEwDAYDVR0TBAUwAwEB/zA7BgkqhkiG 252 | 9w0BAQUTLlRoaXMgaXMgbm90IHRoZSBjZXJ0aWZpY2F0ZSB5b3UncmUgbG9va2lu 253 | ZyBmb3IDggIBAAhYy9VF8ukuCZBqw589VcE2B6ZRQ2OGxukPEodzoOs/RyXYrzX7 254 | f4gENrSB8s9HgBgl3lTxP49ZIL89kW51MUHeXlnS3rv8P8Imch8VoW07ekYY6gVR 255 | Y58dLLe5+q0bfgcPI6boGXw9dUm7plU/1dtBnOOZR39qBIG5D1HJ0wfYLLBc+WeC 256 | ihrOZSB8+GttFnkiRdzyS0wXn5EYRzbn4vy4Y6S1yJsKwvNoOQoQWUuVyFbiWcd1 257 | ZDFomM+HpoF9GFhfyXbWgdnVEO8q036K0OSfW9SZyex/6PQ7F9/7m30N/YMAwcU4 258 | nJ6gvkNw3L94vT78IwjSULhmu8oDHAxJ/3enpUINqh8bakRNNmZTl0wtF5wwCYce 259 | 5siRQPyp798jvUuIxuuuuShvWPPPwh5IdPGC0ezWBYkZsDsY23t5W+nLJfxRZqlF 260 | 744RM81gMSoyNPRknfYWZAfLtezIOOnhGMBd7nyJapmHZVrn40nNgWbmjTTeo7Su 261 | iSqfI4UFMnHoYLVCvjZQUDl087tvJog3WrKEh9pnUdDyw6NeeO/jCxnVeAjr6ixE 262 | U5kK2B65YonA+ZxQgPggkrxhI6NAxjphfzvErcEpjYiieKaT75NohhHTtO3tC20C 263 | Ptn2wuqINkgxl1JbGxvOcKkMNAOwlNX0EqoRQbmWWrgxTFL3ct7/wQCM 264 | -----END CERTIFICATE-----` 265 | 266 | // Certificate: 267 | // Data: 268 | // Version: 3 (0x2) 269 | // Serial Number: 1512556025483463 (0x55fa9649a10c7) 270 | // Signature Algorithm: sha256WithRSAEncryption 271 | // Issuer: C = GB, ST = London, O = Google UK Ltd., OU = Certificate Transparency, CN = Merge Delay Intermediate 1 272 | // Validity 273 | // Not Before: Dec 6 10:27:05 2017 GMT 274 | // Not After : Nov 6 14:27:14 2019 GMT 275 | // Subject: C = GB, L = London, O = Google Certificate Transparency, serialNumber = 1512556025483463 276 | // Subject Public Key Info: 277 | // Public Key Algorithm: rsaEncryption 278 | // RSA Public-Key: (2048 bit) 279 | // Modulus: 280 | // 00:af:14:04:af:91:5a:af:6c:54:91:c4:6a:ea:55: 281 | // 6e:36:a8:5f:83:40:e9:f1:3f:cc:63:ea:a7:98:c0: 282 | // 86:54:ac:e3:4e:62:c1:e8:b9:d6:65:ac:1f:97:dd: 283 | // 2e:14:b6:e3:88:3a:6c:32:ca:4b:7a:bd:70:af:b7: 284 | // 1f:b3:e4:f9:e5:61:44:46:51:cc:14:8d:64:b4:df: 285 | // bd:68:3c:ff:d1:33:75:d5:71:e3:02:8c:09:fc:32: 286 | // 02:d8:53:70:c0:5c:53:90:b4:c3:63:51:92:0b:ae: 287 | // 7f:c3:51:70:89:89:5e:df:07:35:b8:bf:9d:17:66: 288 | // 41:bc:31:b8:fa:a8:0e:c2:c7:a3:40:a7:80:34:dc: 289 | // de:2f:eb:81:c2:46:95:76:b9:26:77:b7:ba:11:e3: 290 | // 8a:0d:8e:f8:f3:df:8c:ec:bc:41:a1:2f:76:de:be: 291 | // 50:c9:c5:c8:da:d1:3a:0c:20:44:8a:ff:2c:52:70: 292 | // 56:eb:c3:02:54:3d:94:9b:05:23:45:24:b0:e4:66: 293 | // 55:37:2c:ae:b3:2b:a1:bf:14:5c:ca:1b:d3:21:7d: 294 | // db:26:f7:6b:4b:57:de:06:56:4d:5d:b1:6f:3a:e4: 295 | // 8c:b7:d6:b2:2f:29:46:f9:dc:3d:b5:34:55:49:e3: 296 | // c3:08:e5:c2:32:71:c0:fb:46:a6:9e:74:5c:3e:ef: 297 | // 2c:2b 298 | // Exponent: 65537 (0x10001) 299 | // X509v3 extensions: 300 | // X509v3 Extended Key Usage: 301 | // TLS Web Server Authentication 302 | // X509v3 Subject Alternative Name: 303 | // DNS:flowers-to-the-world.com 304 | // X509v3 Basic Constraints: critical 305 | // CA:FALSE 306 | // X509v3 Authority Key Identifier: 307 | // keyid:E9:3C:04:E1:80:2F:C2:84:13:2D:26:70:9E:F2:FD:1A:CF:AA:FE:C6 308 | // 309 | // X509v3 Subject Key Identifier: 310 | // B3:8B:4A:FB:9B:7C:28:02:03:87:6B:DF:03:B7:E1:A0:1B:FE:61:F3 311 | // Signature Algorithm: sha256WithRSAEncryption 312 | // 6c:b1:ff:51:15:54:d3:e5:00:f4:76:21:9f:e3:63:07:7e:40: 313 | // a7:b8:d3:14:1c:d3:35:37:e9:47:68:d4:d2:a4:7f:65:81:fc: 314 | // da:eb:46:cd:97:8b:18:9a:d5:60:06:8d:80:0b:35:b6:33:89: 315 | // 69:04:37:00:ab:f1:07:2f:fc:69:83:f2:a2:1e:68:5c:81:92: 316 | // af:be:f9:83:fa:25:7a:6b:9c:36:c7:17:a5:b7:7f:5f:9d:03: 317 | // 73:f4:a9:32:b1:2f:88:e4:82:a5:83:0a:be:b3:69:6b:bd:ed: 318 | // 89:55:33:cc:16:8e:c3:98:f4:a1:96:96:28:45:81:bb:e2:72: 319 | // ac:12:20:18:d7:18:39:33:5c:7e:2f:6f:3b:c3:e4:20:94:fb: 320 | // f1:47:51:92:89:40:2d:c8:96:22:52:41:9c:09:23:67:61:6e: 321 | // ea:04:91:ca:2a:46:df:81:26:68:2e:32:17:52:3a:4e:43:06: 322 | // c1:05:81:95:13:65:fd:2b:6f:81:d3:23:f4:08:f7:53:30:71: 323 | // 59:e7:d9:29:52:37:05:e6:c2:6d:8d:4b:f0:bf:13:ed:11:2c: 324 | // db:6d:9a:13:d0:3f:93:36:73:5f:b6:3e:30:e7:03:ef:17:75: 325 | // ec:84:71:dd:fe:db:16:d3:56:f0:d7:9c:49:cd:4f:5c:80:52: 326 | // be:57:1d:71:a4:4d:f3:67:7b:cd:6f:a3:16:16:aa:67:28:f7: 327 | // 9e:88:3e:fc:2a:a2:75:c0:87:68:e0:3e:61:d5:d3:f6:6e:6e: 328 | // 56:1f:89:08:b8:48:38:5f:90:fe:2e:d9:f8:cf:b4:92:2d:5d: 329 | // fe:99:92:4d:bc:89:59:29:2b:ea:9e:87:99:0d:9a:13:4e:c9: 330 | // 7a:7a:da:83:3a:36:3a:06:e8:0a:b9:be:1f:50:78:4e:75:32: 331 | // dd:b8:f3:a6:31:7a:e5:c7:0d:80:29:c2:76:65:a4:aa:67:fe: 332 | // bf:b9:be:04:1a:38:23:22:06:ce:6d:21:ce:cc:ee:6a:aa:d8: 333 | // c8:05:bf:23:5f:0f:45:9e:cb:ee:22:30:2c:43:b3:fe:97:73: 334 | // 10:71:0a:d2:b4:68:e2:54:06:56:a9:27:dd:ae:20:e2:4a:3d: 335 | // 23:9c:c3:da:c6:9b:25:36:dc:b5:0b:ff:b9:0a:03:2c:f0:6c: 336 | // c3:17:af:11:13:73:0a:d3:d7:10:39:af:25:7c:f9:58:68:7d: 337 | // 5d:6b:0b:89:3d:ec:82:8f:d0:5e:1b:bb:0b:d4:25:45:94:2b: 338 | // 3e:39:fc:c5:a2:76:b5:5d:bb:80:56:78:48:c2:27:61:13:73: 339 | // 46:b7:7d:4a:28:c7:c9:d4:28:9b:da:98:9a:06:b4:7d:ab:36: 340 | // 89:ec:be:db:35:00:c2:8a 341 | LeafCertPEM = `-----BEGIN CERTIFICATE----- 342 | MIIE7zCCAtegAwIBAgIHBV+pZJoQxzANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQG 343 | EwJHQjEPMA0GA1UECAwGTG9uZG9uMRcwFQYDVQQKDA5Hb29nbGUgVUsgTHRkLjEh 344 | MB8GA1UECwwYQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5MSMwIQYDVQQDDBpNZXJn 345 | ZSBEZWxheSBJbnRlcm1lZGlhdGUgMTAeFw0xNzEyMDYxMDI3MDVaFw0xOTExMDYx 346 | NDI3MTRaMGMxCzAJBgNVBAYTAkdCMQ8wDQYDVQQHDAZMb25kb24xKDAmBgNVBAoM 347 | H0dvb2dsZSBDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxGTAXBgNVBAUTEDE1MTI1 348 | NTYwMjU0ODM0NjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvFASv 349 | kVqvbFSRxGrqVW42qF+DQOnxP8xj6qeYwIZUrONOYsHoudZlrB+X3S4UtuOIOmwy 350 | ykt6vXCvtx+z5PnlYURGUcwUjWS0371oPP/RM3XVceMCjAn8MgLYU3DAXFOQtMNj 351 | UZILrn/DUXCJiV7fBzW4v50XZkG8Mbj6qA7Cx6NAp4A03N4v64HCRpV2uSZ3t7oR 352 | 44oNjvjz34zsvEGhL3bevlDJxcja0ToMIESK/yxScFbrwwJUPZSbBSNFJLDkZlU3 353 | LK6zK6G/FFzKG9Mhfdsm92tLV94GVk1dsW865Iy31rIvKUb53D21NFVJ48MI5cIy 354 | ccD7RqaedFw+7ywrAgMBAAGjgYswgYgwEwYDVR0lBAwwCgYIKwYBBQUHAwEwIwYD 355 | VR0RBBwwGoIYZmxvd2Vycy10by10aGUtd29ybGQuY29tMAwGA1UdEwEB/wQCMAAw 356 | HwYDVR0jBBgwFoAU6TwE4YAvwoQTLSZwnvL9Gs+q/sYwHQYDVR0OBBYEFLOLSvub 357 | fCgCA4dr3wO34aAb/mHzMA0GCSqGSIb3DQEBCwUAA4ICAQBssf9RFVTT5QD0diGf 358 | 42MHfkCnuNMUHNM1N+lHaNTSpH9lgfza60bNl4sYmtVgBo2ACzW2M4lpBDcAq/EH 359 | L/xpg/KiHmhcgZKvvvmD+iV6a5w2xxelt39fnQNz9KkysS+I5IKlgwq+s2lrve2J 360 | VTPMFo7DmPShlpYoRYG74nKsEiAY1xg5M1x+L287w+QglPvxR1GSiUAtyJYiUkGc 361 | CSNnYW7qBJHKKkbfgSZoLjIXUjpOQwbBBYGVE2X9K2+B0yP0CPdTMHFZ59kpUjcF 362 | 5sJtjUvwvxPtESzbbZoT0D+TNnNftj4w5wPvF3XshHHd/tsW01bw15xJzU9cgFK+ 363 | Vx1xpE3zZ3vNb6MWFqpnKPeeiD78KqJ1wIdo4D5h1dP2bm5WH4kIuEg4X5D+Ltn4 364 | z7SSLV3+mZJNvIlZKSvqnoeZDZoTTsl6etqDOjY6BugKub4fUHhOdTLduPOmMXrl 365 | xw2AKcJ2ZaSqZ/6/ub4EGjgjIgbObSHOzO5qqtjIBb8jXw9FnsvuIjAsQ7P+l3MQ 366 | cQrStGjiVAZWqSfdriDiSj0jnMPaxpslNty1C/+5CgMs8GzDF68RE3MK09cQOa8l 367 | fPlYaH1dawuJPeyCj9BeG7sL1CVFlCs+OfzFona1XbuAVnhIwidhE3NGt31KKMfJ 368 | 1Cib2piaBrR9qzaJ7L7bNQDCig== 369 | -----END CERTIFICATE-----` 370 | 371 | // SCT is an SCT for the certificate chain {LeafCertPEM, IntermediateCertPEM, RootCertPEM} 372 | SCT = `{"sct_version":0,"id":"CEEUmABxUywWGQRgvPxH/cJlOvopLHKzf/hjrinMyfA=","timestamp":1512556025588,"extensions":"","signature":"BAMARjBEAiAJAPO7EKykH4eOQ81kTzKCb4IEWzcxTBdbdRCHLFPLFAIgBEoGXDUtcIaF3M5HWI+MxwkCQbvqR9TSGUHDCZoOr3Q="}` 373 | 374 | // LeafHash is the leaf hash for the chain {LeafCertPEM, IntermediateCertPEM, RootCertPEM} with SCT. 375 | LeafHash = "uvjbEw+porcnNLYkXBSVecJdl7QfuL4SAwZZWobcwHg=" 376 | 377 | // TreeSize is the tree size passed to get-proof-by-hash, along with LeafHash, to get GetProofByHashResponseBody. 378 | TreeSize uint64 = 30 379 | 380 | // GetProofByHashResponseBody is a valid get-proof-by-hash response for LeafHash and TreeSize. 381 | GetProofByHashResponseBody = `{"leaf_index":10,"audit_path":["pWAVPaJIQdVdHgm/GWo/tf0a0gaG4JjCanqHc49kxpU=","+05OCiIkipWWDKhByJGctdwLiSo1geIvWF8pDGv2VFw=","aBTbMciBy2Ey35az07wjEiFN1kWn+37LVa07BQCH2qo=","t+sKhOFhVnTT/6bmOSyVWKfGagwJBVvcyynO2oJLxsY=","LRdkcLMeof0FdRmX6IVaDTITWJUr8eABhUaHa0vcWNw="]}` 382 | ) 383 | --------------------------------------------------------------------------------