Skip to content

Commit 69dc15d

Browse files
committed
sigstore ui
1 parent efb79da commit 69dc15d

File tree

9 files changed

+820
-35
lines changed

9 files changed

+820
-35
lines changed

ui/README.md

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
# Certificate Transparency Monitor
1+
# Transparency Search
22

3-
A web application for searching and monitoring SSL/TLS certificates from Certificate Transparency logs, similar to crt.sh. Built with Next.js and ClickHouse.
3+
A web application for searching and monitoring transparency data from Certificate Transparency logs and Sigstore Rekor logs. Built with Next.js and ClickHouse.
44

55
## Features
66

7+
### Certificate Transparency
78
- **Multi-type Search**: Search certificates by domain name, common name, serial number, SHA-256 fingerprint, or issuer
89
- **Detailed Certificate View**: View complete certificate information including subject, issuer, validity periods, and extensions
910
- **Subject Alternative Names**: Display all SAN entries for certificates
1011
- **Certificate Transparency Logs**: Track which CT log each certificate was found in
11-
- **Responsive Design**: Works on desktop and mobile devices with dark mode support
12+
13+
### Sigstore Search
14+
- **Data Hash Search**: Find entries by SHA-256 hash of signed artifacts
15+
- **Email Search**: Search by email addresses in both PGP and X.509 certificates
16+
- **X.509 Certificate Search**: Search by common name, Subject Alternative Names (SANs), or serial number
17+
- **PGP Signature Search**: Search by PGP key fingerprint or signer email
18+
- **Artifact URL Search**: Find entries by the URL of signed artifacts
19+
- **Detailed Entry View**: View complete Sigstore entry information including signature details, certificates, and metadata
20+
21+
### General
22+
- **Sidebar Navigation**: Easy switching between Certificate Transparency and Sigstore search
23+
- **Responsive Design**: Works on desktop and mobile devices
24+
- **Real-time Search**: Fast search with loading states and error handling
1225

1326
## Tech Stack
1427

@@ -20,7 +33,7 @@ A web application for searching and monitoring SSL/TLS certificates from Certifi
2033
## Prerequisites
2134

2235
- Node.js 18+
23-
- ClickHouse database with CT log data
36+
- ClickHouse database with CT log data and Sigstore Rekor data
2437
- Environment variables for ClickHouse connection
2538

2639
## Environment Variables
@@ -51,26 +64,48 @@ npm run dev
5164

5265
## Database Schema
5366

54-
The application expects a ClickHouse table named `ct_log_entries` with the schema defined in `/schema.sql`. Key fields include:
67+
The application expects two ClickHouse tables defined in `/schema.sql`:
5568

69+
### Certificate Transparency (`ct_log_entries`)
5670
- Certificate identifiers (SHA-256, serial number)
5771
- Subject and issuer information
5872
- Validity periods
5973
- Subject Alternative Names
6074
- Certificate extensions and key usage
6175
- CT log metadata
6276

77+
### Sigstore Rekor (`rekor_log_entries`)
78+
- Entry metadata (UUID, tree ID, log index)
79+
- Signature format and data hashes
80+
- X.509 certificate information
81+
- PGP signature details
82+
- Artifact URLs and references
83+
6384
## API Endpoints
6485

65-
### Get Certificate Details
86+
### Certificate Transparency
6687
```
6788
GET /api/certificate/{sha256}
6889
```
69-
7090
Returns complete certificate information for a given SHA-256 fingerprint.
7191

92+
### Sigstore Search
93+
```
94+
GET /api/sigstore/search?query={query}&type={type}&limit={limit}
95+
```
96+
Searches Sigstore entries by various criteria. Supported types:
97+
- `hash` - Data hash (SHA-256)
98+
- `email` - Email addresses in PGP or X.509 certificates
99+
- `x509_cn` - X.509 Common Name
100+
- `x509_san` - X.509 Subject Alternative Names
101+
- `x509_serial` - X.509 Serial Number
102+
- `pgp_fingerprint` - PGP Key Fingerprint
103+
- `pgp_email` - PGP Signer Email
104+
- `data_url` - Artifact URL
105+
72106
## Search Types
73107

108+
### Certificate Transparency Search
74109
1. **Domain/SAN**: Search by domain name or Subject Alternative Name
75110
- Example: `example.com`, `*.example.com`
76111

@@ -86,26 +121,58 @@ Returns complete certificate information for a given SHA-256 fingerprint.
86121
5. **Issuer**: Search by certificate issuer Common Name
87122
- Example: `Let's Encrypt Authority X3`
88123

