Skip to content

Commit 5ff73a0

Browse files
authored
Merge pull request #79 from mimi89999/tiled
Add support for tiled CT logs
2 parents 935159e + 9c59d27 commit 5ff73a0

2 files changed

Lines changed: 396 additions & 6 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
// If partialWidth > 0, fetches a partial tile with that width (1-255).
109+
func FetchTile(ctx context.Context, client *http.Client, baseURL string, tileIndex uint64, partialWidth uint64) ([]TileLeaf, error) {
110+
tilePath := EncodeTilePath(tileIndex)
111+
if partialWidth > 0 {
112+
tilePath = fmt.Sprintf("%s.p/%d", tilePath, partialWidth)
113+
}
114+
url := fmt.Sprintf("%s/tile/data/%s", baseURL, tilePath)
115+
116+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
117+
if err != nil {
118+
return nil, fmt.Errorf("creating request: %w", err)
119+
}
120+
req.Header.Set("User-Agent", userAgent)
121+
122+
resp, err := client.Do(req)
123+
if err != nil {
124+
return nil, fmt.Errorf("fetching tile %d: %w", tileIndex, err)
125+
}
126+
defer resp.Body.Close()
127+
128+
if resp.StatusCode != http.StatusOK {
129+
return nil, fmt.Errorf("tile request failed with status: %d", resp.StatusCode)
130+
}
131+
132+
data, err := io.ReadAll(resp.Body)
133+
if err != nil {
134+
return nil, fmt.Errorf("reading tile data: %w", err)
135+
}
136+
137+
return ParseTileData(data)
138+
}
139+
140+
// ParseTileData parses the binary tile data into TileLeaf entries using cryptobyte
141+
func ParseTileData(data []byte) ([]TileLeaf, error) {
142+
var leaves []TileLeaf
143+
s := cryptobyte.String(data)
144+
145+
for !s.Empty() {
146+
var leaf TileLeaf
147+
148+
if !s.ReadUint64(&leaf.Timestamp) || !s.ReadUint16(&leaf.EntryType) {
149+
return nil, fmt.Errorf("invalid data tile header")
150+
}
151+
152+
switch leaf.EntryType {
153+
case 0: // x509_entry
154+
var cert cryptobyte.String
155+
var extensions, fingerprints cryptobyte.String
156+
if !s.ReadUint24LengthPrefixed(&cert) ||
157+
!s.ReadUint16LengthPrefixed(&extensions) ||
158+
!s.ReadUint16LengthPrefixed(&fingerprints) {
159+
return nil, fmt.Errorf("invalid data tile x509_entry")
160+
}
161+
leaf.X509Entry = append([]byte(nil), cert...)
162+
for !fingerprints.Empty() {
163+
var fp [32]byte
164+
if !fingerprints.CopyBytes(fp[:]) {
165+
return nil, fmt.Errorf("invalid fingerprints: truncated")
166+
}
167+
leaf.Chain = append(leaf.Chain, fp[:])
168+
}
169+
170+
case 1: // precert_entry
171+
var issuerKeyHash [32]byte
172+
var defangedCrt, extensions, entry, fingerprints cryptobyte.String
173+
if !s.CopyBytes(issuerKeyHash[:]) ||
174+
!s.ReadUint24LengthPrefixed(&defangedCrt) ||
175+
!s.ReadUint16LengthPrefixed(&extensions) ||
176+
!s.ReadUint24LengthPrefixed(&entry) ||
177+
!s.ReadUint16LengthPrefixed(&fingerprints) {
178+
return nil, fmt.Errorf("invalid data tile precert_entry")
179+
}
180+
leaf.PrecertEntry = append([]byte(nil), defangedCrt...)
181+
leaf.IssuerKeyHash = issuerKeyHash
182+
for !fingerprints.Empty() {
183+
var fp [32]byte
184+
if !fingerprints.CopyBytes(fp[:]) {
185+
return nil, fmt.Errorf("invalid fingerprints: truncated")
186+
}
187+
leaf.Chain = append(leaf.Chain, fp[:])
188+
}
189+
190+
default:
191+
return nil, fmt.Errorf("unknown entry type: %d", leaf.EntryType)
192+
}
193+
194+
leaves = append(leaves, leaf)
195+
}
196+
return leaves, nil
197+
}
198+
199+
// ConvertTileLeafToRawLogEntry converts a TileLeaf to ct.RawLogEntry for compatibility
200+
func ConvertTileLeafToRawLogEntry(leaf TileLeaf, index uint64) *ct.RawLogEntry {
201+
rawEntry := &ct.RawLogEntry{
202+
Index: int64(index),
203+
Leaf: ct.MerkleTreeLeaf{
204+
Version: ct.V1,
205+
LeafType: ct.TimestampedEntryLeafType,
206+
},
207+
}
208+
209+
switch leaf.EntryType {
210+
case 0: // x509_entry
211+
// Use the DER certificate from X509Entry
212+
certData := leaf.X509Entry
213+
rawEntry.Leaf.TimestampedEntry = &ct.TimestampedEntry{
214+
Timestamp: leaf.Timestamp,
215+
EntryType: ct.X509LogEntryType,
216+
X509Entry: &ct.ASN1Cert{Data: certData},
217+
}
218+
rawEntry.Cert = ct.ASN1Cert{Data: certData}
219+
220+
case 1: // precert_entry
221+
// Build a minimal PreCert. TBSCertificate is the defanged TBS; IssuerKeyHash from tile.
222+
rawEntry.Leaf.TimestampedEntry = &ct.TimestampedEntry{
223+
Timestamp: leaf.Timestamp,
224+
EntryType: ct.PrecertLogEntryType,
225+
PrecertEntry: &ct.PreCert{
226+
IssuerKeyHash: leaf.IssuerKeyHash,
227+
TBSCertificate: leaf.PrecertEntry,
228+
},
229+
}
230+
231+
default:
232+
// Unknown type; leave as zero-value
233+
}
234+
235+
return rawEntry
236+
}

0 commit comments

Comments
 (0)