├── .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 |
--------------------------------------------------------------------------------