124+
### Sigstore Search
125+
1. **Data Hash**: Search by SHA-256 hash of signed artifacts
126+
- Example: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
127+
128+
2. **Any Email**: Search by email addresses in both PGP and X.509 certificates
129+
- Example: `user@example.com`
130+
131+
3. **X.509 Common Name**: Search by X.509 certificate Common Name
132+
- Example: `github.com`
133+
134+
4. **X.509 SAN**: Search by X.509 Subject Alternative Names
135+
- Example: `example.com`
136+
137+
5. **X.509 Serial**: Search by X.509 certificate serial number
138+
- Example: `123456789abcdef`
139+
140+
6. **PGP Fingerprint**: Search by PGP key fingerprint
141+
- Example: `ABCD1234EFGH5678IJKL9012MNOP3456QRST7890`
142+
143+
7. **PGP Email**: Search by PGP signer email address
144+
- Example: `signer@example.com`
145+
146+
8. **Artifact URL**: Search by the URL of signed artifacts
147+
- Example: `https://github.com/owner/repo/releases`
148+
89149
## Project Structure
90150

91151
```
92152
ui/
93153
├── app/
94154
│ ├── api/
95-
│ │ ├── search/route.ts # Search API endpoint
96-
│ │ └── certificate/[sha256]/route.ts # Certificate detail API
97-
│ ├── certificate/[sha256]/page.tsx # Certificate detail page
98-
│ ├── layout.tsx # Root layout
99-
│ ├── page.tsx # Main search page
100-
│ └── globals.css # Global styles
155+
│ │ ├── certificate/[sha256]/route.ts # Certificate detail API
156+
│ │ └── sigstore/search/route.ts # Sigstore search API
157+
│ ├── certificate/[sha256]/page.tsx # Certificate detail page
158+
│ ├── sigstore/ # Sigstore pages
159+
│ │ ├── page.tsx # Main Sigstore search page
160+
│ │ └── search/[query]/page.tsx # Sigstore search results
161+
│ ├── search/[domain]/page.tsx # CT search results
162+
│ ├── layout.tsx # Root layout with sidebar
163+
│ ├── page.tsx # Main CT search page
164+
│ └── globals.css # Global styles
101165
├── components/
102-
│ ├── search-form.tsx # Search interface
103-
│ └── certificate-list.tsx # Certificate results list
166+
│ ├── search-form.tsx # CT search interface
167+
│ ├── sigstore-search-form.tsx # Sigstore search interface
168+
│ ├── sigstore-results.tsx # Sigstore results display
169+
│ └── certificate-list.tsx # Certificate results list
104170
├── lib/
105-
│ └── clickhouse.ts # ClickHouse client configuration
171+
│ └── clickhouse.ts # ClickHouse client configuration
106172
├── types/
107-
│ └── certificate.ts # TypeScript interfaces
108-
└── public/ # Static assets
173+
│ ├── certificate.ts # CT TypeScript interfaces
174+
│ └── sigstore.ts # Sigstore TypeScript interfaces
175+
└── public/ # Static assets
109176
```
110177

