Certificate Management
When you visit a website over HTTPS, your browser checks that the website’s certificate was signed by a trusted authority — like a digital passport stamp that proves the site is who it claims to be. For Ghost to intercept and read HTTPS traffic (so you can inspect it), Ghost needs to act as that trusted authority. It does this by running its own Certificate Authority (CA) — essentially a private “passport office” that issues certificates on-the-fly for every website you visit. Your device trusts Ghost’s CA, Ghost creates a certificate that says “I am example.com,” your browser accepts it, and Ghost can decrypt the traffic to show you what’s happening.
This page covers the entire certificate lifecycle: how Ghost creates its CA, how it generates per-host certificates in real time, how it caches them for performance, how it installs the CA into your operating system’s trust store, and how it delivers certificates to mobile devices.
Certificate Authority
Section titled “Certificate Authority”The Certificate Authority is Ghost’s root of trust. Every certificate Ghost generates is signed by this CA. If a device trusts the CA, it trusts all certificates the CA signs — which means Ghost can create a valid-looking certificate for any website.
Generation
Section titled “Generation”When Ghost starts for the first time, it creates a root CA and saves it to disk. On every subsequent start, it loads the existing CA from disk instead of creating a new one — so you only need to trust the certificate once.
| Property | Value | Why |
|---|---|---|
| Algorithm | ECDSA P-256 | Modern elliptic curve cryptography — fast to sign, small keys, widely supported. P-256 (also called secp256r1) is the most common curve, accepted by all browsers and operating systems. |
| Serial Number | 128-bit cryptographically random | RFC 5280 requires every certificate to have a unique serial number (up to 20 bytes). Ghost generates a random 128-bit integer using the operating system’s secure random number generator (crypto/rand). |
| Subject | O=Ghost Proxy, CN=Ghost Root CA | The “name” on the certificate. O (Organization) identifies Ghost, CN (Common Name) identifies this specific certificate. You’ll see “Ghost Root CA” in your keychain or certificate manager. |
| Validity | 10 years, backdated 1 hour | The certificate is valid from “1 hour ago” until “10 years from now.” The 1-hour backdating handles clock skew — if a device’s clock is slightly behind, it won’t reject the certificate as “not yet valid.” |
| Key Usage | CertSign + CRLSign | This certificate can only sign other certificates and Certificate Revocation Lists. It cannot be used as a server certificate itself — it’s purely an authority. |
| Basic Constraints | CA=true, MaxPathLen=0 | CA=true declares this is a Certificate Authority. MaxPathLen=0 means it cannot create intermediate CAs — it can only sign leaf (end-entity) certificates directly. This limits the blast radius if the CA key is compromised. |
The CA is self-signed — it signs itself, which is normal for root CAs. (Root CAs are trusted because you explicitly install them, not because someone else vouched for them.)
File Storage
Section titled “File Storage”The CA is stored at ~/.ghost/ (which is $HOME/.ghost/ on macOS/Linux, %USERPROFILE%\.ghost\ on Windows):
| File | Permissions | Contents |
|---|---|---|
ca.crt | 0644 (world-readable) | The public certificate in PEM format. This is what gets distributed to devices — it contains no secrets. |
ca.key | 0600 (owner-only) | The private key in PEM format (block type EC PRIVATE KEY). This is the secret that lets Ghost sign certificates. If someone else gets this file, they could create certificates that your devices would trust. |
If writing the certificate file fails after the key has already been written, Ghost cleans up by deleting the key file — preventing a state where a key exists without its matching certificate.
Loading and Validation
Section titled “Loading and Validation”On subsequent starts, LoadOrCreateCA() validates the existing CA before using it:
- PEM decode — reads both files and decodes the PEM blocks (the
-----BEGIN CERTIFICATE-----wrapper format) - Parse certificate — converts the binary certificate data (DER format) into a structured object
- Verify CA flag — checks that
IsCAistrue. If someone replaced the CA file with a regular certificate, this catches it. - Check expiry — verifies the current time falls between
NotBeforeandNotAfter. An expired CA would produce leaf certificates that browsers reject. - Parse private key — decodes and validates the ECDSA private key
- Key-certificate match — verifies the private key’s public component matches the certificate’s public key. This catches situations where files from different CAs got mixed up.
If validation fails because the files don’t exist, Ghost generates a new CA. If the files exist but are invalid (corrupted, expired, mismatched), Ghost returns an error rather than silently regenerating — because regenerating would invalidate all previously trusted installations, requiring users to re-trust the new CA on every device.
Per-Host Leaf Certificates
Section titled “Per-Host Leaf Certificates”When your browser connects to example.com through Ghost, Ghost needs a certificate that says “I am example.com.” These are called leaf certificates (or end-entity certificates) — they’re the “leaves” at the end of the certificate chain, as opposed to the CA “root.”
Ghost generates these certificates on-the-fly as new hosts are encountered, caches them for reuse, and uses a single-flight mechanism to avoid duplicate work.
What this diagram shows — the certificate lookup flow:
- A TLS handshake begins — the browser is trying to establish a secure connection to a host through Ghost’s proxy
- Ghost’s
CertIssuerfirst checks its LRU cache (a bounded dictionary that evicts the least-recently-used entry when full). If a valid, unexpired certificate exists for this host, it’s returned immediately — no generation needed - If the cache doesn’t have it (cache miss), Ghost checks if another connection is already generating a certificate for the same host (the “inflight map”). If yes, it waits for that generation to finish and uses the same result — this prevents 50 simultaneous connections to
api.example.comfrom generating 50 identical certificates - If nobody is generating one yet, Ghost creates a new leaf certificate, signs it with the CA, attaches an OCSP response, stores it in the cache, and returns it
- The entire process happens during the TLS handshake — the browser is waiting, so speed matters
Host Normalization
Section titled “Host Normalization”Before any lookup, the host is normalized:
- Port is stripped (
example.com:443becomesexample.com) - Converted to lowercase (
Example.COMbecomesexample.com) - Whitespace is trimmed
This ensures Example.COM:443 and example.com both resolve to the same cached certificate.
Leaf Certificate Properties
Section titled “Leaf Certificate Properties”| Property | Value | Why |
|---|---|---|
| Algorithm | ECDSA P-256 (new key per host) | Each leaf certificate gets its own unique private key. Even if one key is compromised, others remain safe. |
| Serial Number | 128-bit cryptographically random | Same generation method as the CA serial — unique per certificate. |
| Subject | CN=<host>, O=Ghost Proxy | The certificate identifies itself as the target host. Browsers check this matches the URL they’re connecting to. |
| Validity | 24 hours, backdated 1 hour | Short-lived by design. If a leaf certificate somehow leaks, it expires quickly. The 1-hour backdate handles clock skew. |
| Key Usage | DigitalSignature only | This certificate can only prove identity — it cannot sign other certificates. |
| Extended Key Usage | ServerAuth only | Restricted to TLS server authentication — can’t be used for email signing, code signing, or other purposes. |
SAN (Subject Alternative Name) Logic
Section titled “SAN (Subject Alternative Name) Logic”SANs are the modern way browsers verify certificate identity. Ghost sets SANs based on whether the host is a domain name or an IP address:
IP addresses (e.g., 192.168.1.1):
- Sets
IPAddressesto the parsed IP — browsers match this against the connection IP
Domain names (e.g., api.example.com):
- Sets
DNSNamesto include the exact host:["api.example.com"] - Wildcard logic: if the host is a subdomain (has at least two dots), Ghost also adds a wildcard SAN for the parent domain. The logic splits on the first dot —
api.example.comsplits intoapiandexample.com. If the remainder (example.com) itself contains a dot, a wildcard is added:*.example.com. This means the same certificate works forapi.example.com,cdn.example.com, or any other subdomain. - Top-level domains don’t get wildcards:
example.comsplits intoexampleandcom. Sincecomdoesn’t contain a dot, no wildcard is added. This prevents creating*.comcertificates (which would be both useless and dangerous).
Examples:
| Host | DNSNames in Certificate |
|---|---|
api.example.com | api.example.com, *.example.com |
cdn.shop.example.com | cdn.shop.example.com, *.shop.example.com |
example.com | example.com (no wildcard) |
localhost | localhost (no wildcard) |
192.168.1.1 | (IP SAN, not DNS) |
OCSP Stapling
Section titled “OCSP Stapling”OCSP (Online Certificate Status Protocol) is a mechanism for checking whether a certificate has been revoked (declared invalid before its expiry). Normally, a browser would contact the CA’s OCSP server to ask “is this certificate still valid?” Ghost doesn’t run an OCSP server, so instead it staples (attaches) a pre-built OCSP response directly to every leaf certificate.
The stapled OCSP response says:
| Field | Value | Meaning |
|---|---|---|
| Status | Good | ”This certificate is valid and has not been revoked” |
| SerialNumber | The leaf cert’s serial | Identifies which certificate this response is about |
| ThisUpdate | now - 1 hour | ”This response has been valid since 1 hour ago” (clock skew protection) |
| NextUpdate | now + 24 hours | ”Check again after 24 hours” (matches the leaf cert’s 24-hour validity) |
| Signed by | Ghost’s CA | The CA acts as its own OCSP responder — both the issuer certificate and responder certificate are the CA |
Why this matters: Without OCSP stapling, Windows (specifically its TLS library called “schannel”) reports “revocation status unknown” errors. Some browsers and applications block navigation entirely when they can’t verify a certificate’s revocation status. The stapled “good” response prevents all of these issues — the browser gets its answer without needing to contact an external server.
Certificate Chain
Section titled “Certificate Chain”The returned TLS certificate includes a chain of two certificates:
- Leaf certificate — the just-generated certificate for the specific host
- CA certificate — Ghost’s root CA
This chain lets the browser verify: “this leaf cert was signed by Ghost’s CA, and I trust Ghost’s CA (because the user installed it).”
Caching
Section titled “Caching”Ghost uses an LRU cache (Least Recently Used) from the hashicorp/golang-lru/v2 library to store generated certificates:
| Setting | Default | Description |
|---|---|---|
| Cache capacity | 10,000 certificates | Configurable via cert_cache_size in ~/.ghost/config.toml. The LRU eviction policy means the least-recently-accessed certificate gets removed when the cache is full. |
| Expiry check | Every access | Before returning a cached certificate, Ghost checks time.Now().Before(expiresAt). Expired certs are removed from the cache and regenerated. |
| Cache stats | CacheStats() method | Returns current cache size and capacity — exposed through the API for monitoring. |
With 10,000 capacity, Ghost can cache certificates for 10,000 different hosts simultaneously. For typical web browsing and testing, this is far more than enough — you’d need to visit 10,000 unique domains in a 24-hour window to cause evictions.
Single-Flight Deduplication
Section titled “Single-Flight Deduplication”When a popular website sends multiple parallel requests through Ghost (which is normal — browsers open 6+ connections per domain), they all need a certificate for the same host. Without deduplication, Ghost would generate the same certificate multiple times simultaneously — wasting CPU and time.
The single-flight mechanism uses a certFuture struct:
certFuture { done chan struct{} // Closed when generation finishes cert *tls.Certificate err error}How it works:
- First goroutine to request
api.example.comacquires a mutex lock, checks the cache (double-check under lock), creates acertFuture, stores it in theinflightmap, and releases the lock - It then generates the certificate outside the lock — generation involves cryptographic operations that take time, and holding the lock would block all other certificate requests
- Second, third, fourth goroutines requesting
api.example.comacquire the lock, see the existingcertFuturein the inflight map, release the lock, and wait on thedonechannel — they block until generation finishes - When the first goroutine finishes, it sets
future.cert(orfuture.errif something went wrong), closes thedonechannel (which unblocks all waiting goroutines), re-acquires the lock, removes the inflight entry, and adds the certificate to the LRU cache - All waiting goroutines receive the same certificate
This means even with 100 simultaneous connections to the same host, only one certificate is generated.
Trust Store Installation
Section titled “Trust Store Installation”For your browser to accept Ghost’s certificates, your operating system must trust Ghost’s CA. Ghost handles this differently on each platform.
Command:
security add-trusted-cert -p ssl -r trustRoot \ -k ~/Library/Keychains/login.keychain-db ~/.ghost/ca.crt| Detail | Value |
|---|---|
| Keychain | Login Keychain (~/Library/Keychains/login.keychain-db) — user-level, not system-level |
| Admin required | No — but macOS will prompt for your keychain password |
| Trust scope | SSL only (-p ssl) — the CA is trusted for HTTPS but not for email signing, code signing, etc. |
| Trust level | Root (-r trustRoot) — trusted as a root CA |
| Install timeout | 60 seconds — the password dialog may take time |
Verification (two-step):
- Certificate presence:
security find-certificate -c "Ghost Root CA" -a <keychain>(10-second timeout)- If output contains
"could not be found"or"SecKeychainSearchCopyNext"— not installed - If output does not contain
"Ghost Root CA"— not installed
- If output contains
- Trust settings:
security dump-trust-settings(10-second timeout)- Looks for
"Ghost Root CA"in trust settings - If found but
"Number of trust settings : 0"— installed but not trusted (user needs to manually enable trust) - If
dump-trust-settingscommand fails entirely — assumes installed (conservative fallback)
- Looks for
Both checks must pass for Ghost to report the certificate as installed and trusted.
Windows
Section titled “Windows”Command:
certutil -addstore -user Root ca.crt| Detail | Value |
|---|---|
| Store | User Root Store (-user Root) — per-user, not machine-wide |
| Admin required | No — the -user flag installs to the current user’s store, no UAC elevation prompt |
| Timeout | 10 seconds |
Verification: certutil -verifystore -user Root (10-second timeout). Ghost scans the output for "Ghost Root CA". Non-zero exit codes are tolerated because verifystore may return errors for unrelated certificates in the store.
Linux certificate trust varies by distribution. Ghost auto-detects the distribution by reading /etc/os-release and checking the ID and ID_LIKE fields:
| Distro Family | Detected IDs | Certificate Path | Update Command | Requires |
|---|---|---|---|---|
| Debian/Ubuntu | debian, ubuntu, linuxmint, pop (also derivatives via ID_LIKE) | /usr/local/share/ca-certificates/ghost-ca.crt | update-ca-certificates | sudo |
| Fedora/RHEL | fedora, rhel, centos, rocky, alma | /etc/pki/ca-trust/source/anchors/ghost-ca.crt | update-ca-trust | sudo |
| Arch | arch, manjaro, endeavouros | /etc/ca-certificates/trust-source/anchors/ghost-ca.crt | trust anchor --store (preferred), fallback to trust extract-compat or update-ca-trust | sudo |
| SUSE | opensuse, sles | /etc/pki/trust/anchors/ghost-ca.crt | update-ca-certificates | sudo |
| Unknown | Anything else | Tries Debian path first, then Fedora path. If both fail, returns an error with manual instructions. | — | sudo |
All Linux commands have a 30-second timeout. The certificate file is copied with 0644 permissions, and parent directories are created with 0755 if they don’t exist.
Verification: Ghost checks if the certificate file exists at the expected path for the detected distribution. For unknown distributions, it checks all four paths.
Unsupported Platforms
Section titled “Unsupported Platforms”On platforms other than macOS, Windows, and Linux (e.g., FreeBSD), all certificate operations return ErrUnsupported. Users must manually install the CA certificate.
Mobile Certificate Delivery
Section titled “Mobile Certificate Delivery”Mobile devices can’t install certificates through command-line tools — they need to download and install them through the browser or device settings. Ghost provides several delivery mechanisms, all served through the proxy port itself (no API authentication required, since the device may not have Ghost’s API credentials).
Download Endpoints
Section titled “Download Endpoints”| URL | Content-Type | Content-Disposition | Purpose |
|---|---|---|---|
http://<proxy-address>/ghost-ca.crt | application/x-x509-ca-cert | attachment; filename="ghost-ca.crt" | Direct CA certificate download. Works on any host — the path /ghost-ca.crt is intercepted regardless of the Host header. |
http://<proxy-address>/ghost-profile.mobileconfig | application/x-apple-aspen-config | attachment; filename="ghost-proxy.mobileconfig" | iOS Configuration Profile that installs and trusts the CA in one step. |
http://ghost.proxy/ | text/html | — | Branded landing page with download links and setup instructions. |
http://ghost.proxy/verify | application/json | — | Returns {"connected": true, "client_ip": "<ip>"} with CORS headers — used to verify a device can reach Ghost. |
All download endpoints set Cache-Control: no-store to prevent caching (since the CA could be regenerated).
ghost.proxy Landing Page
Section titled “ghost.proxy Landing Page”When a device navigates to http://ghost.proxy/, Ghost serves a branded HTML page with:
- Ghost branding — gradient-styled header with “Traffic Intelligence Proxy” subtitle
- Download button — links to
/ghost-ca.crtfor direct CA download - Info panel — shows the proxy address, certificate URL, host IP, and port
- iOS Quick Setup — 4-step guide: (1) download certificate, (2) allow profile download, (3) install profile in Settings, (4) enable full trust in Certificate Trust Settings
- Android Quick Setup — 3-step guide: (1) download certificate, (2) open Security settings, (3) select the downloaded file
This page works without DNS because Ghost intercepts requests where the Host header is ghost.proxy (or ghost.proxy:<port>) directly in the proxy — no DNS resolution is needed.
The landing page also reverse-proxies two endpoints for injected scripts: /api/v1/script/fetch and /api/v1/script/analyze are forwarded to Ghost’s API server. This avoids mixed-content issues when scripts injected into HTTPS pages need to communicate with Ghost.
iOS Configuration Profile
Section titled “iOS Configuration Profile”The .mobileconfig file is an Apple Configuration Profile — an XML document that iOS understands natively. When a user opens this file on an iOS device, iOS presents a system dialog to install the profile.
Profile structure:
| Field | Value |
|---|---|
| PayloadType | Configuration (top-level container) |
| PayloadIdentifier | com.ghost.proxy.profile |
| PayloadOrganization | Ghost |
| PayloadRemovalDisallowed | false — users can remove the profile at any time |
| PayloadVersion | 1 |
| PayloadUUID | Random v4 UUID (generated fresh each time) |
The profile contains a single payload:
| Field | Value |
|---|---|
| PayloadType | com.apple.security.root — installs a root certificate |
| PayloadIdentifier | com.ghost.proxy.cert |
| PayloadCertificateFileName | ghost-ca.crt |
| PayloadContent | Base64-encoded DER of the CA certificate |
| PayloadDisplayName | Ghost CA Certificate |
| PayloadDescription | ”Installs the Ghost proxy CA certificate for HTTPS inspection.” |
| PayloadUUID | Random v4 UUID (different from the top-level UUID) |
Consent text (shown to the user before installation):
“This profile installs the Ghost CA certificate for HTTPS inspection. You will also need to configure the HTTP proxy in your Wi-Fi settings. Remove this profile when you no longer need traffic capture.”
The profile does not include proxy settings (com.apple.proxy.http.global). This would require a supervised (MDM-managed) device, which most devices aren’t. Users configure the proxy manually in Wi-Fi settings.
Two random v4 UUIDs are generated for each download — one for the profile container and one for the certificate payload. These are different every time, which means downloading the profile twice creates two distinct profiles (though iOS will recognize they have the same identifier and offer to replace the existing one).
The .mobileconfig endpoint is also available through the API at GET /api/v1/mobile/profile — this is the same template, served through the authenticated API for Ghost’s UI to use.
Simulator Certificate Installation
Section titled “Simulator Certificate Installation”For iOS Simulators (Xcode’s virtual iPhones/iPads), Ghost can install the CA certificate directly without user interaction:
Command:
xcrun simctl keychain <UDID> add-root-cert <temp-file>| Detail | Value |
|---|---|
| Endpoint | POST /api/v1/mobile/simulators/install-cert |
| Request body | {"udid": "<UDID>"} (1024-byte body limit) |
| UDID validation | Regex: ^[A-Za-z0-9][A-Za-z0-9.:_-]*$ — prevents command injection |
| Process | Writes CA PEM to a temp file → runs xcrun simctl keychain → deletes temp file |
| Timeout | 10 seconds |
| Platform | macOS only — returns 400 on other platforms |
The simulator trusts the certificate immediately — no user interaction needed. This is possible because simulators run in a sandboxed environment where simctl has full control.
Emulator Certificate Push
Section titled “Emulator Certificate Push”For Android Emulators, Ghost pushes the certificate file to the emulator’s storage:
Command:
adb -s <serial> push <temp-file> /sdcard/ghost-ca.crt| Detail | Value |
|---|---|
| Endpoint | POST /api/v1/mobile/emulators/push-cert |
| Request body | {"serial": "<serial>"} (1024-byte body limit) |
| Serial validation | Same regex as UDID — ^[A-Za-z0-9][A-Za-z0-9.:_-]*$ |
| Process | Writes CA PEM to a temp file → runs adb push → deletes temp file |
| Timeout | 10 seconds |
| Manual step required | User must navigate to Android Settings → Security → Install from storage and select the file |
Unlike iOS simulators, Android emulators can’t auto-trust certificates through ADB. The file is placed on the virtual SD card, but the user must complete installation through the Settings UI.
API Reference
Section titled “API Reference”| Method | Endpoint | Handler | Description |
|---|---|---|---|
GET | /api/v1/cert/ca.crt | downloadCACert | Downloads the CA certificate as a PEM file. Sets Content-Disposition: attachment; filename="ghost-ca.crt". Returns 500 if CA is not loaded. |
GET | /api/v1/cert/status | getCertStatus | Returns CA certificate metadata: loaded status, subject, issuer, validity dates, serial number, isCA flag. Returns {loaded: false} (HTTP 200) if CA isn’t loaded. |
POST | /api/v1/cert/install | installCert | Installs the CA to the OS trust store. Returns 501 if the platform is unsupported, 500 on failure, 200 {ok: true} on success. |
GET | /api/v1/cert/installed | getCertInstalled | Checks if the CA is installed in the OS trust store. Returns {installed: bool, store: string, method: string}. |
POST | /api/v1/mobile/simulators/install-cert | installSimulatorCert | Installs CA to an iOS Simulator’s keychain via xcrun simctl. macOS only. |
POST | /api/v1/mobile/emulators/push-cert | pushEmulatorCert | Pushes CA to an Android emulator’s SD card via adb. User must finish installation in Settings. |
GET | /api/v1/mobile/profile | — | Downloads the iOS .mobileconfig profile (same as proxy endpoint, but through authenticated API). |
Configuration
Section titled “Configuration”Certificate-related settings in ~/.ghost/config.toml:
[proxy]# Maximum number of leaf certificates to keep in cache.# Default: 10000. Higher values use more memory but reduce# certificate generation for sites with many subdomains.cert_cache_size = 10000
# Hosts that should NOT be intercepted (certificate pinning, etc).# Ghost passes traffic through without decrypting.# Supports exact matches and wildcard suffixes.ssl_bypass_hosts = ["*.apple.com", "*.icloud.com"]Security Considerations
Section titled “Security Considerations”CA key protection — The ca.key file has 0600 permissions (owner read/write only). If an attacker obtains this file, they could generate certificates trusted by any device that has Ghost’s CA installed. Keep ~/.ghost/ secure.
24-hour leaf validity — Short-lived certificates limit the window of exposure if a leaf certificate is somehow compromised. After 24 hours, the certificate expires and a new one must be generated.
No intermediate CAs — MaxPathLen=0 prevents the CA from signing other CAs. Even if the CA key is compromised, the attacker can only create leaf certificates, not new CAs.
UDID/serial validation — Both the simulator and emulator endpoints validate device identifiers with a strict regex before passing them to shell commands, preventing command injection attacks.
Body size limits — Mobile API endpoints cap request bodies at 1,024 bytes, preventing denial-of-service through oversized payloads.