Skip to content

Commit 6074c64

Browse files
committed
sunlight: support fetching from local or archived logs
1 parent 1d019a3 commit 6074c64

File tree

4 files changed

+123
-29
lines changed

4 files changed

+123
-29
lines changed

client.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"iter"
1818
"log/slog"
1919
"net/http"
20+
"os"
2021
"runtime/debug"
2122
"strings"
2223
"time"
@@ -41,12 +42,30 @@ type Client struct {
4142
// ClientConfig is the configuration for a [Client].
4243
type ClientConfig struct {
4344
// MonitoringPrefix is the c2sp.org/static-ct-api monitoring prefix.
45+
//
46+
// If the MonitoringPrefix has schema "file://", Client will read tiles from
47+
// the local filesystem, and most other settings will be ignored.
48+
//
49+
// If it has schema "gzip+file://", the data tiles are expected to be
50+
// gzip-compressed.
51+
//
52+
// If it has schema "archive+file://", Client will read tiles from a set of
53+
// [archival zip files] in the specified directory.
54+
//
55+
// [archival zip files]:
56+
// https://github.com/geomys/ct-archive/blob/main/README.md#archival-format
4457
MonitoringPrefix string
4558

4659
// PublicKey is the public key of the log, used to verify checkpoints in
4760
// [Client.Checkpoint] and SCTs in [Client.CheckInclusion].
4861
PublicKey crypto.PublicKey
4962

63+
// AllowRFC6962ArchivalLeafs indicates whether to accept leafs archived from
64+
// RFC 6962 logs, which lack the LeafIndex extension.
65+
//
66+
// See [LogEntry.RFC6962ArchivalLeaf] for details.
67+
AllowRFC6962ArchivalLeafs bool
68+
5069
// HTTPClient is the HTTP client used to fetch tiles. If nil, a client is
5170
// created with default timeouts and settings.
5271
//
@@ -84,7 +103,35 @@ type ClientConfig struct {
84103

85104
// NewClient creates a new [Client].
86105
func NewClient(config *ClientConfig) (*Client, error) {
87-
if config == nil || config.UserAgent == "" {
106+
if schema, path, ok := strings.Cut(config.MonitoringPrefix, "://"); ok &&
107+
(schema == "file" || schema == "archive+file" || schema == "gzip+file") {
108+
if config.Cache != "" {
109+
return nil, errors.New("sunlight: permanent cache cannot be used with file://")
110+
}
111+
root, err := os.OpenRoot(path)
112+
if err != nil {
113+
return nil, fmt.Errorf("sunlight: failed to open file:// monitoring prefix: %w", err)
114+
}
115+
tileFS := root.FS()
116+
if schema == "archive+file" {
117+
tileFS = torchwood.NewTileArchiveFS(root.FS())
118+
}
119+
options := []torchwood.TileFSOption{torchwood.WithTileFSTilePath(TilePath)}
120+
if schema == "gzip+file" {
121+
options = append(options, torchwood.WithGzipDataTiles())
122+
}
123+
tileReader, err := torchwood.NewTileFS(tileFS, options...)
124+
if err != nil {
125+
return nil, fmt.Errorf("sunlight: failed to create file:// tile reader: %w", err)
126+
}
127+
client, err := torchwood.NewClient(tileReader, torchwood.WithCutEntry(cutEntry))
128+
if err != nil {
129+
return nil, fmt.Errorf("sunlight: failed to create file:// client: %w", err)
130+
}
131+
return &Client{c: client, r: tileReader, cc: config}, nil
132+
}
133+
134+
if config.UserAgent == "" {
88135
return nil, errors.New("sunlight: missing UserAgent")
89136
}
90137
if !strings.Contains(config.UserAgent, "@") &&
@@ -142,7 +189,7 @@ func cutEntry(tile []byte) (entry []byte, rh tlog.Hash, rest []byte, err error)
142189
// This implementation is terribly inefficient, parsing the whole entry just
143190
// to re-serialize and throw it away. If this function shows up in profiles,
144191
// let me know and I'll improve it.
145-
e, rest, err := ReadTileLeaf(tile)
192+
e, rest, err := ReadTileLeafMaybeArchival(tile)
146193
if err != nil {
147194
return nil, tlog.Hash{}, nil, err
148195
}
@@ -179,7 +226,16 @@ func (c *Client) Entries(ctx context.Context, tree tlog.Tree, start int64) iter.
179226
c.err = nil
180227
return func(yield func(int64, *LogEntry) bool) {
181228
for i, e := range c.c.Entries(ctx, tree, start) {
182-
entry, rest, err := ReadTileLeaf(e)
229+
var (
230+
entry *LogEntry
231+
rest []byte
232+
err error
233+
)
234+
if c.cc.AllowRFC6962ArchivalLeafs {
235+
entry, rest, err = ReadTileLeafMaybeArchival(e)
236+
} else {
237+
entry, rest, err = ReadTileLeaf(e)
238+
}
183239
if err != nil {
184240
c.err = err
185241
return
@@ -204,14 +260,22 @@ func (c *Client) Entry(ctx context.Context, tree tlog.Tree, index int64) (*LogEn
204260
if err != nil {
205261
return nil, nil, err
206262
}
207-
entry, rest, err := ReadTileLeaf(e)
263+
var (
264+
entry *LogEntry
265+
rest []byte
266+
)
267+
if c.cc.AllowRFC6962ArchivalLeafs {
268+
entry, rest, err = ReadTileLeafMaybeArchival(e)
269+
} else {
270+
entry, rest, err = ReadTileLeaf(e)
271+
}
208272
if err != nil {
209273
return nil, nil, fmt.Errorf("sunlight: failed to parse log entry %d: %w", index, err)
210274
}
211275
if len(rest) > 0 {
212276
return nil, nil, fmt.Errorf("sunlight: unexpected trailing data in entry %d", index)
213277
}
214-
if entry.LeafIndex != index {
278+
if !entry.RFC6962ArchivalLeaf && entry.LeafIndex != index {
215279
return nil, nil, fmt.Errorf("sunlight: log entry index %d does not match requested index %d", entry.LeafIndex, index)
216280
}
217281
return entry, proof, nil

example_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package sunlight_test
22

33
import (
44
"context"
5+
"crypto/x509"
56
"encoding/pem"
67
"errors"
78
"fmt"
89
"strconv"
910
"strings"
1011

1112
"filippo.io/sunlight"
12-
"github.com/google/certificate-transparency-go/x509"
13+
x509ct "github.com/google/certificate-transparency-go/x509"
1314
"golang.org/x/mod/sumdb/tlog"
1415
)
1516

@@ -100,7 +101,7 @@ ybky1bC4rbimZJIjvhnqMcMkf/I=
100101
panic(err)
101102
}
102103

103-
cert, err := x509.ParseCertificate(certificate.Bytes)
104+
cert, err := x509ct.ParseCertificate(certificate.Bytes)
104105
if err != nil {
105106
panic(err)
106107
}
@@ -161,3 +162,32 @@ func ExampleClient_UnauthenticatedTrimmedEntries() {
161162
}
162163
}
163164
}
165+
166+
func ExampleClient_localFilesystem() {
167+
block, _ := pem.Decode([]byte(`-----BEGIN PUBLIC KEY-----
168+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4i7AmqGoGHsorn/eyclTMjrAnM0J
169+
UUbyGJUxXqq1AjQ4qBC77wXkWt7s/HA8An2vrEBKIGQzqTjV8QIHrmpd4w==
170+
-----END PUBLIC KEY-----`))
171+
key, err := x509.ParsePKIXPublicKey(block.Bytes)
172+
if err != nil {
173+
panic(err)
174+
}
175+
176+
client, err := sunlight.NewClient(&sunlight.ClientConfig{
177+
MonitoringPrefix: "gzip+file:///tank/logs/navigli2025h2/data/",
178+
PublicKey: key,
179+
})
180+
if err != nil {
181+
panic(err)
182+
}
183+
184+
checkpoint, _, err := client.Checkpoint(context.TODO())
185+
if err != nil {
186+
panic(err)
187+
}
188+
entry, _, err := client.Entry(context.TODO(), checkpoint.Tree, 142424242)
189+
if err != nil {
190+
panic(err)
191+
}
192+
println(entry.Timestamp)
193+
}

go.mod

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
module filippo.io/sunlight
22

3-
go 1.24.4
3+
go 1.24.0
44

55
require (
66
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c
7-
filippo.io/torchwood v0.5.1-0.20251108121651-147243fa786b
7+
filippo.io/torchwood v0.8.0
88
github.com/aws/aws-sdk-go-v2 v1.30.3
99
github.com/aws/aws-sdk-go-v2/config v1.27.27
1010
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4
1111
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3
1212
github.com/aws/smithy-go v1.20.3
1313
github.com/google/certificate-transparency-go v1.3.2
1414
github.com/prometheus/client_golang v1.22.0
15-
golang.org/x/crypto v0.39.0
16-
golang.org/x/mod v0.25.0
17-
golang.org/x/net v0.40.0
18-
golang.org/x/sync v0.15.0
15+
golang.org/x/crypto v0.44.0
16+
golang.org/x/mod v0.29.0
17+
golang.org/x/net v0.46.0
18+
golang.org/x/sync v0.18.0
1919
gopkg.in/yaml.v3 v3.0.1
2020
)
2121

@@ -57,8 +57,8 @@ require (
5757
github.com/prometheus/client_model v0.6.1 // indirect
5858
github.com/prometheus/common v0.62.0 // indirect
5959
github.com/prometheus/procfs v0.15.1 // indirect
60-
golang.org/x/sys v0.33.0 // indirect
61-
golang.org/x/text v0.26.0 // indirect
60+
golang.org/x/sys v0.38.0 // indirect
61+
golang.org/x/text v0.31.0 // indirect
6262
golang.org/x/time v0.11.0 // indirect
6363
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
6464
google.golang.org/grpc v1.72.2 // indirect

go.sum

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
88
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
99
filippo.io/keygen v0.0.0-20240718133620-7f162efbbd87 h1:HlcHAMbI9Xvw3aWnhPngghMl5AKE2GOvjmvSGOKzCcI=
1010
filippo.io/keygen v0.0.0-20240718133620-7f162efbbd87/go.mod h1:nAs0+DyACEQGudhkTwlPC9atyqDYC7ZotgZR7D8OwXM=
11-
filippo.io/torchwood v0.5.1-0.20251108121651-147243fa786b h1:jGqc2PtRwqj8vUstjGJeaMUTL91OKWbr01u+6p2ycgE=
12-
filippo.io/torchwood v0.5.1-0.20251108121651-147243fa786b/go.mod h1:B1iitdrTUBuGJIHrh4r0PsdKmI4XzlAsPDe/9B4iAnw=
11+
filippo.io/torchwood v0.8.0 h1:vZsUJRwcy/TE+qR6mBNzWOcQc2rYnP1rtLUzyzO8E/U=
12+
filippo.io/torchwood v0.8.0/go.mod h1:bV91zf15ZZ3E4h5nCSfAAiuDe/8aT54s/R5gXBLKhVk=
1313
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
1414
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
1515
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
@@ -134,18 +134,18 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
134134
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
135135
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
136136
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
137-
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
138-
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
139-
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
140-
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
141-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
142-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
143-
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
144-
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
145-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
146-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
147-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
148-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
137+
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
138+
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
139+
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
140+
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
141+
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
142+
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
143+
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
144+
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
145+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
146+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
147+
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
148+
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
149149
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
150150
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
151151
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=

0 commit comments

Comments
 (0)