111178
## Development
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import client from "@/lib/clickhouse";
3+
import { SigstoreEntry, SigstoreSearchQuery } from "@/types/sigstore";
4+
5+
export async function GET(request: NextRequest) {
6+
const searchParams = request.nextUrl.searchParams;
7+
const query = searchParams.get("query");
8+
const queryType = searchParams.get("type") as SigstoreSearchQuery["queryType"];
9+
const limit = parseInt(searchParams.get("limit") || "100", 10);
10+
11+
if (!query || !queryType) {
12+
return NextResponse.json(
13+
{ error: "Query and type parameters are required" },
14+
{ status: 400 }
15+
);
16+
}
17+
18+
try {
19+
let whereClause = "";
20+
const queryParams: Record<string, string> = {};
21+
22+
switch (queryType) {
23+
case "hash":
24+
whereClause = "data_hash_value = {query:String}";
25+
queryParams.query = query;
26+
break;
27+
case "email":
28+
whereClause = "(pgp_signer_email ILIKE {queryPattern:String} OR x509_sans HAS {query:String})";
29+
queryParams.query = query;
30+
queryParams.queryPattern = `%${query}%`;
31+
break;
32+
case "x509_cn":
33+
whereClause = "x509_subject_cn ILIKE {queryPattern:String}";
34+
queryParams.queryPattern = `%${query}%`;
35+
break;
36+
case "x509_san":
37+
whereClause = "has(x509_sans, {query:String})";
38+
queryParams.query = query;
39+
break;
40+
case "x509_serial":
41+
whereClause = "x509_serial_number = {query:String}";
42+
queryParams.query = query;
43+
break;
44+
case "pgp_fingerprint":
45+
whereClause = "pgp_public_key_fingerprint = {query:String}";
46+
queryParams.query = query.toUpperCase().replace(/\s/g, "");
47+
break;
48+
case "pgp_email":
49+
whereClause = "pgp_signer_email ILIKE {queryPattern:String}";
50+
queryParams.queryPattern = `%${query}%`;
51+
break;
52+
case "data_url":
53+
whereClause = "data_url ILIKE {queryPattern:String}";
54+
queryParams.queryPattern = `%${query}%`;
55+
break;
56+
default:
57+
return NextResponse.json(
58+
{ error: "Invalid query type" },
59+
{ status: 400 }
60+
);
61+
}
62+
63+
const sql = `
64+
SELECT
65+
tree_id,
66+
log_index,
67+
entry_uuid,
68+
retrieval_timestamp,
69+
integrated_time,
70+
log_id,
71+
kind,
72+
api_version,
73+
signature_format,
74+
data_hash_algorithm,
75+
data_hash_value,
76+
data_url,
77+
signature_url,
78+
public_key_url,
79+
x509_certificate_sha256,
80+
x509_subject_dn,
81+
x509_subject_cn,
82+
x509_subject_organization,
83+
x509_subject_ou,
84+
x509_issuer_dn,
85+
x509_issuer_cn,
86+
x509_issuer_organization,
87+
x509_issuer_ou,
88+
x509_serial_number,
89+
x509_not_before,
90+
x509_not_after,
91+
x509_sans,
92+
x509_signature_algorithm,
93+
x509_public_key_algorithm,
94+
x509_public_key_size,
95+
x509_is_ca,
96+
x509_key_usage,
97+
x509_extended_key_usage,
98+
pgp_signature_hash,
99+
pgp_public_key_fingerprint,
100+
pgp_key_id,
101+
pgp_signer_user_id,
102+
pgp_signer_email,
103+
pgp_signer_name,
104+
pgp_key_algorithm,
105+
pgp_key_size,
106+
pgp_subkey_fingerprints
107+
FROM rekor_log_entries
108+
WHERE ${whereClause}
109+
ORDER BY integrated_time DESC
110+
LIMIT {limit:UInt32}
111+
SETTINGS max_execution_time = 30, max_threads = 1, max_memory_usage = 268435456
112+
`;
113+
114+
const resultSet = await client.query({
115+
query: sql,
116+
query_params: {
117+
...queryParams,
118+
limit: limit,
119+
},
120+
format: "JSONEachRow",
121+
});
122+
123+
const entries = await resultSet.json<SigstoreEntry>();
124+
125+
// Extract statistics from ClickHouse response headers
126+
const headers = resultSet.response_headers;
127+
const summaryHeader = headers["x-clickhouse-summary"];
128+
const summaryValue = Array.isArray(summaryHeader)
129+
? summaryHeader[0]
130+
: summaryHeader;
131+
132+
let statistics = undefined;
133+
134+
if (summaryValue) {
135+
try {
136+
const summary = JSON.parse(summaryValue);
137+
statistics = {
138+
elapsed: parseFloat(summary.elapsed_ns || "0") / 1000000000, // Convert nanoseconds to seconds
139+
rows_read: parseInt(summary.read_rows || "0"),
140+
bytes_read: parseInt(summary.read_bytes || "0"),
141+
};
142+
} catch (error) {
143+
console.error("Failed to parse ClickHouse summary:", error);
144+
}
145+
}
146+
147+
return NextResponse.json({
148+
entries,
149+
count: entries.length,
150+
query,
151+
queryType,
152+
limit,
153+
statistics,
154+
});
155+
} catch (error) {
156+
console.error("Sigstore search error:", error);
157+
return NextResponse.json(
158+
{ error: "Internal server error" },
159+
{ status: 500 }
160+
);
161+
}
162+
}

ui/app/layout.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
3+
import Link from "next/link";
34
import "./globals.css";
45

56
const geistSans = Geist({
@@ -13,9 +14,9 @@ const geistMono = Geist_Mono({
1314
});
1415

1516
export const metadata: Metadata = {
16-
title: "Certificate Search",
17+
title: "Transparency Search",
1718
description:
18-
"Search SSL/TLS certificates from Certificate Transparency logs",
19+
"Search SSL/TLS certificates and Sigstore entries from transparency logs",
1920
};
2021

2122
export default function RootLayout({
@@ -28,7 +29,36 @@ export default function RootLayout({
2829
<body
2930
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3031
>
31-
{children}
32+
<div className="flex h-screen">
33+
<nav className="w-64 bg-gray-50 border-r border-gray-200 p-6">
34+
<div className="mb-8">
35+
<h1 className="text-xl font-semibold text-gray-900">
36+
Transparency Search
37+
</h1>
38+
</div>
39+
<ul className="space-y-2">
40+
<li>
41+
<Link
42+
href="/"
43+
className="block px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
44+
>
45+
Certificate Transparency
46+
</Link>
47+
</li>
48+
<li>
49+
<Link
50+
href="/sigstore"
51+
className="block px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
52+
>
53+
Sigstore
54+
</Link>
55+
</li>
56+
</ul>
57+
</nav>
58+
<main className="flex-1 overflow-auto">
59+
{children}
60+
</main>
61+
</div>
3262
</body>
3363
</html>
3464
);

0 commit comments

Comments
 (0)