Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Unified-diff context preserves upstream whitespace and pinned patch hashes.
# -text keeps patch bytes LF-exact on any checkout: CRLF conversion would break
# their recorded sha256 receipts.
*.patch -text -whitespace
12 changes: 9 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# run-tests.sh drives clang++ (C++ unit tests), bash (shell tests) and shellcheck
# (lint of the shipped device shell); install the compiler + linter explicitly.
# Host suites need no reMarkable SDK, device, WPE engine, or live phone.
- name: Install toolchain
run: sudo apt-get update && sudo apt-get install -y clang shellcheck
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build qt6-base-dev python3 openssl shellcheck
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: tools/passkey-acceptance/package-lock.json
- name: Install passkey test dependencies
run: npm ci --ignore-scripts --prefix tools/passkey-acceptance
- name: Run host tests
run: bash scripts/run-tests.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
# build artifacts
build/
dist/
__pycache__/
*.pyc
*.o
*.so

Expand Down
28 changes: 22 additions & 6 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ rmweb is MIT-licensed (see LICENSE). It builds on, links against, or ships
fragments of the following third-party software. Their licenses apply to
those components.

Runtime libraries (linked, not modified)
----------------------------------------
Runtime libraries and optional authentication components
--------------------------------------------------------
* WPE WebKit (https://wpewebkit.org) — LGPL-2.1+ / BSD-2-Clause (WebCore).
Dynamic linking; no modifications. Releases: https://wpewebkit.org/releases/
Dynamic linking. The optional authentication runtime applies the native
assertion-provider patch under patches/. Its new GLib API files are
LGPL-2.0-or-later and the WPE coordinator is BSD-2-Clause; original notices
remain intact. Runtime exports include corresponding source and patches.
Releases: https://wpewebkit.org/releases/
* Qt 6 (https://www.qt.io) — LGPL-3.0. Dynamic linking; no modifications.
reMarkable ships Qt on the device; rmweb links against the system build.
* Mesa (https://mesa3d.org) — MIT. llvmpipe software rasterizer.
* glib-networking / OpenSSL — LGPL-2.1+ / Apache-2.0 (TLS backend).
* libwebauthn (https://github.com/linux-credentials/libwebauthn) — LGPL-2.1-or-later.
The optional phone helper pins and patches this dependency. Its export
includes complete source, Cargo.lock, patches, build recipe and crate license
inventory under licenses/. See engine/auth-passkey-helper/vendor/libwebauthn-COPYING.
* Public Suffix List (https://publicsuffix.org) — MPL-2.0. The phone helper uses
a checksum-pinned snapshot retaining its license notice.
* SimpleWebAuthn server (https://github.com/MasterKale/SimpleWebAuthn) — MIT.
The disposable acceptance server pins @simplewebauthn/server in its lockfile;
it is not part of the tablet runtime.

Bundled binaries (redistributed inside build/bundle, not modified)
------------------------------------------------------------------
Expand Down Expand Up @@ -42,9 +55,12 @@ Iconography
that the above copyright notice and this permission notice appear in all
copies.

Device-side integration (not shipped, documented only)
------------------------------------------------------
Device-side integration
-----------------------
* XOVI + AppLoad by asivery (https://github.com/asivery) — used on-device to
place rmweb in the stock launcher. See docs/install.md for versions.
place rmweb in the stock launcher. AppLoad is GPL-3.0; the authentication
integration includes source patches, not its binary or firmware resources.
Preserve complete corresponding AppLoad source, patches and license when
distributing a rebuilt loader. See docs/install.md and patches/appload.md.

Fonts and media bundled with the OS or device are property of their owners.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ docker build -f toolchain/Dockerfile -t rmweb-sdk . # cross-compile image

Full instructions: [`docs/install.md`](docs/install.md)

## Phone passkeys and application sign-in

The separate `rmweb-auth-browser` provides an ephemeral AppLoad window for
caller-supplied HTTPS sign-in pages, using AppLoad's keyboard and a native QR
flow for phone passkeys. The caller owns its callback and credentials. This
requires a patched WPE runtime and a qualified AppLoad build; it does not change
the regular browser's stored-profile behavior.

The qualified profile is Paper Pro software **3.28.0.172 / Qt 6.10.3**. The
contributor reported the disposable test verifier's **PASS** after phone
approval on that profile; maintainer verification of that on-device result is
still pending. Arbitrary account sign-in, other firmware and other reMarkable
models remain unverified. Start with the [authentication build and integration guide](docs/auth-browser.md)
or the [disposable phone-passkey demo](tools/passkey-acceptance/DEMO.md).

## Roadmap / Planned

Not implemented yet (earlier docs claimed some of these by mistake - see the review above):
Expand Down
31 changes: 31 additions & 0 deletions device/auth/entry
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/sh
# AppLoad retains the PID and display; the caller owns this installed directory.
# Assertions below intentionally use the `condition && condition || fail` idiom.
# shellcheck disable=SC2015
set -eu
umask 077
fail() { echo '[Authentication] Browser is unavailable.' >&2; exit 1; }
# The qualified tablet's BusyBox ash supports the core-size limit.
# shellcheck disable=SC3045
ulimit -c 0 || fail
[ "$#" -eq 1 ] || [ "$#" -eq 3 ] || fail
if [ "$#" -eq 3 ]; then
[ "$2" = --device-code ] || fail
fi
case "${QTFB_KEY:-}" in ''|*[!0-9]*) fail;; esac
[ ! -L "$0" ] || fail
auth_root=$(CDPATH='' cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P) || fail
auth_runtime=$auth_root/runtime
[ -d "$auth_root/bin" ] && [ ! -L "$auth_root/bin" ] || fail
[ -d "$auth_runtime" ] && [ ! -L "$auth_runtime" ] || fail
[ -f "$auth_root/bin/rmweb-auth-entry" ] && [ ! -L "$auth_root/bin/rmweb-auth-entry" ] \
&& [ -x "$auth_root/bin/rmweb-auth-entry" ] || fail
[ -f "$auth_runtime/rmweb-env.sh" ] && [ ! -L "$auth_runtime/rmweb-env.sh" ] || fail
unset LD_PRELOAD RMWEB_JSC_OPTS RMWEB_JIT RMWEB_SKIA_THREADS
RMWEB_AUTH_RUNTIME=$auth_runtime
export RMWEB_AUTH_RUNTIME
# shellcheck source=/dev/null
. "$auth_runtime/rmweb-env.sh"
unset QT_QPA_PLATFORM QT_QUICK_BACKEND QSG_RENDER_LOOP
unset WEBKIT_INSPECTOR_SERVER WEBKIT_INSPECTOR_HTTP_SERVER WEBKIT_DEBUG SSLKEYLOGFILE
exec "$auth_root/bin/rmweb-auth-entry" "$@"
255 changes: 255 additions & 0 deletions docs/auth-browser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Reusable AppLoad authentication window

`rmweb-auth-browser` is a temporary WebKit window for caller-supplied HTTPS
authentication pages, including phone-passkey assertions. It uses an isolated
patched WPE runtime and the existing AppLoad QTFB transport. It has no article
reader, history, saved passwords, page capture, or persistent browser profile.
The caller owns authorization state, its callback listener, token exchange,
credentials, and the decision that authentication has completed. The browser
does not require a particular provider, catalog ID, or install path.

The supported device profile remains **Paper Pro (Ferrari), firmware
3.28.0.172 / Qt 6.10.3**. The phone helper requires that model's built-in
`btnxpuart` Bluetooth adapter. Generic URLs and movable packaging do not
establish RM2 or other hardware support.

## Launch and callback contract

The launcher accepts an initial URL and an optional display code:

```text
entry HTTPS_URL
entry HTTPS_URL --device-code ABCD-1234
```

The initial URL is limited to 8,192 bytes and must be valid HTTPS with a host,
without credentials, a fragment, or literal whitespace/control bytes. The
provider, path, query, and HTTPS port are supplied by the caller. A display code
may contain up to 64 uppercase letters, digits, or hyphens; it is displayed in
the window, never inserted into page fields.

If the initial query contains `state` or `redirect_uri`, both must occur exactly
once. State must contain 16–256 ASCII letters, digits, hyphens, or underscores.
The decoded redirect must be a canonical literal HTTP loopback URL using
`localhost`, `127.0.0.1`, or `[::1]`, an explicit port from 1024 through 65535,
and a nonempty absolute path. Credentials, query, fragment, residual percent
escapes, and paths requiring dot-segment normalization are rejected. The outer
`redirect_uri` value may be percent encoded. Display codes and callback flows
are separate launch modes.

For example, a caller may issue:

```text
https://login.example.test:8443/authorize?state=abcdefghijklmnop1234567890&redirect_uri=http%3A%2F%2F127.0.0.1%3A49152%2Foauth%2Freturn
```

The browser permits an HTTP callback only at that exact address, port, and path,
with one matching state and either one nonempty `code` or one nonempty `error`.
Without a callback pair, HTTP navigation is blocked. HTTPS redirects remain
available for provider login flows. A callback receipt, including an error
callback, means only that the caller's callback page was reached. It is not proof
of successful authentication, token exchange, or enrollment.

## Build and install layout

Use the pinned SDK recipe in `toolchain/Dockerfile.app-only-sdk-3.28` and separate
external caches. Obtain the official Ferrari OS 5.8.203 SDK installer named
`remarkable-production-image-5.8.203-ferrari-public-aarch64-toolchain.sh` from
[reMarkable's developer downloads](https://developer.remarkable.com/links).
Keep it outside Git; the Dockerfile verifies its pinned size and SHA-256.
Build the ARM64-host SDK image:

```sh
docker buildx build --platform linux/arm64 --load \
-f toolchain/Dockerfile.app-only-sdk-3.28 \
--build-context sdk_source=/absolute/path/to/sdk-download-directory \
-t rmweb-app-sdk:3.28.0.172 .
```

Build/export the helper using
[its rebuild instructions](../engine/auth-passkey-helper/README.md), then build
the engine and browser:

```sh
engine_cache="$HOME/.cache/rmweb/auth-engine-3.28"
auth_cache="$HOME/.cache/rmweb/auth-browser-3.28"
python3 scripts/build-auth-engine.py --cache "$engine_cache" \
--sdk-image rmweb-app-sdk:3.28.0.172 --jobs 6
python3 scripts/build-auth-browser.py --cache "$auth_cache" \
--engine-artifacts "$engine_cache/artifacts" \
--helper-artifacts /absolute/path/to/helper/artifacts
```

The app recipe builds `rmweb-auth-browser` and `rmweb-auth-entry`, then adds the
verified `rmweb-auth-passkey` helper. Manifest version 2 binds source, SDK,
runtime, helper, and executable hashes. The artifact layout remains the three
ELF executables, `runtime/`, `licenses/`, and `manifest.json`; dependency sources,
licenses, and rebuild materials accompany the runtime and helper. Use a dedicated authentication cache; the regular browser runtime does not
contain this native WebAuthn provider.

The caller assembles an application root from those verified artifacts and
`device/auth/entry` from the matching source checkout. The browser manifest's
source inventory records the launcher hash. A generic example layout is:

```text
/home/root/example-auth/
entry # device/auth/entry
bin/rmweb-auth-entry # verified native namespace entry
bin/rmweb-auth-browser # verified authentication browser
bin/rmweb-auth-passkey # verified phone helper
runtime/rmweb-env.sh
runtime/lib/...
runtime/libexec/...
runtime/licenses/...
licenses/...
```

The wrapper derives its root from its own location and exports the absolute
`RMWEB_AUTH_RUNTIME` before sourcing that root's runtime environment. It does
not accept a runtime or helper override. The native entry locates the browser
and runtime from its executable location; the browser locates the phone helper
beside its executable. Install real, root-owned files and directories with no
symlinks or group/world-write permission. Preserve private application permissions.

The native entry verifies owned paths and executables, creates a private mount
namespace, and mounts its bundled `runtime/libexec` read-only over
`/usr/libexec`. It preserves the process PID when executing the browser. Stock
interface mounts and other AppLoad applications remain outside that namespace.
Use a matching launcher, native entry, browser, helper, and runtime export;
older environment scripts still contain a fixed consumer path.

The caller owns its catalog directory/ID, display name, install location, and
installation/rollback lifecycle. For example, a caller-owned catalog's
`external.manifest.json` can contain:

```json
{
"name": "Example sign-in",
"application": "/home/root/example-auth/entry",
"workingDirectory": "/home/root/example-auth",
"args": ["https://login.example.test/sign-in"],
"environment": {},
"qtfb": true,
"supportsVirtualKeyboard": true,
"supportsRotation": false,
"disablesWindowedMode": true,
"aspectRatio": "auto"
}
```

This is a layout example, not an installer. Supply one-time codes and private
callback state through transient launch arguments, not a persistent catalog.

## Dependency and upgrade boundaries

The separate AppLoad installation must include the reviewed QTFB lifetime,
key-log removal and touch-cancellation patches. Follow the pinned source and
apply order in [AppLoad prerequisites](../patches/appload.md), then qualify its
stock-UI hooks for the exact firmware. Rebuilding rmweb alone does not update
AppLoad. No boot configuration or stock-UI hook is installed by these tools.

The helper serializes Bluetooth ownership through `/run/rmweb-passkey.lock`
and uses finite `rmweb-passkey-…` wake locks. Stop new launches and let every
active helper finish restoration and exit before changing versions. Never unlink
a lock while a helper may hold it. The empty lock file may remain until reboot.

Use a fresh, dedicated engine build volume. Its purpose marker is
`rmweb-auth-webauthn-engine-v1`; unrelated or mismatched volumes are rejected
without mutation. Use the matching launcher, runtime environment, native entry,
browser and helper from the same reviewed source/package receipts.

## Keyboard, privacy, and lifetime

Enable `supportsVirtualKeyboard` in the caller's AppLoad manifest. Swipe down
briefly with a finger from the top-center edge to expose AppLoad's toolbar,
then tap its far-left keyboard button within three seconds. Long swipes ending
below 400 framebuffer pixels do not trigger this AppLoad v0.5.3 gesture.
The browser translates the pinned layout's QTFB key packets into native WPE
events, including modifiers and releases. It does not inspect or replace DOM
field values. No custom keyboard is included.

Unmodified AppLoad v0.5.3 logs key labels. Apply the supplied
[key-log removal patch](../patches/appload-v0.5.3-no-key-logging.patch) and load
the rebuilt AppLoad library before entering credentials.
The launcher clears inherited preload, inspector, key-log, and diagnostic
overrides, sets a private umask, and disables core dumps. Page/engine stdout and
stderr are suppressed and the WebKit network session is ephemeral. Native
passkey diagnostics use fixed `AUTHPRIV` syslog labels; HTTP diagnostics contain
only failure status numbers (400–599), capped at 16 per browser. URLs, page text,
request options, QR contents, and assertions are never included. Return closes
this window; the caller retains its own lifecycle.

The patched provider presents a native QR for ordinary same-origin HTTPS phone
assertions, with the verified relying-party ID, progress, and Cancel. The phone
retains its passkeys. Registration, conditional/silent mediation, platform
authenticators, and unsupported extensions fail explicitly. WebKit's trusted
origin/RP checks, exact client-data hash, assertion binding, and cancellation on
navigation, abort, or timeout remain in force. See `patches/README.md` for the
trust boundary. An unpatched runtime without the WebAuthn APIs cannot perform
this flow; provider-offered alternate sign-in methods are a separate option.

## Navigation and input

Exact `about:blank` and `about:srcdoc` child documents are permitted because
ordinary authentication pages use them. Either URL committing in the main frame
is stopped and rejected. Neither replaces the visible origin nor becomes a
valid launch URL. Query/fragment variants and other local schemes remain
blocked. The native provider separately rejects passkey requests from inherited
blank/srcdoc frames. Pop-ups, downloads, unsupported schemes, and TLS exceptions
remain blocked. Ordinary stopped or superseded loads do not create a failure
card; ignoring cancellation never clears an existing failure.

Both AppLoad finger and pen packets use the existing native page-input path.
Mixed contacts cancel the gesture; loading, stale-frame, and navigation guards
apply to both. WPE supplies tap-to-click and drag-to-scroll without an extra
synthetic click. Pen contact IDs map to WPE ID zero, with finger IDs shifted by
one to keep the sources distinct and avoid reserved IDs.

## Validation and remaining gates

```sh
./scripts/run-tests.sh
python3 -m unittest discover -s tests -p auth_launcher_test.py
python3 -m unittest discover -s tests -p auth_build_recipe_test.py
python3 -m unittest discover -s tests -p test_auth_engine_build.py
cmake -S tests/auth-policy -B build/auth-policy -G Ninja
cmake --build build/auth-policy
ctest --test-dir build/auth-policy --output-on-failure
bash tests/auth-wpe-smoke/run.sh "$auth_cache"
# Optional anonymous check; no code, credentials, or sign-in interaction:
bash tests/auth-wpe-smoke/run.sh "$auth_cache" --public
```

The launcher tests execute the real script from a relocated temporary directory
with stub runtime/native-entry fixtures. They check literal arguments, derived
runtime paths, cleared overrides, private umask/core limits, same-PID execution,
and fixed failure messages. They do not exercise the tablet's namespace or
WebKit. `tests/auth_entry_test.py` exercises the actual native namespace entry
inside a disposable root-owned Docker container and refuses to run on the host.

The host suites cover auth policy, surface, helper protocol, and Linux QTFB.
Actual-engine fixtures cover native input/navigation, the WebAuthn provider,
and the Qt/GLib bridge with a fake helper. Synthetic assertions test binding and
cancellation, not phone cryptography. The separate
`tools/passkey-acceptance/README.md` describes a disposable relying-party test;
only a verified assertion from its live verifier establishes that test's phone
passkey result.

On 2026-09-17, the contributor reported the results below. The host suites are
reproducible from this checkout; the SDK-built and on-device results are
contributor-reported with maintainer verification pending. Reported: host
policy, surface, launcher, helper and packaging suites passed, along with the
actual SDK-built entry's 29 namespace/ownership cases and the acceptance app's
four actual-WPE route fixtures. The separate self-contained acceptance app was
built with the official Paper Pro 3.28 SDK, its package hashes and private
helper namespace were checked on-device, and a user confirmed its **PASS**
result after phone QR approval on software **3.28.0.172 / Qt 6.10.3**. The
tested runtime used the already built patched WPE libraries; this was not a
fresh full-engine rebuild.

Pending independent verification, that contributor-reported physical result
stands as evidence for the disposable relying party's assertion
verification. It does not establish arbitrary provider compatibility, account
sign-in, caller enrollment, RM2 support, or another firmware profile. Cancellation,
Bluetooth restoration, stock-UI return and input should be rechecked for each
new installation. A source/build check alone is not a device qualification.
2 changes: 2 additions & 0 deletions engine/auth-passkey-helper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target/
/vendor/libwebauthn/
Loading
Loading