This commit is contained in:
Laan Tungir
2026-07-11 11:32:55 -04:00
commit 6803eec526
6 changed files with 743 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# build artifacts
sovereign_browser
*.o
# editor / OS
*.swp
.DS_Store
+23
View File
@@ -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
+101
View File
@@ -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).
+345
View File
@@ -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 (~150200 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 | ~150200 MB `libcef.so` | ~100130 MB `libxul.so` | ~3050 MB `servo` | ~3040 MB `libwebkit2gtk-4.1.so` |
| Binary size — full install | ~400500 MB extracted (bundles Chromium runtime; full Chromium is ~684 MB) | ~200300 MB (Firefox install) | ~3050 MB | ~4050 MB package (GTK deps usually already present on desktop) |
| Binary size — compressed dist | ~100150 MB tar.xz | ~80100 MB tar.bz2 | ~1525 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 (~3040 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 (~3050 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).
+140
View File
@@ -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
View File
@@ -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;
}