commit 6803eec52683dc5708becefabde37bc8015562c5 Author: Laan Tungir Date: Sat Jul 11 11:32:55 2026 -0400 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7324350 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# build artifacts +sovereign_browser +*.o + +# editor / OS +*.swp +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d75378 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdafda6 --- /dev/null +++ b/README.md @@ -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://.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). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..5c2686b --- /dev/null +++ b/docs/architecture.md @@ -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://…` or `.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:///` → 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://:/path`, or reuse + `http://.fips/…` and let FIPS's TUN+DNS handle it? The latter is less + work but less explicit. (Note: FIPS already supports `http://.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). diff --git a/docs/webkit-poc-findings.md b/docs/webkit-poc-findings.md new file mode 100644 index 0000000..436d68e --- /dev/null +++ b/docs/webkit-poc-findings.md @@ -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 `` 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. diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1f87111 --- /dev/null +++ b/src/main.c @@ -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; +}