Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions submission/loglist_refresher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,35 @@ package submission

import (
"bytes"
"crypto"
"fmt"
"net/http"
"sync"
"time"

ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/loglist3"
"github.com/google/certificate-transparency-go/x509util"
)

const (
// HttpClientTimeout timeout for Log list reader http client.
httpClientTimeout = 10 * time.Second
// chromeLogListPublicKeyPEM is the published key for verifying Chrome's v3 CT log lists.
chromeLogListPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsu0BHGnQ++W2CTdyZyxv
HHRALOZPlnu/VMVgo2m+JZ8MNbAOH2cgXb8mvOj8flsX/qPMuKIaauO+PwROMjiq
fUpcFm80Kl7i97ZQyBDYKm3MkEYYpGN+skAR2OebX9G2DfDqFY8+jUpOOWtBNr3L
rmVcwx+FcFdMjGDlrZ5JRmoJ/SeGKiORkbbu9eY1Wd0uVhz/xI5bQb0OgII7hEj+
i/IPbJqOHgB8xQ5zWAJJ0DmG+FM6o7gk403v6W3S8qRYiR84c50KppGwe4YqSMkF
bLDleGQWLoaDSpEWtESisb4JiLaY4H+Kk0EyAhPSb+49JfUozYl+lf7iFN3qRq/S
IXXTh6z0S7Qa8EYDhKGCrpI03/+qprwy+my6fpWHi6aUIk4holUCmWvFxZDfixox
K0RlqbFDl2JXMBquwlQpm8u5wrsic1ksIv9z8x9zh4PJqNpCah0ciemI3YGRQqSe
/mRRXBiSn9YQBUPcaeqCYan+snGADFwHuXCd9xIAdFBolw9R9HTedHGUfVXPJDiF
4VusfX6BRR/qaadB+bqEArF/TzuDUr6FvOR4o8lUUxgLuZ/7HO+bHnaPFKYHHSm+
+z1lVDhhYuSZ8ax3T0C3FZpb7HMjZtpEorSV5ElKJEJwrhrBCMOD8L01EoSPrGlS
1w22i9uGHMn/uGQKo28u7AsCAwEAAQ==
-----END PUBLIC KEY-----`
)

// LogListData wraps info on external LogList, keeping its JSON source and time
Expand All @@ -53,22 +70,81 @@ type logListRefresherImpl struct {
lastJSON []byte
path string
client *http.Client
sigPath string
pubKey crypto.PublicKey
}

// NewCustomLogListRefresher creates and inits a LogListRefresherImpl instance.
func NewCustomLogListRefresher(client *http.Client, llPath string) LogListRefresher {
return &logListRefresherImpl{
path: llPath,
client: client,
sigPath, pubKeyPEM, ok := defaultLogListSignatureConfig(llPath)
if ok {
llr, err := newLogListRefresher(client, llPath, sigPath, pubKeyPEM)
if err != nil {
panic(fmt.Sprintf("failed to initialize built-in log list verifier: %v", err))
}
return llr
}
llr, err := newLogListRefresher(client, llPath, "", "")
if err != nil {
panic(fmt.Sprintf("failed to initialize log list refresher: %v", err))
}
return llr
}

// NewLogListRefresher creates and inits a LogListRefresherImpl instance using
// default http.Client
// default http.Client. Built-in Chrome log list URLs are verified against the
// published signature and public key.
func NewLogListRefresher(llPath string) LogListRefresher {
return NewCustomLogListRefresher(&http.Client{Timeout: httpClientTimeout}, llPath)
}

// NewVerifiedLogListRefresher creates a refresher that verifies the log list
// signature using the provided PEM-encoded public key.
func NewVerifiedLogListRefresher(llPath, sigPath, pubKeyPEM string) (LogListRefresher, error) {
return NewCustomVerifiedLogListRefresher(&http.Client{Timeout: httpClientTimeout}, llPath, sigPath, pubKeyPEM)
}

// NewCustomVerifiedLogListRefresher creates a refresher that verifies the log
// list signature using the provided PEM-encoded public key.
func NewCustomVerifiedLogListRefresher(client *http.Client, llPath, sigPath, pubKeyPEM string) (LogListRefresher, error) {
return newLogListRefresher(client, llPath, sigPath, pubKeyPEM)
}

func defaultLogListSignatureConfig(llPath string) (string, string, bool) {
switch llPath {
case loglist3.LogListURL:
return loglist3.LogListSignatureURL, chromeLogListPublicKeyPEM, true
case loglist3.AllLogListURL:
return loglist3.AllLogListSignatureURL, chromeLogListPublicKeyPEM, true
default:
return "", "", false
}
}

func newLogListRefresher(client *http.Client, llPath, sigPath, pubKeyPEM string) (*logListRefresherImpl, error) {
llr := &logListRefresherImpl{
path: llPath,
client: client,
}
if sigPath == "" && pubKeyPEM == "" {
return llr, nil
}
if sigPath == "" || pubKeyPEM == "" {
return nil, fmt.Errorf("signature path and public key must both be provided")
}

pubKey, _, rest, err := ct.PublicKeyFromPEM([]byte(pubKeyPEM))
if err != nil {
return nil, fmt.Errorf("failed to parse log list public key: %v", err)
}
if len(rest) != 0 {
return nil, fmt.Errorf("failed to parse log list public key: trailing data (%d bytes)", len(rest))
}
llr.sigPath = sigPath
llr.pubKey = pubKey
return llr, nil
}

// Refresh fetches the log list and returns its source, formed LogList and
// timestamp if source has changed compared to previous Refresh.
func (llr *logListRefresherImpl) Refresh() (*LogListData, error) {
Expand All @@ -85,9 +161,21 @@ func (llr *logListRefresherImpl) Refresh() (*LogListData, error) {
return nil, nil
}

ll, err := loglist3.NewFromJSON(json)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", llr.path, err)
var ll *loglist3.LogList
if llr.sigPath != "" && llr.pubKey != nil {
sig, err := x509util.ReadFileOrURL(llr.sigPath, llr.client)
if err != nil {
return nil, fmt.Errorf("failed to read %q signature: %v", llr.sigPath, err)
}
ll, err = loglist3.NewFromSignedJSON(json, sig, llr.pubKey)
if err != nil {
return nil, fmt.Errorf("failed to verify %q: %v", llr.path, err)
}
} else {
ll, err = loglist3.NewFromJSON(json)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", llr.path, err)
}
}
llr.lastJSON = json
return &LogListData{JSON: json, List: ll, DownloadTime: t}, nil
Expand Down
122 changes: 122 additions & 0 deletions submission/loglist_refresher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ package submission

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/pem"
"fmt"
"log"
"net/http"
Expand All @@ -27,6 +31,8 @@ import (

"github.com/google/certificate-transparency-go/loglist3"
"github.com/google/certificate-transparency-go/schedule"
cttls "github.com/google/certificate-transparency-go/tls"
ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/google/go-cmp/cmp"
)

Expand All @@ -48,6 +54,52 @@ func createTempFile(data string) (string, error) {
return f.Name(), nil
}

func createTempBytesFile(data []byte) (string, error) {
f, err := os.CreateTemp("", "")
if err != nil {
return "", err
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("Operation to close file failed: %v", err)
}
}()
if _, err := f.Write(data); err != nil {
return "", err
}
return f.Name(), nil
}

func createSignedLogListFiles(t *testing.T, ll string) (string, string, string) {
t.Helper()

privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey() = %v, want nil", err)
}
sig, err := cttls.CreateSignature(*privKey, cttls.SHA256, []byte(ll))
if err != nil {
t.Fatalf("tls.CreateSignature() = %v, want nil", err)
}
pubDER, err := ctx509.MarshalPKIXPublicKey(privKey.Public())
if err != nil {
t.Fatalf("x509.MarshalPKIXPublicKey() = %v, want nil", err)
}
pubKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})
llPath, err := createTempFile(ll)
if err != nil {
t.Fatalf("createTempFile() = %v, want nil", err)
}
sigPath, err := createTempBytesFile(sig.Signature)
if err != nil {
if rmErr := os.Remove(llPath); rmErr != nil {
t.Fatalf("createTempBytesFile() = %v; cleanup err = %v", err, rmErr)
}
t.Fatalf("createTempBytesFile() = %v, want nil", err)
}
return llPath, sigPath, string(pubKeyPEM)
}

func ExampleLogListRefresher() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down Expand Up @@ -118,6 +170,76 @@ func TestNewCustomLogListRefresher(t *testing.T) {
}
}

func TestNewLogListRefresherBuiltInURLVerifiesSignature(t *testing.T) {
llr := NewLogListRefresher(loglist3.LogListURL)
got, ok := llr.(*logListRefresherImpl)
if !ok {
t.Fatalf("NewLogListRefresher(%q) returned %T, want *logListRefresherImpl", loglist3.LogListURL, llr)
}
if diff := cmp.Diff(loglist3.LogListSignatureURL, got.sigPath); diff != "" {
t.Fatalf("NewLogListRefresher(%q) sigPath diff (-want +got):\n%s", loglist3.LogListURL, diff)
}
if got.pubKey == nil {
t.Fatalf("NewLogListRefresher(%q) pubKey = nil, want non-nil", loglist3.LogListURL)
}
}

func TestNewVerifiedLogListRefresher(t *testing.T) {
ll := `{"operators": [{"id":0,"name":"Google"}]}`
llPath, sigPath, pubKeyPEM := createSignedLogListFiles(t, ll)
defer func() {
if err := os.Remove(llPath); err != nil {
t.Fatalf("Operation to remove temp file failed: %v", err)
}
if err := os.Remove(sigPath); err != nil {
t.Fatalf("Operation to remove temp file failed: %v", err)
}
}()

llr, err := NewVerifiedLogListRefresher(llPath, sigPath, pubKeyPEM)
if err != nil {
t.Fatalf("NewVerifiedLogListRefresher() = (_, %v), want (_, nil)", err)
}
got, err := llr.Refresh()
if err != nil {
t.Fatalf("llr.Refresh() = (_, %v), want (_, nil)", err)
}
want := &loglist3.LogList{Operators: []*loglist3.Operator{{Name: "Google"}}}
if diff := cmp.Diff(want, got.List); diff != "" {
t.Fatalf("llr.Refresh() LogList diff (-want +got):\n%s", diff)
}
}

func TestNewVerifiedLogListRefresherRejectsBadSignature(t *testing.T) {
llPath, err := createTempFile(`{"operators": [{"id":0,"name":"Google"}]}`)
if err != nil {
t.Fatalf("createTempFile() = %v, want nil", err)
}
sigPath, err := createTempBytesFile([]byte("not-a-valid-signature"))
if err != nil {
if rmErr := os.Remove(llPath); rmErr != nil {
t.Fatalf("createTempBytesFile() = %v; cleanup err = %v", err, rmErr)
}
t.Fatalf("createTempBytesFile() = %v, want nil", err)
}
defer func() {
if err := os.Remove(llPath); err != nil {
t.Fatalf("Operation to remove temp file failed: %v", err)
}
if err := os.Remove(sigPath); err != nil {
t.Fatalf("Operation to remove temp file failed: %v", err)
}
}()

llr, err := NewVerifiedLogListRefresher(llPath, sigPath, chromeLogListPublicKeyPEM)
if err != nil {
t.Fatalf("NewVerifiedLogListRefresher() = (_, %v), want (_, nil)", err)
}
if _, err := llr.Refresh(); err == nil || !strings.Contains(err.Error(), "failed to verify") {
t.Fatalf("llr.Refresh() = (_, %v), want err containing %q", err, "failed to verify")
}
}

func TestNewLogListRefresher(t *testing.T) {
testCases := []struct {
name string
Expand Down
4 changes: 2 additions & 2 deletions submission/proxy_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ type ProxyServer struct {
}

// NewProxyServer creates ProxyServer instance. Call Run() to init.
func NewProxyServer(logListPath string, dBuilder DistributorBuilder, reqTimeout time.Duration, mf monitoring.MetricFactory) *ProxyServer {
func NewProxyServer(llr LogListRefresher, dBuilder DistributorBuilder, reqTimeout time.Duration, mf monitoring.MetricFactory) *ProxyServer {
s := &ProxyServer{addTimeout: reqTimeout}
s.p = NewProxy(NewLogListManager(NewLogListRefresher(logListPath), mf), dBuilder, mf)
s.p = NewProxy(NewLogListManager(llr, mf), dBuilder, mf)
return s
}

Expand Down
25 changes: 24 additions & 1 deletion submission/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/google/certificate-transparency-go/submission"
"github.com/google/certificate-transparency-go/x509util"
"github.com/google/trillian/monitoring/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/klog/v2"
Expand All @@ -33,6 +34,8 @@ import (
var (
httpEndpoint = flag.String("http_endpoint", "localhost:5951", "Endpoint for HTTP (host:port)")
logListPath = flag.String("loglist_path", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "Path for list of CT Logs in JSON format")
logListSigPath = flag.String("loglist_sig_path", "", "Optional path for the signature over the CT log list (URL or filename)")
logListPublicKeyPath = flag.String("loglist_public_key", "", "Optional path for a PEM-encoded public key used to verify the CT log list signature")
logListRefreshInterval = flag.Duration("loglist_refresh_interval", 24*time.Hour, "Interval between consecutive reads of Log-list")
rootsRefreshInterval = flag.Duration("roots_refresh_interval", 24*time.Hour, "Interval between consecutive get-roots calls")
policyType = flag.String("policy_type", "chrome", "CT-policy <chrome|apple>")
Expand All @@ -52,6 +55,25 @@ func parsePolicyType() submission.CTPolicyType {
return submission.ChromeCTPolicy
}

func buildLogListRefresher() submission.LogListRefresher {
if *logListSigPath == "" && *logListPublicKeyPath == "" {
return submission.NewLogListRefresher(*logListPath)
}
if *logListSigPath == "" || *logListPublicKeyPath == "" {
klog.Fatalf("flags -loglist_sig_path and -loglist_public_key must be provided together")
}

pubKeyPEM, err := x509util.ReadFileOrURL(*logListPublicKeyPath, &http.Client{Timeout: 10 * time.Second})
if err != nil {
klog.Fatalf("failed to read -loglist_public_key %q: %v", *logListPublicKeyPath, err)
}
llr, err := submission.NewVerifiedLogListRefresher(*logListPath, *logListSigPath, string(pubKeyPEM))
if err != nil {
klog.Fatalf("failed to configure log list signature verification: %v", err)
}
return llr
}

func main() {
klog.InitFlags(nil)
flag.Parse()
Expand All @@ -63,8 +85,9 @@ func main() {
lcb = submission.NewStubLogClient
}
mf := prometheus.MetricFactory{}
llr := buildLogListRefresher()

s := submission.NewProxyServer(*logListPath, submission.GetDistributorBuilder(plc, lcb, mf), *addPreChainTimeout, mf)
s := submission.NewProxyServer(llr, submission.GetDistributorBuilder(plc, lcb, mf), *addPreChainTimeout, mf)
s.Run(context.Background(), *logListRefreshInterval, *rootsRefreshInterval, *loadPendingQualifiedLogs)
http.HandleFunc("/ct/v1/proxy/add-pre-chain/", s.HandleAddPreChain)
http.HandleFunc("/ct/v1/proxy/add-chain/", s.HandleAddChain)
Expand Down