first
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# build artifacts
|
||||
sovereign_browser
|
||||
*.o
|
||||
|
||||
# editor / OS
|
||||
*.swp
|
||||
.DS_Store
|
||||
@@ -0,0 +1,23 @@
|
||||
# sovereign_browser — WebKitGTK + C99
|
||||
#
|
||||
# Requires: libwebkit2gtk-4.1-dev (Debian: sudo apt install libwebkit2gtk-4.1-dev)
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -std=c99 -Wall -Wextra -O2
|
||||
CFLAGS += $(shell pkg-config --cflags webkit2gtk-4.1)
|
||||
LDFLAGS ?=
|
||||
LDLIBS += $(shell pkg-config --libs webkit2gtk-4.1)
|
||||
|
||||
BIN := sovereign_browser
|
||||
SRC := src/main.c
|
||||
|
||||
$(BIN): $(SRC)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||
|
||||
run: $(BIN)
|
||||
./$(BIN)
|
||||
|
||||
clean:
|
||||
rm -f $(BIN)
|
||||
|
||||
.PHONY: run clean
|
||||
@@ -0,0 +1,101 @@
|
||||
# sovereign_browser
|
||||
|
||||
A Linux x86 web browser built in C99 on WebKitGTK, designed around
|
||||
**sovereign identity** instead of the traditional web's permissioned
|
||||
infrastructure.
|
||||
|
||||
The thesis: the browser's security model (DNS/domains, TLS/CAs, same-origin
|
||||
policy, CORS, cookie sandboxing) is what forces you to rely on permissioned
|
||||
domains, certificate authorities, and centralized account systems. By
|
||||
deprecating that model and replacing it with **Nostr identity** and **FIPS
|
||||
mesh transport**, you get a browser that's more capable, not less safe —
|
||||
because trust moves to the layer where it belongs: your keys.
|
||||
|
||||
> "Not your keys, not your browser." Walk up to a computer with your Nostr
|
||||
> identity (via [n_signer](https://github.com/laantungir/n_signer)) and the
|
||||
> browser is yours — your identity, your relays, your mesh.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **A real browser.** Loads any normal HTTP/HTTPS page via WebKitGTK.
|
||||
2. **FIPS addresses are first-class.** `http://<npub>.fips/` and `fips://`
|
||||
resolve over the [FIPS](https://github.com/jmcorgan/fips) mesh, not DNS/IP.
|
||||
Reach any FIPS node by its Nostr npub, no domain, no public IP, no TLS cert.
|
||||
3. **Built-in Nostr signing.** The browser injects `window.nostr` (the
|
||||
[nos2x](https://github.com/fiatjaf/nos2x) surface) into every page, backed
|
||||
by [n_signer](https://github.com/laantungir/n_signer) — a foreground,
|
||||
RAM-only, human-attended signing program. The browser never holds your
|
||||
private key; every signature is approved at the signer's terminal.
|
||||
4. **Deprecated web security, deliberately.** Same-origin policy, CORS, and
|
||||
certificate enforcement are stripped so pages (and the agent runtime to
|
||||
come) can freely call any endpoint — including FIPS mesh services — without
|
||||
the workarounds traditional browsers force on automators.
|
||||
|
||||
## Non-goals (for now)
|
||||
|
||||
- Agent integration, multi-window agent hosts, didactyl hosting, and the
|
||||
broader "browser as agent runtime" vision are documented in
|
||||
[`docs/architecture.md`](docs/architecture.md) and the linked plans, but
|
||||
**not in scope for the first build**. First: a usable browser with basic
|
||||
Nostr signing. Then the rest.
|
||||
|
||||
## Current status
|
||||
|
||||
Working proof-of-concept: a C99 + WebKitGTK window with a URL bar that loads
|
||||
real HTTPS pages. Verified loading `https://laantungir.net` cleanly. See
|
||||
[`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the friction
|
||||
report from the POC phase (and the Servo fallback exploration).
|
||||
|
||||
```
|
||||
[loaded] https://laantungir.net/ -- title: Laan Tungir
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Requires Debian 13 (trixie) or similar with WebKitGTK 4.1 dev headers:
|
||||
|
||||
```bash
|
||||
sudo apt install libwebkit2gtk-4.1-dev
|
||||
make
|
||||
./sovereign_browser [url]
|
||||
```
|
||||
|
||||
## Architecture (summary)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ sovereign_browser host (C99) │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌────────────────────┐ │
|
||||
│ │ UI / │ │ Request │ │ Nostr signer shim │ │
|
||||
│ │ tabs │ │ router │ │ (nostr_core_lib → │ │
|
||||
│ │ URL bar │ │ │ │ n_signer socket) │ │
|
||||
│ └─────────┘ └────┬─────┘ └─────────┬──────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌──────────┼───────────┬───────┘ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ http/https fips:// nostr:// │
|
||||
│ (WebKit net) (FIPS TUN) (relays) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ WebKitGTK (system lib, ~35 MB) │
|
||||
│ Blink-grade renderer + V8 + libsoup network stack │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Full architecture, engine comparison, and roadmap in
|
||||
[`docs/architecture.md`](docs/architecture.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. ✅ Base browser (WebKitGTK + C99, loads pages)
|
||||
2. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
|
||||
3. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
|
||||
4. ⏳ Nostr signing (`window.nostr` → nostr_core_lib → n_signer)
|
||||
5. ⏳ `nostr://` content scheme
|
||||
6. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
|
||||
|
||||
## License
|
||||
|
||||
MIT (to match nostr_core_lib / n_signer / didactyl).
|
||||
@@ -0,0 +1,345 @@
|
||||
# Reckless Browser — Implementation Options
|
||||
|
||||
## Goal
|
||||
|
||||
A Linux x86 browser written in C99 that:
|
||||
|
||||
1. **Keeps traditional HTTP/IP** — can still load normal web pages.
|
||||
2. **Makes FIPS addresses first-class citizens** — `npub…fips` (or similar) is a
|
||||
resolvable URL scheme that routes over the FIPS mesh, not DNS/IP.
|
||||
3. **Has Nostr signing built in** — the browser itself acts like the `nos2x`
|
||||
extension: pages can request `getPublicKey` / `signEvent` and the browser
|
||||
signs with the user's Nostr keypair, no extension install required.
|
||||
4. **Deprecates traditional web security** where it gets in the way:
|
||||
- TLS/CA certificate system (replace with Nostr-identity auth or raw).
|
||||
- Same-origin policy / CORS (pages may call any endpoint freely).
|
||||
- Cookie/session sandboxing (shared Nostr identity across sites).
|
||||
- DNS/domain reliance (FIPS addresses and npubs resolve without DNS).
|
||||
|
||||
The name "reckless" is intentional: we are trading the browser's security
|
||||
sandbox for functionality, on the assumption that identity and transport are
|
||||
handled at a different layer (Nostr keys + FIPS mesh encryption).
|
||||
|
||||
---
|
||||
|
||||
## The core decision: which engine to embed
|
||||
|
||||
A browser is really three things glued together: a **rendering engine** (HTML/
|
||||
CSS/layout), a **JS engine**, and a **networking stack**. Writing all three from
|
||||
scratch in C99 is a multi-year effort and would never reach modern web compat.
|
||||
The realistic path is to embed an existing engine and write the "chrome" (URL
|
||||
bar, tabs, the FIPS/Nostr integration, the security-stripping layer) in C99.
|
||||
|
||||
There are four credible options.
|
||||
|
||||
### Option A — Chromium Embedded Framework (CEF)
|
||||
|
||||
**What it is.** CEF is a third-party distribution that wraps Chromium (the same
|
||||
engine Chrome uses: Blink + V8 + the Chromium network stack) and exposes it
|
||||
through a C/C++ API. You link it into your own application and drive the browser
|
||||
from your own process. It is maintained by Marshall Bause and tracks Chromium
|
||||
releases closely.
|
||||
|
||||
**Why it fits C99.** CEF ships a C API wrapper (`libcef_dll_wrapper` plus the
|
||||
`cef_app.h` C interface). You can write the host application in C99 and only
|
||||
drop into C++ for the thin callback shims CEF requires. This is the standard
|
||||
way embedded-browser vendors (Spotify, Steam, many games) do it.
|
||||
|
||||
**Pros.**
|
||||
- Full modern web compat — it *is* Chromium, so every site works.
|
||||
- `nos2x` and other Chrome extensions work via the standard extension
|
||||
mechanism, so we can study how they inject `window.nostr` and replicate it
|
||||
natively.
|
||||
- Mature, documented, large community.
|
||||
- Chromium's networking stack is hackable: we can intercept requests at the
|
||||
`URLLoader` / `ResourceDispatcherHost` layer and reroute `*.fips` to the FIPS
|
||||
TUN interface, and strip CORS/same-origin checks there.
|
||||
|
||||
**Cons.**
|
||||
- Large binary (~150–200 MB for the Chromium runtime it bundles).
|
||||
- Upstream is Google-controlled; CEF lags Chromium by a few weeks.
|
||||
- The C++ shim requirement means "pure C99" is aspirational — the glue is C++.
|
||||
- Building CEF from source is heavy; most users consume prebuilt binaries.
|
||||
|
||||
**Security-stripping leverage.** Chromium's security is layered (site
|
||||
isolation, CORS, mixed-content, certificate enforcement). In CEF we can:
|
||||
- Disable site isolation and the renderer sandbox via command-line switches.
|
||||
- Register a custom `CefURLRequestHandler` / `SchemeHandlerFactory` for `fips://`
|
||||
and `nostr://` schemes.
|
||||
- Override certificate validation to accept self-signed / raw connections.
|
||||
- Inject a `window.nostr` object from the browser process into every frame via
|
||||
`OnContextCreated`, before any page script runs.
|
||||
|
||||
### Option B — Firefox / Gecko via Mozac or GeckoView
|
||||
|
||||
**What it is.** Mozilla's engine (Gecko) can be embedded. On Linux the relevant
|
||||
entry points are `libxul` (the Gecko shared library) and the newer Mozac
|
||||
components (Rust). There is no first-class "embed Gecko in C" story comparable
|
||||
to CEF; the supported embedding paths are Android (GeckoView) and the Firefox
|
||||
product itself.
|
||||
|
||||
**Pros.**
|
||||
- Mozilla upstream, non-Google.
|
||||
- WebExtensions API is the same one `nos2x`-style Firefox extensions use.
|
||||
- Gecko is more hackable at the protocol-handler level than Chromium.
|
||||
|
||||
**Cons.**
|
||||
- No supported C embedding story on Linux desktop. `libxul` is loadable but
|
||||
undocumented for embedders; you'd be reverse-engineering Firefox internals.
|
||||
- Mozac is Rust + Kotlin/Android-focused, not C99-friendly.
|
||||
- Smaller embedder community than CEF; most docs assume you're building Firefox.
|
||||
- Binary size comparable to CEF.
|
||||
|
||||
**Verdict.** Possible but uphill for a C99 project. Only choose this if there
|
||||
is a strong reason to avoid Chromium specifically.
|
||||
|
||||
### Option C — Servo
|
||||
|
||||
**What it is.** Servo is the Rust-based browser engine originally spun out of
|
||||
Mozilla, now community-driven. It is designed to be embeddable and has a
|
||||
relatively clean API.
|
||||
|
||||
**Pros.**
|
||||
- Pure Rust — aligns with the FIPS stack (also Rust) and could eventually share
|
||||
crypto/networking code.
|
||||
- Hackable, small, modern architecture.
|
||||
- Embedding API is improving and intentionally minimal.
|
||||
|
||||
**Cons.**
|
||||
- **No WebExtensions / extension system.** We'd have to build the `nos2x`-style
|
||||
`window.nostr` injection ourselves (doable, but more work).
|
||||
- Web compat is incomplete — complex sites may not render correctly yet.
|
||||
- C API for embedding is nascent; you'd be calling Rust from C via FFI, which is
|
||||
fine but means the "C99" layer is thin.
|
||||
- Networking stack is less mature than Chromium's; intercepting/rerouting for
|
||||
FIPS is more work.
|
||||
|
||||
**Verdict.** Attractive long-term for a Rust-aligned stack, but risky for a
|
||||
first version that must load arbitrary HTTP/IP sites and behave like a real
|
||||
browser today.
|
||||
|
||||
### Option D — WebKitGTK (via WPE or the GTK binding)
|
||||
|
||||
**What it is.** WebKitGTK exposes the WebKit engine (used by Safari/Epiphany)
|
||||
through a GObject/C API. WPE is the embedded-focused port. Both are C-callable.
|
||||
|
||||
**Pros.**
|
||||
- Genuine C API — no C++ shim required, best fit for "write it in C99."
|
||||
- Linux-native, packaged in distros, modest binary size.
|
||||
- WebKit is a real, full engine (Safari-class compat).
|
||||
- Custom URL schemes and request interception are supported via
|
||||
`WebKitURISchemeRequest` and the `WebKitWebContext` / `WebsiteDataManager`.
|
||||
- Extension model exists (WebKitWebExtension) for injecting `window.nostr`.
|
||||
|
||||
**Cons.**
|
||||
- Smaller community than Chromium; fewer Stack Overflow answers.
|
||||
- WebKit's process model and security flags are less documented to disable than
|
||||
Chromium's command-line switches.
|
||||
- Upstream is Apple-controlled; the GTK port tracks it with a lag.
|
||||
- `nos2x` itself is a Chrome/Firefox extension and won't run unchanged; we
|
||||
replicate its behavior natively regardless.
|
||||
|
||||
**Verdict.** The most C99-native option and a serious contender against CEF.
|
||||
|
||||
---
|
||||
|
||||
## Comparison matrix
|
||||
|
||||
| Criterion | CEF (Chromium) | Gecko/Firefox | Servo | WebKitGTK/WPE |
|
||||
|-------------------------------|:-------------------:|:-------------------:|:--------------:|:------------------:|
|
||||
| Web compat (loads real sites) | Excellent | Excellent | Partial | Good |
|
||||
| C99 friendliness | C++ shim needed | Poor | FFI | Excellent |
|
||||
| Binary size — engine lib | ~150–200 MB `libcef.so` | ~100–130 MB `libxul.so` | ~30–50 MB `servo` | ~30–40 MB `libwebkit2gtk-4.1.so` |
|
||||
| Binary size — full install | ~400–500 MB extracted (bundles Chromium runtime; full Chromium is ~684 MB) | ~200–300 MB (Firefox install) | ~30–50 MB | ~40–50 MB package (GTK deps usually already present on desktop) |
|
||||
| Binary size — compressed dist | ~100–150 MB tar.xz | ~80–100 MB tar.bz2 | ~15–25 MB | distro package |
|
||||
| Extension / `window.nostr` | Easy (Chrome ext) | Easy (WebExt) | Build it | Build it |
|
||||
| Request interception for FIPS | Good | Good | Manual | Good |
|
||||
| Security flags easy to strip | Best-documented | Moderate | N/A | Moderate |
|
||||
| Upstream control | Google | Mozilla | Comm. | Apple |
|
||||
| Aligns with FIPS Rust stack | No | No | Yes | No |
|
||||
|
||||
---
|
||||
|
||||
## Decision: WebKitGTK + C99 (Path A)
|
||||
|
||||
**POC results settled it.** See [`poc/webkit_c99/FINDINGS.md`](../poc/webkit_c99/FINDINGS.md)
|
||||
and [`poc/servo_rust/FINDINGS.md`](../poc/servo_rust/FINDINGS.md).
|
||||
|
||||
| Path | Result |
|
||||
|------|--------|
|
||||
| **A — WebKitGTK/C99** | ✅ Working in <1 min. 17 KB binary loaded `laantungir.net` cleanly. |
|
||||
| **B — Servo/Rust** | ⚠️ Prebuilt servoshell loaded pages but tripped on an SVG JS gap; custom embedder `cargo build` OOM-killed on this machine. |
|
||||
|
||||
WebKitGTK is the primary engine. Servo is the documented fallback if
|
||||
Rust-alignment with FIPS later outweighs build-cost and web-compat concerns.
|
||||
|
||||
The rest of this section documents the POC exploration that led here.
|
||||
|
||||
---
|
||||
|
||||
## Recommended path: dual-engine POC exploration
|
||||
|
||||
We pursued **two parallel proof-of-concept paths** and compared them before
|
||||
committing to a primary engine:
|
||||
|
||||
### Path A — WebKitGTK + C99 host
|
||||
|
||||
**Why.** WebKitGTK gives a genuine C API, honoring the "write it in C99" goal.
|
||||
It is Linux-native, distro-packaged, and the smallest practical footprint on a
|
||||
desktop (~30–40 MB engine lib; GTK deps usually already present). Request
|
||||
interception and custom URI schemes are first-class — exactly what we need for
|
||||
`fips://` / `nostr://` and CORS stripping.
|
||||
|
||||
**POC goal.** A minimal C99 application that opens a WebKitGTK window and can
|
||||
load any normal web page (`https://example.com`, etc.). No FIPS, no Nostr, no
|
||||
security stripping yet — just prove the embedding path and feel out the
|
||||
friction.
|
||||
|
||||
**What we're evaluating.**
|
||||
- How clean is the C API in practice? Any forced C++?
|
||||
- Build complexity: pkg-config, GTK deps, WebKit version (4.0 vs 4.1).
|
||||
- Does it load heavy/real-world sites acceptably?
|
||||
- How hard is it to register a custom URI scheme handler (foreshadowing FIPS)?
|
||||
- How hard is it to inject a JS object before page scripts run (foreshadowing
|
||||
`window.nostr`)?
|
||||
|
||||
### Path B — Servo + Rust host
|
||||
|
||||
**Why.** Servo is pure Rust, which aligns with the FIPS stack (also Rust). If
|
||||
this path wins, the browser and FIPS could eventually share crypto/networking
|
||||
code directly rather than via FFI. Servo is also the smallest (~30–50 MB) and
|
||||
the most hackable.
|
||||
|
||||
**POC goal.** Same as Path A — a minimal application (Rust this time) that
|
||||
embeds Servo and loads any normal web page.
|
||||
|
||||
**What we're evaluating.**
|
||||
- How mature is Servo's embedding API? Is there a stable C ABI, or do we pin a
|
||||
specific commit?
|
||||
- Web compat: do real-world sites render, or do we hit missing-feature walls?
|
||||
- How hard is request interception / custom schemes (foreshadowing FIPS)?
|
||||
- How hard is injecting `window.nostr` with no extension system?
|
||||
- Can we link FIPS Rust crates directly into the host, or do we still need the
|
||||
TUN interface?
|
||||
|
||||
### Comparison criteria for the POC phase
|
||||
|
||||
| Criterion | Path A (WebKitGTK/C99) | Path B (Servo/Rust) |
|
||||
|----------------------------|:----------------------:|:-------------------:|
|
||||
| Time to first page load | ? (measure) | ? (measure) |
|
||||
| Build friction | ? | ? |
|
||||
| Real-site compat | ? | ? |
|
||||
| Custom scheme handler cost | ? | ? |
|
||||
| `window.nostr` injection | ? | ? |
|
||||
| FIPS integration path | FFI to libfips | Direct crate link? |
|
||||
| C99 purity | High | N/A (Rust host) |
|
||||
|
||||
Both POCs are intentionally minimal — just "open a window, load a URL." The
|
||||
FIPS integration, Nostr signing, and security stripping come *after* we pick a
|
||||
primary engine based on the POC experience.
|
||||
|
||||
---
|
||||
|
||||
## Proposed architecture (engine-agnostic)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Host[Reckless Browser Host - C99]
|
||||
UI[UI / URL bar / tabs]
|
||||
Router[Request Router]
|
||||
NostrKey[Nostr Key Store + Signer]
|
||||
SecStrip[Security Strip Layer]
|
||||
end
|
||||
|
||||
subgraph Engine[Embedded Web Engine]
|
||||
Render[Renderer / JS]
|
||||
Net[Network Stack]
|
||||
end
|
||||
|
||||
subgraph FIPS[FIPS Mesh]
|
||||
Tun[TUN fd00::/8]
|
||||
end
|
||||
|
||||
subgraph Nostr[Nostr Relays]
|
||||
Relay[Relay pool]
|
||||
end
|
||||
|
||||
UI --> Router
|
||||
Router -->|http/https| Net
|
||||
Router -->|fips:// npub.fips| Tun
|
||||
Router -->|nostr://| Relay
|
||||
Net --> SecStrip --> Render
|
||||
Render -->|window.nostr.signEvent| NostrKey
|
||||
NostrKey --> Relay
|
||||
```
|
||||
|
||||
### Layers
|
||||
|
||||
1. **Host application (C99).** Owns the window, URL bar, tabs, key store, and
|
||||
the request router. This is where the "reckless" policy lives.
|
||||
|
||||
2. **Request router.** Inspects every outgoing URL:
|
||||
- `http://` / `https://` → engine's normal network stack (with CORS /
|
||||
same-origin checks disabled at the engine level).
|
||||
- `fips://<npub>…` or `<npub>.fips` → resolve via FIPS TUN interface
|
||||
(`fd00::/8` mapping), hand the raw TCP stream to the engine as a custom
|
||||
scheme handler. No TLS, no CA — FIPS provides Noise IK/XK encryption.
|
||||
- `nostr://<npub>/<kind>` → fetch the Nostr event set from relays, render
|
||||
natively or as a synthesized HTML document.
|
||||
|
||||
3. **Security strip layer.** Engine configuration + request interception that:
|
||||
- Disables same-origin policy and CORS enforcement.
|
||||
- Accepts any certificate (or none) for raw connections.
|
||||
- Shares the Nostr identity across all origins (no per-site cookie sandbox).
|
||||
|
||||
4. **Nostr signer (built-in nos2x equivalent).** Before page scripts run, inject
|
||||
`window.nostr = { getPublicKey, signEvent, getRelays, … }` into every frame.
|
||||
Calls are marshalled to the host's key store, which signs with the user's
|
||||
secp256k1 key and returns the signature. This is the same surface `nos2x`
|
||||
exposes, so existing Nostr web apps work without an extension.
|
||||
|
||||
5. **FIPS integration.** Link against `libfips` (or shell out to `fipsctl`) so
|
||||
the browser can bring up / query the mesh and resolve `.fips` names. FIPS
|
||||
already maps npubs to `fd00::/8` IPv6 addresses and provides a `.fips` DNS
|
||||
resolver, so the browser may not need its own resolver at all — it can lean
|
||||
on the FIPS TUN interface and just treat `*.fips` as "route to mesh."
|
||||
|
||||
---
|
||||
|
||||
## Open questions to resolve (post-POC)
|
||||
|
||||
These are deferred until *after* the dual-path POC comparison, since the
|
||||
answers may depend on which engine wins:
|
||||
|
||||
1. **FIPS URL scheme** — `fips://<npub>:<port>/path`, or reuse
|
||||
`http://<npub>.fips/…` and let FIPS's TUN+DNS handle it? The latter is less
|
||||
work but less explicit. (Note: FIPS already supports `http://<npub>.fips/`
|
||||
today per [`fips_setup/plans/FIPS_ADDRESSING.md`](../../lt/fips_setup/plans/FIPS_ADDRESSING.md).)
|
||||
2. **Nostr content rendering** — should `nostr://` render as native UI, or
|
||||
synthesize an HTML document from events? Native is cleaner; HTML reuses the
|
||||
engine.
|
||||
3. **Key storage** — plain file on disk, OS keyring, or a hardware signer
|
||||
(NIP-46 bunker / nos2x-style external signer)?
|
||||
4. **How far to strip** — do we keep *any* isolation (e.g. per-origin process
|
||||
separation) for stability, or go fully single-process reckless?
|
||||
|
||||
---
|
||||
|
||||
## Suggested next steps (WebKitGTK chosen)
|
||||
|
||||
1. ~~Path A — WebKitGTK/C99 POC~~ ✅ Done — loads `laantungir.net` cleanly.
|
||||
2. ~~Path B — Servo/Rust POC~~ ⚠️ Done — OOM on custom build; servoshell tripped on real site.
|
||||
3. ~~Compare and pick primary engine~~ ✅ Done — **WebKitGTK**.
|
||||
4. **Next: FIPS custom URI scheme.** Register `fips://` (or handle `*.fips`)
|
||||
via `webkit_web_context_register_uri_scheme()` on the existing POC, proxying
|
||||
to the FIPS TUN interface. Verify a `.fips` URL loads.
|
||||
5. **Nostr signing.** Inject `window.nostr = { getPublicKey, signEvent, ... }`
|
||||
via `WebKitUserContentManager` + JSC, marshalling calls to a C-side key
|
||||
store. Verify a Nostr web app signs without an extension.
|
||||
6. **Security stripping.** Accept any cert via
|
||||
`webkit_web_context_allow_tls_certificate_for_host()`; share a single
|
||||
`WebKitWebContext` for cross-origin identity. Spike CORS/same-origin
|
||||
stripping (may need a `WebKitWebExtension` or source patch — the one open
|
||||
unknown from Path A).
|
||||
7. Resolve the deferred open questions (FIPS URL scheme form, Nostr content
|
||||
rendering, key storage, how far to strip).
|
||||
@@ -0,0 +1,140 @@
|
||||
# Path A POC — WebKitGTK + C99: Findings
|
||||
|
||||
## Status: ✅ Working
|
||||
|
||||
A 17 KB C99 binary opens a GTK window with a URL bar and a WebKitWebView,
|
||||
and successfully loads real-world HTTPS pages.
|
||||
|
||||
Verified load:
|
||||
|
||||
```
|
||||
[loaded] https://laantungir.net/ -- title: Laan Tungir
|
||||
```
|
||||
|
||||
`laantungir.net` is a real site (the one from the FIPS setup docs) and
|
||||
WebKitGTK fetched, rendered, and parsed its `<title>` correctly.
|
||||
|
||||
## Environment
|
||||
|
||||
- Debian 13 (trixie), x86_64
|
||||
- WebKitGTK 2.52.3 (API `webkit2gtk-4.1`)
|
||||
- GTK 3.24.49
|
||||
- gcc 14.2.0
|
||||
- `libwebkit2gtk-4.1-dev` installed via apt (~one `apt install`)
|
||||
|
||||
## What we built
|
||||
|
||||
- [`main.c`](main.c) — ~130 lines of C99. A `GtkWindow` containing a `GtkBox`
|
||||
with a `GtkEntry` URL bar and a `WebKitWebView`. Signals:
|
||||
- URL entry `activate` → `webkit_web_view_load_uri()`
|
||||
- `load-changed` → sync URL bar + print `[loaded]` on `WEBKIT_LOAD_FINISHED`
|
||||
- `load-failed` → print `[failed]` with the error
|
||||
- [`Makefile`](Makefile) — pkg-config driven, one `make`.
|
||||
|
||||
## Time to first page load
|
||||
|
||||
Very fast. From "install package" to "page rendered":
|
||||
|
||||
1. `apt install libwebkit2gtk-4.1-dev` — one command.
|
||||
2. Write ~130 lines of C.
|
||||
3. `make` → 17 KB binary.
|
||||
4. Run → page loads.
|
||||
|
||||
Total active effort: under an hour, most of which was the C file.
|
||||
|
||||
## Problems encountered (the friction we came to measure)
|
||||
|
||||
### 1. `-DGTK_DISABLE_DEPRECATED` breaks the build (hard error)
|
||||
|
||||
Adding `-DGTK_DISABLE_DEPRECATED` to be "clean" caused:
|
||||
|
||||
```
|
||||
WebKitContextMenuItem.h:56:60: error: unknown type name 'GtkAction'
|
||||
```
|
||||
|
||||
WebKitGTK's context-menu API still references `GtkAction`, which GTK has
|
||||
deprecated and hides behind `GTK_DISABLE_DEPRECATED`. **Lesson:** do not
|
||||
define `GTK_DISABLE_DEPRECATED` when building against WebKitGTK 4.1. This is
|
||||
mildly concerning for the "reckless" goal — it means WebKitGTK drags along
|
||||
some legacy GTK API surface.
|
||||
|
||||
### 2. `-Wpedantic` is unusable with GObject headers (warning storm)
|
||||
|
||||
`-std=c99 -Wpedantic` produces dozens of `redefinition of typedef` warnings
|
||||
because GObject's `WEBKIT_DECLARE_*` / `G_DECLARE_*` macros redeclare typedefs
|
||||
(a pattern that is legal in C11 but flagged under C99 `-Wpedantic`). **Lesson:**
|
||||
drop `-Wpedantic`, or move to `-std=c11`/`-std=gnu11` if strict pedantry
|
||||
matters. `-Wall -Wextra` is fine and clean.
|
||||
|
||||
### 3. Header layout is non-obvious
|
||||
|
||||
The pkg-config include is `-I/usr/include/webkitgtk-4.1`, and the umbrella
|
||||
header is `#include <webkit2/webkit2.h>`. Individual headers live under
|
||||
`webkit/` (e.g. `webkit/WebKitWebView.h`), not under `webkit2/WebKit/` as the
|
||||
API docs sometimes imply. Not a real problem once you know to just include the
|
||||
umbrella — but worth noting.
|
||||
|
||||
### 4. `gtk_main()` is deprecated in GTK 4 (not a problem here)
|
||||
|
||||
We targeted GTK 3 (WebKitGTK 4.1 API pairs with GTK 3). GTK 4 would require
|
||||
`gtk_application_run()` and a different lifecycle. Since WebKitGTK's 4.1 API is
|
||||
GTK-3-based, this is the correct pairing for now. A GTK-4-based WebKit
|
||||
(`webkitgtk-6.0`) exists in newer distros but is not in Debian 13.
|
||||
|
||||
## What this tells us about the reckless browser goals
|
||||
|
||||
### FIPS integration (custom URI scheme) — looks straightforward
|
||||
|
||||
WebKitGTK has a first-class custom URI scheme API:
|
||||
`webkit_web_context_register_uri_scheme()`. You register a scheme (e.g.
|
||||
`fips`) and get a `WebKitURISchemeRequest` callback where you feed bytes into
|
||||
an input stream. This is exactly the hook we need to route `fips://` (or
|
||||
`*.fips`) requests to the FIPS TUN interface. The header
|
||||
`webkit/WebKitURISchemeRequest.h` is present. **No obvious blocker.**
|
||||
|
||||
### Nostr signing (`window.nostr` injection) — looks straightforward
|
||||
|
||||
WebKitGTK exposes JavaScriptCore via `webkit/WebKitUserContentManager.h` and
|
||||
`jsc/jsc.h`. You can inject a script into every page with
|
||||
`webkit_user_content_manager_add_script()` and a
|
||||
`WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES` +
|
||||
`WEBKIT_USER_INJECTED_FRAMES_ALL_FRAMES` script. The injected script can define
|
||||
`window.nostr = { getPublicKey, signEvent, ... }` and bridge calls back to the
|
||||
C host via `jsc_value_object_set_property` or a message handler. **No obvious
|
||||
blocker**, and no extension system needed — this is the native mechanism.
|
||||
|
||||
### Security stripping — needs investigation
|
||||
|
||||
WebKitGTK exposes `WebKitSettings` (disable JS, enable devtools, etc.) and
|
||||
`WebKitWebContext` / `WebKitWebsiteDataManager` for cookies, TLS, etc. The
|
||||
specific "reckless" asks:
|
||||
- **Disable CORS / same-origin** — not exposed as a simple setting in the 4.1
|
||||
API. Likely requires a `WebKitWebExtension` (a separate shared lib loaded
|
||||
into the web process) that hooks the WebCore loader, or building WebKit from
|
||||
source with patches. This is the biggest unknown.
|
||||
- **Accept any certificate** — `WebKitWebContext` has a
|
||||
`load-failed-with-tls-errors` signal where you can call
|
||||
`webkit_web_context_allow_tls_certificate_for_host()`. Easy.
|
||||
- **Shared identity across origins** — cookies are per-`WebKitWebContext`; a
|
||||
single shared context gives a shared cookie jar by default. Easy.
|
||||
|
||||
The CORS/same-origin strip is the one item that may require deeper engine
|
||||
access (a WebExtension or a custom build). Worth a follow-up spike.
|
||||
|
||||
## Binary size (actual)
|
||||
|
||||
- Host binary (`reckless_webkit`): **17 KB** (stripped of debug info by `-O2`).
|
||||
- Engine library `libwebkit2gtk-4.1.so`: ~35 MB on this system.
|
||||
- The host binary is tiny because it links dynamically against the system
|
||||
WebKitGTK. This matches the "smallest practical footprint on a Linux desktop"
|
||||
prediction in the options doc.
|
||||
|
||||
## Verdict for Path A
|
||||
|
||||
WebKitGTK + C99 is a **very comfortable** embedding path. The C API is genuine
|
||||
(no C++ required), the docs are adequate, custom URI schemes and JS injection
|
||||
are first-class, and a real site loaded on the first try. The only real
|
||||
unknown for the full reckless browser is CORS/same-origin stripping, which may
|
||||
need a WebExtension or source patch.
|
||||
|
||||
Path A is a strong candidate for the primary engine.
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* sovereign_browser — WebKitGTK + C99
|
||||
*
|
||||
* Minimal browser: a GTK window with a URL entry and a WebKitWebView.
|
||||
* Type a URL, press Enter, the page loads. That's it for now.
|
||||
*
|
||||
* Build: make. Run: ./sovereign_browser [url]
|
||||
*
|
||||
* Goal: a usable browser first. FIPS URI scheme, Nostr signing (via
|
||||
* n_signer), and security stripping come next. See README.md and
|
||||
* docs/architecture.md.
|
||||
*/
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Normalize a bare string into a loadable URL.
|
||||
* "example.com" -> "https://example.com"; "http://..." left as-is. */
|
||||
static char *normalize_url(const char *input) {
|
||||
if (input == NULL || input[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
if (strstr(input, "://") != NULL) {
|
||||
return g_strdup(input);
|
||||
}
|
||||
return g_strdup_printf("https://%s", input);
|
||||
}
|
||||
|
||||
/* Enter pressed in the URL bar: load the typed URL. */
|
||||
static void on_url_activate(GtkEntry *entry, WebKitWebView *webview) {
|
||||
const char *text = gtk_entry_get_text(entry);
|
||||
char *url = normalize_url(text);
|
||||
if (url != NULL) {
|
||||
webkit_web_view_load_uri(webview, url);
|
||||
g_free(url);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep the URL bar in sync as the page navigates. */
|
||||
static void on_load_changed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
GtkEntry *url_entry) {
|
||||
if (load_event == WEBKIT_LOAD_COMMITTED) {
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
if (uri != NULL) {
|
||||
gtk_entry_set_text(url_entry, uri);
|
||||
}
|
||||
} else if (load_event == WEBKIT_LOAD_FINISHED) {
|
||||
const gchar *title = webkit_web_view_get_title(webview);
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
g_print("[loaded] %s -- title: %s\n",
|
||||
uri ? uri : "(null)",
|
||||
(title && title[0]) ? title : "(none)");
|
||||
}
|
||||
}
|
||||
|
||||
/* Surface load failures so we can see them in the terminal. */
|
||||
static gboolean on_load_failed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
gchar *failing_uri,
|
||||
GError *error,
|
||||
gpointer data) {
|
||||
(void)webview;
|
||||
(void)load_event;
|
||||
(void)data;
|
||||
g_print("[failed] %s -- %s\n",
|
||||
failing_uri ? failing_uri : "(null)",
|
||||
error ? error->message : "(unknown)");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Window closed: quit the app. */
|
||||
static void on_window_destroy(GtkWidget *widget, gpointer data) {
|
||||
(void)widget;
|
||||
(void)data;
|
||||
gtk_main_quit();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
/* GTK requires argv parsing first. */
|
||||
gtk_init(&argc, &argv);
|
||||
|
||||
const char *start_url = (argc > 1) ? argv[1] : "https://example.com";
|
||||
char *normalized = normalize_url(start_url);
|
||||
|
||||
/* Top-level window. */
|
||||
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(window), "sovereign browser");
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);
|
||||
|
||||
/* Vertical box: URL bar on top, web view below. */
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(window), vbox);
|
||||
|
||||
/* URL bar. */
|
||||
GtkWidget *url_entry = gtk_entry_new();
|
||||
gtk_entry_set_text(GTK_ENTRY(url_entry),
|
||||
normalized ? normalized : start_url);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), url_entry, FALSE, FALSE, 0);
|
||||
|
||||
/* The web view itself. */
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
|
||||
gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(webview), TRUE, TRUE, 0);
|
||||
|
||||
/* Wire signals. */
|
||||
g_signal_connect(url_entry, "activate", G_CALLBACK(on_url_activate),
|
||||
webview);
|
||||
g_signal_connect(webview, "load-changed", G_CALLBACK(on_load_changed),
|
||||
url_entry);
|
||||
g_signal_connect(webview, "load-failed", G_CALLBACK(on_load_failed),
|
||||
NULL);
|
||||
|
||||
/* Load the start URL. */
|
||||
if (normalized != NULL) {
|
||||
webkit_web_view_load_uri(webview, normalized);
|
||||
g_free(normalized);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(window);
|
||||
gtk_main();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user