Skip to content

Commit 589a4ed

Browse files
committed
Add support for tiled CT logs
1 parent bf24abb commit 589a4ed

File tree

2 files changed

+443
-82
lines changed

2 files changed

+443
-82
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package certificatetransparency
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strconv"
10+
"strings"
11+
12+
ct "github.com/google/certificate-transparency-go"
13+
"golang.org/x/crypto/cryptobyte"
14+
)
15+
16+
const TileSize = 256
17+
18+
// TiledCheckpoint represents the checkpoint information from a tiled CT log
19+
type TiledCheckpoint struct {
20+
Origin string
21+
Size uint64
22+
Hash string
23+
}
24+
25+
// TileLeaf represents a single entry in a tile
26+
type TileLeaf struct {
27+
Timestamp uint64
28+
EntryType uint16
29+
X509Entry []byte // For X.509 certificates
30+
PrecertEntry []byte // For precertificates
31+
Chain [][]byte
32+
IssuerKeyHash [32]byte
33+
}
34+
35+
// EncodeTilePath encodes a tile index into the proper path format
36+
func EncodeTilePath(index uint64) string {
37+
if index == 0 {
38+
return "000"
39+
}
40+
41+
// Collect 3-digit groups
42+
var groups []uint64
43+
for n := index; n > 0; n /= 1000 {
44+
groups = append(groups, n%1000)
45+
}
46+
47+
// Build path from groups in reverse
48+
var b strings.Builder
49+
for i := len(groups) - 1; i >= 0; i-- {
50+
if i < len(groups)-1 {
51+
b.WriteByte('/')
52+
}
53+
if i > 0 {
54+
b.WriteByte('x')
55+
}
56+
fmt.Fprintf(&b, "%03d", groups[i])
57+
}
58+
59+
return b.String()
60+
}
61+
62+
// FetchCheckpoint fetches the checkpoint from a tiled CT log using the provided client
63+
func FetchCheckpoint(ctx context.Context, client *http.Client, baseURL string) (*TiledCheckpoint, error) {
64+
url := fmt.Sprintf("%s/checkpoint", baseURL)
65+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
66+
if err != nil {
67+
return nil, fmt.Errorf("creating request: %w", err)
68+
}
69+
req.Header.Set("User-Agent", userAgent)
70+
71+
resp, err := client.Do(req)
72+
if err != nil {
73+
return nil, fmt.Errorf("fetching checkpoint: %w", err)
74+
}
75+
defer resp.Body.Close()
76+
77+
if resp.StatusCode != http.StatusOK {
78+
return nil, fmt.Errorf("checkpoint request failed with status: %d", resp.StatusCode)
79+
}
80+
81+
scanner := bufio.NewScanner(resp.Body)
82+
lines := make([]string, 0, 3)
83+
for scanner.Scan() {
84+
lines = append(lines, scanner.Text())
85+
}
86+
87+
if err := scanner.Err(); err != nil {
88+
return nil, fmt.Errorf("reading checkpoint response: %w", err)
89+
}
90+
91+
if len(lines) < 3 {
92+
return nil, fmt.Errorf("invalid checkpoint format: expected at least 3 lines, got %d", len(lines))
93+
}
94+
95+
size, err := strconv.ParseUint(lines[1], 10, 64)
96+
if err != nil {
97+
return nil, fmt.Errorf("parsing tree size: %w", err)
98+
}
99+
100+
return &TiledCheckpoint{
101+
Origin: lines[0],
102+
Size: size,
103+
Hash: lines[2],
104+
}, nil
105+
}
106+
107+
// FetchTile fetches a tile from the tiled CT log using the provided client
108+
func FetchTile(ctx context.Context, client *http.Client, baseURL string, tileIndex uint64) ([]TileLeaf, error) {
109+
tilePath := EncodeTilePath(tileIndex)
110+
url := fmt.Sprintf("%s/tile/data/%s", baseURL, tilePath)
111+
112+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
113+
if err != nil {
114+
return nil, fmt.Errorf("creating request: %w", err)
115+
}
116+
req.Header.Set("User-Agent", userAgent)
117+
118+
resp, err := client.Do(req)
119+
if err != nil {
120+
return nil, fmt.Errorf("fetching tile %d: %w", tileIndex, err)
121+
}
122+
defer resp.Body.Close()
123+
124+
if resp.StatusCode != http.StatusOK {
125+
return nil, fmt.Errorf("tile request failed with status: %d", resp.StatusCode)
126+
}
127+
128+
data, err := io.ReadAll(resp.Body)
129+
if err != nil {
130+
return nil, fmt.Errorf("reading tile data: %w", err)
131+
}
132+
133+
return ParseTileData(data)
134+
}
135+
136+
// ParseTileData parses the binary tile data into TileLeaf entries using cryptobyte
137+
func ParseTileData(data []byte) ([]TileLeaf, error) {
138+
var leaves []TileLeaf
139+
s := cryptobyte.String(data)
140+
141+
for !s.Empty() {
142+
var leaf TileLeaf
143+
144+
if !s.ReadUint64(&leaf.Timestamp) || !s.ReadUint16(&leaf.EntryType) {
145+
return nil, fmt.Errorf("invalid data tile header")
146+
}
147+
148+
switch leaf.EntryType {
149+
case 0: // x509_entry
150+
var cert cryptobyte.String
151+
var extensions, fingerprints cryptobyte.String
152+
if !s.ReadUint24LengthPrefixed(&cert) ||
153+
!s.ReadUint16LengthPrefixed(&extensions) ||
154+
!s.ReadUint16LengthPrefixed(&fingerprints) {
155+
return nil, fmt.Errorf("invalid data tile x509_entry")
156+
}
157+
leaf.X509Entry = append([]byte(nil), cert...)
158+
for !fingerprints.Empty() {
159+
var fp [32]byte
160+
if !fingerprints.CopyBytes(fp[:]) {
161+
return nil, fmt.Errorf("invalid fingerprints: truncated")
162+
}
163+
leaf.Chain = append(leaf.Chain, fp[:])
164+
}
165+
166+
case 1: // precert_entry
167+
var issuerKeyHash [32]byte
168+
var defangedCrt, extensions, entry, fingerprints cryptobyte.String
169+
if !s.CopyBytes(issuerKeyHash[:]) ||
170+
!s.ReadUint24LengthPrefixed(&defangedCrt) ||
171+
!s.ReadUint16LengthPrefixed(&extensions) ||
172+
!s.ReadUint24LengthPrefixed(&entry) ||
173+
!s.ReadUint16LengthPrefixed(&fingerprints) {
174+
return nil, fmt.Errorf("invalid data tile precert_entry")
175+
}
176+
leaf.PrecertEntry = append([]byte(nil), defangedCrt...)
177+
leaf.IssuerKeyHash = issuerKeyHash
178+
for !fingerprints.Empty() {
179+
var fp [32]byte
180+
if !fingerprints.CopyBytes(fp[:]) {
181+
return nil, fmt.Errorf("invalid fingerprints: truncated")
182+
}
183+
leaf.Chain = append(leaf.Chain, fp[:])
184+
}
185+
186+
default:
187+
return nil, fmt.Errorf("unknown entry type: %d", leaf.EntryType)
188+
}
189+
190+
leaves = append(leaves, leaf)
191+
}
192+
return leaves, nil
193+
}
194+
195+
// ConvertTileLeafToRawLogEntry converts a TileLeaf to ct.RawLogEntry for compatibility
196+
func ConvertTileLeafToRawLogEntry(leaf TileLeaf, index uint64) *ct.RawLogEntry {
197+
rawEntry := &ct.RawLogEntry{
198+
Index: int64(index),
199+
Leaf: ct.MerkleTreeLeaf{
200+
Version: ct.V1,
201+
LeafType: ct.TimestampedEntryLeafType,
202+
},
203+
}
204+
205+
switch leaf.EntryType {
206+
case 0: // x509_entry
207+
// Use the DER certificate from X509Entry
208+
certData := leaf.X509Entry
209+
rawEntry.Leaf.TimestampedEntry = &ct.TimestampedEntry{
210+
Timestamp: leaf.Timestamp,
211+
EntryType: ct.X509LogEntryType,
212+
X509Entry: &ct.ASN1Cert{Data: certData},
213+
}
214+
rawEntry.Cert = ct.ASN1Cert{Data: certData}
215+
216+
case 1: // precert_entry
217+
// Build a minimal PreCert. TBSCertificate is the defanged TBS; IssuerKeyHash from tile.
218+
rawEntry.Leaf.TimestampedEntry = &ct.TimestampedEntry{
219+
Timestamp: leaf.Timestamp,
220+
EntryType: ct.PrecertLogEntryType,
221+
PrecertEntry: &ct.PreCert{
222+
IssuerKeyHash: leaf.IssuerKeyHash,
223+
TBSCertificate: leaf.PrecertEntry,
224+
},
225+
}
226+
227+
default:
228+
// Unknown type; leave as zero-value
229+
}
230+
231+
return rawEntry
232+
}
233+
234+
// CalculateCompleteTiles returns the number of complete tiles for a given tree size
235+
func CalculateCompleteTiles(treeSize uint64) uint64 {
236+
return treeSize / TileSize
237+
}
238+
239+
// CalculatePartialTileSize returns the size of the partial tile (if any)
240+
func CalculatePartialTileSize(treeSize uint64) uint64 {
241+
return treeSize % TileSize
242+
}

0 commit comments

Comments
 (0)