diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7db0e87287..f711b36316 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,8 +23,6 @@ updates: - package-ecosystem: docker directories: - /integration - - /internal/witness/cmd/feeder - - /internal/witness/cmd/witness - /trillian/examples/deployment/docker/ctfe - /trillian/examples/deployment/docker/envsubst schedule: diff --git a/CHANGELOG.md b/CHANGELOG.md index 91edf2db15..6bca1ecfd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ * [CTFE] Enforce max request body size using `http.MaxBytesHandler`. * Support tiled logs in the loglist3 logfilter functions by @robstradling in https://github.com/google/certificate-transparency-go/pull/1762 * Add FindTiledLog* functions by @robstradling in https://github.com/google/certificate-transparency-go/pull/1763 - +* Remove internal witness. ## v1.3.2 diff --git a/internal/witness/README.md b/internal/witness/README.md deleted file mode 100644 index ebb12fda60..0000000000 --- a/internal/witness/README.md +++ /dev/null @@ -1,37 +0,0 @@ -CT Witness -============== - -The witness is an HTTP service that stores STHs it has seen from -a configurable list of Certificate Transparency logs in a sqlite database. This -is a lightweight way to help detect or even prevent split-view attacks. An -overview of witnessing can be found in -[trillian-examples](https://github.com/google/trillian-examples/tree/master/witness), -along with "generic" witness implementations. This witness is designed to be -compatible with the specific formats used by CT. - -Once up and running, the witness provides three API endpoints (as defined in -[api/http.go](api/http.go)): -- `/ctwitness/v0/logs` returns a list of all logs for which the witness is - currently storing an STH. -- `/ctwitness/v0/logs//update` acts to update the STH stored for `logid`. -- `/ctwitness/v0/logs//sth` returns the latest STH for `logid`. - -Running the witness --------------------- - -Running the witness is as simple as running `go run ./cmd/witness/main.go` from -this directory, with the following flags: -- `listen`, which specifies the address and port to listen on. -- `db_file`, which specifies the desired location of the sqlite database. The - use of sqlite limits the scalability and reliability of the witness (because - this is a local file), so if that is required a different database backend - would be needed. -- `config_file`, which specifies configuration information for the logs. This - repository contains a [sample configuration file](cmd/witness/example.conf), - and in general it is necessary to specify the following fields for each log: - - `logID`, which is the alphanumeric identifier for the log. - - `pubKey`, which is the base64-encoded public key of the log. - Both of these fields should be populated using an "official" - [CT log list](https://www.gstatic.com/ct/log_list/v3/log_list.json). -- `private_key`, which specifies the private signing key of the witness. In its - current state the witness does not sign STHs so this can exist in any form. diff --git a/internal/witness/api/http.go b/internal/witness/api/http.go deleted file mode 100644 index 9d5265e8e6..0000000000 --- a/internal/witness/api/http.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package api provides the API endpoints for the witness. -package api - -import ( - ct "github.com/google/certificate-transparency-go" -) - -const ( - // HTTPGetSTH is the path of the URL to get an STH. The - // placeholder is for the logID (an alphanumeric string). - HTTPGetSTH = "/ctwitness/v0/logs/%s/sth" - // HTTPUpdate is the path of the URL to update to a new STH. - // Again the placeholder is for the logID. - HTTPUpdate = "/ctwitness/v0/logs/%s/update" - // HTTPGetLogs is the path of the URL to get a list of all logs the - // witness is aware of. - HTTPGetLogs = "/ctwitness/v0/logs" -) - -// UpdateRequest encodes the inputs to the witness Update function: a (raw) -// STH byte slice and a consistency proof (slice of slices). The logID -// is part of the request URL. -type UpdateRequest struct { - STH []byte - Proof [][]byte -} - -// CosignedSTH has all the fields from a CT SignedTreeHead but adds a -// WitnessSigs field that holds the extra witness signatures. -type CosignedSTH struct { - ct.SignedTreeHead - WitnessSigs []ct.DigitallySigned `json:"witness_signatures"` -} diff --git a/internal/witness/client/http/witness_client.go b/internal/witness/client/http/witness_client.go deleted file mode 100644 index c7c1d06d9a..0000000000 --- a/internal/witness/client/http/witness_client.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package http is a simple client for interacting with witnesses over HTTP. -package http - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - - wit_api "github.com/google/certificate-transparency-go/internal/witness/api" - "k8s.io/klog/v2" -) - -// ErrSTHTooOld is returned if the STH passed to Update needs to be updated. -var ErrSTHTooOld = errors.New("STH too old") - -// Witness consists of the witness' URL and signature verifier. -type Witness struct { - URL *url.URL -} - -// GetLatestSTH returns a recent STH from the witness for the specified log ID. -func (w Witness) GetLatestSTH(ctx context.Context, logID string) ([]byte, error) { - u, err := w.URL.Parse(fmt.Sprintf(wit_api.HTTPGetSTH, url.PathEscape(logID))) - if err != nil { - return nil, fmt.Errorf("failed to parse URL: %v", err) - } - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %v", err) - } - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("failed to do http request: %v", err) - } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("Failed to close response body: %v", err) - } - }() - if resp.StatusCode == 404 { - return nil, os.ErrNotExist - } else if resp.StatusCode != 200 { - return nil, fmt.Errorf("bad status response: %s", resp.Status) - } - return io.ReadAll(resp.Body) -} - -// Update attempts to clock the witness forward for the given logID. -// The latest signed STH will be returned if this succeeds, or if the error is -// http.ErrSTHTooOld. In all other cases no STH should be expected. -func (w Witness) Update(ctx context.Context, logID string, sth []byte, proof [][]byte) ([]byte, error) { - reqBody, err := json.MarshalIndent(&wit_api.UpdateRequest{ - STH: sth, - Proof: proof, - }, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal update request: %v", err) - } - u, err := w.URL.Parse(fmt.Sprintf(wit_api.HTTPUpdate, url.PathEscape(logID))) - if err != nil { - return nil, fmt.Errorf("failed to parse URL: %v", err) - } - req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(reqBody)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %v", err) - } - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("failed to do http request: %v", err) - } - if resp.Request.Method != "PUT" { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections - return nil, fmt.Errorf("PUT request to %q was converted to %s request to %q", u.String(), resp.Request.Method, resp.Request.URL) - } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("Failed to close response body: %v", err) - } - }() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read body: %v", err) - } - if resp.StatusCode != 200 { - if resp.StatusCode == 409 { - return body, ErrSTHTooOld - } - return nil, fmt.Errorf("bad status response (%s): %q", resp.Status, body) - } - return body, nil -} diff --git a/internal/witness/cmd/client/main.go b/internal/witness/cmd/client/main.go deleted file mode 100644 index a6ed2b7f03..0000000000 --- a/internal/witness/cmd/client/main.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2022 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// client fetches and verifies new STHs for a set of logs from a single witness. -package main - -import ( - "context" - "crypto/x509" - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "io" - "net/http" - "net/url" - "os" - "sync" - "time" - - ct "github.com/google/certificate-transparency-go" - wit_api "github.com/google/certificate-transparency-go/internal/witness/api" - wh "github.com/google/certificate-transparency-go/internal/witness/client/http" - "github.com/google/certificate-transparency-go/internal/witness/verifier" - "github.com/google/certificate-transparency-go/loglist3" - "k8s.io/klog/v2" -) - -var ( - logList = flag.String("log_list_url", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "The location of the log list") - witness = flag.String("witness_url", "", "The endpoint of the witness HTTP API") - witnessPK = flag.String("witness_pk", "", "The base64-encoded witness public key") - interval = flag.Duration("poll", 10*time.Second, "How frequently to poll to get new witnessed STHs") -) - -// WitnessSigVerifier verifies the witness' signature on a cosigned STH. -type WitnessSigVerifier interface { - VerifySignature(cosigned wit_api.CosignedSTH) error -} - -// Witness consists of the witness' URL and signature verifier. -type Witness struct { - Client *wh.Witness - Verifier WitnessSigVerifier -} - -type ctLog struct { - id string - name string - wsth *wit_api.CosignedSTH - verifier *ct.SignatureVerifier -} - -func main() { - klog.InitFlags(nil) - flag.Parse() - if *witness == "" { - klog.Exit("--witness_url must not be empty") - } - if *witnessPK == "" { - klog.Exit("--witness_pk must not be empty") - } - ctx := context.Background() - // Set up the witness client. - wURL, err := url.Parse(*witness) - if err != nil { - klog.Exitf("Failed to parse witness URL: %v", err) - } - pk, err := ct.PublicKeyFromB64(*witnessPK) - if err != nil { - klog.Exitf("Failed to create witness public key: %v", err) - } - wv, err := verifier.NewWitnessVerifier(pk) - if err != nil { - klog.Exitf("Failed to create witness signature verifier: %v", err) - } - w := Witness{ - Client: &wh.Witness{ - URL: wURL, - }, - Verifier: wv, - } - // Set up the log data. - ctLogs, err := populateLogs(*logList) - if err != nil { - klog.Exitf("Failed to set up log data: %v", err) - } - // Now poll the witness for each log. - wg := &sync.WaitGroup{} - for _, log := range ctLogs { - wg.Add(1) - go func(witness *Witness, log ctLog) { - defer wg.Done() - if err := log.getSTH(ctx, witness, *interval); err != nil { - klog.Errorf("getSTH: %v", err) - } - }(&w, log) - } - wg.Wait() -} - -// populateLogs populates a list of ctLogs based on the log list. -func populateLogs(logListURL string) ([]ctLog, error) { - u, err := url.Parse(logListURL) - if err != nil { - return nil, fmt.Errorf("failed to parse URL: %v", err) - } - body, err := readURL(u) - if err != nil { - return nil, fmt.Errorf("failed to get log list data: %v", err) - } - // Get data for all usable logs. - logList, err := loglist3.NewFromJSON(body) - if err != nil { - return nil, fmt.Errorf("failed to parse JSON: %v", err) - } - usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus}) - var logs []ctLog - for _, operator := range usable.Operators { - for _, log := range operator.Logs { - logID := base64.StdEncoding.EncodeToString(log.LogID) - //logPK := base64.StdEncoding.EncodeToString(log.Key) - //pk, err := ct.PublicKeyFromB64(logPK) - pk, err := x509.ParsePKIXPublicKey(log.Key) - if err != nil { - return nil, fmt.Errorf("failed to create public key for %s: %v", log.Description, err) - } - v, err := ct.NewSignatureVerifier(pk) - if err != nil { - return nil, fmt.Errorf("failed to create signature verifier: %v", err) - } - l := ctLog{ - id: logID, - name: log.Description, - verifier: v, - } - logs = append(logs, l) - } - } - return logs, nil -} - -// getSTH gets cosigned STHs for a given log continuously from the witness, -// returning only when the context is done. -func (l *ctLog) getSTH(ctx context.Context, witness *Witness, interval time.Duration) error { - tik := time.NewTicker(interval) - defer tik.Stop() - for { - func() { - ctx, cancel := context.WithTimeout(ctx, interval) - defer cancel() - - klog.V(2).Infof("Requesting STH for %s from witness", l.name) - if err := l.getOnce(ctx, witness); err != nil { - klog.Warningf("Failed to retrieve STH for %s: %v", l.name, err) - } else { - klog.Infof("Verified the STH for %s at size %d!", l.name, l.wsth.TreeSize) - } - }() - - select { - case <-ctx.Done(): - return ctx.Err() - case <-tik.C: - } - } -} - -// getOnce gets a new cosigned STH once and verifies it, replacing the stored STH -// in the case that it does verify. -func (l *ctLog) getOnce(ctx context.Context, witness *Witness) error { - // Get and parse the latest cosigned STH from the witness. - var cSTH wit_api.CosignedSTH - sthRaw, err := witness.Client.GetLatestSTH(ctx, l.id) - if err != nil { - return fmt.Errorf("failed to get STH: %v", err) - } - if err := json.Unmarshal(sthRaw, &cSTH); err != nil { - return fmt.Errorf("failed to unmarshal STH: %v", err) - } - // First verify the witness signature(s). - if err := witness.Verifier.VerifySignature(cSTH); err != nil { - return fmt.Errorf("failed to verify witness signature: %v", err) - } - // Then verify the log signature. - plainSTH := cSTH.SignedTreeHead - if err := l.verifier.VerifySTHSignature(plainSTH); err != nil { - return fmt.Errorf("failed to verify log signature: %v", err) - } - l.wsth = &cSTH - return nil -} - -var getByScheme = map[string]func(*url.URL) ([]byte, error){ - "http": readHTTP, - "https": readHTTP, - "file": func(u *url.URL) ([]byte, error) { - return os.ReadFile(u.Path) - }, -} - -// readHTTP fetches and reads data from an HTTP-based URL. -func readHTTP(u *url.URL) ([]byte, error) { - resp, err := http.Get(u.String()) - if err != nil { - return nil, err - } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("failed to close response body %v", err) - } - }() - return io.ReadAll(resp.Body) -} - -// readURL fetches and reads data from an HTTP-based or filesystem URL. -func readURL(u *url.URL) ([]byte, error) { - s := u.Scheme - queryFn, ok := getByScheme[s] - if !ok { - return nil, fmt.Errorf("failed to identify suitable scheme for the URL %q", u.String()) - } - return queryFn(u) -} diff --git a/internal/witness/cmd/feeder/Dockerfile b/internal/witness/cmd/feeder/Dockerfile deleted file mode 100644 index 0d692e21bc..0000000000 --- a/internal/witness/cmd/feeder/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM golang:1.25.6-bookworm@sha256:2f768d462dbffbb0f0b3a5171009f162945b086f326e0b2a8fd5d29c3219ff14 as builder - -ARG GOFLAGS="" -ENV GOFLAGS=$GOFLAGS - -# Move to working directory /build -WORKDIR /build - -# Copy and download dependency using go mod -COPY go.mod . -COPY go.sum . -RUN go mod download - -# Copy the code into the container -COPY . . - -# Build the application -RUN go build -o /build/bin/feeder ./internal/witness/cmd/feeder - -# Build release image -FROM alpine:3.23@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 - -COPY --from=builder /build/bin/feeder /bin/feeder -ENTRYPOINT ["/bin/feeder"] diff --git a/internal/witness/cmd/feeder/main.go b/internal/witness/cmd/feeder/main.go deleted file mode 100644 index 0d097df7c2..0000000000 --- a/internal/witness/cmd/feeder/main.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// feeder polls the sumdb log and pushes the results to a generic witness. -package main - -import ( - "context" - "encoding/base64" - "encoding/json" - "encoding/pem" - "errors" - "flag" - "fmt" - "io" - "net/http" - "net/url" - "os" - "sync" - "time" - - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/client" - wh "github.com/google/certificate-transparency-go/internal/witness/client/http" - "github.com/google/certificate-transparency-go/jsonclient" - "github.com/google/certificate-transparency-go/loglist3" - "k8s.io/klog/v2" -) - -var ( - logList = flag.String("log_list_url", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "The location of the log list") - witness = flag.String("witness_url", "", "The endpoint of the witness HTTP API") - interval = flag.Duration("poll", 10*time.Second, "How quickly to poll the log to get updates") -) - -// ctLog contains the latest witnessed STH for a log and a log client. -type ctLog struct { - id string - name string - wsth *ct.SignedTreeHead - client *client.LogClient -} - -// populateLogs populates a list of ctLogs based on the log list. -func populateLogs(logListURL string) ([]ctLog, error) { - u, err := url.Parse(logListURL) - if err != nil { - return nil, fmt.Errorf("failed to parse URL: %v", err) - } - body, err := readURL(u) - if err != nil { - return nil, fmt.Errorf("failed to get log list data: %v", err) - } - // Get data for all usable logs. - logList, err := loglist3.NewFromJSON(body) - if err != nil { - return nil, fmt.Errorf("failed to parse JSON: %v", err) - } - usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus}) - var logs []ctLog - for _, operator := range usable.Operators { - for _, log := range operator.Logs { - logID := base64.StdEncoding.EncodeToString(log.LogID) - c, err := createLogClient(log.Key, log.URL) - if err != nil { - return nil, fmt.Errorf("failed to create log client: %v", err) - } - l := ctLog{ - id: logID, - name: log.Description, - client: c, - } - logs = append(logs, l) - } - } - return logs, nil -} - -// createLogClient creates a CT log client from a public key and URL. -func createLogClient(key []byte, url string) (*client.LogClient, error) { - pemPK := pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: key, - }) - opts := jsonclient.Options{PublicKey: string(pemPK)} - c, err := client.New(url, http.DefaultClient, opts) - if err != nil { - return nil, fmt.Errorf("failed to create JSON client: %v", err) - } - return c, nil -} - -func main() { - klog.InitFlags(nil) - flag.Parse() - if *witness == "" { - klog.Exit("--witness_url must not be empty") - } - ctx := context.Background() - // Set up the witness client. - var w wh.Witness - if wURL, err := url.Parse(*witness); err != nil { - klog.Exitf("Failed to parse witness URL: %v", err) - } else { - w = wh.Witness{ - URL: wURL, - } - } - // Now set up the log data (with no initial witness STH). - ctLogs, err := populateLogs(*logList) - if err != nil { - klog.Exitf("Failed to set up log data: %v", err) - } - // Now feed each log. - wg := &sync.WaitGroup{} - for _, log := range ctLogs { - wg.Add(1) - go func(witness *wh.Witness, log ctLog) { - defer wg.Done() - if err := log.feed(ctx, witness, *interval); err != nil { - klog.Errorf("feedLog: %v", err) - } - }(&w, log) - } - wg.Wait() -} - -// feed feeds continuously for a given log, returning only when the context -// is done. -func (l *ctLog) feed(ctx context.Context, witness *wh.Witness, interval time.Duration) error { - tik := time.NewTicker(interval) - defer tik.Stop() - for { - func() { - ctx, cancel := context.WithTimeout(ctx, interval) - defer cancel() - - klog.V(2).Infof("Start feedOnce for %s", l.name) - if err := l.feedOnce(ctx, witness); err != nil { - klog.Warningf("Failed to feed for %s: %v", l.name, err) - } - klog.V(2).Infof("feedOnce complete for %s", l.name) - }() - - select { - case <-ctx.Done(): - return ctx.Err() - case <-tik.C: - } - } -} - -// feedOnce attempts to update the STH held by the witness to the latest STH -// provided by the log. -func (l *ctLog) feedOnce(ctx context.Context, w *wh.Witness) error { - // Get and parse the latest STH from the log. - var sthResp ct.GetSTHResponse - _, csthRaw, err := l.client.GetAndParse(ctx, ct.GetSTHPath, nil, &sthResp) - if err != nil { - return fmt.Errorf("failed to get latest STH: %v", err) - } - csth, err := sthResp.ToSignedTreeHead() - if err != nil { - return fmt.Errorf("failed to parse response as STH: %v", err) - } - wSize, err := l.latestSize(ctx, w) - if err != nil { - return fmt.Errorf("failed to get latest size for %s: %v", l.name, err) - } - if wSize >= csth.TreeSize { - klog.V(1).Infof("Witness size %d >= log size %d for %s - nothing to do", wSize, csth.TreeSize, l.name) - return nil - } - - klog.Infof("Updating witness from size %d to %d for %s", wSize, csth.TreeSize, l.name) - // If we want to update the witness then let's get a consistency proof. - var pf [][]byte - if wSize > 0 { - pf, err = l.client.GetSTHConsistency(ctx, wSize, csth.TreeSize) - if err != nil { - return fmt.Errorf("failed to get consistency proof: %v", err) - } - } - // Now give the new STH and consistency proof to the witness. - wsthRaw, err := w.Update(ctx, l.id, csthRaw, pf) - if err != nil && !errors.Is(err, wh.ErrSTHTooOld) { - return fmt.Errorf("failed to update STH: %v", err) - } - if errors.Is(err, wh.ErrSTHTooOld) { - klog.Infof("STH mismatch at log size %d for %s", wSize, l.name) - klog.Infof("%s", wsthRaw) - } - // Parse the STH it returns. - var wsthJSON ct.GetSTHResponse - if err := json.Unmarshal(wsthRaw, &wsthJSON); err != nil { - return fmt.Errorf("failed to unmarshal json: %v", err) - } - wsth, err := wsthJSON.ToSignedTreeHead() - if err != nil { - return fmt.Errorf("failed to create STH: %v", err) - } - // For now just update our local state with whatever the witness - // returns, even if we got wh.ErrSTHTooOld. This is fine if we're the - // only feeder for this witness. - l.wsth = wsth - return nil -} - -// latestSize returns the size of the latest witness STH. If this is nil then -// it first checks with the witness to see if it has anything stored before -// returning 0. -func (l *ctLog) latestSize(ctx context.Context, w *wh.Witness) (uint64, error) { - if l.wsth != nil { - return l.wsth.TreeSize, nil - } - wsthRaw, err := w.GetLatestSTH(ctx, l.id) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - // If the witness has no stored STH then 0 is the correct size. - return 0, nil - } - return 0, err - } - var wsthJSON ct.GetSTHResponse - if err := json.Unmarshal(wsthRaw, &wsthJSON); err != nil { - return 0, fmt.Errorf("failed to unmarshal json: %v", err) - } - wsth, err := wsthJSON.ToSignedTreeHead() - if err != nil { - return 0, fmt.Errorf("failed to create STH: %v", err) - } - l.wsth = wsth - return wsth.TreeSize, nil -} - -var getByScheme = map[string]func(*url.URL) ([]byte, error){ - "http": readHTTP, - "https": readHTTP, - "file": func(u *url.URL) ([]byte, error) { - return os.ReadFile(u.Path) - }, -} - -// readHTTP fetches and reads data from an HTTP-based URL. -func readHTTP(u *url.URL) ([]byte, error) { - resp, err := http.Get(u.String()) - if err != nil { - return nil, err - } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("Failed to close response body: %v", err) - } - }() - return io.ReadAll(resp.Body) -} - -// readURL fetches and reads data from an HTTP-based or filesystem URL. -func readURL(u *url.URL) ([]byte, error) { - s := u.Scheme - queryFn, ok := getByScheme[s] - if !ok { - return nil, fmt.Errorf("failed to identify suitable scheme for the URL %q", u.String()) - } - return queryFn(u) -} diff --git a/internal/witness/cmd/witness/Dockerfile b/internal/witness/cmd/witness/Dockerfile deleted file mode 100644 index eca0aa45da..0000000000 --- a/internal/witness/cmd/witness/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM golang:1.25.6-bookworm@sha256:2f768d462dbffbb0f0b3a5171009f162945b086f326e0b2a8fd5d29c3219ff14 AS builder - -ARG GOFLAGS="" -ENV GOFLAGS=$GOFLAGS - -# Move to working directory /build -WORKDIR /build - -# Copy and download dependency using go mod -COPY go.mod . -COPY go.sum . -RUN go mod download - -# Copy the code into the container -COPY . . - -# Build the application -RUN go build -o /build/bin/witness ./internal/witness/cmd/witness - -# Build release image -FROM golang:1.25.6-bookworm@sha256:2f768d462dbffbb0f0b3a5171009f162945b086f326e0b2a8fd5d29c3219ff14 - -COPY --from=builder /build/bin/witness /bin/witness -ENTRYPOINT ["/bin/witness"] diff --git a/internal/witness/cmd/witness/config/config.go b/internal/witness/cmd/witness/config/config.go deleted file mode 100644 index cb66aada59..0000000000 --- a/internal/witness/cmd/witness/config/config.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// config is a tool to populate the witness config file according to a set of logs. -package main - -import ( - "encoding/base64" - "flag" - "fmt" - "io" - "net/http" - "net/url" - "os" - - "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/impl" - "github.com/google/certificate-transparency-go/loglist3" - "gopkg.in/yaml.v3" - "k8s.io/klog/v2" -) - -var ( - // This can be either an HTTP- or filesystem-based URL. - logList = flag.String("log_list_url", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "The location of the log list") - configFile = flag.String("config_file", "config.yaml", "path to a YAML config file that specifies the logs followed by this witness") -) - -func main() { - flag.Parse() - // Get all usable logs from the log list. - u, err := url.Parse(*logList) - if err != nil { - klog.Exitf("Failed to parse log_list_url as a URL: %v", err) - } - body, err := readURL(u) - if err != nil { - klog.Exitf("Failed to get log list data: %v", err) - } - // Get data for all usable logs. - logList, err := loglist3.NewFromJSON(body) - if err != nil { - klog.Exitf("failed to parse JSON: %v", err) - } - var config impl.LogConfig - usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus}) - for _, operator := range usable.Operators { - for _, log := range operator.Logs { - key := base64.StdEncoding.EncodeToString(log.Key) - l := impl.LogInfo{PubKey: key} - config.Logs = append(config.Logs, l) - } - } - data, err := yaml.Marshal(&config) - if err != nil { - klog.Exitf("Failed to marshal log config into YAML: %v", err) - } - if err := os.WriteFile(*configFile, data, 0644); err != nil { - klog.Exitf("Failed to write config to file: %v", err) - } -} - -var getByScheme = map[string]func(*url.URL) ([]byte, error){ - "http": readHTTP, - "https": readHTTP, - "file": func(u *url.URL) ([]byte, error) { - return os.ReadFile(u.Path) - }, -} - -// readHTTP fetches and reads data from an HTTP-based URL. -func readHTTP(u *url.URL) ([]byte, error) { - resp, err := http.Get(u.String()) - if err != nil { - return nil, err - } - defer func() { - if err := resp.Body.Close(); err != nil { - klog.Errorf("Operation to close response body failed: %v", err) - } - }() - return io.ReadAll(resp.Body) -} - -// readURL fetches and reads data from an HTTP-based or filesystem URL. -func readURL(u *url.URL) ([]byte, error) { - s := u.Scheme - queryFn, ok := getByScheme[s] - if !ok { - return nil, fmt.Errorf("failed to identify suitable scheme for the URL %q", u.String()) - } - return queryFn(u) -} diff --git a/internal/witness/cmd/witness/config/config.yaml b/internal/witness/cmd/witness/config/config.yaml deleted file mode 100644 index d094aa2272..0000000000 --- a/internal/witness/cmd/witness/config/config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -Logs: -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETeBmZOrzZKo4xYktx9gI2chEce3cw/tbr5xkoQlmhB18aKfsxD+MnILgGNl0FOm0eYGilFVi85wLRIOhK8lxKw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeIPc6fGmuBg6AJkv/z7NFckmHvf/OqmjchZJ6wm2qN200keRDg352dWpi7CHnSV51BpQYAj1CQY5JuRAwrrDwg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0JCPZFJOQqyEti5M8j13ALN3CAVHqkVM4yyOcKWCu2yye5yYeqDpEXYoALIgtM3TmHtNlifmt+4iatGwLpF3eA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER+1MInu8Q39BwDZ5Rp9TwXhwm3ktvgJzpk/r7dDgGk7ZacMm3ljfcoIvP1E72T8jvyLT1bvdapylajZcTH6W5g== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+WS9FSxAYlCVEzg8xyGwOrmPonoV14nWjjETAIdZvLvukPzIWBMKv6tDNlQjpIHNrUcUt1igRPpqoKDXw2MeKw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEchY+C+/vzj5g3ZXLY3q5qY1Kb2zcYYCmRV4vg6yU84WI0KV00HuO/8XuQqLwLZPjwtCymeLhQunSxgAnaXSuzg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExpon7ipsqehIeU1bmpog9TFo4Pk8+9oN8OYHl1Q2JGVXnkVFnuuvPgSo2Ep+6vLffNLcmEbxOucz03sFiematg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESLJHTlAycmJKDQxIv60pZG8g33lSYxYpCi5gteI6HLevWbFVCdtZx+m9b+0LrwWWl/87mkNN6xE0M4rnrIPA/w== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/8tkhjLRp0SXrlZdTzNkTd6HqmcmXiDJz3fAdWLgOhjmv4mohvRhwXul9bgW0ODgRwC9UGAgH/vpGHPvIS1qA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6J4EbcpIAl1+AkSRsbhoY5oRTj3VoFfaf1DlQkfi7Rbe/HcjfVtrwN8jaC+tQDGjF+dqvKhWJAQ6Q6ev6q9Mew== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfQ0DsdWYitzwFTvG3F4Nbj8Nv5XIVYzQpkyWsU4nuSYlmcwrAp6m092fsdXEw6w1BAeHlzaqrSgNfyvZaJ9y0Q== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9o7AiwrbGBIX6Lnc47I6OfLMdZnRzKoP5u072nBi6vpIOEooktTi1gNwlRPzGC2ySGfuc1xLDeaA/wSFGgpYFg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJyTdaAMoy/5jvg4RR019F2ihEV1McclBKMe2okuX7MCv/C87v+nxsfz1Af+p+0lADGMkmNd5LqZVqxbGvlHYcQ== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXu8iQwSCRSf2CbITGpUpBtFVt8+I0IU0d1C36Lfe1+fbwdaI0Z5FktfM2fBoI1bXBd18k2ggKGYGgdZBgLKTg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8m/SiQ8/xfiHHqtls9m7FyOMBg4JVZY9CgiixXGz0akvKD6DEL8S0ERmFe9U4ZiA0M4kbT5nmuk3I85Sk4bagA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7+R9dC4VFbbpuyOL+yy14ceAmEf7QGlo/EmtYU6DRzwat43f/3swtLr/L8ugFOOt1YU/RFmMjGCL17ixv66MZw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELsYzGMNwo8rBIlaklBIdmD2Ofn6HkfrjK0Ukz1uOIUC6Lm0jTITCXhoIdjs7JkyXnwuwYiJYiH7sE1YeKu8k9w== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhjyxDVIjWt5u9sB/o2S8rcGJ2pdZTGA8+IpXhI/tvKBjElGE5r3de4yAfeOPhqTqqc+o7vPgXnDgu/a9/B+RLg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz0OeL7jrVxEXJu+o4QWQYLKyokXHiPOOKVUL3/TNFFquVzDSer7kZ3gijxzBp98ZTgRgMSaWgCmZ8OD74mFUQ== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESdAfC+h1SZNsSARs188n/dCiNYjGSgkT7avLYe1mmXJzzHhsmxmAorHtOzhDkFgaCSCrUPrXdunK946eyIeSmA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu1LyFs+SC8555lRtwjdTpPX5OqmzBewdvRbsMKwu+HliNRWOGtgWLuRIa/bGE/GWLlwQ/hkeqBi4Dy3DpIZRlw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpBFS2xdBTpDUVlESMFL4mwPPTJ/4Lji18Vq6+ji50o8agdqVzDPsIShmxlY+YDYhINnUrF36XBmhBX3+ICP89Q== diff --git a/internal/witness/cmd/witness/impl/witness.go b/internal/witness/cmd/witness/impl/witness.go deleted file mode 100644 index d2a44ebb31..0000000000 --- a/internal/witness/cmd/witness/impl/witness.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package impl is the implementation of the witness server. -package impl - -import ( - "context" - "crypto/sha256" - "database/sql" - "encoding/base64" - "errors" - "fmt" - "net/http" - "time" - - ct "github.com/google/certificate-transparency-go" - ih "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/internal/http" - "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/internal/witness" - "github.com/gorilla/mux" - _ "github.com/mattn/go-sqlite3" // Load drivers for sqlite3 - "k8s.io/klog/v2" -) - -// LogConfig contains a list of LogInfo (configuration options for a log). -type LogConfig struct { - Logs []LogInfo `yaml:"Logs"` -} - -// LogInfo contains the configuration options for a log, which is just its public key. -type LogInfo struct { - PubKey string `yaml:"PubKey"` -} - -// ServerOpts provides the options for a server (specified in main.go). -type ServerOpts struct { - // Where to listen for requests. - ListenAddr string - // The file for sqlite3 storage. - DBFile string - // The signing key for the witness. - PrivKey string - // The log configuration information. - Config LogConfig -} - -// buildLogMap loads the log configuration information into a map. -func buildLogMap(config LogConfig) (map[string]ct.SignatureVerifier, error) { - logMap := make(map[string]ct.SignatureVerifier) - for _, log := range config.Logs { - // Use the PubKey string to first create the verifier. - pk, err := ct.PublicKeyFromB64(log.PubKey) - if err != nil { - return nil, fmt.Errorf("failed to create public key: %v", err) - } - logV, err := ct.NewSignatureVerifier(pk) - if err != nil { - return nil, fmt.Errorf("failed to create signature verifier: %v", err) - } - // And then to create the logID. - logID, err := LogIDFromPubKey(log.PubKey) - if err != nil { - return nil, fmt.Errorf("failed to create log id: %v", err) - } - logMap[logID] = *logV - } - return logMap, nil -} - -// LogIDFromPubKey builds the logID given the base64-encoded public key. -func LogIDFromPubKey(pk string) (string, error) { - der, err := base64.StdEncoding.DecodeString(pk) - if err != nil { - return "", fmt.Errorf("failed to decode public key: %v", err) - } - sha := sha256.Sum256(der) - return base64.StdEncoding.EncodeToString(sha[:]), nil -} - -// Main sets up and runs the witness given the options. -func Main(ctx context.Context, opts ServerOpts) error { - if len(opts.DBFile) == 0 { - return errors.New("DBFile is required") - } - // Start up local database. - klog.Infof("Connecting to local DB at %q", opts.DBFile) - db, err := sql.Open("sqlite3", opts.DBFile) - if err != nil { - return fmt.Errorf("failed to connect to DB: %w", err) - } - - // Avoid multiple writes colliding and resulting in a "database locked" error. - db.SetMaxOpenConns(1) - - // Load log configuration into the map. - logMap, err := buildLogMap(opts.Config) - if err != nil { - return fmt.Errorf("failed to load configurations: %v", err) - } - - w, err := witness.New(witness.Opts{ - DB: db, - PrivKey: opts.PrivKey, - KnownLogs: logMap, - }) - if err != nil { - return fmt.Errorf("error creating witness: %v", err) - } - - klog.Infof("Starting witness server...") - srv := ih.NewServer(w) - // These options are required as some logID values contain forward - // slashes, and will be PathUnescape-d later. - r := mux.NewRouter().UseEncodedPath() - srv.RegisterHandlers(r) - hServer := &http.Server{ - Addr: opts.ListenAddr, - Handler: r, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 2 * time.Minute, - WriteTimeout: 2 * time.Minute, - IdleTimeout: 2 * time.Minute, - } - e := make(chan error, 1) - go func() { - e <- hServer.ListenAndServe() - close(e) - }() - <-ctx.Done() - klog.Info("Server shutting down") - if err := hServer.Shutdown(ctx); err != nil { - klog.Errorf("Shutdown(): %v", err) - } - return <-e -} diff --git a/internal/witness/cmd/witness/internal/http/server.go b/internal/witness/cmd/witness/internal/http/server.go deleted file mode 100644 index 75f9d133e3..0000000000 --- a/internal/witness/cmd/witness/internal/http/server.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package http contains private implementation details for the witness server. -package http - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/google/certificate-transparency-go/internal/witness/api" - "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/internal/witness" - "github.com/gorilla/mux" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "k8s.io/klog/v2" -) - -// Server is the core handler implementation of the witness. -type Server struct { - w *witness.Witness -} - -// NewServer creates a new server. -func NewServer(witness *witness.Witness) *Server { - return &Server{ - w: witness, - } -} - -// update handles requests to update STHs. -// It expects a PUT body containing a JSON-formatted api.UpdateRequest -// statement and returns a JSON-formatted api.UpdateResponse statement. -func (s *Server) update(w http.ResponseWriter, r *http.Request) { - v := mux.Vars(r) - logID, err := url.PathUnescape(v["logid"]) - if err != nil { - http.Error(w, fmt.Sprintf("cannot parse URL: %v", err.Error()), http.StatusBadRequest) - } - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, fmt.Sprintf("cannot read request body: %v", err.Error()), http.StatusBadRequest) - return - } - var req api.UpdateRequest - if err := json.Unmarshal(body, &req); err != nil { - http.Error(w, fmt.Sprintf("cannot parse request body as proper JSON struct: %v", err.Error()), http.StatusBadRequest) - return - } - // Get the output from the witness. - sth, err := s.w.Update(r.Context(), logID, req.STH, req.Proof) - if err != nil { - // If there was a failed precondition it's possible the caller was - // just out of date. Give the returned STH to help them - // form a new request. - if c := status.Code(err); c == codes.FailedPrecondition { - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(httpForCode(c)) - // The returned STH gets written a few lines below. - } else { - http.Error(w, fmt.Sprintf("failed to update to new STH: %v", err), httpForCode(http.StatusInternalServerError)) - return - } - } - w.Header().Set("Content-Type", "text/plain") - if _, err := w.Write(sth); err != nil { - klog.Errorf("Write(): %v", err) - } -} - -// getSTH returns an STH stored for a given log. -func (s *Server) getSTH(w http.ResponseWriter, r *http.Request) { - v := mux.Vars(r) - logID, err := url.PathUnescape(v["logid"]) - if err != nil { - http.Error(w, fmt.Sprintf("cannot parse URL: %v", err.Error()), http.StatusBadRequest) - } - // Get the STH from the witness. - sth, err := s.w.GetSTH(logID) - if err != nil { - http.Error(w, fmt.Sprintf("failed to get STH: %v", err), httpForCode(status.Code(err))) - return - } - w.Header().Set("Content-Type", "text/plain") - if _, err := w.Write(sth); err != nil { - klog.Errorf("Write(): %v", err) - } -} - -// getLogs returns a list of all logs the witness is aware of. -func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) { - logs, err := s.w.GetLogs() - if err != nil { - http.Error(w, fmt.Sprintf("failed to get log list: %v", err), http.StatusInternalServerError) - return - } - logList, err := json.Marshal(logs) - if err != nil { - http.Error(w, fmt.Sprintf("failed to convert log list to JSON: %v", err), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "text/json") - if _, err := w.Write(logList); err != nil { - klog.Errorf("Write(): %v", err) - } -} - -// RegisterHandlers registers HTTP handlers for witness endpoints. -func (s *Server) RegisterHandlers(r *mux.Router) { - logStr := "{logid}" - r.HandleFunc(fmt.Sprintf(api.HTTPGetSTH, logStr), s.getSTH).Methods("GET") - r.HandleFunc(fmt.Sprintf(api.HTTPUpdate, logStr), s.update).Methods("PUT") - r.HandleFunc(api.HTTPGetLogs, s.getLogs).Methods("GET") -} - -func httpForCode(c codes.Code) int { - switch c { - case codes.NotFound: - return http.StatusNotFound - case codes.FailedPrecondition: - return http.StatusConflict - default: - return http.StatusInternalServerError - } -} diff --git a/internal/witness/cmd/witness/internal/http/server_test.go b/internal/witness/cmd/witness/internal/http/server_test.go deleted file mode 100644 index 7a616e67d3..0000000000 --- a/internal/witness/cmd/witness/internal/http/server_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package http - -import ( - "context" - "crypto" - "database/sql" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "sort" - "strings" - "testing" - - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/internal/witness/api" - "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/internal/witness" - "github.com/gorilla/mux" - - _ "github.com/mattn/go-sqlite3" // Load drivers for sqlite3 -) - -var ( - // https://play.golang.org/p/gCY2Zi2BJ8G to generate keys and - // https://play.golang.org/p/KUXRShKdYTb to sign things with loaded keys. - mSK = `-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIECRHc4ORynd+lpqWYmjCIAmDjyLEJZSuvv4KdcIi+hEoAoGCCqGSM49 -AwEHoUQDQgAEn1Ahe5/kYQgqYk1kSzp0ZCvL1Cf/tOZ+GUrGjNC0CrTqSylMuU1f -AcWDaKYB/Yr3fq/5lNqJBRjsOnI4KkaEtw== ------END EC PRIVATE KEY-----` - mPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn1Ahe5/kYQgqYk1kSzp0ZCvL1Cf/ -tOZ+GUrGjNC0CrTqSylMuU1fAcWDaKYB/Yr3fq/5lNqJBRjsOnI4KkaEtw== ------END PUBLIC KEY-----`) - mID = "fRThG/6Ymon8NnpRMQJIgCMgjtrBVnOidYenOB0n6FI=" - bSK = `-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIICRst6QhwffAkeOQGIhcCSmB7/LYQXevwrv8TD9FjU7oAoGCCqGSM49 -AwEHoUQDQgAE5FTw9vYXDEFiZb9kS1LV7GzU1Mo/xQ8D2Vnkl7WqNTB2kJ45aTtl -F2bBk8i50oWNRlRLyi5MVl7j+6LVhMiBeA== ------END EC PRIVATE KEY-----` - bPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5FTw9vYXDEFiZb9kS1LV7GzU1Mo/ -xQ8D2Vnkl7WqNTB2kJ45aTtlF2bBk8i50oWNRlRLyi5MVl7j+6LVhMiBeA== ------END PUBLIC KEY-----`) - bID = "CwWwEY4IKzy1bfZ6QW0IU9mky0ruOQvzWOYkmRGMVP4=" - wSK = `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+/pzQGPt88nmVlMC -CjHXGLH93bZ5ZkLVTjsHLi2UQiKhRANCAAQ2DYOW5eMnGcMCDtfK7aFIJg0JBKIZ -cx8fz81azP6v6s8oYMyU5e5bYAfgm1RjGvjC2YTLqCpMvSIeK+rudqg4 ------END PRIVATE KEY-----` - wPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENg2DluXjJxnDAg7Xyu2hSCYNCQSi -GXMfH8/NWsz+r+rPKGDMlOXuW2AH4JtUYxr4wtmEy6gqTL0iHivq7naoOA== ------END PUBLIC KEY-----`) - mInit = []byte(`{"tree_size":5,"timestamp":0,"sha256_root_hash":"41smjBUiAU70EtKlT6lIOIYtRTYxYXsDB+XHfcvu/BE=","tree_head_signature":"BAMARzBFAiEA4CEXH2Z+T4Rcj3YTvgK5qM9NuFYHipI13Il6A/ozTFUCIBDY1VDFy8ZezXsuWNs+iLzkyO5I5kCZldGeMvspHOof"}`) - bInit = []byte(`{"tree_size":5,"timestamp":0,"sha256_root_hash":"41smjBUiAU70EtKlT6lIOIYtRTYxYXsDB+XHfcvu/BE=","tree_head_signature":"BAMASDBGAiEAjSUy1d7/n1MOYWCnx2DzU3nQk1OUHzRtFJl+eDCquBsCIQDEG2vk1A+LmHZfyt/BN4by2324rxWFFzAeG1f2EyXk9w=="}`) - mNext = []byte(`{"tree_size":8,"timestamp":1,"sha256_root_hash":"V8K9aklZ4EPB+RMOk1/8VsJUdFZR77GDtZUQq84vSbo=","tree_head_signature":"BAMARjBEAiB9SZfr3JJbLsSE4mhnHE9hbcbu97nsbKcONnXeJXeigwIgJTWVh5FLNfUre5uCRLY4B1KEyS8tcGbaaHdEMk2WAmc="}`) - consProof = [][]byte{ - dh("b9e1d62618f7fee8034e4c5010f727ab24d8e4705cb296c374bf2025a87a10d2", 32), - dh("aac66cd7a79ce4012d80762fe8eec3a77f22d1ca4145c3f4cee022e7efcd599d", 32), - dh("89d0f753f66a290c483b39cd5e9eafb12021293395fad3d4a2ad053cfbcfdc9e", 32), - dh("29e40bb79c966f4c6fe96aff6f30acfce5f3e8d84c02215175d6e018a5dee833", 32), - } - _ = mSK - _ = bSK - _ = wPK -) - -type logOpts struct { - ID string - PK crypto.PublicKey -} - -func newWitness(t *testing.T, d *sql.DB, logs []logOpts) *witness.Witness { - // Set up Opts for the witness. - logMap := make(map[string]ct.SignatureVerifier) - for _, log := range logs { - logV, err := ct.NewSignatureVerifier(log.PK) - if err != nil { - t.Fatalf("couldn't create a log verifier: %v", err) - } - logMap[log.ID] = *logV - } - opts := witness.Opts{ - DB: d, - PrivKey: wSK, - KnownLogs: logMap, - } - // Create the witness - w, err := witness.New(opts) - if err != nil { - t.Fatalf("couldn't create witness: %v", err) - } - return w -} - -func dh(h string, expLen int) []byte { - r, err := hex.DecodeString(h) - if err != nil { - panic(err) - } - if got := len(r); got != expLen { - panic(fmt.Sprintf("decode %q: len=%d, want %d", h, got, expLen)) - } - return r -} - -func mustCreateDB(t *testing.T) (*sql.DB, func()) { - t.Helper() - db, err := sql.Open("sqlite3", ":memory:") - if err != nil { - t.Fatalf("failed to open temporary in-memory DB: %v", err) - } - return db, func() { - if err := db.Close(); err != nil { - t.Error(err) - } - } -} - -func mustCreatePK(pkPem string) crypto.PublicKey { - pk, _, _, err := ct.PublicKeyFromPEM([]byte(pkPem)) - if err != nil { - panic(err) - } - return pk -} - -func createTestEnv(w *witness.Witness) (*httptest.Server, func()) { - r := mux.NewRouter().UseEncodedPath() - server := NewServer(w) - server.RegisterHandlers(r) - ts := httptest.NewServer(r) - return ts, ts.Close -} - -func TestGetLogs(t *testing.T) { - for _, test := range []struct { - desc string - logIDs []string - logPKs []crypto.PublicKey - sths [][]byte - wantStatus int - wantBody []string - }{ - { - desc: "no logs", - logIDs: []string{}, - wantStatus: http.StatusOK, - wantBody: []string{}, - }, { - desc: "one log", - logIDs: []string{mID}, - logPKs: []crypto.PublicKey{mPK}, - sths: [][]byte{mInit}, - wantStatus: http.StatusOK, - wantBody: []string{mID}, - }, { - desc: "two logs", - logIDs: []string{bID, mID}, - logPKs: []crypto.PublicKey{bPK, mPK}, - sths: [][]byte{bInit, mInit}, - wantStatus: http.StatusOK, - wantBody: []string{bID, mID}, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - // Set up witness and give it some STHs. - logs := make([]logOpts, len(test.logIDs)) - for i, logID := range test.logIDs { - logs[i] = logOpts{ID: logID, - PK: test.logPKs[i], - } - } - w := newWitness(t, d, logs) - for i, logID := range test.logIDs { - if _, err := w.Update(ctx, logID, test.sths[i], nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - } - // Now set up the http server. - ts, tsCloseFn := createTestEnv(w) - defer tsCloseFn() - client := ts.Client() - url := fmt.Sprintf("%s%s", ts.URL, api.HTTPGetLogs) - resp, err := client.Get(url) - if err != nil { - t.Errorf("error response: %v", err) - } - if got, want := resp.StatusCode, test.wantStatus; got != want { - t.Errorf("status code got %d, want %d", got, want) - } - if len(test.wantBody) > 0 { - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("failed to read body: %v", err) - } - var logs []string - if err := json.Unmarshal(body, &logs); err != nil { - t.Fatalf("failed to unmarshal body: %v", err) - } - if len(logs) != len(test.wantBody) { - t.Fatalf("got %d logs, want %d", len(logs), len(test.wantBody)) - } - sort.Strings(logs) - for i := range logs { - if logs[i] != test.wantBody[i] { - t.Fatalf("got %q, want %q", logs[i], test.wantBody[i]) - } - } - } - }) - } -} - -func TestGetChkpt(t *testing.T) { - for _, test := range []struct { - desc string - setID string - setPK crypto.PublicKey - queryID string - queryPK crypto.PublicKey - sth []byte - wantStatus int - }{ - { - desc: "happy path", - setID: mID, - setPK: mPK, - queryID: mID, - queryPK: mPK, - sth: mInit, - wantStatus: http.StatusOK, - }, { - desc: "other log", - setID: mID, - setPK: mPK, - queryID: bID, - sth: mInit, - wantStatus: http.StatusNotFound, - }, { - desc: "nothing there", - setID: mID, - setPK: mPK, - queryID: mID, - sth: nil, - wantStatus: http.StatusNotFound, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - // Set up witness. - w := newWitness(t, d, []logOpts{{ID: test.setID, - PK: test.setPK}}) - // Set an STH for the log if we want to for this test. - if test.sth != nil { - if _, err := w.Update(ctx, test.setID, test.sth, nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - } - // Now set up the http server. - ts, tsCloseFn := createTestEnv(w) - defer tsCloseFn() - client := ts.Client() - chkptQ := fmt.Sprintf(api.HTTPGetSTH, url.PathEscape(test.queryID)) - url := fmt.Sprintf("%s%s", ts.URL, chkptQ) - resp, err := client.Get(url) - if err != nil { - t.Errorf("error response: %v", err) - } - if got, want := resp.StatusCode, test.wantStatus; got != want { - t.Errorf("status code got %d, want %d", got, want) - } - }) - } -} - -func TestUpdate(t *testing.T) { - for _, test := range []struct { - desc string - initC []byte - initSize uint64 - body api.UpdateRequest - wantStatus int - }{ - { - desc: "happy path", - initC: mInit, - initSize: 5, - body: api.UpdateRequest{STH: mNext, Proof: consProof}, - wantStatus: http.StatusOK, - }, { - desc: "smaller STH", - initC: mNext, - initSize: 8, - body: api.UpdateRequest{STH: mInit, Proof: consProof}, - wantStatus: http.StatusConflict, - }, { - desc: "garbage proof", - initC: mInit, - initSize: 5, - body: api.UpdateRequest{STH: mNext, Proof: [][]byte{ - dh("aaaa", 2), - dh("bbbb", 2), - dh("cccc", 2), - dh("dddd", 2), - }}, - wantStatus: http.StatusConflict, - }, { - desc: "garbage STH", - initC: mInit, - initSize: 5, - body: api.UpdateRequest{STH: []byte("aaa"), Proof: consProof}, - wantStatus: http.StatusInternalServerError, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - logID := mID - // Set up witness. - w := newWitness(t, d, []logOpts{{ID: logID, - PK: mPK}}) - // Set an initial STH for the log. - if _, err := w.Update(ctx, logID, test.initC, nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - // Now set up the http server. - ts, tsCloseFn := createTestEnv(w) - defer tsCloseFn() - // Update to a newer STH. - client := ts.Client() - reqBody, err := json.Marshal(test.body) - if err != nil { - t.Fatalf("couldn't parse request: %v", err) - } - url := fmt.Sprintf("%s%s", ts.URL, fmt.Sprintf(api.HTTPUpdate, url.PathEscape(logID))) - req, err := http.NewRequest(http.MethodPut, url, strings.NewReader(string(reqBody))) - if err != nil { - t.Fatalf("couldn't form http request: %v", err) - } - resp, err := client.Do(req) - if err != nil { - t.Errorf("error response: %v", err) - } - if got, want := resp.StatusCode, test.wantStatus; got != want { - t.Errorf("status code got %d, want %d", got, want) - } - }) - } -} diff --git a/internal/witness/cmd/witness/internal/witness/witness.go b/internal/witness/cmd/witness/internal/witness/witness.go deleted file mode 100644 index bd158af965..0000000000 --- a/internal/witness/cmd/witness/internal/witness/witness.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package witness is designed to make sure the STHs of CT logs are consistent -// and store/serve/sign them if so. It is expected that a separate feeder -// component would be responsible for the actual interaction with logs. -package witness - -import ( - "bytes" - "context" - "crypto" - "crypto/x509" - "database/sql" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "reflect" - - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/internal/witness/api" - "github.com/google/certificate-transparency-go/tls" - "github.com/transparency-dev/merkle/proof" - "github.com/transparency-dev/merkle/rfc6962" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "k8s.io/klog/v2" -) - -// Opts is the options passed to a witness. -type Opts struct { - DB *sql.DB - PrivKey string - KnownLogs map[string]ct.SignatureVerifier -} - -// Witness consists of a database for storing STHs, a signing key, and a list -// of logs for which it stores and verifies STHs. -type Witness struct { - db *sql.DB - sk crypto.PrivateKey - Logs map[string]ct.SignatureVerifier -} - -// New creates a new witness, which initially has no logs to follow. -func New(wo Opts) (*Witness, error) { - // Create the sths table if needed. - _, err := wo.DB.Exec(`CREATE TABLE IF NOT EXISTS sths (logID BLOB PRIMARY KEY, sth BLOB)`) - if err != nil { - return nil, fmt.Errorf("failed to create table: %v", err) - } - // Parse the PEM-encoded secret key. - p, _ := pem.Decode([]byte(wo.PrivKey)) - if p == nil { - return nil, errors.New("no PEM block found in secret key string") - } - sk, err := x509.ParsePKCS8PrivateKey(p.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %v", err) - } - // x509 can return either the key object or a pointer to it. This - // discrepancy is handled in the same way as in tls/signature_test.go, - // namely changing it to be the object if it's a pointer. - if reflect.TypeOf(sk).Kind() == reflect.Ptr { - sk = reflect.ValueOf(sk).Elem().Interface() - } - return &Witness{ - db: wo.DB, - sk: sk, - Logs: wo.KnownLogs, - }, nil -} - -// parse verifies the STH under the appropriate key for logID and returns -// the parsed STH. If the STH contained an incorrect logID the witness returns -// an error indicating this, and if the logID is missing the witness fills it in. -// This assumes sthRaw parses as a SignedTreeHead (not a CosignedSTH), so STHs are -// stored unsigned and signed only right when they are being returned. -func (w *Witness) parse(sthRaw []byte, logID string) (*ct.SignedTreeHead, error) { - sv, ok := w.Logs[logID] - if !ok { - return nil, fmt.Errorf("log %q not found", logID) - } - var sth ct.SignedTreeHead - if err := json.Unmarshal(sthRaw, &sth); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %v", err) - } - var idHash ct.SHA256Hash - if err := idHash.FromBase64String(logID); err != nil { - return nil, fmt.Errorf("failed to decode logID: %v", err) - } - var empty ct.SHA256Hash - if bytes.Equal(sth.LogID[:], empty[:]) { - sth.LogID = idHash - } else if !bytes.Equal(sth.LogID[:], idHash[:]) { - return nil, status.Errorf(codes.FailedPrecondition, "STH logID = %q, input logID = %q", sth.LogID.Base64String(), logID) - } - if err := sv.VerifySTHSignature(sth); err != nil { - return nil, fmt.Errorf("failed to verify STH signature: %v", err) - } - return &sth, nil -} - -// GetLogs returns a list of all logs the witness is aware of. -func (w *Witness) GetLogs() ([]string, error) { - rows, err := w.db.Query("SELECT logID FROM sths") - if err != nil { - return nil, err - } - defer func() { - if err := rows.Close(); err != nil { - klog.Errorf("Operation to close rows failed: %v", err) - } - }() - var logs []string - for rows.Next() { - var logID string - err := rows.Scan(&logID) - if err != nil { - return nil, err - } - logs = append(logs, logID) - } - if err := rows.Err(); err != nil { - return nil, err - } - return logs, nil -} - -// GetSTH gets a cosigned STH for a given log, which is consistent with all -// other STHs for the same log signed by this witness. -func (w *Witness) GetSTH(logID string) ([]byte, error) { - sthRaw, err := w.getLatestSTH(w.db.QueryRow, logID) - if err != nil { - return nil, err - } - sth, err := w.parse(sthRaw, logID) - if err != nil { - return nil, fmt.Errorf("couldn't parse raw STH: %v", err) - } - signed, err := w.signSTH(sth) - if err != nil { - return nil, fmt.Errorf("couldn't sign retrieved STH: %v", err) - } - return signed, nil -} - -// Update updates the latest STH if nextRaw is consistent with the current -// latest one for this log. It returns the latest cosigned STH held by -// the witness, which is a signed version of nextRaw if the update was applied. -func (w *Witness) Update(ctx context.Context, logID string, nextRaw []byte, pf [][]byte) ([]byte, error) { - // If we don't witness this log then no point in going further. - _, ok := w.Logs[logID] - if !ok { - return nil, status.Errorf(codes.NotFound, "log %q not found", logID) - } - // Check the signatures on the raw STH and parse it into the STH format. - next, err := w.parse(nextRaw, logID) - if err != nil { - return nil, fmt.Errorf("couldn't parse input STH: %v", err) - } - // Get the latest one for the log because we don't want consistency proofs - // with respect to older STHs. Bind this all in a transaction to - // avoid race conditions when updating the database. - tx, err := w.db.BeginTx(ctx, nil) - if err != nil { - return nil, fmt.Errorf("couldn't create db tx: %v", err) - } - defer func() { - if err := tx.Rollback(); err != nil { - klog.Errorf("Rollback(): %v", err) - } - }() - - // Get the latest STH (if one exists). - prevRaw, err := w.getLatestSTH(tx.QueryRow, logID) - if err != nil { - // If there was nothing stored already then treat this new - // STH as trust-on-first-use (TOFU). - if status.Code(err) == codes.NotFound { - if err := w.setSTH(tx, logID, nextRaw); err != nil { - return nil, fmt.Errorf("couldn't set TOFU STH: %v", err) - } - signed, err := w.signSTH(next) - if err != nil { - return nil, fmt.Errorf("couldn't sign STH: %v", err) - } - return signed, nil - } - return nil, fmt.Errorf("couldn't retrieve latest STH: %w", err) - } - // Parse the raw retrieved STH into the STH format. - prev, err := w.parse(prevRaw, logID) - if err != nil { - return nil, fmt.Errorf("couldn't parse stored STH: %v", err) - } - if next.TreeSize < prev.TreeSize { - // Complain if prev is bigger than next. - return prevRaw, status.Errorf(codes.FailedPrecondition, "cannot prove consistency backwards (%d < %d)", next.TreeSize, prev.TreeSize) - } - if next.TreeSize == prev.TreeSize { - if !bytes.Equal(next.SHA256RootHash[:], prev.SHA256RootHash[:]) { - return prevRaw, status.Errorf(codes.FailedPrecondition, "STH for same size log with differing hash (got %x, have %x)", next.SHA256RootHash, prev.SHA256RootHash) - } - // If it's identical to the previous one do nothing. - return prevRaw, nil - } - // The only remaining option is next.Size > prev.Size. This might be - // valid so we verify the consistency proof. - if err := proof.VerifyConsistency(rfc6962.DefaultHasher, prev.TreeSize, next.TreeSize, pf, prev.SHA256RootHash[:], next.SHA256RootHash[:]); err != nil { - // Complain if the STHs aren't consistent. - return prevRaw, status.Errorf(codes.FailedPrecondition, "failed to verify consistency proof: %v", err) - } - // If the consistency proof is good we store the raw STH and return the - // signed one. - if err := w.setSTH(tx, logID, nextRaw); err != nil { - return nil, fmt.Errorf("failed to store new STH: %v", err) - } - signed, err := w.signSTH(next) - if err != nil { - return nil, fmt.Errorf("failed to sign new STH: %v", err) - } - return signed, nil -} - -// signSTH adds the witness' signature to an STH. -func (w *Witness) signSTH(sth *ct.SignedTreeHead) ([]byte, error) { - sigInput, err := tls.Marshal(*sth) - if err != nil { - return nil, fmt.Errorf("couldn't marshal signature input: %v", err) - } - sig, err := tls.CreateSignature(w.sk, tls.SHA256, sigInput) - if err != nil { - return nil, fmt.Errorf("couldn't sign STH data: %v", err) - } - cosigned := api.CosignedSTH{SignedTreeHead: *sth, - WitnessSigs: []ct.DigitallySigned{ct.DigitallySigned(sig)}, - } - cosignedRaw, err := json.Marshal(cosigned) - if err != nil { - return nil, fmt.Errorf("couldn't marshal cosigned STH: %v", err) - } - return cosignedRaw, nil -} - -// getLatestSTH returns the raw stored data for the latest STH of a given log. -func (w *Witness) getLatestSTH(queryRow func(query string, args ...interface{}) *sql.Row, logID string) ([]byte, error) { - row := queryRow("SELECT sth FROM sths WHERE logID = ?", logID) - if err := row.Err(); err != nil { - return nil, err - } - var sth []byte - if err := row.Scan(&sth); err != nil { - if err == sql.ErrNoRows { - return nil, status.Errorf(codes.NotFound, "no STH for log %q", logID) - } - return nil, err - } - return sth, nil -} - -// setSTH writes the STH to the database for a given log. -func (w *Witness) setSTH(tx *sql.Tx, logID string, sth []byte) error { - if _, err := tx.Exec(`INSERT OR REPLACE INTO sths (logID, sth) VALUES (?, ?)`, logID, sth); err != nil { - return fmt.Errorf("failed to update STH; %v", err) - } - return tx.Commit() -} diff --git a/internal/witness/cmd/witness/internal/witness/witness_test.go b/internal/witness/cmd/witness/internal/witness/witness_test.go deleted file mode 100644 index a894c5f9af..0000000000 --- a/internal/witness/cmd/witness/internal/witness/witness_test.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package witness - -import ( - "context" - "crypto" - "database/sql" - "encoding/hex" - "encoding/json" - "fmt" - "sort" - "testing" - - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/internal/witness/api" - "github.com/google/certificate-transparency-go/tls" - _ "github.com/mattn/go-sqlite3" // Load drivers for sqlite3 -) - -var ( - // https://play.golang.org/p/gCY2Zi2BJ8G to generate keys and - // https://play.golang.org/p/KUXRShKdYTb to sign things with loaded - // keys. Importantly, need to switch to using MarshalPKCS8PrivateKey - // for the witness keys. - mSK = `-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIECRHc4ORynd+lpqWYmjCIAmDjyLEJZSuvv4KdcIi+hEoAoGCCqGSM49 -AwEHoUQDQgAEn1Ahe5/kYQgqYk1kSzp0ZCvL1Cf/tOZ+GUrGjNC0CrTqSylMuU1f -AcWDaKYB/Yr3fq/5lNqJBRjsOnI4KkaEtw== ------END EC PRIVATE KEY-----` - mPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn1Ahe5/kYQgqYk1kSzp0ZCvL1Cf/ -tOZ+GUrGjNC0CrTqSylMuU1fAcWDaKYB/Yr3fq/5lNqJBRjsOnI4KkaEtw== ------END PUBLIC KEY-----`) - mID = "fRThG/6Ymon8NnpRMQJIgCMgjtrBVnOidYenOB0n6FI=" - bSK = `-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIICRst6QhwffAkeOQGIhcCSmB7/LYQXevwrv8TD9FjU7oAoGCCqGSM49 -AwEHoUQDQgAE5FTw9vYXDEFiZb9kS1LV7GzU1Mo/xQ8D2Vnkl7WqNTB2kJ45aTtl -F2bBk8i50oWNRlRLyi5MVl7j+6LVhMiBeA== ------END EC PRIVATE KEY-----` - bPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5FTw9vYXDEFiZb9kS1LV7GzU1Mo/ -xQ8D2Vnkl7WqNTB2kJ45aTtlF2bBk8i50oWNRlRLyi5MVl7j+6LVhMiBeA== ------END PUBLIC KEY-----`) - bID = "CwWwEY4IKzy1bfZ6QW0IU9mky0ruOQvzWOYkmRGMVP4=" - wSK = `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+/pzQGPt88nmVlMC -CjHXGLH93bZ5ZkLVTjsHLi2UQiKhRANCAAQ2DYOW5eMnGcMCDtfK7aFIJg0JBKIZ -cx8fz81azP6v6s8oYMyU5e5bYAfgm1RjGvjC2YTLqCpMvSIeK+rudqg4 ------END PRIVATE KEY-----` - wPK = mustCreatePK(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENg2DluXjJxnDAg7Xyu2hSCYNCQSi -GXMfH8/NWsz+r+rPKGDMlOXuW2AH4JtUYxr4wtmEy6gqTL0iHivq7naoOA== ------END PUBLIC KEY-----`) - mInit = []byte(`{"tree_size":5,"timestamp":0,"sha256_root_hash":"41smjBUiAU70EtKlT6lIOIYtRTYxYXsDB+XHfcvu/BE=","tree_head_signature":"BAMARzBFAiEA4CEXH2Z+T4Rcj3YTvgK5qM9NuFYHipI13Il6A/ozTFUCIBDY1VDFy8ZezXsuWNs+iLzkyO5I5kCZldGeMvspHOof"}`) - bInit = []byte(`{"tree_size":5,"timestamp":0,"sha256_root_hash":"41smjBUiAU70EtKlT6lIOIYtRTYxYXsDB+XHfcvu/BE=","tree_head_signature":"BAMASDBGAiEAjSUy1d7/n1MOYWCnx2DzU3nQk1OUHzRtFJl+eDCquBsCIQDEG2vk1A+LmHZfyt/BN4by2324rxWFFzAeG1f2EyXk9w=="}`) - mNext = []byte(`{"tree_size":8,"timestamp":1,"sha256_root_hash":"V8K9aklZ4EPB+RMOk1/8VsJUdFZR77GDtZUQq84vSbo=","tree_head_signature":"BAMARjBEAiB9SZfr3JJbLsSE4mhnHE9hbcbu97nsbKcONnXeJXeigwIgJTWVh5FLNfUre5uCRLY4B1KEyS8tcGbaaHdEMk2WAmc="}`) - consProof = [][]byte{ - dh("b9e1d62618f7fee8034e4c5010f727ab24d8e4705cb296c374bf2025a87a10d2", 32), - dh("aac66cd7a79ce4012d80762fe8eec3a77f22d1ca4145c3f4cee022e7efcd599d", 32), - dh("89d0f753f66a290c483b39cd5e9eafb12021293395fad3d4a2ad053cfbcfdc9e", 32), - dh("29e40bb79c966f4c6fe96aff6f30acfce5f3e8d84c02215175d6e018a5dee833", 32), - } - _ = mSK - _ = bSK - _ = wPK -) - -type logOpts struct { - ID string - PK crypto.PublicKey -} - -func mustCreatePK(pkPem string) crypto.PublicKey { - pk, _, _, err := ct.PublicKeyFromPEM([]byte(pkPem)) - if err != nil { - panic(err) - } - return pk -} - -func newWitness(t *testing.T, d *sql.DB, logs []logOpts) *Witness { - // Set up Opts for the witness. - logMap := make(map[string]ct.SignatureVerifier) - for _, log := range logs { - sigV, err := ct.NewSignatureVerifier(log.PK) - if err != nil { - t.Fatalf("couldn't create a log verifier: %v", err) - } - logMap[log.ID] = *sigV - } - opts := Opts{ - DB: d, - PrivKey: wSK, - KnownLogs: logMap, - } - // Create the witness. - w, err := New(opts) - if err != nil { - t.Fatalf("couldn't create witness: %v", err) - } - return w -} - -func dh(h string, expLen int) []byte { - r, err := hex.DecodeString(h) - if err != nil { - panic(err) - } - if got := len(r); got != expLen { - panic(fmt.Sprintf("decode %q: len=%d, want %d", h, got, expLen)) - } - return r -} - -func mustCreateDB(t *testing.T) (*sql.DB, func()) { - t.Helper() - db, err := sql.Open("sqlite3", ":memory:") - if err != nil { - t.Fatalf("failed to open temporary in-memory DB: %v", err) - } - return db, func() { - if err := db.Close(); err != nil { - t.Error(err) - } - } -} - -func TestGetLogs(t *testing.T) { - for _, test := range []struct { - desc string - logIDs []string - logPKs []crypto.PublicKey - sths [][]byte - }{ - { - desc: "no logs", - logIDs: []string{}, - }, { - desc: "one log", - logIDs: []string{mID}, - logPKs: []crypto.PublicKey{mPK}, - sths: [][]byte{mInit}, - }, { - desc: "two logs", - logIDs: []string{bID, mID}, - logPKs: []crypto.PublicKey{bPK, mPK}, - sths: [][]byte{bInit, mInit}, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - // Set up witness. - logs := make([]logOpts, len(test.logIDs)) - for i, logID := range test.logIDs { - logs[i] = logOpts{ID: logID, - PK: test.logPKs[i], - } - } - w := newWitness(t, d, logs) - // Update to an STH for all logs. - for i, logID := range test.logIDs { - if _, err := w.Update(ctx, logID, test.sths[i], nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - } - // Now see if the witness knows about these logs. - knownLogs, err := w.GetLogs() - if err != nil { - t.Fatalf("couldn't get logs from witness: %v", err) - } - if len(knownLogs) != len(test.logIDs) { - t.Fatalf("got %d logs, want %d", len(knownLogs), len(test.logIDs)) - } - sort.Strings(knownLogs) - for i := range knownLogs { - if knownLogs[i] != test.logIDs[i] { - t.Fatalf("got %q, want %q", test.logIDs[i], knownLogs[i]) - } - } - }) - } -} - -func TestGetSTH(t *testing.T) { - for _, test := range []struct { - desc string - setID string - setPK crypto.PublicKey - queryID string - queryPK crypto.PublicKey - sth []byte - wantThere bool - }{ - { - desc: "happy path", - setID: mID, - setPK: mPK, - queryID: mID, - queryPK: mPK, - sth: mInit, - wantThere: true, - }, { - desc: "other log", - setID: mID, - setPK: mPK, - queryID: bID, - queryPK: bPK, - sth: mInit, - wantThere: false, - }, { - desc: "nothing there", - setID: mID, - setPK: mPK, - queryID: mID, - queryPK: mPK, - sth: nil, - wantThere: false, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - // Set up witness. - w := newWitness(t, d, []logOpts{{ID: test.setID, - PK: test.setPK}}) - // Set an STH for the log if we want to for this test. - if test.sth != nil { - if _, err := w.Update(ctx, test.setID, test.sth, nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - } - // Try to get the latest STH. - sthRaw, err := w.GetSTH(test.queryID) - if !test.wantThere && err == nil { - t.Fatalf("returned an STH but shouldn't have") - } - // Check to see if we got something. - if test.wantThere { - if err != nil { - t.Fatalf("failed to get latest: %v", err) - } - sv, err := ct.NewSignatureVerifier(wPK) - if err != nil { - t.Fatalf("failed to create signature verifier: %v", err) - } - var sth api.CosignedSTH - if err := json.Unmarshal(sthRaw, &sth); err != nil { - t.Fatalf("failed to unmarshal raw STH: %v", err) - } - sig := tls.DigitallySigned(sth.WitnessSigs[0]) - sigData, err := tls.Marshal(sth.SignedTreeHead) - if err != nil { - t.Fatalf("failed to marshal internal STH: %v", err) - } - if err := sv.VerifySignature(sigData, sig); err != nil { - t.Fatal("failed to verify co-signature") - } - } - }) - } -} - -func TestUpdate(t *testing.T) { - for _, test := range []struct { - desc string - initSTH []byte - initSize uint64 - newSTH []byte - pf [][]byte - isGood bool - }{ - { - desc: "happy path", - initSTH: mInit, - initSize: 5, - newSTH: mNext, - pf: consProof, - isGood: true, - }, { - desc: "smaller STH", - initSTH: mNext, - initSize: 8, - newSTH: mInit, - pf: consProof, - isGood: false, - }, { - desc: "garbage proof", - initSTH: mInit, - initSize: 5, - newSTH: mNext, - pf: [][]byte{ - dh("aaaa", 2), - dh("bbbb", 2), - dh("cccc", 2), - dh("dddd", 2), - }, - isGood: false, - }, { - desc: "right logID", - initSTH: mInit, - initSize: 5, - newSTH: []byte(`{"log_id":"fRThG/6Ymon8NnpRMQJIgCMgjtrBVnOidYenOB0n6FI=","tree_size":8,"timestamp":1,"sha256_root_hash":"V8K9aklZ4EPB+RMOk1/8VsJUdFZR77GDtZUQq84vSbo=","tree_head_signature":"BAMARzBFAiEA2yPvkeRF0cvGOAxx0s+NUf7LT9gumx3MDYob3swzgHgCICGN1tbbu8FqagkE5kV0DSL3CsQWjv095AeL7b+iFMOu"}`), - pf: consProof, - isGood: true, - }, { - desc: "wrong logID", - initSTH: mInit, - initSize: 5, - newSTH: []byte(`{"log_id":"aaaaa/6Ymon8NnpRMQJIgCMgjtrBVnOidYenOB0n6FI=","tree_size":8,"timestamp":1,"sha256_root_hash":"V8K9aklZ4EPB+RMOk1/8VsJUdFZR77GDtZUQq84vSbo=","tree_head_signature":"BAMARzBFAiEA2yPvkeRF0cvGOAxx0s+NUf7LT9gumx3MDYob3swzgHgCICGN1tbbu8FqagkE5kV0DSL3CsQWjv095AeL7b+iFMOu"}`), - pf: consProof, - isGood: false, - }, - } { - t.Run(test.desc, func(t *testing.T) { - d, closeFn := mustCreateDB(t) - defer closeFn() - ctx := context.Background() - logID := mID - // Set up witness. - w := newWitness(t, d, []logOpts{{ID: logID, - PK: mPK}}) - // Set an initial STH for the log. - if _, err := w.Update(ctx, logID, test.initSTH, nil); err != nil { - t.Errorf("failed to set STH: %v", err) - } - // Now update from this STH to a newer one. - _, err := w.Update(ctx, logID, test.newSTH, test.pf) - if test.isGood { - if err != nil { - t.Fatalf("can't update to new STH: %v", err) - } - } else { - if err == nil { - t.Fatal("should have gotten an error but didn't") - } - } - }) - } -} diff --git a/internal/witness/cmd/witness/main.go b/internal/witness/cmd/witness/main.go deleted file mode 100644 index 0905cd0ed3..0000000000 --- a/internal/witness/cmd/witness/main.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package witness is designed to make sure the STHs of CT logs are consistent -// and store/serve/sign them if so. It is expected that a separate feeder -// component would be responsible for the actual interaction with logs. -package main - -import ( - "context" - "flag" - "os" - - "github.com/google/certificate-transparency-go/internal/witness/cmd/witness/impl" - "gopkg.in/yaml.v3" - "k8s.io/klog/v2" -) - -var ( - listenAddr = flag.String("listen", ":8000", "address:port to listen for requests on") - // If this is run with an in-memory database there is a good chance of the - // witness occasionally hitting a race condition and returning a 500. If - // a file is specified this won't happen. - dbFile = flag.String("db_file", ":memory:", "path to a file to be used as sqlite3 storage for STHs, e.g. /tmp/sths.db") - configFile = flag.String("config_file", "config/config.yaml", "path to a YAML config file that specifies the logs followed by this witness") - witnessSK = flag.String("private_key", "", "private signing key for the witness") -) - -func main() { - klog.InitFlags(nil) - flag.Parse() - - if *witnessSK == "" { - klog.Exit("--private_key must not be empty") - } - - if *configFile == "" { - klog.Exit("--config_file must not be empty") - } - fileData, err := os.ReadFile(*configFile) - if err != nil { - klog.Exitf("Failed to read from config file: %v", err) - } - var lc impl.LogConfig - if err := yaml.Unmarshal(fileData, &lc); err != nil { - klog.Exitf("Failed to parse config file as proper YAML: %v", err) - } - - ctx := context.Background() - if err := impl.Main(ctx, impl.ServerOpts{ - ListenAddr: *listenAddr, - DBFile: *dbFile, - PrivKey: *witnessSK, - Config: lc, - }); err != nil { - klog.Exitf("Error running witness: %v", err) - } -} diff --git a/internal/witness/omniwitness/.env b/internal/witness/omniwitness/.env deleted file mode 100644 index cf1cca1f3c..0000000000 --- a/internal/witness/omniwitness/.env +++ /dev/null @@ -1,3 +0,0 @@ -WITNESS_PRIVATE_KEY="-----BEGIN PRIVATE KEY----- -YOURPRIVATEKEYHERE ------END PRIVATE KEY-----" diff --git a/internal/witness/omniwitness/README.md b/internal/witness/omniwitness/README.md deleted file mode 100644 index 50eeb70224..0000000000 --- a/internal/witness/omniwitness/README.md +++ /dev/null @@ -1,18 +0,0 @@ - -## A CT omniwitness - -This docker configuration allows a witness to be deployed that witnesses STHs -for all usable CT logs. The witness obtains a new STH from these logs every -10 seconds and exposes port 8100 outside the container, meaning it could also be -used to distribute the cosigned STHs. - -### Configuration - -The only file that needs to be edited is the `.env` file. In particular, it -should be populated with a PEM-encoded `WITNESS_PRIVATE_KEY`. - -### Running - -From the directory containing the `docker-compose.yaml` file, run -`docker-compose up -d`. After a few minutes both the feeder and the witness -processes should be running inside the container. diff --git a/internal/witness/omniwitness/docker-compose.yaml b/internal/witness/omniwitness/docker-compose.yaml deleted file mode 100644 index b882ae88bd..0000000000 --- a/internal/witness/omniwitness/docker-compose.yaml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.2' -services: - witness: - build: - context: ../../.. - dockerfile: ./internal/witness/cmd/witness/Dockerfile - volumes: - - type: volume - source: data - target: /data - volume: - nocopy: true - - type: bind - source: ./witness_configs - target: /witness-config - read_only: true - command: - - "--listen=:8100" - - "--db_file=/data/witness.sqlite" - - "--private_key=${WITNESS_PRIVATE_KEY}" - - "--config_file=/witness-config/witness.yaml" - - "--logtostderr" - restart: always - ports: - - "8100:8100" - - feeder: - depends_on: - - witness - build: - context: ../../.. - dockerfile: ./internal/witness/cmd/feeder/Dockerfile - command: - - "--witness_url=http://witness:8100" - - "--alsologtostderr" - restart: always -volumes: - data: diff --git a/internal/witness/omniwitness/witness_configs/witness.yaml b/internal/witness/omniwitness/witness_configs/witness.yaml deleted file mode 100644 index d094aa2272..0000000000 --- a/internal/witness/omniwitness/witness_configs/witness.yaml +++ /dev/null @@ -1,28 +0,0 @@ -Logs: -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETeBmZOrzZKo4xYktx9gI2chEce3cw/tbr5xkoQlmhB18aKfsxD+MnILgGNl0FOm0eYGilFVi85wLRIOhK8lxKw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeIPc6fGmuBg6AJkv/z7NFckmHvf/OqmjchZJ6wm2qN200keRDg352dWpi7CHnSV51BpQYAj1CQY5JuRAwrrDwg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0JCPZFJOQqyEti5M8j13ALN3CAVHqkVM4yyOcKWCu2yye5yYeqDpEXYoALIgtM3TmHtNlifmt+4iatGwLpF3eA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER+1MInu8Q39BwDZ5Rp9TwXhwm3ktvgJzpk/r7dDgGk7ZacMm3ljfcoIvP1E72T8jvyLT1bvdapylajZcTH6W5g== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+WS9FSxAYlCVEzg8xyGwOrmPonoV14nWjjETAIdZvLvukPzIWBMKv6tDNlQjpIHNrUcUt1igRPpqoKDXw2MeKw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEchY+C+/vzj5g3ZXLY3q5qY1Kb2zcYYCmRV4vg6yU84WI0KV00HuO/8XuQqLwLZPjwtCymeLhQunSxgAnaXSuzg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExpon7ipsqehIeU1bmpog9TFo4Pk8+9oN8OYHl1Q2JGVXnkVFnuuvPgSo2Ep+6vLffNLcmEbxOucz03sFiematg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESLJHTlAycmJKDQxIv60pZG8g33lSYxYpCi5gteI6HLevWbFVCdtZx+m9b+0LrwWWl/87mkNN6xE0M4rnrIPA/w== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/8tkhjLRp0SXrlZdTzNkTd6HqmcmXiDJz3fAdWLgOhjmv4mohvRhwXul9bgW0ODgRwC9UGAgH/vpGHPvIS1qA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6J4EbcpIAl1+AkSRsbhoY5oRTj3VoFfaf1DlQkfi7Rbe/HcjfVtrwN8jaC+tQDGjF+dqvKhWJAQ6Q6ev6q9Mew== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfQ0DsdWYitzwFTvG3F4Nbj8Nv5XIVYzQpkyWsU4nuSYlmcwrAp6m092fsdXEw6w1BAeHlzaqrSgNfyvZaJ9y0Q== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9o7AiwrbGBIX6Lnc47I6OfLMdZnRzKoP5u072nBi6vpIOEooktTi1gNwlRPzGC2ySGfuc1xLDeaA/wSFGgpYFg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJyTdaAMoy/5jvg4RR019F2ihEV1McclBKMe2okuX7MCv/C87v+nxsfz1Af+p+0lADGMkmNd5LqZVqxbGvlHYcQ== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXu8iQwSCRSf2CbITGpUpBtFVt8+I0IU0d1C36Lfe1+fbwdaI0Z5FktfM2fBoI1bXBd18k2ggKGYGgdZBgLKTg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8m/SiQ8/xfiHHqtls9m7FyOMBg4JVZY9CgiixXGz0akvKD6DEL8S0ERmFe9U4ZiA0M4kbT5nmuk3I85Sk4bagA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7+R9dC4VFbbpuyOL+yy14ceAmEf7QGlo/EmtYU6DRzwat43f/3swtLr/L8ugFOOt1YU/RFmMjGCL17ixv66MZw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELsYzGMNwo8rBIlaklBIdmD2Ofn6HkfrjK0Ukz1uOIUC6Lm0jTITCXhoIdjs7JkyXnwuwYiJYiH7sE1YeKu8k9w== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhjyxDVIjWt5u9sB/o2S8rcGJ2pdZTGA8+IpXhI/tvKBjElGE5r3de4yAfeOPhqTqqc+o7vPgXnDgu/a9/B+RLg== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz0OeL7jrVxEXJu+o4QWQYLKyokXHiPOOKVUL3/TNFFquVzDSer7kZ3gijxzBp98ZTgRgMSaWgCmZ8OD74mFUQ== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESdAfC+h1SZNsSARs188n/dCiNYjGSgkT7avLYe1mmXJzzHhsmxmAorHtOzhDkFgaCSCrUPrXdunK946eyIeSmA== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu1LyFs+SC8555lRtwjdTpPX5OqmzBewdvRbsMKwu+HliNRWOGtgWLuRIa/bGE/GWLlwQ/hkeqBi4Dy3DpIZRlw== -- PubKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpBFS2xdBTpDUVlESMFL4mwPPTJ/4Lji18Vq6+ji50o8agdqVzDPsIShmxlY+YDYhINnUrF36XBmhBX3+ICP89Q== diff --git a/internal/witness/verifier/verifier.go b/internal/witness/verifier/verifier.go deleted file mode 100644 index 7b064fbb2c..0000000000 --- a/internal/witness/verifier/verifier.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package verifier is designed to verify the signatures produced by a witness. -package verifier - -import ( - "crypto" - "errors" - "fmt" - - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/internal/witness/api" - "github.com/google/certificate-transparency-go/tls" -) - -// WitnessVerifier consists of a CT signature verifier. -type WitnessVerifier struct { - SigVerifier *ct.SignatureVerifier -} - -// NewWitnessVerifier creates a witness signature verifier from a public key. -func NewWitnessVerifier(pk crypto.PublicKey) (*WitnessVerifier, error) { - sv, err := ct.NewSignatureVerifier(pk) - if err != nil { - return nil, fmt.Errorf("failed to create signature verifier: %v", err) - } - return &WitnessVerifier{SigVerifier: sv}, nil -} - -// VerifySignature finds and verifies this witness' signature on a cosigned STH. -// This may mean that there are other witness signatures that remain unverified, -// so future implementations may want to take in multiple signature verifiers -// like in the Note package (https://pkg.go.dev/golang.org/x/mod/sumdb/note). -func (wv WitnessVerifier) VerifySignature(sth api.CosignedSTH) error { - if len(sth.WitnessSigs) == 0 { - return errors.New("no witness signature present in the STH") - } - sigData, err := tls.Marshal(sth.SignedTreeHead) - if err != nil { - return fmt.Errorf("failed to marshal internal STH: %v", err) - } - for _, sig := range sth.WitnessSigs { - // If we find a signature that verifies then we're okay. - if err := wv.SigVerifier.VerifySignature(sigData, tls.DigitallySigned(sig)); err == nil { - return nil - } - } - return errors.New("failed to verify any signature for this witness") -}