Documentation
¶
Overview ¶
Package sunlight implements the Static Certificate Transparency API, including a Static CT log client.
Index ¶
- Constants
- Variables
- func AppendTileLeaf(t []byte, e *LogEntry) []byte
- func FormatCheckpoint(c Checkpoint) string
- func MarshalExtensions(e Extensions) ([]byte, error)
- func NewRFC6962InjectedSigner(name string, key crypto.PublicKey, sig []byte, timestamp int64) (note.Signer, error)
- func NewRFC6962Verifier(name string, key crypto.PublicKey) (note.Verifier, error)
- func ParseTilePath(path string) (tlog.Tile, error)
- func RFC6962SignatureTimestamp(sig note.Signature) (int64, error)
- func TilePath(t tlog.Tile) string
- type Checkpoint
- type Client
- func (c *Client) CheckInclusion(ctx context.Context, tree tlog.Tree, sct []byte) (*LogEntry, tlog.RecordProof, error)
- func (c *Client) Checkpoint(ctx context.Context) (torchwood.Checkpoint, *note.Note, error)
- func (c *Client) Entries(ctx context.Context, tree tlog.Tree, start int64) iter.Seq2[int64, *LogEntry]
- func (c *Client) Entry(ctx context.Context, tree tlog.Tree, index int64) (*LogEntry, tlog.RecordProof, error)
- func (c *Client) Err() error
- func (c *Client) Fetcher() torchwood.TileReaderdeprecated
- func (c *Client) Issuer(ctx context.Context, fp [32]byte) (*x509.Certificate, error)
- func (c *Client) TileReader() torchwood.TileReader
- func (c *Client) UnauthenticatedTrimmedEntries(ctx context.Context, start, end int64) iter.Seq2[int64, *TrimmedEntry]
- type ClientConfig
- type Extensions
- type LogEntry
- type TrimmedEntry
Examples ¶
Constants ¶
const TileHeight = 8
const TileWidth = 1 << TileHeight
Variables ¶
var ErrWrongLogID = errors.New("sunlight: SCT log ID does not match public key")
ErrWrongLogID indicates that the log ID in the SCT does not match the public key of the log. Client.CheckInclusion can return an error wrapping this.
Functions ¶
func AppendTileLeaf ¶
AppendTileLeaf appends a LogEntry to a data tile.
func FormatCheckpoint ¶
func FormatCheckpoint(c Checkpoint) string
func MarshalExtensions ¶
func MarshalExtensions(e Extensions) ([]byte, error)
func NewRFC6962InjectedSigner ¶ added in v0.7.0
func NewRFC6962InjectedSigner(name string, key crypto.PublicKey, sig []byte, timestamp int64) (note.Signer, error)
NewRFC6962InjectedSigner constructs a note.Signer that uses the provided signature bytes as-is. It is useful to construct a signed checkpoint from a RFC 6962 TreeHeadSignature obtained from elsewhere.
func NewRFC6962Verifier ¶
NewRFC6962Verifier constructs a new note.Verifier that verifies a RFC 6962 TreeHeadSignature formatted according to c2sp.org/static-ct-api.
func ParseTilePath ¶ added in v0.4.0
ParseTilePath parses a tile coordinate path according to c2sp.org/static-st-api. It differs from tlog.ParseTilePath in that it doesn't include an explicit tile height. It also supports names tiles at level -2.
func TilePath ¶
TilePath returns a tile coordinate path describing t, according to c2sp.org/static-st-api. It differs from tlog.Tile.Path in that it doesn't include an explicit tile height. It also supports names tiles at level -2.
If t.Height is not TileHeight, TilePath panics.
Types ¶
type Checkpoint ¶
type Checkpoint = torchwood.Checkpoint
func ParseCheckpoint ¶
func ParseCheckpoint(text string) (Checkpoint, error)
type Client ¶ added in v0.4.0
type Client struct {
// contains filtered or unexported fields
}
Client is a Certificate Transparency log client that fetches and authenticates tiles according to c2sp.org/static-ct-api, and exposes log entries as a Go iterator.
Example (LocalFilesystem) ¶
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"filippo.io/sunlight"
)
func main() {
block, _ := pem.Decode([]byte(`-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4i7AmqGoGHsorn/eyclTMjrAnM0J
UUbyGJUxXqq1AjQ4qBC77wXkWt7s/HA8An2vrEBKIGQzqTjV8QIHrmpd4w==
-----END PUBLIC KEY-----`))
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
client, err := sunlight.NewClient(&sunlight.ClientConfig{
MonitoringPrefix: "gzip+file:///tank/logs/navigli2025h2/data/",
PublicKey: key,
})
if err != nil {
panic(err)
}
checkpoint, _, err := client.Checkpoint(context.TODO())
if err != nil {
panic(err)
}
entry, _, err := client.Entry(context.TODO(), checkpoint.Tree, 142424242)
if err != nil {
panic(err)
}
println(entry.Timestamp)
}
func NewClient ¶ added in v0.4.0
func NewClient(config *ClientConfig) (*Client, error)
NewClient creates a new Client.
func (*Client) CheckInclusion ¶ added in v0.5.1
func (c *Client) CheckInclusion(ctx context.Context, tree tlog.Tree, sct []byte) (*LogEntry, tlog.RecordProof, error)
CheckInclusion fetches the log entry for the given SCT, and verifies that it is included in the given tree and that the SCT is valid for the entry.
The provided tree should have been verified by the caller, for example using Client.Checkpoint.
If the SCT log ID does not match [ClientConfig.PublicKey], CheckInclusion returns an error wrapping ErrWrongLogID.
Example ¶
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"filippo.io/sunlight"
x509ct "github.com/google/certificate-transparency-go/x509"
"golang.org/x/mod/sumdb/tlog"
)
func main() {
block, _ := pem.Decode([]byte(`-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4i7AmqGoGHsorn/eyclTMjrAnM0J
UUbyGJUxXqq1AjQ4qBC77wXkWt7s/HA8An2vrEBKIGQzqTjV8QIHrmpd4w==
-----END PUBLIC KEY-----`))
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
certificate, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE-----
MIID0DCCA1WgAwIBAgISLJBonEz2NlVeQFEPlG5vsIrMMAoGCCqGSM49BAMDMFMx
CzAJBgNVBAYTAlVTMSAwHgYDVQQKExcoU1RBR0lORykgTGV0J3MgRW5jcnlwdDEi
MCAGA1UEAxMZKFNUQUdJTkcpIEZhbHNlIEZlbm5lbCBFNjAeFw0yNTA3MjcyMjA4
NDlaFw0yNTEwMjUyMjA4NDhaMCMxITAfBgNVBAMTGGhhcmRlcnJhZGlvZm1qdW1w
LnJhZC5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCLE1WbEwJ1Y+k3bj+vf
R4s6nDem8eZea0vZ8sgJqh13mm89lHZZTr5l/qRRFbcl6fL8LJNw0vapzr3rpnTu
7NGjggI3MIICMzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFvoTvFTQNkRzVpfP8JF
v0YNc908MB8GA1UdIwQYMBaAFKF0GgZtULeGLUoswX60jYhJbM0WMDYGCCsGAQUF
BwEBBCowKDAmBggrBgEFBQcwAoYaaHR0cDovL3N0Zy1lNi5pLmxlbmNyLm9yZy8w
IwYDVR0RBBwwGoIYaGFyZGVycmFkaW9mbWp1bXAucmFkLmlvMBMGA1UdIAQMMAow
CAYGZ4EMAQIBMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9zdGctZTYuYy5sZW5j
ci5vcmcvNzcuY3JsMIIBDQYKKwYBBAHWeQIEAgSB/gSB+wD5AHYA3Zk0/KXnJIDJ
Vmh9gTSZCEmySfe1adjHvKs/XMHzbmQAAAGYTiQDlAAABAMARzBFAiAZaM/o9pZJ
AoaMVTaHqM6aViSIjLam0CiEe8OK5M8RTAIhANS+35smBUCZvpM+zRNwSQ1siDDm
f2F8ayHSru9+BTeEAH8A5Pt3SiEkxYZAsYMvUKv63ISjiu1xke62aSI3ksv2KJEA
AAGYTiQDpAAIAAAFAATjOI4EAwBIMEYCIQC9ARnxeUgUL8Gkvl1lgKkuFVJaAOkv
TQ6H8sYzVcbliQIhAN5nTObp15PQSusjd0Qd+povk1DJ4tVA9rNKFEGOpTVoMAoG
CCqGSM49BAMDA2kAMGYCMQCuw26zAJbmCgvfsDu9ong073LppgwPWogX1DI050uS
scMeHBWmB0jXuic4zkVzVBQCMQD+IkFkLg8qOHNtipO+mtTCtdW8mEl7Ptb3yv04
ybky1bC4rbimZJIjvhnqMcMkf/I=
-----END CERTIFICATE-----`))
client, err := sunlight.NewClient(&sunlight.ClientConfig{
MonitoringPrefix: "https://navigli2025h2.skylight.geomys.org/",
PublicKey: key,
UserAgent: "ExampleClient ([email protected], +https://example.com)",
})
if err != nil {
panic(err)
}
checkpoint, _, err := client.Checkpoint(context.TODO())
if err != nil {
panic(err)
}
cert, err := x509ct.ParseCertificate(certificate.Bytes)
if err != nil {
panic(err)
}
for _, sct := range cert.SCTList.SCTList {
entry, proof, err := client.CheckInclusion(context.TODO(), checkpoint.Tree, sct.Val)
if errors.Is(err, sunlight.ErrWrongLogID) {
println("SCT log ID does not match public key, skipping")
continue
}
if err != nil {
panic(err)
}
println("Entry leaf index:", entry.LeafIndex)
println("Entry timestamp:", entry.Timestamp)
// There is no need to check the inclusion proof, but if provided to a third
// party, it can be checked as follows.
rh := tlog.RecordHash(entry.MerkleTreeLeaf())
if err := tlog.CheckRecord(proof, checkpoint.N, checkpoint.Hash, entry.LeafIndex, rh); err != nil {
panic(err)
}
}
}
func (*Client) Checkpoint ¶ added in v0.5.1
Checkpoint fetches the latest checkpoint and verifies it.
The signature by [ClientConfig.PublicKey] is always the first note.Note.Sigs entry. RFC6962SignatureTimestamp can be used to extract the STH timestamp from the signature.
func (*Client) Entries ¶ added in v0.4.0
func (c *Client) Entries(ctx context.Context, tree tlog.Tree, start int64) iter.Seq2[int64, *LogEntry]
Entries returns an iterator that yields entries from the given tree, starting at the given index. The first item in the yielded pair is the overall entry index in the log, starting at start.
The provided tree should have been verified by the caller, for example using Client.Checkpoint.
Iteration may stop before the size of the tree to avoid fetching a partial data tile. Resuming with the same tree will yield the remaining entries, however clients tailing a growing log are encouraged to fetch the next checkpoint and use that as the tree argument.
Callers must check Client.Err after the iteration breaks.
Example ¶
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"filippo.io/sunlight"
)
func main() {
// Note: clients that don't participate in the transparency ecosystem
// and are only interested in a feed of names can consider using the
// more efficient UnauthenticatedTrimmedEntries method instead.
block, _ := pem.Decode([]byte(`-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4i7AmqGoGHsorn/eyclTMjrAnM0J
UUbyGJUxXqq1AjQ4qBC77wXkWt7s/HA8An2vrEBKIGQzqTjV8QIHrmpd4w==
-----END PUBLIC KEY-----`))
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
client, err := sunlight.NewClient(&sunlight.ClientConfig{
MonitoringPrefix: "https://navigli2025h2.skylight.geomys.org/",
PublicKey: key,
UserAgent: "ExampleClient ([email protected], +https://example.com)",
})
if err != nil {
panic(err)
}
var start int64
for {
checkpoint, _, err := client.Checkpoint(context.TODO())
if err != nil {
panic(err)
}
for i, entry := range client.Entries(context.TODO(), checkpoint.Tree, start) {
fmt.Printf("%d: %d %d %x\n", i, entry.LeafIndex, entry.Timestamp, entry.IssuerKeyHash)
start = i + 1
}
if err := client.Err(); err != nil {
panic(err)
}
}
}
func (*Client) Entry ¶ added in v0.7.0
func (c *Client) Entry(ctx context.Context, tree tlog.Tree, index int64) (*LogEntry, tlog.RecordProof, error)
Entry returns a log entry by its index, and an inclusion proof in the tree.
The provided tree should have been verified by the caller, for example using Client.Checkpoint.
func (*Client) Err ¶ added in v0.4.0
Err returns the error encountered by the latest Client.Entries call.
func (*Client) Fetcher
deprecated
added in
v0.5.2
func (c *Client) Fetcher() torchwood.TileReader
Fetcher used to return the underlying torchwood.TileFetcher. It is now a compatibility alias of Client.TileReader, which now exposes a torchwood.TileReader.ReadEndpoint method.
Deprecated: use Client.TileReader instead.
func (*Client) Issuer ¶ added in v0.5.2
Issuer returns the issuer matching the fingerprint from [LogEntry.ChainFingerprints].
func (*Client) TileReader ¶ added in v0.7.0
func (c *Client) TileReader() torchwood.TileReader
TileReader returns the underlying torchwood.TileReader, which can be used to fetch endpoints directly, or as a tlog.HashReader via torchwood.TileHashReaderWithContext.
Its torchwood.TileReader.ReadTiles method respects [ClientConfig.Cache] if set. torchwood.TileReader.ReadEndpoint does not.
func (*Client) UnauthenticatedTrimmedEntries ¶ added in v0.6.0
func (c *Client) UnauthenticatedTrimmedEntries(ctx context.Context, start, end int64) iter.Seq2[int64, *TrimmedEntry]
UnauthenticatedTrimmedEntries returns an iterator that yields trimmed entries, starting and ending at the given index. The first item in the yielded pair is the overall entry index in the log, starting at start.
Entries are NOT authenticated against a checkpoint and, if supported by the log, are fetched through a more efficient protocol than Client.Entries. This method is only suitable for clients that don't participate in the transparency ecosystem, and are only interested in a feed of names.
Callers must check Client.Err after the iteration breaks.
Example ¶
package main
import (
"context"
"fmt"
"strconv"
"strings"
"filippo.io/sunlight"
)
func main() {
// Important: UnauthenticatedTrimmedEntries does NOT verify the signed tree
// head. It is only suitable for clients that don't participate in the
// transparency ecosystem, and are only interested in a feed of names.
client, err := sunlight.NewClient(&sunlight.ClientConfig{
MonitoringPrefix: "https://navigli2025h2.skylight.geomys.org/",
UserAgent: "ExampleClient ([email protected], +https://example.com)",
})
if err != nil {
panic(err)
}
var start int64
for {
checkpoint, err := client.Fetcher().ReadEndpoint(context.TODO(), "checkpoint")
if err != nil {
panic(err)
}
_, rest, _ := strings.Cut(string(checkpoint), "\n")
size, _, _ := strings.Cut(rest, "\n")
end, err := strconv.ParseInt(size, 10, 64)
if err != nil {
panic(err)
}
for i, entry := range client.UnauthenticatedTrimmedEntries(context.TODO(), start, end) {
fmt.Printf("%d: %s\n", i, entry.DNS)
start = i + 1
}
if err := client.Err(); err != nil {
panic(err)
}
}
}
type ClientConfig ¶ added in v0.4.0
type ClientConfig struct {
// MonitoringPrefix is the c2sp.org/static-ct-api monitoring prefix.
//
// If the MonitoringPrefix has schema "file://", Client will read tiles from
// the local filesystem, and most other settings will be ignored.
//
// If it has schema "gzip+file://", the data tiles are expected to be
// gzip-compressed.
//
// If it has schema "archive+file://", Client will read tiles from a set of
// [archival zip files] in the specified directory.
//
// [archival zip files]:
// https://github.com/geomys/ct-archive/blob/main/README.md#archival-format
MonitoringPrefix string
// PublicKey is the public key of the log, used to verify checkpoints in
// [Client.Checkpoint] and SCTs in [Client.CheckInclusion].
PublicKey crypto.PublicKey
// AllowRFC6962ArchivalLeafs indicates whether to accept leafs archived from
// RFC 6962 logs, which lack the LeafIndex extension.
//
// See [LogEntry.RFC6962ArchivalLeaf] for details.
AllowRFC6962ArchivalLeafs bool
// HTTPClient is the HTTP client used to fetch tiles. If nil, a client is
// created with default timeouts and settings.
//
// Note that Client may need to make multiple parallel requests to
// the same host, more than the default MaxIdleConnsPerHost.
HTTPClient *http.Client
// UserAgent is the User-Agent string used for HTTP requests. It must be
// set, and it must include an email address and/or an HTTPS URL.
//
// The library version will be appended to the User-Agent string.
UserAgent string
// Timeout is how long the Entries iterator can take to yield an entry.
// This includes any Retry-After waits. If zero, it defaults to five minutes.
Timeout time.Duration
// ConcurrencyLimit is the maximum number of concurrent requests
// made by the Client. If zero, there is no limit.
ConcurrencyLimit int
// Cache, if set, is a directory where the client will permanently cache
// verified non-partial tiles, following the same structure as the URLs.
//
// This directory always grows and is never pruned by the client. Most
// clients, especially those scanning a log sequentially, have no need to
// set this. [Client.Entries] will still use in-memory caching for the
// duration of the call.
Cache string
// Logger is the logger used to log errors and progress.
// If nil, log lines are discarded.
Logger *slog.Logger
}
ClientConfig is the configuration for a Client.
type Extensions ¶
type Extensions struct {
LeafIndex int64
}
Extensions is the CTExtensions field of SignedCertificateTimestamp and TimestampedEntry, according to c2sp.org/static-ct-api.
func ParseExtensions ¶
func ParseExtensions(extensions []byte) (Extensions, error)
ParseExtensions parse a CTExtensions field, ignoring unknown extensions. It is an error if the leaf_index extension is missing.
type LogEntry ¶
type LogEntry struct {
// Certificate is either the TimestampedEntry.signed_entry, or the
// PreCert.tbs_certificate for Precertificates.
// It must be at most 2^24-1 bytes long.
Certificate []byte
// IsPrecert is true if LogEntryType is precert_entry. Otherwise, the
// following three fields are zero and ignored.
IsPrecert bool
// IssuerKeyHash is the PreCert.issuer_key_hash.
IssuerKeyHash [32]byte
// ChainFingerprints are the SHA-256 hashes of the certificates in the
// X509ChainEntry.certificate_chain or
// PrecertChainEntry.precertificate_chain.
ChainFingerprints [][32]byte
// PreCertificate is the PrecertChainEntry.pre_certificate.
// It must be at most 2^24-1 bytes long.
PreCertificate []byte
// LeafIndex is the zero-based index of the leaf in the log.
// It must be between 0 and 2^40-1.
//
// If RFC6962ArchivalLeaf is true, this field is ignored and
// should be set to 0.
LeafIndex int64
// RFC6962ArchivalLeaf is true if this LogEntry is an archived
// RFC 6962 log leaf, which is missing the leaf index extension.
//
// [ReadTileLeaf] always sets this to false. To read such leaves,
// use [ReadTileLeafMaybeArchival].
RFC6962ArchivalLeaf bool
// Timestamp is the TimestampedEntry.timestamp.
Timestamp int64
}
func ReadTileLeaf ¶
ReadTileLeaf reads a LogEntry from a data tile, and returns the remaining data in the tile.
func ReadTileLeafMaybeArchival ¶ added in v0.7.0
ReadTileLeafMaybeArchival reads a LogEntry from a data tile, and returns the remaining data in the tile.
If the leaf is missing the leaf index extension, the returned LogEntry has RFC6962ArchivalLeaf set to true, and LeafIndex set to zero.
func (*LogEntry) MerkleTreeLeaf ¶
MerkleTreeLeaf returns a RFC 6962 MerkleTreeLeaf.
This is the Merkle tree leaf that can be passed, for example, to tlog.RecordHash for use with tlog.CheckRecord.
It also matches the digitally-signed data of an SCT, which is technically not a MerkleTreeLeaf, but a completely identical structure (except for the second field, which is a SignatureType of value 0 and length 1 instead of a MerkleLeafType of value 0 and length 1).
func (*LogEntry) TrimmedEntry ¶ added in v0.6.0
func (e *LogEntry) TrimmedEntry() (*TrimmedEntry, error)
type TrimmedEntry ¶ added in v0.6.0
type TrimmedEntry struct {
// Timestamp is the UNIX timestamp in milliseconds of when the entry was
// added to the log.
Timestamp int64
// Subject contains the parsed Subject information from the certificate.
Subject struct {
Country, Organization, OrganizationalUnit []string `json:",omitempty"`
Locality, Province []string `json:",omitempty"`
StreetAddress, PostalCode []string `json:",omitempty"`
CommonName string `json:",omitempty"`
} `json:",omitzero"`
// DNS and IP are the Subject Alternative Names of the certificate.
DNS []string `json:",omitempty"`
IP []string `json:",omitempty"`
}
A TrimmedEntry is a subset of the information in a LogEntry, including names parsed from the certificate or pre-certificate.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
names-prism
command
Command names-prism creates missing names tiles for existing data tiles.
|
Command names-prism creates missing names tiles for existing data tiles. |
|
partial-aftersun
command
Command partial-aftersun deletes partial tiles from a Sunlight local backend where a corresponding full tile exists.
|
Command partial-aftersun deletes partial tiles from a Sunlight local backend where a corresponding full tile exists. |
|
skylight
command
Command skylight runs a Certificate Transparency log read-path server.
|
Command skylight runs a Certificate Transparency log read-path server. |
|
sunlight
command
Command sunlight runs a Certificate Transparency log write-path server.
|
Command sunlight runs a Certificate Transparency log write-path server. |
|
sunlight-keygen
command
|
|
|
internal
|
|
|
durable
Package durable provides equivalent functionality to the os package, but with additional guarantees for durability based on os.File.Sync.
|
Package durable provides equivalent functionality to the os package, but with additional guarantees for durability based on os.File.Sync. |
|
frequent
Package frequent implements the Space Saving algorithm for counting appearances of the most frequent items in a stream of data (sometimes referred to as the heavy hitters).
|
Package frequent implements the Space Saving algorithm for counting appearances of the most frequent items in a stream of data (sometimes referred to as the heavy hitters). |
|
heavyhitter
Package heavyhitter registers two endpoints /debug/heavyhitter/useragents and /debug/heavyhitter/ips as a side-effect.
|
Package heavyhitter registers two endpoints /debug/heavyhitter/useragents and /debug/heavyhitter/ips as a side-effect. |
|
immutable
Package immutable provides best-effort support for setting and unsetting the immutable flag on files.
|
Package immutable provides best-effort support for setting and unsetting the immutable flag on files. |
|
keylog
Package keylog registers two endpoints /debug/keylog/on and /debug/keylog/off as a side-effect.
|
Package keylog registers two endpoints /debug/keylog/on and /debug/keylog/off as a side-effect. |
|
reused
Package reused allows tracking connection reuse in HTTP requests.
|
Package reused allows tracking connection reuse in HTTP requests. |
|
stdlog
Package heavyhitter registers two endpoints, /debug/logs/on and /debug/logs/off, as a side-effect.
|
Package heavyhitter registers two endpoints, /debug/logs/on and /debug/logs/off, as a side-effect. |