diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6382a69 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 902ab9c..db1d387 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index 8c7ca80..cf691bc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ # build artifacts build/ dist/ +__pycache__/ +*.pyc *.o *.so diff --git a/NOTICE b/NOTICE index 6e234d2..c009b5c 100644 --- a/NOTICE +++ b/NOTICE @@ -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) ------------------------------------------------------------------ @@ -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. diff --git a/README.md b/README.md index dc3b71e..ce5add9 100644 --- a/README.md +++ b/README.md @@ -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): diff --git a/device/auth/entry b/device/auth/entry new file mode 100755 index 0000000..682cde6 --- /dev/null +++ b/device/auth/entry @@ -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" "$@" diff --git a/docs/auth-browser.md b/docs/auth-browser.md new file mode 100644 index 0000000..b23a5dd --- /dev/null +++ b/docs/auth-browser.md @@ -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. diff --git a/engine/auth-passkey-helper/.gitignore b/engine/auth-passkey-helper/.gitignore new file mode 100644 index 0000000..d24da09 --- /dev/null +++ b/engine/auth-passkey-helper/.gitignore @@ -0,0 +1,2 @@ +/target/ +/vendor/libwebauthn/ diff --git a/engine/auth-passkey-helper/Cargo.lock b/engine/auth-passkey-helper/Cargo.lock new file mode 100644 index 0000000..b4c0758 --- /dev/null +++ b/engine/auth-passkey-helper/Cargo.lock @@ -0,0 +1,4231 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "apdu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa1a20ca6e9b354419bd6c2714beb435203b3e942440e09016e6deeffb08ffd" +dependencies = [ + "apdu-core", + "apdu-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "apdu-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5ab921a56bbe68325ba6d3711ee2c681239fe4c9c295c6a1c2fe6992e27f86" + +[[package]] +name = "apdu-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd675f7ce10250005ac39b9ee8e618fe51370ce6f39170559726cdd0ff7fe7c" +dependencies = [ + "apdu-core", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-url" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261c4eaab63106ddf34d377f47e5428aa863b61e4a647c872778d9caf8e2c819" +dependencies = [ + "base64", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bluer" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901" +dependencies = [ + "custom_debug", + "dbus", + "dbus-crossroads", + "dbus-tokio", + "displaydoc", + "futures", + "hex", + "lazy_static", + "libc", + "log", + "macaddr", + "nix", + "num-derive", + "num-traits", + "pin-project", + "serde", + "serde_json", + "strum", + "tokio", + "tokio-stream", + "uuid", +] + +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags 2.13.2", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.14.0", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.20", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.13.2", + "bluez-async", + "dashmap 6.2.1", + "dbus", + "futures", + "jni", + "jni-utils", + "log", + "objc2", + "objc2-core-bluetooth", + "objc2-foundation", + "once_cell", + "static_assertions", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "uuid", + "windows", + "windows-future", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbor-smol" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6dd31f7069836e87169bc5910212571b873cebe389c7c7f2d8b1fb3e55c80d" +dependencies = [ + "delog", + "heapless", + "heapless-bytes", + "serde_core", +] + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cosey" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75494895fa1a9713ca725ddf2db084ee84fb0c20938fdd7c89293febe732d30a" +dependencies = [ + "heapless-bytes", + "serde", + "serde_repr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctap-types" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8b105c5e728afd373e99874f0c1911c170b3a56848456cc16feb4506321606" +dependencies = [ + "bitflags 1.3.2", + "cbor-smol", + "cosey", + "delog", + "heapless", + "heapless-bytes", + "iso7816", + "serde", + "serde-indexed 0.1.1", + "serde_bytes", + "serde_repr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "custom_debug" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da7d1ad9567b3e11e877f1d7a0fa0360f04162f94965fc4448fbed41a65298e" +dependencies = [ + "custom_debug_derive", +] + +[[package]] +name = "custom_debug_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a707ceda8652f6c7624f2be725652e9524c815bf3b9d55a0b2320be2303f9c11" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" +dependencies = [ + "dbus", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + +[[package]] +name = "delog" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed991f9823b19e8a0380e198dcbb6aa6ac82727b40bfecd2c6cc634f46b7e01c" +dependencies = [ + "log", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "find-winsdk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e" +dependencies = [ + "serde", + "serde_derive", + "winreg", +] + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless-bytes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7285eba272c6af3e9f15fb9e1c1b6e7d35aa70580ffe0d47af017e97dfb6f48b" +dependencies = [ + "heapless", + "serde", + "typenum", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818c0e1d27887aaf76fe737042e27a66b796a7b099e6d2e1a72d106c2dff3fa6" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.61.2", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni", + "log", + "once_cell", + "static_assertions", + "uuid", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libwebauthn" +version = "0.9.0" +dependencies = [ + "aes", + "aes-gcm", + "apdu", + "apdu-core", + "async-trait", + "base64-url", + "bitflags 2.13.2", + "bluer", + "btleplug", + "byteorder", + "cbc", + "cosey", + "ctap-types", + "curve25519-dalek", + "dbus", + "der", + "flate2", + "futures", + "heapless", + "hex", + "hidapi", + "hkdf", + "hmac", + "http", + "icu_normalizer", + "idna", + "maplit", + "mockall", + "nfc1", + "nfc1-sys", + "num-derive", + "num-traits", + "num_enum", + "p256", + "pcsc", + "publicsuffix", + "qrcode", + "rand 0.8.8", + "reqwest", + "rustls", + "serde", + "serde-indexed 0.2.0", + "serde_bytes", + "serde_cbor_2", + "serde_derive", + "serde_json", + "serde_repr", + "sha2", + "snow", + "spki", + "test-log", + "text_io", + "thiserror 2.0.20", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "tungstenite", + "url", + "uuid", + "x509-parser", + "zeroize", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nfc1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d6dc2e4110af159c220d2d004661e380b6c40d93c5b04e839e4944f9d5291d" +dependencies = [ + "nfc1-sys", +] + +[[package]] +name = "nfc1-sys" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e41b61907c3ee9730969de23b433428ea9e934874320d1605e1214760f06cfd" +dependencies = [ + "bindgen", + "cc", + "find-winsdk", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pcsc" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd833ecf8967e65934c49d3521a175929839bf6d0e497f3bd0d3a2ca08943da" +dependencies = [ + "bitflags 2.13.2", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ef017e15d2e5592a9e39a346c1dbaea5120bab7ed7106b210ef58ebd97003" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmweb-auth-passkey-helper" +version = "0.1.0" +dependencies = [ + "base64-url", + "bluer", + "libc", + "libwebauthn", + "publicsuffix", + "qrcode", + "serde", + "serde_bytes", + "serde_cbor_2", + "serde_json", + "sha2", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-indexed" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca2da10b1f1623f47130256065e05e94fd7a98dbd26a780a4c5de831b21e5c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde-indexed" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68cf7478db8b81abcf71b6d195a34a4891bd3d39868731c4d73194d74ec7a3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror 2.0.20", + "xml", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_cbor_2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "snow" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "getrandom 0.3.4", + "p256", + "ring", + "rustc_version", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "test-log" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9c218384242b5c89b68303ab6f6fc53a312d923f0c14dc6bb860c6aeee40f1" +dependencies = [ + "env_logger", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-core" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26ef8b00e4d382e59f6a8ddb3cd790b3a5bb29f21a358a9a69ea2f29f13f27b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "test-log-macros" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944ad38adcbb71eaa682c56bceeb079e4ca82b4b3edc2a0fde5cb297b77dac8d" +dependencies = [ + "syn 2.0.119", + "test-log-core", +] + +[[package]] +name = "text_io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d8d3ca3b06292094e03841d8995e910712d2a10b5869c8f9725385b29761115" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.20", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab72a15cf68d77cb0987d3684aa8a45c5ef827e8cb49ee2f30bfd7ba2feb519f" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" +dependencies = [ + "serde", + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure 0.14.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/engine/auth-passkey-helper/Cargo.toml b/engine/auth-passkey-helper/Cargo.toml new file mode 100644 index 0000000..e58e21a --- /dev/null +++ b/engine/auth-passkey-helper/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rmweb-auth-passkey-helper" +version = "0.1.0" +edition = "2021" +rust-version = "1.90" +[workspace] +[dependencies] +libwebauthn = { path = "vendor/libwebauthn/libwebauthn" } +tokio = { version = "1.45", features = ["full"] } +qrcode = { version = "0.14.1", default-features = false } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +base64-url = "3" +publicsuffix = "2.3" +url = "2.5" +sha2 = "0.10" +libc = "0.2" +bluer = { version = "0.17", features = ["bluetoothd"] } +tracing = { version = "0.1", features = ["max_level_off", "release_max_level_off"] } +[dev-dependencies] +serde_cbor_2 = "0.13" +serde_bytes = "0.11" +[profile.release] +strip = true +panic = "abort" diff --git a/engine/auth-passkey-helper/README.md b/engine/auth-passkey-helper/README.md new file mode 100644 index 0000000..fc2555f --- /dev/null +++ b/engine/auth-passkey-helper/README.md @@ -0,0 +1,66 @@ +# Phone passkey helper + +This Linux/AArch64 helper performs one assertion through the standard caBLE v2 phone transport. It is called by the native WebKit authentication provider, never by page JavaScript. It cannot register credentials, enumerate accounts, reset authenticators, request a host PIN, or retain phone pairings. Native WebKit must first enforce secure context, frame, RP, user-activation and cancellation rules. This helper additionally validates the HTTPS origin/RP with the embedded Public Suffix List, then signs WebKit's exact 32-byte `clientDataHash`; WebKit retains its original `clientDataJSON`. + +## Private pipe protocol + +Launch without arguments, with anonymous stdin/stdout pipes. Write one UTF-8 JSON object and close stdin within two seconds: + +```json +{"version":1,"origin":"https://login.example.com","options":{"challenge":"AA","rpId":"example.com","allowCredentials":[],"userVerification":"required","timeout":120000},"clientDataHash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} +``` + +The example is synthetic. The native provider supplies real values in memory, never through arguments, URLs, logs or files. Input is at most 128 KiB. Unknown fields, duplicate fields and nonempty extensions fail closed. Supported challenges are 1–16384 bytes; allowlists at most 64 entries with 1–1024 byte IDs. Binary fields use canonical unpadded base64url. The caller's timeout is capped at 120 seconds; the process reserves cleanup time within that limit. + +The 16 KiB challenge limit is a local compatibility budget, not a WebAuthn maximum: the [WebAuthn specification](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges) sets no 1 KiB ceiling. A maximum challenge and 64 maximum-size credential IDs fit within the unchanged 128 KiB JSON budget. Tests exercise 1025/16384-byte challenges, preserve the exact browser hash and request binding, and reject both a 16385-byte challenge and input exceeding the total budget. This bound is a local interoperability limit; it does not imply that every relying party is supported. + +Stdout contains bounded JSON lines, each at most 64 KiB and total at most 512 KiB: + +- `{"type":"qr","size":N,"modules":"0101…"}`: a 21–177 square QR matrix in row order, without quiet zone; the frontend adds four modules around it. +- `{"type":"status","state":"waiting_for_phone"}`: states also include `connecting`, `verifying`, `connected`, `confirm_on_phone`. +- One terminal `result` containing `credentialId`, `authenticatorData`, `signature`, and optional `userHandle`, all canonical unpadded base64url; or `error` with name `NotAllowedError`, `NotSupportedError`, or `OperationError`. + +Signed authenticator bytes are returned verbatim, not reconstructed. The helper checks RP hash, presence, required verification, allowlist binding and bounded response fields (authenticator data 37–16384 bytes, signature 1–4096 bytes, user handle 1–64 bytes). It does not have the RP's stored public key; signature verification remains the RP's responsibility. Multiple results requiring a separate account-selection flow are unsupported. A transport close is not reported as a successful assertion or decoded `NoCredentials`. + +## Lifetime and hardware + +The helper sets parent-death SIGTERM, handles SIGTERM/SIGINT, disables core dumps and all dependency tracing, and suppresses stderr. It opens no credential or QR files. Only a verified Paper Pro (Ferrari) with one built-in `btnxpuart` adapter is supported. A request-scoped platform guard holds a finite wake lock named `rmweb-passkey--`, prepares the stock Bluetooth module/service only if no adapter exists, powers the adapter when needed, and attempts bounded restoration of its prior power after success, failure or ordinary cancellation. A module/service stack the helper itself created is additionally stopped and unloaded on the same bounded path, so restoration leaves no driver behind. Existing powered adapters are preserved; unrelated adapters are rejected. A root-owned empty lock at `/run/rmweb-passkey.lock` serializes helpers through power restoration; it contains no credential data. No boot files are changed or pre-existing modules removed. SIGKILL/power loss cannot run asynchronous restoration; the kernel wake lock expires independently. + +Before replacing the helper, stop new launches and allow active helpers to finish +restoration and exit. Never unlink the lock while a helper may hold it: replacing +its inode breaks serialization. The empty lock file can remain until reboot. + +## Rebuild and validation + +`prepare.py --cache /absolute/private/cache` verifies the pinned upstream archive and PSL, then prints a fresh source-stage path outside the checkout. It applies `patches/libwebauthn-buffered-response.patch`; generated dependency source is not committed. `Cargo.lock` fixes crate versions. In an ARM64 container with Rust 1.90 and the official Paper Pro 3.28 SDK, run: + +```sh +/path/to/stage/build-sdk.sh /path/to/stage /absolute/build-cache +``` + +The script runs helper/platform tests with the SDK target loader, exercises the vendored patch's `private_probe_order_tests` against the staged libwebauthn checkout (skipped with a notice before `prepare.py` stages it), builds the release executable and runs real-process pipe, EOF, signal and parent-death tests. No test performs a phone ceremony. Output is `build-cache/target/aarch64-unknown-linux-gnu/release/rmweb-auth-passkey-helper`. + +To export the executable, license map and complete source archive, run in the same build container (with `--init` when invoking standalone process tests): + +```sh +python3 /path/to/stage/package.py --stage /path/to/stage \ + --binary /absolute/build-cache/target/aarch64-unknown-linux-gnu/release/rmweb-auth-passkey-helper \ + --output /absolute/private/helper-artifacts --cargo /usr/local/cargo/bin/cargo \ + --image-id sha256:YOUR_VERIFIED_BUILD_IMAGE --rmweb-revision YOUR_SOURCE_COMMIT +``` + +Use an absent output directory. The exporter verifies the prepared source hashes, vendors every locked crate source/license, checks the ELF target, and writes `rmweb-auth-passkey`, `licenses/`, and `manifest.json`. It refuses altered staged sources, changed binaries, reused dependency directories and existing output. Capture the actual image ID with `docker image inspect` and repository revision with `git rev-parse HEAD`; the separate helper-source hash covers uncommitted helper changes. + +Source pin: libwebauthn v0.9.0 prerelease, commit `a6bdb700918f4c8c06c52d41f4d2d9e79888c5ad`, archive SHA256 `9cbd2d5afe03cc33f7bdbb7a9d5c81922d8008e55520beb2f4e6cfe35016bb54`, from the URL in `prepare.py`. LGPL-2.1-or-later terms are retained in `vendor/libwebauthn-COPYING`. The explicit patch preserves a decoded response queued before peer closure; its actual-source regression was red before the patch and green afterward. The PSL snapshot is a checksum-pinned official download (`2026-09-15_10-18-26_UTC`) under MPL-2.0, with notice in the file. + +On 2026-09-17, the contributor reported **PASS** from the disposable +relying-party verifier on a Paper Pro running software 3.28.0.172 / Qt 6.10.3, +after creating a test passkey on a phone and approving the browser's QR +assertion; maintainer verification of that on-device result is pending. Taken +as establishing that test's signed assertion path only once independently +verified; arbitrary account sign-in and other hardware remain unverified. See +[the acceptance harness](../../tools/passkey-acceptance/README.md). + +Ship this helper's complete source, Cargo.lock, dependency pin, patch, license +notices and rebuild recipe alongside the binary; preserve the ability to +rebuild/relink the LGPL dependency. diff --git a/engine/auth-passkey-helper/build-sdk.sh b/engine/auth-passkey-helper/build-sdk.sh new file mode 100755 index 0000000..4d78fa5 --- /dev/null +++ b/engine/auth-passkey-helper/build-sdk.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Run inside the ARM64 Rust1.90+ / official PaperPro3.28 SDK container. +set -euo pipefail +if [ "$#" -ne 2 ]; then + echo 'usage: build-sdk.sh ABSOLUTE_STAGED_SOURCE ABSOLUTE_BUILD_CACHE' >&2 + exit 2 +fi +source /opt/remarkable-sdk/environment-setup-cortexa53-crypto-remarkable-linux +test "$(qmake -query QT_VERSION)" = 6.10.3 +case "$(/usr/local/cargo/bin/rustc --version)" in "rustc 1.90.0 "*) ;; *) exit 2 ;; esac +helper_source=$1 +helper_cache=$2 +case "$helper_source:$helper_cache" in /*:/*) ;; *) exit 2 ;; esac +mkdir -p "$helper_cache/target" "$helper_cache/cargo-home" +export CARGO_HOME="$helper_cache/cargo-home" +export CARGO_TARGET_DIR="$helper_cache/target" +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$helper_source/sdk-linker.sh" +export CARGO_BUILD_TARGET=aarch64-unknown-linux-gnu +export PKG_CONFIG_ALLOW_CROSS=1 +export RUSTFLAGS='-C link-arg=-Wl,-z,relro,-z,now' +helper_sysroot=/opt/remarkable-sdk/sysroots/cortexa53-crypto-remarkable-linux +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="$helper_sysroot/lib/ld-linux-aarch64.so.1 --library-path $helper_sysroot/lib:$helper_sysroot/usr/lib" +cd "$helper_source" +/usr/local/cargo/bin/cargo test --locked --tests +# The vendored patch adds private_probe_order_tests inside libwebauthn; as a +# path dependency its tests never run above. Exercise them against the vendored +# workspace lock; scope the run to the patched module so the helper build does +# not depend on upstream's unrelated hardware-adjacent cases. prepare.py is the +# only step that unpacks the vendored checkout, so skip clearly before it. +vendor_manifest="$helper_source/vendor/libwebauthn/libwebauthn/Cargo.toml" +if [ -f "$vendor_manifest" ]; then + /usr/local/cargo/bin/cargo test --locked --manifest-path "$vendor_manifest" private_probe_order_tests +else + echo '[build-sdk] no staged libwebauthn checkout; skipping vendored patch tests (run prepare.py first)' >&2 +fi +/usr/local/cargo/bin/cargo build --release --locked +python3 tests/process.py --binary "$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/rmweb-auth-passkey-helper" \ + --loader "$helper_sysroot/lib/ld-linux-aarch64.so.1" --library-path "$helper_sysroot/lib:$helper_sysroot/usr/lib" diff --git a/engine/auth-passkey-helper/package.py b/engine/auth-passkey-helper/package.py new file mode 100644 index 0000000..8dac8dd --- /dev/null +++ b/engine/auth-passkey-helper/package.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Export the verified helper binary and complete source/license materials.""" +import argparse +import gzip +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import stat +import subprocess +import tarfile +import tempfile + +PIN = "a6bdb700918f4c8c06c52d41f4d2d9e79888c5ad" +SDK_SHA = "65e5b98f9f7c83d857c5c720f7f6cb2d61fe20f4513dec023ad00aac27c85ae4" + + +def sha(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def record(path): + return {"sha256":sha(path), "bytes":path.stat().st_size, "mode":f"{stat.S_IMODE(path.stat().st_mode):04o}"} + + +def owned_directory(path): + if not path.is_absolute() or path.is_symlink() or not path.is_dir() or path.stat().st_uid != os.getuid(): + raise ValueError("expected an owned absolute source/cache directory") + for parent in path.parents: + if parent.is_symlink(): + raise ValueError("symlink directory component") + + +def export(stage, binary, output, image_id, rmweb_revision, cargo): + owned_directory(stage) + if output.exists() or output.is_symlink(): + raise ValueError("artifact output must not already exist") + owned_directory(output.parent) + if not re.fullmatch(r"sha256:[0-9a-f]{64}",image_id) or not re.fullmatch(r"[0-9a-f]{40}",rmweb_revision): + raise ValueError("invalid build provenance") + receipt = json.loads((stage / "source-receipt.json").read_text()) + if receipt["libraryCommit"] != PIN: + raise ValueError("unexpected library revision") + for name, digest in receipt["files"].items(): + path = stage / name + if path.is_symlink() or not path.is_file() or sha(path) != digest: + raise ValueError("staged source changed after preparation") + if binary.is_symlink() or not binary.is_file() or binary.stat().st_uid != os.getuid(): + raise ValueError("expected owned regular helper binary") + if binary.stat().st_size > 64 * 1024 * 1024: + raise ValueError("helper binary exceeds size bound") + with binary.open("rb") as stream: + header = stream.read(64) + if len(header) != 64 or header[:6] != b"\x7fELF\x02\x01" or int.from_bytes(header[18:20],"little") != 183: + raise ValueError("helper must be a little-endian AArch64 ELF") + before_binary = sha(binary) + # A fresh directory ensures Cargo cannot reuse editable vendored sources. + dependencies = stage / "dependencies" + if dependencies.exists() or dependencies.is_symlink(): + raise ValueError("use a freshly prepared stage for export") + config = subprocess.check_output([cargo,"vendor","--locked",str(dependencies)],cwd=stage,text=True) + config = config.replace(str(dependencies),"dependencies") + (stage / ".cargo").mkdir() + (stage / ".cargo/config.toml").write_text(config) + temporary = Path(tempfile.mkdtemp(prefix=".helper-artifacts-",dir=output.parent)) + try: + licenses = temporary / "licenses" + licenses.mkdir() + shutil.copy2(binary,temporary / "rmweb-auth-passkey") + (temporary / "rmweb-auth-passkey").chmod(0o755) + for source,destination in [("vendor/libwebauthn-COPYING","COPYING-libwebauthn"), + ("vendor/LICENSE-rmweb","LICENSE-rmweb"), + ("patches/libwebauthn-buffered-response.patch","libwebauthn-buffered-response.patch"), + ("README.md","README-helper.md")]: + shutil.copy2(stage/source,licenses/destination) + (licenses/"NOTICE.txt").write_text("""rmweb phone passkey helper + +Statically linked libwebauthn v0.9.0 is LGPL-2.1-or-later, revision +"""+PIN+""", with the supplied buffered-response patch. +helper-source.tar.gz contains complete helper and patched dependency source, +all Cargo.lock dependency sources/license notices, the MPL-2.0 Public Suffix +List source/notice, and build/relink scripts. Its .cargo/config.toml selects +bundled crates. Unpack, modify the desired source, and run build-sdk.sh with +Rust1.90 and the official PaperPro3.28 SDK. No signature check prevents use +of a rebuilt helper. Runtime system libraries/SDK are supplied separately. +Public Suffix List terms: https://mozilla.org/MPL/2.0/. +""") + source_files = sorted(path for path in stage.rglob("*") if path.is_file()) + if any(path.is_symlink() for path in stage.rglob("*")): + raise ValueError("source archive may not contain symlinks") + with (licenses/"helper-source.tar.gz").open("wb") as raw: + with gzip.GzipFile(filename="",mode="wb",fileobj=raw,mtime=0) as compressed: + with tarfile.open(fileobj=compressed,mode="w") as archive: + for path in source_files: + info = archive.gettarinfo(str(path),arcname="auth-passkey-helper/"+str(path.relative_to(stage))) + info.uid=info.gid=0;info.uname=info.gname="";info.mtime=0 + with path.open("rb") as data:archive.addfile(info,data) + for path in licenses.iterdir():path.chmod(0o644) + if before_binary != sha(binary) or before_binary != sha(temporary/"rmweb-auth-passkey"): + raise ValueError("helper changed during export") + for name,digest in receipt["files"].items(): + if sha(stage/name) != digest:raise ValueError("source changed during export") + manifest = {"schemaVersion":1,"binary":{"path":"rmweb-auth-passkey",**record(temporary/"rmweb-auth-passkey")}, + "sourceRevision":{"rmweb":rmweb_revision,"libwebauthn":PIN,"helperSourceSha256":receipt["helperSourceSha256"]}, + "patchSHA256":sha(stage/"patches/libwebauthn-buffered-response.patch"),"SDKsha256":SDK_SHA, + "rustVersion":"1.90.0","imageId":image_id, + "licenses":{str(path.relative_to(temporary)):record(path) for path in sorted(licenses.iterdir())}} + (temporary/"manifest.json").write_text(json.dumps(manifest,indent=2)+"\n") + temporary.rename(output) + finally: + if temporary.exists():shutil.rmtree(temporary) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument("--stage",type=Path,required=True) + parser.add_argument("--binary",type=Path,required=True) + parser.add_argument("--output",type=Path,required=True) + parser.add_argument("--image-id",required=True) + parser.add_argument("--rmweb-revision",required=True) + parser.add_argument("--cargo",default="cargo") + args=parser.parse_args() + export(args.stage,args.binary,args.output,args.image_id,args.rmweb_revision,args.cargo) + print(args.output) + +if __name__ == "__main__": + try: main() + except (OSError,ValueError,KeyError,subprocess.CalledProcessError) as error: + raise SystemExit(f"Helper export failed: {error}") diff --git a/engine/auth-passkey-helper/patches/libwebauthn-buffered-response.patch b/engine/auth-passkey-helper/patches/libwebauthn-buffered-response.patch new file mode 100644 index 0000000..a6a2c6f --- /dev/null +++ b/engine/auth-passkey-helper/patches/libwebauthn-buffered-response.patch @@ -0,0 +1,65 @@ +--- a/libwebauthn/src/transport/cable/channel.rs ++++ b/libwebauthn/src/transport/cable/channel.rs +@@ -182,7 +182,11 @@ + + async fn cbor_recv(&mut self, timeout: Duration) -> Result { + // First, wait for connection to be established (no timeout for handshake) +- self.wait_for_connection().await?; ++ if let Err(error) = self.wait_for_connection().await { ++ // Protocol queues a decoded reply before marking the connection ++ // terminated. Preserve that reply even if the peer then closes. ++ return self.cbor_receiver.try_recv().map_err(|_| error); ++ } + + // Now apply timeout only to the actual CBOR operation + match time::timeout(timeout, self.cbor_receiver.recv()).await { +@@ -224,3 +228,49 @@ + self.persistent_token_store.clone() + } + } ++ ++#[cfg(test)] ++mod private_probe_order_tests { ++ use super::*; ++ use crate::webauthn::error::CtapError; ++ ++ fn channel(state: ConnectionState) -> (CableChannel, mpsc::Sender, watch::Sender) { ++ let (cbor_sender, _) = mpsc::channel(1); ++ let (response_sender, cbor_receiver) = mpsc::channel(1); ++ let (ux_update_sender, _) = broadcast::channel(1); ++ let (state_sender, connection_state_receiver) = watch::channel(state); ++ (CableChannel { ++ handle_connection: tokio::spawn(std::future::pending()), ++ cbor_sender, cbor_receiver, ux_update_sender, connection_state_receiver, ++ persistent_token_store: None, ++ }, response_sender, state_sender) ++ } ++ ++ #[tokio::test] ++ async fn buffered_error_survives_peer_shutdown() { ++ let (mut channel, response, state) = channel(ConnectionState::Connected); ++ // Actual protocol queues the decoded CTAP frame before a later peer ++ // shutdown marks this watch Terminated. Force that valid ordering. ++ response.send(CborResponse::try_from(&vec![0x2e]).unwrap()).await.unwrap(); ++ state.send(ConnectionState::Terminated).unwrap(); ++ drop(response); ++ let result = channel.cbor_recv(Duration::from_millis(50)).await; ++ assert!(result.is_ok(), "buffered CTAP response was hidden by peer shutdown"); ++ assert_eq!(result.unwrap().status_code, CtapError::NoCredentials); ++ } ++ ++ #[tokio::test] ++ async fn connected_error_is_decoded_without_body() { ++ let (mut channel, response, _state) = channel(ConnectionState::Connected); ++ response.send(CborResponse::try_from(&vec![0x2e]).unwrap()).await.unwrap(); ++ let result = channel.cbor_recv(Duration::from_millis(50)).await.unwrap(); ++ assert_eq!(result.status_code, CtapError::NoCredentials); ++ } ++ ++ #[tokio::test] ++ async fn closed_empty_channel_does_not_invent_response() { ++ let (mut channel, response, _state) = channel(ConnectionState::Terminated); ++ drop(response); ++ assert!(channel.cbor_recv(Duration::from_millis(50)).await.is_err()); ++ } ++} diff --git a/engine/auth-passkey-helper/prepare.py b/engine/auth-passkey-helper/prepare.py new file mode 100755 index 0000000..8b44131 --- /dev/null +++ b/engine/auth-passkey-helper/prepare.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Stage source and the checksum-pinned patched dependency outside the checkout.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import subprocess +import tarfile +import tempfile +import urllib.request + +PIN = "a6bdb700918f4c8c06c52d41f4d2d9e79888c5ad" +SHA256 = "9cbd2d5afe03cc33f7bdbb7a9d5c81922d8008e55520beb2f4e6cfe35016bb54" +BYTES = 328132 +PSL_SHA256 = "bb3d3bb844f1d172de41f0c5089e7b2b7243b02698e4a64120ffcf49ec84c0ee" +URL = f"https://codeload.github.com/linux-credentials/libwebauthn/tar.gz/{PIN}" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cache", type=Path, required=True) + args = parser.parse_args() + cache = args.cache.absolute() + repository = Path(__file__).resolve().parents[2] + if cache == repository or repository in cache.parents: + raise SystemExit("build cache must remain outside the checkout") + for item in [cache, *cache.parents]: + if item.is_symlink(): + raise SystemExit("cache must not contain symlink components") + cache.mkdir(mode=0o700, parents=True, exist_ok=True) + info = cache.stat() + if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077: + raise SystemExit("cache must be owned and private (0700)") + source = Path(__file__).resolve().parent + if hashlib.sha256((source / "vendor/public_suffix_list.dat").read_bytes()).hexdigest() != PSL_SHA256: + raise SystemExit("public suffix list checksum mismatch") + archive = cache / f"libwebauthn-{PIN}.tar.gz" + if archive.exists() or archive.is_symlink(): + info = archive.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_size != BYTES: + raise SystemExit("source archive is not the expected regular file") + data = archive.read_bytes() + else: + with urllib.request.urlopen(URL, timeout=30) as response: + data = response.read(BYTES + 1) + if len(data) != BYTES or hashlib.sha256(data).hexdigest() != SHA256: + raise SystemExit("source archive checksum mismatch") + with archive.open("xb") as output: + output.write(data) + archive.chmod(0o600) + if hashlib.sha256(data).hexdigest() != SHA256: + raise SystemExit("source archive checksum mismatch") + stage = Path(tempfile.mkdtemp(prefix="helper-", dir=cache)) + for name in [".gitignore", "Cargo.toml", "Cargo.lock", "prepare.py", "package.py", "src", "tests", "patches", "vendor", "README.md", "build-sdk.sh", "sdk-linker.sh"]: + item = source / name + if not item.exists(): + if name in {"Cargo.lock", "README.md"}: + continue + raise SystemExit(f"source missing: {name}") + if item.is_dir(): + shutil.copytree(item, stage / name, ignore=shutil.ignore_patterns("libwebauthn")) + else: + shutil.copy2(item, stage / name) + unpack = stage / "unpack" + unpack.mkdir() + with tarfile.open(archive, "r:gz") as bundle: + for entry in bundle.getmembers(): + if not (entry.isfile() or entry.isdir()) or entry.name.startswith("/") or ".." in Path(entry.name).parts: + raise SystemExit("unexpected archive member") + bundle.extractall(unpack, filter="data") + upstream = unpack / f"libwebauthn-{PIN}" + upstream.rename(stage / "vendor/libwebauthn") + unpack.rmdir() + subprocess.run(["patch", "--batch", "--forward", "-p1", "-i", str(stage / "patches/libwebauthn-buffered-response.patch")], + cwd=stage / "vendor/libwebauthn", check=True, stdout=subprocess.DEVNULL) + files = {str(item.relative_to(stage)): hashlib.sha256(item.read_bytes()).hexdigest() + for item in stage.rglob("*") if item.is_file()} + helper_files = {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest() + for p in sorted(source.rglob("*")) if p.is_file() and "__pycache__" not in p.parts + and "target" not in p.relative_to(source).parts and "libwebauthn" not in p.relative_to(source).parts} + helper_sha = hashlib.sha256(json.dumps(helper_files,sort_keys=True,separators=(",", ":")).encode()).hexdigest() + (stage / "source-receipt.json").write_text(json.dumps({"libraryCommit": PIN, "archiveSha256": SHA256, "helperSourceSha256": helper_sha, + "archiveUrl": URL, "files": files}, indent=2) + "\n") + print(stage) + + +if __name__ == "__main__": + main() diff --git a/engine/auth-passkey-helper/sdk-linker.sh b/engine/auth-passkey-helper/sdk-linker.sh new file mode 100755 index 0000000..b49cbeb --- /dev/null +++ b/engine/auth-passkey-helper/sdk-linker.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -eu +output= +previous= +for argument in "$@"; do + if [ "$previous" = -o ]; then output=$argument; break; fi + previous=$argument +done +case "$output" in + "$CARGO_TARGET_DIR"/aarch64-unknown-linux-gnu/*) ;; + *) exec /usr/bin/cc "$@" ;; +esac +# Cargo's host loader path is incompatible with the SDK environment script. +unset LD_LIBRARY_PATH +source /opt/remarkable-sdk/environment-setup-cortexa53-crypto-remarkable-linux +# CC is the trusted SDK compiler plus its required sysroot/architecture flags. +exec $CC "$@" diff --git a/engine/auth-passkey-helper/src/ceremony.rs b/engine/auth-passkey-helper/src/ceremony.rs new file mode 100644 index 0000000..7c82414 --- /dev/null +++ b/engine/auth-passkey-helper/src/ceremony.rs @@ -0,0 +1,67 @@ +use std::time::Duration; +use libwebauthn::transport::cable::channel::{CableUpdate, CableUxUpdate}; +use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, CableTransports, QrCodeOperationHint}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; +use libwebauthn::proto::ctap2::Ctap2; +use libwebauthn::webauthn::error::{CtapError, WebAuthnError}; +use libwebauthn::UvUpdate; +use tokio::io::AsyncWrite; +use crate::protocol::{Prepared, Output, Message, ErrorName, qr_message, assertion_message}; + +pub async fn assertion(prepared: &Prepared, output: &mut Output) -> Result { + let mut device = CableQrCodeDevice::new_transient(QrCodeOperationHint::GetAssertionRequest, CableTransports::CloudAssistedOnly) + .map_err(|_| ErrorName::OperationError)?; + // The payload stays in memory and is emitted only as a QR matrix to the + // owning browser process. No pairing record or credential file is created. + let qr = qr_message(&device.qr_code.to_string())?; + output.send(&qr).await.map_err(|_| ErrorName::OperationError)?; + let mut channel = tokio::time::timeout(Duration::from_secs(2), device.channel(ChannelSettings::default())) + .await.map_err(|_| ErrorName::NotAllowedError)?.map_err(|_| ErrorName::NotAllowedError)?; + let mut updates = channel.get_ux_update_receiver(); + let result; + { + let ceremony = channel.ctap2_get_assertion(&prepared.ctap, prepared.timeout); + tokio::pin!(ceremony); + let deadline = tokio::time::sleep(prepared.timeout); + tokio::pin!(deadline); + let mut last_status = None; + result = loop { + tokio::select! { + biased; + response = &mut ceremony => { + break match response { + Ok(response) => assertion_message(prepared, response), + Err(WebAuthnError::Transport(_)) => Err(ErrorName::NotAllowedError), + Err(WebAuthnError::Ctap(CtapError::UnsupportedOption | CtapError::UnsupportedExtension | CtapError::UnsupportedAlgorithm)) => Err(ErrorName::NotSupportedError), + Err(WebAuthnError::Ctap(_)) => Err(ErrorName::NotAllowedError), + Err(_) => Err(ErrorName::OperationError), + }; + } + _ = &mut deadline => { break Err(ErrorName::NotAllowedError); } + update = updates.recv() => { + let state = match update { + Ok(CableUxUpdate::CableUpdate(CableUpdate::ProximityCheck)) => "waiting_for_phone", + Ok(CableUxUpdate::CableUpdate(CableUpdate::Connecting)) => "connecting", + Ok(CableUxUpdate::CableUpdate(CableUpdate::Authenticating)) => "verifying", + Ok(CableUxUpdate::CableUpdate(CableUpdate::Connected)) => "connected", + Ok(CableUxUpdate::CableUpdate(CableUpdate::Error(_))) => { break Err(ErrorName::NotAllowedError); } + Ok(CableUxUpdate::UvUpdate(UvUpdate::PresenceRequired)) => "confirm_on_phone", + Ok(CableUxUpdate::UvUpdate(UvUpdate::PinRequired(pin))) => { pin.cancel(); break Err(ErrorName::NotSupportedError); } + Ok(CableUxUpdate::UvUpdate(UvUpdate::PinNotSet(pin))) => { pin.cancel(); break Err(ErrorName::NotSupportedError); } + Ok(CableUxUpdate::UvUpdate(UvUpdate::UvRetry { .. })) => { break Err(ErrorName::NotAllowedError); } + Err(_) => { break Err(ErrorName::NotAllowedError); } + }; + if last_status != Some(state) { + if output.send(&Message::Status { state }).await.is_err() { break Err(ErrorName::OperationError); } + last_status = Some(state); + } + } + } + }; + } + // Upstream close currently does no work; Drop aborts the owned connection + // task. Process exit subsequently closes its D-Bus discovery ownership. + let _ = tokio::time::timeout(Duration::from_secs(1), channel.close()).await; + drop(channel); + result +} diff --git a/engine/auth-passkey-helper/src/lib.rs b/engine/auth-passkey-helper/src/lib.rs new file mode 100644 index 0000000..6407908 --- /dev/null +++ b/engine/auth-passkey-helper/src/lib.rs @@ -0,0 +1,2 @@ +pub mod protocol; +pub mod ceremony; diff --git a/engine/auth-passkey-helper/src/main.rs b/engine/auth-passkey-helper/src/main.rs new file mode 100644 index 0000000..570be59 --- /dev/null +++ b/engine/auth-passkey-helper/src/main.rs @@ -0,0 +1,92 @@ +use std::os::fd::{FromRawFd, OwnedFd}; +use std::time::Duration; +use rmweb_auth_passkey_helper::{ceremony, protocol::{self, ErrorName, Message, Output}}; +mod platform; + +fn duplicate(fd: i32) -> Option { + let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) }; + if duplicate < 0 { None } else { Some(unsafe { OwnedFd::from_raw_fd(duplicate) }) } +} + +#[tokio::main(flavor = "multi_thread", worker_threads = 2)] +async fn main() { + let deadline = tokio::time::Instant::now() + Duration::from_secs(115); + // Errors and panic payloads are never diagnostics on the helper's pipes. + std::panic::set_hook(Box::new(|_| {})); + unsafe { + libc::umask(0o077); + if libc::setrlimit(libc::RLIMIT_CORE, &libc::rlimit { rlim_cur: 0, rlim_max: 0 }) != 0 { + std::process::exit(1); + } + let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC); + if null < 0 || libc::dup2(null, libc::STDERR_FILENO) != libc::STDERR_FILENO { + std::process::exit(1); + } + if null != libc::STDERR_FILENO { libc::close(null); } + } + let Some(input_fd) = duplicate(libc::STDIN_FILENO) else { std::process::exit(1); }; + let Some(output_fd) = duplicate(libc::STDOUT_FILENO) else { std::process::exit(1); }; + // Anonymous pipes only; these APIs set nonblocking mode and do no blocking + // stdio worker I/O that could outlive the overall deadline. + let Ok(mut input) = tokio::net::unix::pipe::Receiver::from_owned_fd(input_fd) else { std::process::exit(1); }; + let Ok(output_pipe) = tokio::net::unix::pipe::Sender::from_owned_fd(output_fd) else { std::process::exit(1); }; + let mut output = Output::new(output_pipe); + let Ok(mut terminate) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) else { std::process::exit(1); }; + let Ok(mut interrupt) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) else { std::process::exit(1); }; + let parent = unsafe { libc::getppid() }; + if parent <= 1 || unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) } != 0 || unsafe { libc::getppid() } != parent { + let _ = output.send(&Message::Error { name: ErrorName::NotAllowedError }).await; + std::process::exit(1); + } + let validated = { + let work = async { + if std::env::args_os().len() != 1 { return Err(ErrorName::OperationError); } + let bytes = tokio::time::timeout(Duration::from_secs(2), protocol::read_input(&mut input)) + .await.map_err(|_| ErrorName::NotAllowedError)??; + protocol::prepare(&bytes).await + }; + tokio::pin!(work); + tokio::select! { + biased; + _ = terminate.recv() => Err(ErrorName::NotAllowedError), + _ = interrupt.recv() => Err(ErrorName::NotAllowedError), + result = &mut work => result, + } + }; + let mut guard = None; + let result = match validated { + Err(error) => Err(error), + Ok(prepared) => { + // Keep the original signal receivers: a TERM queued after input + // validation must still cancel before acquisition touches hardware. + // Acquire owns partial restoration; never drop it midway. + match platform::PlatformGuard::acquire(async { + tokio::select! { + biased; + _ = terminate.recv() => (), + _ = interrupt.recv() => (), + } + }).await { + Err(()) => Err(ErrorName::NotAllowedError), + Ok(active_guard) => { + guard = Some(active_guard); + tokio::select! { + biased; + _ = terminate.recv() => Err(ErrorName::NotAllowedError), + _ = interrupt.recv() => Err(ErrorName::NotAllowedError), + _ = tokio::time::sleep_until(deadline) => Err(ErrorName::NotAllowedError), + result = ceremony::assertion(&prepared, &mut output) => result, + } + } + } + } + }; + // Reserve the final five seconds of the 120-second process budget for + // platform restoration and a bounded terminal IPC record. + if let Some(guard) = &mut guard { guard.restore().await; } + drop(guard); + let ok = result.is_ok(); + let terminal = result.unwrap_or_else(|name| Message::Error { name }); + let delivered = output.send(&terminal).await.is_ok(); + std::process::exit(if ok && delivered { 0 } else { 1 }); +} diff --git a/engine/auth-passkey-helper/src/platform.rs b/engine/auth-passkey-helper/src/platform.rs new file mode 100644 index 0000000..893f7e1 --- /dev/null +++ b/engine/auth-passkey-helper/src/platform.rs @@ -0,0 +1,519 @@ +//! Request-scoped Ferrari Bluetooth preparation. Call only after request validation. +//! Normal/error cancellation must await the bounded `restore` attempt; a D-Bus +//! failure cannot guarantee power restoration. A module/service stack created by +//! preparation is stopped and unloaded on the same bounded path. Drop only +//! attempts wake unlock synchronously. The finite kernel timeout also bounds a +//! SIGKILL wake leak. + +use std::fs::{self, OpenOptions}; +use std::io::{Read, Write}; +use std::os::fd::AsRawFd; +use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; +use std::path::Path; +use std::process::Stdio; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; +use tokio::process::Command; +use tokio::time::{sleep, timeout}; + +const ACQUIRE_BUDGET: Duration = Duration::from_secs(12); +const RESTORE_BUDGET: Duration = Duration::from_secs(3); +const WAKE_NANOSECONDS: u64 = 130_000_000_000; +const ADAPTERS: &str = "/sys/class/bluetooth"; +const DRIVER: &str = "/sys/bus/serial/drivers/btnxpuart"; + +pub struct PlatformGuard(Guard); + +impl PlatformGuard { + pub async fn acquire(cancelled: impl std::future::Future) -> Result { + // Reuse the caller's already-registered signal receivers, including any + // queued cancellation. Await acquisition so its error path can restore. + Guard::acquire_until(System::new(), ACQUIRE_BUDGET, RESTORE_BUDGET, cancelled) + .await + .map(Self) + } + + pub async fn restore(&mut self) { + self.0.restore().await; + } +} + +// A small injectable hardware boundary lets tests exercise the same restoration +// state machine without touching sysfs, D-Bus, services or kernel modules. +trait Platform { + fn wake(&mut self) -> Result<(), ()>; + fn unlock(&mut self); + fn created_adapter(&self) -> bool; + async fn prepare(&mut self) -> Result; + async fn set_powered(&mut self, powered: bool) -> Result<(), ()>; + async fn release_created(&mut self) -> Result<(), ()>; +} + +struct Guard { + platform: P, + restore_power: Option, + restore_budget: Duration, + restored: bool, +} + +impl Guard

{ + async fn acquire(platform: P, acquire_budget: Duration, restore_budget: Duration) -> Result { + Self::acquire_until(platform, acquire_budget, restore_budget, std::future::pending()).await + } + + async fn acquire_until(platform: P, acquire_budget: Duration, restore_budget: Duration, + cancelled: impl std::future::Future) -> Result { + let mut guard = Self { platform, restore_power: None, restore_budget, restored: false }; + let result = tokio::select! { + biased; + _ = cancelled => Err(()), + result = timeout(acquire_budget, async { + guard.platform.wake()?; + let powered = guard.platform.prepare().await?; + // A newly introduced adapter did not have an enabled prior state. + if guard.platform.created_adapter() || !powered { + guard.restore_power = Some(false); + } + if !powered { + // Record the restoration before the D-Bus write: its acknowledgement + // may time out even though the adapter changed power state. + guard.platform.set_powered(true).await?; + } + Ok(()) + }) => result.map_err(|_| ()).and_then(|value| value), + }; + if result.is_ok() { + Ok(guard) + } else { + guard.restore().await; + Err(()) + } + } + + async fn restore(&mut self) { + if self.restored { self.platform.unlock(); return; } + self.restored = true; + let target = self.restore_power.or_else(|| self.platform.created_adapter().then_some(false)); + if let Some(powered) = target { + if matches!(timeout(self.restore_budget, self.platform.set_powered(powered)).await, Ok(Ok(()))) { + self.restore_power = None; + } + } + if self.platform.created_adapter() { + // A created stack has no prior state: remove the service and module + // started during preparation so restoration leaves no driver behind. + // Bounded like the power write; the wake lock is still released. + let _ = timeout(self.restore_budget, self.platform.release_created()).await; + } + self.platform.unlock(); + } +} + +impl Drop for Guard

{ + fn drop(&mut self) { + self.platform.unlock(); + } +} + +struct System { + wake_name: String, + wake_held: bool, + created: bool, + session: Option, + adapter: Option<(String, bluer::Adapter)>, + lease: Option, +} + +impl System { + fn new() -> Self { + static SEQUENCE: AtomicU64 = AtomicU64::new(0); + Self { + wake_name: format!("rmweb-passkey-{}-{}", std::process::id(), SEQUENCE.fetch_add(1, Ordering::Relaxed)), + wake_held: false, + created: false, + session: None, + adapter: None, + lease: None, + } + } + + async fn adapter(&mut self) -> Result { + let name = sole_builtin_adapter(Path::new(ADAPTERS), Path::new(DRIVER))?; + if let Some((previous, adapter)) = &self.adapter { + // Recheck kernel ownership before each write; never switch to a + // different adapter because BlueZ changed its default selection. + if previous != &name { return Err(()); } + return Ok(adapter.clone()); + } + if self.session.is_none() { + self.session = Some(bluer::Session::new().await.map_err(|_| ())?); + } + let adapter = self.session.as_ref().ok_or(())?.adapter(&name).map_err(|_| ())?; + self.adapter = Some((name, adapter.clone())); + Ok(adapter) + } +} + +impl Platform for System { + fn wake(&mut self) -> Result<(), ()> { + // Keep one process responsible for prior-power restoration. Never unlink + // this file: replacing its inode would allow overlapping leases. + self.lease = Some(acquire_lease(Path::new("/run/rmweb-passkey.lock"), 0)?); + let mut model = Vec::new(); + let file = OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK) + .open("/sys/firmware/devicetree/base/model").map_err(|_| ())?; + if !file.metadata().map_err(|_| ())?.is_file() { return Err(()); } + file.take(129).read_to_end(&mut model).map_err(|_| ())?; + if !ferrari_model(&model) { return Err(()); } + self.wake_held = true; + write_sysfs("/sys/power/wake_lock", format!("{} {}", self.wake_name, WAKE_NANOSECONDS).as_bytes()) + } + + fn unlock(&mut self) { + if self.wake_held { + if write_sysfs("/sys/power/wake_unlock", self.wake_name.as_bytes()).is_ok() { + self.wake_held = false; + } + } + self.lease.take(); + } + + fn created_adapter(&self) -> bool { self.created } + + async fn prepare(&mut self) -> Result { + let (builtin, count) = builtin_adapter(Path::new(ADAPTERS), Path::new(DRIVER))?; + // The transport library selects its own adapter. With no public pinning + // API, require the built-in to be the only available adapter. + if builtin.is_some() && count != 1 { return Err(()); } + if builtin.is_none() { + // The presence of an unrelated adapter is not permission to alter it. + if count != 0 { return Err(()); } + self.created = true; + command("/sbin/modprobe", &["btnxpuart"]).await?; + command("/bin/systemctl", &["start", "bluetooth.service"]).await?; + } + // Existing built-in adapters must already expose their prior BlueZ power + // state. Do not restart a service or reload a pre-existing driver. + loop { + if let Ok(adapter) = self.adapter().await { + if let Ok(powered) = adapter.is_powered().await { + return Ok(powered); + } + } + sleep(Duration::from_millis(200)).await; + } + } + + async fn set_powered(&mut self, powered: bool) -> Result<(), ()> { + self.adapter().await?.set_powered(powered).await.map_err(|_| ()) + } + + async fn release_created(&mut self) -> Result<(), ()> { + // Reverse the preparation order: the service releases the adapter before + // its driver module can unload. A failing stop must not strand the + // loaded module, so each step runs independently. + let service = command("/bin/systemctl", &["stop", "bluetooth.service"]).await; + let module = command("/sbin/modprobe", &["-r", "btnxpuart"]).await; + service.and(module) + } +} + +fn ferrari_model(bytes: &[u8]) -> bool { + bytes == b"reMarkable Ferrari" || bytes == b"reMarkable Ferrari\0" +} + +fn builtin_adapter(directory: &Path, expected_driver: &Path) -> Result<(Option, usize), ()> { + let entries = match fs::read_dir(directory) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok((None, 0)), + Err(_) => return Err(()), + }; + let expected_driver = fs::canonicalize(expected_driver).ok(); + let mut builtin = None; + let mut count = 0; + for (index, entry) in entries.enumerate() { + if index >= 32 { return Err(()); } + let entry = entry.map_err(|_| ())?; + let name = entry.file_name().into_string().map_err(|_| ())?; + let Some(number) = name.strip_prefix("hci") else { continue; }; + // Connection and rfcomm class entries can coexist with adapters. + if number.is_empty() || number.len() > 8 || !number.bytes().all(|c| c.is_ascii_digit()) { continue; } + count += 1; + let driver = fs::canonicalize(entry.path().join("device/driver")).ok(); + if expected_driver.is_some() && driver == expected_driver { + if builtin.replace(name).is_some() { return Err(()); } + } + } + Ok((builtin, count)) +} + +fn sole_builtin_adapter(directory: &Path, expected_driver: &Path) -> Result { + let (name, count) = builtin_adapter(directory, expected_driver)?; + if count != 1 { return Err(()); } + name.ok_or(()) +} + +fn write_sysfs(path: &str, bytes: &[u8]) -> Result<(), ()> { + let mut file = OpenOptions::new().write(true) + .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK).open(path).map_err(|_| ())?; + let metadata = file.metadata().map_err(|_| ())?; + let mut filesystem = std::mem::MaybeUninit::::uninit(); + if unsafe { libc::fstatfs(file.as_raw_fd(), filesystem.as_mut_ptr()) } != 0 { return Err(()); } + let filesystem = unsafe { filesystem.assume_init() }; + if !trusted_sysfs_node(metadata.mode(), metadata.uid(), filesystem.f_type) { return Err(()); } + file.write_all(bytes).map_err(|_| ()) +} + +fn trusted_sysfs_node(mode: u32, uid: u32, filesystem: libc::c_long) -> bool { + // Stock Ferrari wake nodes are root:500 mode 0660. Group write is valid only + // after confirming this opened descriptor belongs to the kernel sysfs, where + // that group cannot replace the node with an ordinary file. + filesystem == libc::SYSFS_MAGIC && uid == 0 + && mode & libc::S_IFMT == libc::S_IFREG && mode & 0o002 == 0 +} + +fn acquire_lease(path: &Path, owner: u32) -> Result { + let file = OpenOptions::new().read(true).write(true).create(true).mode(0o600) + .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC) + .open(path).map_err(|_| ())?; + let metadata = file.metadata().map_err(|_| ())?; + if !metadata.is_file() || metadata.uid() != owner || metadata.mode() & 0o777 != 0o600 + || metadata.len() != 0 || metadata.nlink() != 1 { return Err(()); } + if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 { + return Err(()); + } + Ok(file) +} + +async fn command(program: &str, arguments: &[&str]) -> Result<(), ()> { + let mut child = Command::new(program).args(arguments) + .stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()) + .kill_on_drop(true).spawn().map_err(|_| ())?; + match timeout(Duration::from_secs(3), child.wait()).await { + Ok(Ok(status)) if status.success() => Ok(()), + _ => Err(()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + #[derive(Default)] + struct State { calls: Vec<&'static str>, awake: bool } + struct Fake { + state: Arc>, powered: bool, created: bool, + fail_prepare: bool, fail_power_on: bool, stall_restore: bool, + fail_teardown: bool, stall_teardown: bool, + } + impl Platform for Fake { + fn wake(&mut self) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); state.calls.push("wake"); state.awake = true; Ok(()) + } + fn unlock(&mut self) { + let mut state = self.state.lock().unwrap(); + if state.awake { state.calls.push("unlock"); state.awake = false; } + } + fn created_adapter(&self) -> bool { self.created } + async fn prepare(&mut self) -> Result { + self.state.lock().unwrap().calls.push("prepare"); + if self.fail_prepare { Err(()) } else { Ok(self.powered) } + } + async fn set_powered(&mut self, powered: bool) -> Result<(), ()> { + self.state.lock().unwrap().calls.push(if powered { "on" } else { "off" }); + if !powered && self.stall_restore { std::future::pending::<()>().await; } + if powered && self.fail_power_on { Err(()) } else { Ok(()) } + } + async fn release_created(&mut self) -> Result<(), ()> { + self.state.lock().unwrap().calls.push("teardown"); + if self.stall_teardown { std::future::pending::<()>().await; } + if self.fail_teardown { Err(()) } else { Ok(()) } + } + } + fn fake(powered: bool) -> (Fake, Arc>) { + let state = Arc::new(Mutex::new(State::default())); + (Fake { state: state.clone(), powered, created: false, fail_prepare: false, + fail_power_on: false, stall_restore: false, fail_teardown: false, + stall_teardown: false }, state) + } + async fn acquire(fake: Fake) -> Result, ()> { + Guard::acquire(fake, Duration::from_millis(100), Duration::from_millis(20)).await + } + + #[tokio::test] + async fn already_powered_adapter_is_not_changed() { + let (fake, state) = fake(true); + let mut guard = acquire(fake).await.unwrap(); guard.restore().await; drop(guard); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "unlock"]); + } + #[tokio::test] + async fn queued_cancellation_prevents_all_hardware_access() { + let (fake, state) = fake(false); + let result = Guard::acquire_until(fake, Duration::from_millis(100), + Duration::from_millis(20), std::future::ready(())).await; + assert!(result.is_err()); + assert!(state.lock().unwrap().calls.is_empty()); + } + #[tokio::test] + async fn unpowered_adapter_is_restored_after_use() { + let (fake, state) = fake(false); + let mut guard = acquire(fake).await.unwrap(); guard.restore().await; drop(guard); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "off", "unlock"]); + } + #[tokio::test] + async fn uncertain_power_on_still_restores() { + let (mut fake, state) = fake(false); fake.fail_power_on = true; + assert!(acquire(fake).await.is_err()); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "off", "unlock"]); + } + #[tokio::test] + async fn partial_new_adapter_failure_restores_power_off() { + let (mut fake, state) = fake(true); fake.created = true; fake.fail_prepare = true; + assert!(acquire(fake).await.is_err()); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "off", "teardown", "unlock"]); + } + #[tokio::test] + async fn newly_created_powered_adapter_is_disabled_after_use() { + let (mut fake, state) = fake(true); fake.created = true; + let mut guard = acquire(fake).await.unwrap(); guard.restore().await; guard.restore().await; + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "off", "teardown", "unlock"]); + } + #[tokio::test] + async fn created_stack_teardown_failure_still_releases_wake_lock() { + let (mut fake, state) = fake(true); fake.created = true; fake.fail_teardown = true; + let mut guard = acquire(fake).await.unwrap(); guard.restore().await; + assert!(!state.lock().unwrap().awake); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "off", "teardown", "unlock"]); + } + #[tokio::test] + async fn created_stack_teardown_is_bounded() { + let (mut fake, state) = fake(true); fake.created = true; fake.stall_teardown = true; + let mut guard = acquire(fake).await.unwrap(); + timeout(Duration::from_millis(100), guard.restore()).await.unwrap(); + assert!(!state.lock().unwrap().awake); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "off", "teardown", "unlock"]); + } + #[tokio::test] + async fn stalled_power_off_does_not_skip_created_teardown() { + let (mut fake, state) = fake(false); fake.created = true; fake.stall_restore = true; + let mut guard = acquire(fake).await.unwrap(); + timeout(Duration::from_millis(100), guard.restore()).await.unwrap(); + assert!(!state.lock().unwrap().awake); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "off", "teardown", "unlock"]); + } + #[tokio::test] + async fn restore_timeout_releases_wake_lock() { + let (mut fake, state) = fake(false); fake.stall_restore = true; + let mut guard = acquire(fake).await.unwrap(); + timeout(Duration::from_millis(100), guard.restore()).await.unwrap(); + assert!(!state.lock().unwrap().awake); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "off", "unlock"]); + } + #[tokio::test] + async fn drop_releases_only_its_wake_lock() { + let (fake, state) = fake(false); drop(acquire(fake).await.unwrap()); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "unlock"]); + } + #[tokio::test] + async fn cancellation_during_uncertain_power_write_restores() { + let (mut fake, state) = fake(false); + fake.stall_restore = true; + // The cancellation path shares the same bounded restoration as failure. + let cancelled = async { sleep(Duration::from_millis(5)).await; }; + struct PendingPower(Fake); + impl Platform for PendingPower { + fn wake(&mut self) -> Result<(), ()> { self.0.wake() } + fn unlock(&mut self) { self.0.unlock(); } + fn created_adapter(&self) -> bool { false } + async fn prepare(&mut self) -> Result { self.0.prepare().await } + async fn set_powered(&mut self, powered: bool) -> Result<(), ()> { + if powered { + self.0.state.lock().unwrap().calls.push("on"); + std::future::pending().await + } else { self.0.set_powered(false).await } + } + async fn release_created(&mut self) -> Result<(), ()> { self.0.release_created().await } + } + let result = Guard::acquire_until(PendingPower(fake), Duration::from_millis(100), + Duration::from_millis(20), cancelled).await; + assert!(result.is_err()); + assert_eq!(state.lock().unwrap().calls, ["wake", "prepare", "on", "off", "unlock"]); + } + #[test] + fn adapter_selection_ignores_connections_and_never_uses_external_adapter() { + use std::os::unix::fs::symlink; + static NEXT: AtomicU64 = AtomicU64::new(0); + let root = std::env::temp_dir().join(format!("rmweb-platform-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + fs::create_dir(&root).unwrap(); + let adapters = root.join("bluetooth"); + let driver = root.join("btnxpuart"); + let external = root.join("usb"); + fs::create_dir(&adapters).unwrap(); fs::create_dir(&driver).unwrap(); fs::create_dir(&external).unwrap(); + let add = |name: &str, target: &Path| { + let device = adapters.join(name).join("device"); + fs::create_dir_all(&device).unwrap(); symlink(target, device.join("driver")).unwrap(); + }; + add("hci1", &external); add("hci0:11", &driver); + assert_eq!(builtin_adapter(&adapters, &driver), Ok((None, 1))); + assert!(sole_builtin_adapter(&adapters, &driver).is_err()); + add("hci0", &driver); + assert_eq!(builtin_adapter(&adapters, &driver), Ok((Some("hci0".into()), 2))); + assert!(sole_builtin_adapter(&adapters, &driver).is_err()); + fs::remove_dir_all(adapters.join("hci1")).unwrap(); + assert_eq!(sole_builtin_adapter(&adapters, &driver), Ok("hci0".into())); + add("hci2", &driver); + assert!(builtin_adapter(&adapters, &driver).is_err()); + fs::remove_dir_all(root).unwrap(); + } + #[test] + fn model_gate_is_exact() { + assert!(ferrari_model(b"reMarkable Ferrari\0")); + assert!(ferrari_model(b"reMarkable Ferrari")); + for bad in [b"reMarkable Ferrari2".as_slice(), b"reMarkable Ferrari\0trailing", b"reMarkable 2", b""] { + assert!(!ferrari_model(bad)); + } + } + #[test] + fn stock_group_writable_wake_nodes_require_kernel_sysfs() { + let stock_mode = libc::S_IFREG | 0o660; // observed uid 0, gid 500 + assert!(trusted_sysfs_node(stock_mode, 0, libc::SYSFS_MAGIC)); + assert!(trusted_sysfs_node(libc::S_IFREG | 0o600, 0, libc::SYSFS_MAGIC)); + assert!(!trusted_sysfs_node(stock_mode, 0, 0)); + assert!(!trusted_sysfs_node(stock_mode, 500, libc::SYSFS_MAGIC)); + assert!(!trusted_sysfs_node(libc::S_IFREG | 0o662, 0, libc::SYSFS_MAGIC)); + assert!(!trusted_sysfs_node(libc::S_IFDIR | 0o660, 0, libc::SYSFS_MAGIC)); + } + #[test] + fn ordinary_group_writable_file_is_never_written() { + use std::os::unix::fs::PermissionsExt; + let path = std::env::temp_dir().join(format!("rmweb-not-sysfs-{}", std::process::id())); + let mut file = OpenOptions::new().create_new(true).write(true).mode(0o660).open(&path).unwrap(); + file.write_all(b"preserve").unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o660)).unwrap(); + assert!(write_sysfs(path.to_str().unwrap(), b"changed!").is_err()); + assert_eq!(fs::read(&path).unwrap(), b"preserve"); + fs::remove_file(path).unwrap(); + } + #[test] + fn lease_rejects_overlap_and_releases_without_replacing_inode() { + let root = std::env::temp_dir().join(format!("rmweb-platform-lock-{}", std::process::id())); + fs::create_dir(&root).unwrap(); + let path = root.join("lease"); + let uid = unsafe { libc::geteuid() }; + let first = acquire_lease(&path, uid).unwrap(); + let inode = first.metadata().unwrap().ino(); + assert!(acquire_lease(&path, uid).is_err()); + drop(first); + let second = acquire_lease(&path, uid).unwrap(); + assert_eq!(second.metadata().unwrap().ino(), inode); + drop(second); + fs::write(&path, b"unrelated").unwrap(); + assert!(acquire_lease(&path, uid).is_err()); + assert_eq!(fs::read(&path).unwrap(), b"unrelated"); + let link = root.join("link"); + std::os::unix::fs::symlink(&path, &link).unwrap(); + assert!(acquire_lease(&link, uid).is_err()); + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/engine/auth-passkey-helper/src/protocol.rs b/engine/auth-passkey-helper/src/protocol.rs new file mode 100644 index 0000000..0c70448 --- /dev/null +++ b/engine/auth-passkey-helper/src/protocol.rs @@ -0,0 +1,222 @@ +use std::io; +use std::time::Duration; + +use libwebauthn::ops::webauthn::{GetAssertionRequest, OriginValidation, PublicSuffixList, RelatedOrigins, RequestOrigin, RequestSettings}; +use libwebauthn::proto::ctap2::{Ctap2GetAssertionRequest, Ctap2GetAssertionResponse}; +use publicsuffix::Psl; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +pub const INPUT_LIMIT: usize = 128 * 1024; +pub const RECORD_LIMIT: usize = 64 * 1024; +pub const OUTPUT_LIMIT: usize = 512 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +pub enum ErrorName { NotAllowedError, NotSupportedError, OperationError } + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Input { + version: u32, + origin: String, + options: Options, + client_data_hash: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Options { + challenge: String, + #[serde(rename = "rpId", skip_serializing_if = "Option::is_none")] + rp_id: Option, + #[serde(default)] + allow_credentials: Vec, + #[serde(default = "preferred")] + user_verification: String, + #[serde(skip_serializing_if = "Option::is_none")] + timeout: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + hints: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + extensions: Option>, +} +fn preferred() -> String { "preferred".into() } + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct Descriptor { + r#type: String, + id: String, + #[serde(skip_serializing_if = "Option::is_none")] + transports: Option>, +} + +pub struct Prepared { + pub ctap: Ctap2GetAssertionRequest, + pub timeout: Duration, + pub require_uv: bool, +} + +struct EmbeddedPsl(publicsuffix::List); +impl PublicSuffixList for EmbeddedPsl { + fn public_suffix(&self, host: &str) -> Option { + let suffix = self.0.suffix(host.as_bytes())?; + if !suffix.is_known() { return None; } + std::str::from_utf8(suffix.as_bytes()).ok().map(String::from) + } + fn registrable_domain(&self, host: &str) -> Option { + if !self.0.suffix(host.as_bytes())?.is_known() { return None; } + std::str::from_utf8(self.0.domain(host.as_bytes())?.as_bytes()).ok().map(String::from) + } +} + +fn decode(value: &str, min: usize, max: usize) -> Result, ErrorName> { + if value.len() > max.div_ceil(3) * 4 || !value.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') { + return Err(ErrorName::OperationError); + } + let bytes = base64_url::decode(value).map_err(|_| ErrorName::OperationError)?; + if !(min..=max).contains(&bytes.len()) || base64_url::encode(&bytes) != value { + return Err(ErrorName::OperationError); + } + Ok(bytes) +} + +pub async fn prepare(bytes: &[u8]) -> Result { + if bytes.len() > INPUT_LIMIT { return Err(ErrorName::OperationError); } + // Typed deserialization rejects duplicate fields and unknown request keys. + let mut input: Input = serde_json::from_slice(bytes).map_err(|_| ErrorName::OperationError)?; + if input.version != 1 { return Err(ErrorName::NotSupportedError); } + let url = url::Url::parse(&input.origin).map_err(|_| ErrorName::OperationError)?; + if input.origin.len() > 2048 || url.scheme() != "https" || url.host_str().is_none() + || !url.username().is_empty() || url.password().is_some() + || url.origin().ascii_serialization() != input.origin { + return Err(ErrorName::OperationError); + } + let hash = decode(&input.client_data_hash, 32, 32)?; + decode(&input.options.challenge, 1, 16 * 1024)?; + if input.options.rp_id.as_ref().is_some_and(|v| v.is_empty() || v.len() > 253) + || input.options.allow_credentials.len() > 64 { + return Err(ErrorName::OperationError); + } + if !matches!(input.options.user_verification.as_str(), "required" | "preferred" | "discouraged") { + return Err(ErrorName::OperationError); + } + if input.options.extensions.as_ref().is_some_and(|v| !v.is_empty()) { + return Err(ErrorName::NotSupportedError); + } + if input.options.hints.len() > 3 || input.options.hints.iter().any(|v| !matches!(v.as_str(), "security-key" | "client-device" | "hybrid")) { + return Err(ErrorName::NotSupportedError); + } + for descriptor in &input.options.allow_credentials { + if descriptor.r#type != "public-key" { return Err(ErrorName::NotSupportedError); } + decode(&descriptor.id, 1, 1024)?; + if let Some(transports) = &descriptor.transports { + if transports.len() > 6 || transports.iter().any(|v| !matches!(v.as_str(), "usb" | "nfc" | "ble" | "internal" | "hybrid" | "smart-card")) { + return Err(ErrorName::NotSupportedError); + } + } + } + let timeout = input.options.timeout.unwrap_or(120_000).min(120_000); + if timeout == 0 { return Err(ErrorName::NotAllowedError); } + input.options.timeout = Some(timeout); + let origin: RequestOrigin = input.origin.as_str().try_into().map_err(|_| ErrorName::OperationError)?; + let psl = EmbeddedPsl(include_str!("../vendor/public_suffix_list.dat").parse().map_err(|_| ErrorName::OperationError)?); + let options = serde_json::to_string(&input.options).map_err(|_| ErrorName::OperationError)?; + let request = GetAssertionRequest::prepare(&origin, &options, &RequestSettings { + origin: OriginValidation::Validate { public_suffix_list: &psl, related_origins: RelatedOrigins::Disabled }, + }).await.map_err(|_| ErrorName::OperationError)?; + let require_uv = input.options.user_verification == "required"; + let mut ctap = Ctap2GetAssertionRequest::from(request); + // WebKit owns clientDataJSON. Sign its exact hash; do not regenerate JSON. + ctap.client_data_hash = hash.into(); + Ok(Prepared { ctap, timeout: Duration::from_millis(timeout.into()), require_uv }) +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Message { + Qr { size: usize, modules: String }, + Status { state: &'static str }, + Result { + #[serde(rename = "credentialId")] + credential_id: String, + #[serde(rename = "authenticatorData")] + authenticator_data: String, + signature: String, + #[serde(rename = "userHandle", skip_serializing_if = "Option::is_none")] + user_handle: Option, + }, + Error { name: ErrorName }, +} + +pub fn qr_message(payload: &str) -> Result { + let code = qrcode::QrCode::with_error_correction_level(payload.as_bytes(), qrcode::EcLevel::M) + .map_err(|_| ErrorName::OperationError)?; + let size = code.width(); + if !(21..=177).contains(&size) { return Err(ErrorName::OperationError); } + let modules: String = code.to_colors().into_iter().map(|c| if c == qrcode::Color::Dark { '1' } else { '0' }).collect(); + if modules.len() != size * size { return Err(ErrorName::OperationError); } + Ok(Message::Qr { size, modules }) +} + +pub fn assertion_message(prepared: &Prepared, response: Ctap2GetAssertionResponse) -> Result { + // Native CTAP decoder retains the exact signed bytes. Never reconstruct them. + let auth = response.authenticator_data.raw.as_ref().ok_or(ErrorName::OperationError)?; + if !(37..=16 * 1024).contains(&auth.len()) || response.signature.is_empty() + || response.signature.len() > 4096 || response.authenticator_data.attested_credential.is_some() { + return Err(ErrorName::OperationError); + } + let rp_hash = Sha256::digest(prepared.ctap.relying_party_id.as_bytes()); + if auth[..32] != rp_hash[..] || auth[32] & 1 == 0 || (prepared.require_uv && auth[32] & 4 == 0) { + return Err(ErrorName::OperationError); + } + let credential = match response.credential_id { + Some(value) => value.id.into_vec(), + None if prepared.ctap.allow.len() == 1 => prepared.ctap.allow[0].id.to_vec(), + _ => return Err(ErrorName::OperationError), + }; + if credential.is_empty() || credential.len() > 1024 + || (!prepared.ctap.allow.is_empty() && !prepared.ctap.allow.iter().any(|item| item.id.as_ref() == credential)) { + return Err(ErrorName::OperationError); + } + // A single response cannot silently select the first of several accounts. + if response.credentials_count.is_some_and(|count| count > 1) { + return Err(ErrorName::NotSupportedError); + } + let user_handle = match response.user { + Some(user) if !user.id.is_empty() && user.id.len() <= 64 => Some(base64_url::encode(&user.id)), + Some(_) => return Err(ErrorName::OperationError), + None if prepared.ctap.allow.is_empty() => return Err(ErrorName::OperationError), + None => None, + }; + Ok(Message::Result { credential_id: base64_url::encode(&credential), authenticator_data: base64_url::encode(auth), + signature: base64_url::encode(&response.signature), user_handle }) +} + +pub async fn read_input(reader: &mut R) -> Result, ErrorName> { + let mut bytes = Vec::new(); + reader.take(INPUT_LIMIT as u64 + 1).read_to_end(&mut bytes).await.map_err(|_| ErrorName::OperationError)?; + if bytes.is_empty() || bytes.len() > INPUT_LIMIT { return Err(ErrorName::OperationError); } + Ok(bytes) +} + +pub struct Output { writer: W, used: usize, terminal: bool } +impl Output { + pub fn new(writer: W) -> Self { Self { writer, used: 0, terminal: false } } + pub async fn send(&mut self, message: &Message) -> Result<(), io::Error> { + if self.terminal { return Err(io::Error::other("output complete")); } + let mut line = serde_json::to_vec(message).map_err(io::Error::other)?; + line.push(b'\n'); + if line.len() > RECORD_LIMIT || self.used + line.len() > OUTPUT_LIMIT { + return Err(io::Error::other("output bound")); + } + tokio::time::timeout(Duration::from_secs(1), async { + self.writer.write_all(&line).await?; + self.writer.flush().await + }).await.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "output deadline"))??; + self.used += line.len(); + self.terminal = matches!(message, Message::Result { .. } | Message::Error { .. }); + Ok(()) + } +} diff --git a/engine/auth-passkey-helper/tests/contract.rs b/engine/auth-passkey-helper/tests/contract.rs new file mode 100644 index 0000000..a0ed919 --- /dev/null +++ b/engine/auth-passkey-helper/tests/contract.rs @@ -0,0 +1,205 @@ +use std::collections::BTreeMap; +use std::time::Duration; +use rmweb_auth_passkey_helper::protocol::*; +use libwebauthn::proto::ctap2::Ctap2GetAssertionResponse; +use serde_json::{json, Value}; +use serde_cbor_2::Value as Cbor; +use sha2::{Digest, Sha256}; + +fn input() -> Value { + json!({"version":1,"origin":"https://login.example.com","clientDataHash":base64_url::encode(&[0x92;32]), + "options":{"challenge":base64_url::encode(&[0x17;32]),"rpId":"example.com","userVerification":"required", + "allowCredentials":[{"type":"public-key","id":base64_url::encode(&[0x48;32]),"transports":["hybrid"]}]}}) +} +async fn prepare_value(value: &Value) -> Result { prepare(&serde_json::to_vec(value).unwrap()).await } +fn assertion(raw: Vec, credential: Option>, user: Option>) -> Ctap2GetAssertionResponse { + let mut map = BTreeMap::from([ + (Cbor::Integer(2), Cbor::Bytes(raw)), + (Cbor::Integer(3), Cbor::Bytes(vec![0xaa;72])), + ]); + if let Some(id) = credential { + map.insert(Cbor::Integer(1), Cbor::Map(BTreeMap::from([ + (Cbor::Text("type".into()), Cbor::Text("public-key".into())), + (Cbor::Text("id".into()), Cbor::Bytes(id)), + ]))); + } + if let Some(id) = user { map.insert(Cbor::Integer(4), Cbor::Map(BTreeMap::from([(Cbor::Text("id".into()), Cbor::Bytes(id))]))); } + serde_cbor_2::from_slice(&serde_cbor_2::to_vec(&Cbor::Map(map)).unwrap()).unwrap() +} +fn raw_auth() -> Vec { + let mut raw = Sha256::digest(b"example.com").to_vec(); + raw.extend([5, 0, 0, 0, 17]); + raw +} + +#[tokio::test] async fn exact_browser_hash_and_request_binding() { + let p = prepare_value(&input()).await.unwrap(); + assert_eq!(p.ctap.client_data_hash.as_ref(), &[0x92;32]); + assert_eq!(p.ctap.relying_party_id,"example.com"); + assert_eq!(p.ctap.allow[0].id.as_ref(), &[0x48;32]); + let options = p.ctap.options.unwrap(); + assert!(options.require_user_presence && options.require_user_verification); + assert!(p.ctap.pin_auth_param.is_none() && p.ctap.pin_auth_proto.is_none()); + assert_eq!(p.timeout,Duration::from_secs(120)); +} + +#[tokio::test] async fn larger_challenges_preserve_browser_hash_and_request_binding() { + let mut accepted = Vec::new(); + for length in [1025, 16384] { + let mut value = input(); + value["options"]["challenge"] = base64_url::encode(&vec![0x17; length]).into(); + let result = prepare_value(&value).await; + accepted.push(result.is_ok()); + if let Ok(prepared) = result { + assert_eq!(prepared.ctap.client_data_hash.as_ref(), &[0x92; 32]); + assert_eq!(prepared.ctap.relying_party_id, "example.com"); + assert_eq!(prepared.ctap.allow[0].id.as_ref(), &[0x48; 32]); + let options = prepared.ctap.options.unwrap(); + assert!(options.require_user_presence && options.require_user_verification); + assert!(prepared.ctap.pin_auth_param.is_none() && prepared.ctap.pin_auth_proto.is_none()); + } + } + assert_eq!(accepted, [true, true]); +} + +#[tokio::test] async fn largest_challenge_and_allowlist_still_obey_total_input_budget() { + let mut value = input(); + value["options"]["challenge"] = base64_url::encode(&vec![0x17; 16384]).into(); + value["options"]["allowCredentials"] = json!((0..64).map(|index| json!({ + "type": "public-key", "id": base64_url::encode(&vec![index as u8; 1024]), + "transports": ["usb", "nfc", "ble", "internal", "hybrid", "smart-card"] + })).collect::>()); + value["options"]["hints"] = json!(["security-key", "client-device", "hybrid"]); + let mut bytes = serde_json::to_vec(&value).unwrap(); + let total_limit = 128 * 1024; + assert!(bytes.len() < total_limit); + let prepared = prepare(&bytes).await.unwrap(); + assert_eq!(prepared.ctap.client_data_hash.as_ref(), &[0x92; 32]); + assert_eq!(prepared.ctap.allow.len(), 64); + assert_eq!(prepared.ctap.allow[63].id.as_ref(), &[63; 1024]); + // JSON whitespace reaches the aggregate boundary without violating field limits. + bytes.resize(total_limit, b' '); + assert!(prepare(&bytes).await.is_ok()); + bytes.push(b' '); + assert!(matches!(prepare(&bytes).await, Err(ErrorName::OperationError))); +} + +#[tokio::test] async fn origin_and_public_suffix_validation_precedes_transport() { + for origin in ["http://login.example.com", "https://other.example.org", "https://login.example.com/path", "https://u:p@login.example.com", "https://login.example.com#private", "https://login.example.com/"] { + let mut v=input();v["origin"]=origin.into();assert!(prepare_value(&v).await.is_err()); + } + for rp in ["com", "org", "other.example.com", "https://example.com", "", "example.com:443"] { + let mut v=input();v["options"]["rpId"]=rp.into();assert!(prepare_value(&v).await.is_err()); + } +} + +#[tokio::test] async fn typed_schema_rejects_duplicates_multiple_inputs_and_unknown_fields() { + let bytes=serde_json::to_vec(&input()).unwrap(); + let mut extra=bytes.clone();extra.extend_from_slice(&bytes);assert!(prepare(&extra).await.is_err()); + let duplicate=String::from_utf8(bytes).unwrap().replacen("\"version\":1", "\"version\":1,\"version\":1",1); + assert!(prepare(duplicate.as_bytes()).await.is_err()); + for path in ["root","options","descriptor"] { + let mut v=input(); + match path {"root"=>v["untrusted"]=true.into(),"options"=>v["options"]["mediation"]="conditional".into(),_=>v["options"]["allowCredentials"][0]["secret"]=true.into()}; + assert!(prepare_value(&v).await.is_err()); + } + assert!(prepare(&vec![b' ';INPUT_LIMIT+1]).await.is_err()); +} + +#[tokio::test] async fn unknown_extensions_are_not_silently_ignored() { + for extensions in [json!({"appid":"https://other.test"}),json!({"prf":{}}),json!({"future":false})] { + let mut v=input();v["options"]["extensions"]=extensions; + assert!(matches!(prepare_value(&v).await,Err(ErrorName::NotSupportedError))); + } + let mut v=input();v["options"]["extensions"]=json!({});assert!(prepare_value(&v).await.is_ok()); +} + +#[tokio::test] async fn binary_inputs_are_canonical_and_bounded() { + for value in ["", "AA==", "AB", "private payload", &base64_url::encode(&[0;31]), &base64_url::encode(&[0;33])] { + let mut v=input();v["clientDataHash"]=value.into();assert!(prepare_value(&v).await.is_err()); + } + for length in [0, 16385] { + let mut v=input();v["options"]["challenge"]=base64_url::encode(&vec![0;length]).into();assert!(matches!(prepare_value(&v).await,Err(ErrorName::OperationError))); + } + let mut v=input();v["options"]["allowCredentials"][0]["id"]=base64_url::encode(&vec![0;1025]).into();assert!(prepare_value(&v).await.is_err()); + let mut v=input();let descriptor=v["options"]["allowCredentials"][0].clone();v["options"]["allowCredentials"]=json!(vec![descriptor;65]);assert!(prepare_value(&v).await.is_err()); +} + +#[tokio::test] async fn timeout_and_verification_are_explicit() { + let mut v=input();v["options"]["timeout"]=300000.into();assert_eq!(prepare_value(&v).await.unwrap().timeout,Duration::from_secs(120)); + v["options"]["timeout"]=0.into();assert!(matches!(prepare_value(&v).await,Err(ErrorName::NotAllowedError))); + v["options"]["timeout"]=42.into();v["options"]["userVerification"]="discouraged".into();let p=prepare_value(&v).await.unwrap();assert!(!p.require_uv);assert!(p.ctap.options.unwrap().require_user_presence); + v["options"]["userVerification"]="silent".into();assert!(prepare_value(&v).await.is_err()); +} + +#[tokio::test] async fn exact_signed_authenticator_bytes_survive_result() { + let p=prepare_value(&input()).await.unwrap(); + let mut raw=raw_auth();raw[32]|=0x80; + // Deliberately noncanonical CBOR ordering within an unknown signed extension. + raw.extend([0xa2,0x61,b'z',0x01,0x61,b'a',0x02]); + let response=assertion(raw.clone(),Some(vec![0x48;32]),Some(vec![0x88;16])); + let json=serde_json::to_value(assertion_message(&p,response).unwrap()).unwrap(); + assert_eq!(json["authenticatorData"],base64_url::encode(&raw)); + assert_eq!(json["credentialId"],base64_url::encode(&[0x48;32])); + assert_eq!(json["signature"],base64_url::encode(&[0xaa;72])); + assert_eq!(json["userHandle"],base64_url::encode(&[0x88;16])); + assert!(json.get("userName").is_none()); +} + +#[tokio::test] async fn missing_or_mismatched_result_binding_fails() { + let p=prepare_value(&input()).await.unwrap(); + for change in [0,1,2] { + let mut raw=raw_auth();match change {0=>raw[0]^=1,1=>raw[32]&=!1,_=>raw[32]&=!4}; + assert!(assertion_message(&p,assertion(raw,Some(vec![0x48;32]),None)).is_err()); + } + assert!(assertion_message(&p,assertion(raw_auth(),Some(vec![0x49;32]),None)).is_err()); + let mut response=assertion(raw_auth(),Some(vec![0x48;32]),None);response.authenticator_data.raw=None; + assert!(assertion_message(&p,response).is_err()); +} + +#[tokio::test] async fn exactly_one_allowlisted_id_can_fill_omitted_descriptor() { + let p=prepare_value(&input()).await.unwrap(); + assert!(assertion_message(&p,assertion(raw_auth(),None,None)).is_ok()); + let mut v=input();v["options"]["allowCredentials"]=json!([]);let p=prepare_value(&v).await.unwrap(); + assert!(assertion_message(&p,assertion(raw_auth(),None,None)).is_err()); + assert!(assertion_message(&p,assertion(raw_auth(),Some(vec![0x48;32]),None)).is_err()); + assert!(assertion_message(&p,assertion(raw_auth(),Some(vec![0x48;32]),Some(vec![1;16]))).is_ok()); +} + +#[tokio::test] async fn multiple_accounts_require_supported_selection() { + let p=prepare_value(&input()).await.unwrap();let mut response=assertion(raw_auth(),Some(vec![0x48;32]),None);response.credentials_count=Some(2); + assert!(matches!(assertion_message(&p,response),Err(ErrorName::NotSupportedError))); +} + +#[test] fn qr_is_only_a_bounded_binary_matrix() { + let value=serde_json::to_value(qr_message("FIDO:/1234567890").unwrap()).unwrap(); + let size=value["size"].as_u64().unwrap() as usize;let modules=value["modules"].as_str().unwrap(); + assert!((21..=177).contains(&size));assert_eq!(modules.len(),size*size);assert!(modules.bytes().all(|c|matches!(c,b'0'|b'1'))); + assert_eq!(value.as_object().unwrap().len(),3); +} + +#[tokio::test] async fn input_is_bounded_and_requires_eof() { + assert!(read_input(&mut &b""[..]).await.is_err()); + assert!(read_input(&mut vec![b' ';INPUT_LIMIT+1].as_slice()).await.is_err()); + assert_eq!(read_input(&mut &b"{}"[..]).await.unwrap(),b"{}"); + let (mut reader,_writer)=tokio::io::duplex(32); + assert!(tokio::time::timeout(Duration::from_millis(20),read_input(&mut reader)).await.is_err()); +} + +#[tokio::test] async fn output_has_one_terminal_record_and_no_secret_diagnostics() { + let mut bytes=Vec::new(); + let mut output=Output::new(&mut bytes); + output.send(&Message::Error{name:ErrorName::NotAllowedError}).await.unwrap(); + assert!(output.send(&Message::Status{state:"connected"}).await.is_err()); + assert_eq!(bytes,b"{\"type\":\"error\",\"name\":\"NotAllowedError\"}\n"); +} + +#[tokio::test] async fn output_limits_record_total_and_blocked_reader() { + let mut bytes=Vec::new();let mut out=Output::new(&mut bytes); + assert!(out.send(&Message::Qr{size:177,modules:"1".repeat(RECORD_LIMIT)}).await.is_err()); + let record=Message::Qr{size:177,modules:"1".repeat(177*177)}; + let mut count=0;while out.send(&record).await.is_ok(){count+=1;assert!(count<20);} + assert!(bytes.len()<=OUTPUT_LIMIT);assert!(count>0); + let (writer,_reader)=tokio::io::duplex(1);let mut blocked=Output::new(writer); + assert!(tokio::time::timeout(Duration::from_secs(2),blocked.send(&Message::Status{state:"connected"})).await.unwrap().is_err()); +} diff --git a/engine/auth-passkey-helper/tests/package_test.py b/engine/auth-passkey-helper/tests/package_test.py new file mode 100644 index 0000000..c33b2be --- /dev/null +++ b/engine/auth-passkey-helper/tests/package_test.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import hashlib +import importlib.util +import json +from pathlib import Path +import stat +import tarfile +import tempfile +import unittest +from unittest.mock import patch + +SOURCE=Path(__file__).resolve().parents[1] +spec=importlib.util.spec_from_file_location("helper_package",SOURCE/"package.py") +package=importlib.util.module_from_spec(spec);spec.loader.exec_module(package) + +class PackageTest(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.root=Path(self.temp.name).resolve() + self.stage=self.root/"stage";self.stage.mkdir() + for name in ["README.md","Cargo.lock","vendor/libwebauthn-COPYING","vendor/LICENSE-rmweb","patches/libwebauthn-buffered-response.patch","src/main.rs"]: + path=self.stage/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_text(name+"\n") + files={str(p.relative_to(self.stage)):package.sha(p) for p in self.stage.rglob("*") if p.is_file()} + (self.stage/"source-receipt.json").write_text(json.dumps({"libraryCommit":package.PIN,"files":files,"helperSourceSha256":"1"*64})) + self.binary=self.root/"helper";header=bytearray(64);header[:6]=b"\x7fELF\x02\x01";header[18:20]=(183).to_bytes(2,"little");self.binary.write_bytes(header);self.binary.chmod(0o755) + self.output=self.root/"artifacts" + def tearDown(self):self.temp.cleanup() + def vendor(self,*args,**kwargs): + (self.stage/"dependencies").mkdir();(self.stage/"dependencies/LICENSE").write_text("dependency notice\n") + return f'[source.vendored-sources]\ndirectory = "{self.stage}/dependencies"\n' + def export(self): + package.export(self.stage,self.binary,self.output,"sha256:"+"2"*64,"3"*40,"cargo") + def test_complete_sources_and_exact_receipt(self): + with patch.object(package.subprocess,"check_output",side_effect=self.vendor):self.export() + manifest=json.loads((self.output/"manifest.json").read_text()) + self.assertEqual(manifest["binary"]["sha256"],package.sha(self.binary)) + self.assertEqual(manifest["binary"]["mode"],"0755") + with tarfile.open(self.output/"licenses/helper-source.tar.gz") as archive: + self.assertIn("auth-passkey-helper/src/main.rs",archive.getnames()) + self.assertIn("auth-passkey-helper/dependencies/LICENSE",archive.getnames()) + self.assertIn("auth-passkey-helper/.cargo/config.toml",archive.getnames()) + for name,record in manifest["licenses"].items():self.assertEqual(record["sha256"],package.sha(self.output/name)) + def test_source_change_fails_before_output(self): + (self.stage/"src/main.rs").write_text("changed") + with self.assertRaises(ValueError):self.export() + self.assertFalse(self.output.exists()) + def test_existing_output_is_preserved(self): + self.output.mkdir();(self.output/"unrelated").write_text("preserve") + with self.assertRaises(ValueError):self.export() + self.assertEqual((self.output/"unrelated").read_text(),"preserve") + def test_wrong_architecture_fails(self): + self.binary.write_bytes(b"not an executable") + with self.assertRaises(ValueError):self.export() + self.assertFalse(self.output.exists()) + def test_binary_change_during_export_fails(self): + def mutate(*args,**kwargs): + self.binary.write_bytes(self.binary.read_bytes()+b"changed") + return self.vendor() + with patch.object(package.subprocess,"check_output",side_effect=mutate),self.assertRaises(ValueError):self.export() + self.assertFalse(self.output.exists()) + def test_reused_vendor_tree_is_rejected(self): + (self.stage/"dependencies").mkdir() + with self.assertRaises(ValueError):self.export() + self.assertFalse(self.output.exists()) + +if __name__ == "__main__":unittest.main() diff --git a/engine/auth-passkey-helper/tests/process.py b/engine/auth-passkey-helper/tests/process.py new file mode 100644 index 0000000..5a57e22 --- /dev/null +++ b/engine/auth-passkey-helper/tests/process.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Real helper-process tests; all inputs terminate before Bluetooth acquisition.""" +import argparse +import json +import os +from pathlib import Path +import select +import signal +import subprocess +import sys +import time +import unittest + +COMMAND = [] + + +def signal_ready(pid): + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + try: + status = Path(f"/proc/{pid}/status").read_text() + caught = next(line.split()[1] for line in status.splitlines() if line.startswith("SigCgt:")) + if int(caught, 16) & (1 << (signal.SIGTERM - 1)): + return + except (FileNotFoundError, StopIteration): + pass + time.sleep(0.01) + raise AssertionError("helper did not install its signal handler") + + +class ProcessTest(unittest.TestCase): + def rejected(self, payload, expected="OperationError"): + result = subprocess.run(COMMAND, input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=4) + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stderr, b"") + self.assertEqual(json.loads(result.stdout), {"type":"error", "name":expected}) + self.assertEqual(result.stdout.count(b"\n"), 1) + + def test_malformed_private_input_is_not_echoed(self): + self.rejected(b'{"private":"MUST_NOT_APPEAR_IN_OUTPUT"}') + + def test_oversized_input_is_bounded(self): + self.rejected(b"x" * (128 * 1024 + 1)) + + def test_multiple_requests_are_rejected(self): + self.rejected(b"{}\n{}\n") + + def test_missing_eof_has_two_second_deadline(self): + process = subprocess.Popen(COMMAND, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + start = time.monotonic() + try: + process.wait(timeout=4) + self.assertLess(time.monotonic() - start, 3.5) + self.assertEqual(json.loads(process.stdout.read()), {"type":"error","name":"NotAllowedError"}) + self.assertEqual(process.stderr.read(),b"") + finally: + if process.poll() is None: process.kill() + process.communicate() + + def test_sigterm_cancels_waiting_input(self): + process = subprocess.Popen(COMMAND, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + signal_ready(process.pid) + process.send_signal(signal.SIGTERM) + # Keep stdin open until exit so EOF cannot win the signal race. + process.wait(timeout=3) + stdout, stderr = process.communicate(timeout=3) + self.assertEqual(json.loads(stdout), {"type":"error","name":"NotAllowedError"}) + self.assertEqual(stderr,b"") + self.assertEqual(process.returncode,1) + finally: + if process.poll() is None: process.kill(); process.wait() + + def test_closed_stderr_is_replaced_without_closing_null_descriptor(self): + process = subprocess.Popen(COMMAND, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, preexec_fn=lambda: os.close(2)) + try: + signal_ready(process.pid) + self.assertEqual(os.readlink(f"/proc/{process.pid}/fd/2"), "/dev/null") + process.send_signal(signal.SIGTERM) + process.wait(timeout=3) + finally: + if process.poll() is None: process.kill() + process.communicate() + + def test_parent_death_cancels_waiting_input(self): + input_read, input_write = os.pipe() + output_read, output_write = os.pipe() + wrapper = None + helper_pid = None + try: + program = "import subprocess,sys,time; p=subprocess.Popen(sys.argv[3:],stdin=int(sys.argv[1]),stdout=int(sys.argv[2]),stderr=subprocess.DEVNULL); print(p.pid,flush=True); time.sleep(10)" + wrapper = subprocess.Popen([sys.executable,"-c",program,str(input_read),str(output_write),*COMMAND], + pass_fds=(input_read,output_write),stdout=subprocess.PIPE,stderr=subprocess.PIPE) + os.close(input_read); input_read = -1 + os.close(output_write); output_write = -1 + helper_pid = int(wrapper.stdout.readline()) + signal_ready(helper_pid) + wrapper.kill(); wrapper.wait(timeout=2) + # Keep stdin's writer open: EOF must not explain this cancellation. + chunks = [] + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + ready,_,_ = select.select([output_read],[],[],deadline-time.monotonic()) + self.assertTrue(ready,"helper did not terminate after parent death") + data = os.read(output_read,4096) + if not data: break + chunks.append(data) + else: + self.fail("helper pipe remained open") + self.assertEqual(json.loads(b"".join(chunks)), {"type":"error","name":"NotAllowedError"}) + finally: + if wrapper is not None: + if wrapper.poll() is None: wrapper.kill() + wrapper.communicate() + for fd in [input_read,input_write,output_read,output_write]: + if fd >= 0: os.close(fd) + if helper_pid is not None: + try: + state = Path(f"/proc/{helper_pid}/status").read_text() + if "\nState:\tZ" not in state: os.kill(helper_pid,signal.SIGKILL) + except ProcessLookupError: pass + except FileNotFoundError: pass + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--binary",required=True) + parser.add_argument("--loader",required=True) + parser.add_argument("--library-path",required=True) + args = parser.parse_args() + COMMAND[:] = [args.loader,"--library-path",args.library_path,args.binary] + unittest.main(argv=[sys.argv[0]]) diff --git a/engine/auth-passkey-helper/vendor/LICENSE-rmweb b/engine/auth-passkey-helper/vendor/LICENSE-rmweb new file mode 100644 index 0000000..eaf555c --- /dev/null +++ b/engine/auth-passkey-helper/vendor/LICENSE-rmweb @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eugene Tarakanov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/engine/auth-passkey-helper/vendor/libwebauthn-COPYING b/engine/auth-passkey-helper/vendor/libwebauthn-COPYING new file mode 100644 index 0000000..e2bbecb --- /dev/null +++ b/engine/auth-passkey-helper/vendor/libwebauthn-COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/engine/auth-passkey-helper/vendor/public_suffix_list.dat b/engine/auth-passkey-helper/vendor/public_suffix_list.dat new file mode 100644 index 0000000..46fc796 --- /dev/null +++ b/engine/auth-passkey-helper/vendor/public_suffix_list.dat @@ -0,0 +1,16477 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat, +// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported. + +// VERSION: 2026-09-15_10-18-26_UTC +// COMMIT: 3955e3ec29b94c3cca7bd4509c5f14a7c0959e26 + +// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/. + +// ===BEGIN ICANN DOMAINS=== + +// ac : http://nic.ac/rules.htm +ac +com.ac +edu.ac +gov.ac +mil.ac +net.ac +org.ac + +// ad : https://www.iana.org/domains/root/db/ad.html +// Confirmed by Amadeu Abril i Abril (CORE) 2024-11-17 +ad + +// ae : https://www.iana.org/domains/root/db/ae.html +ae +ac.ae +co.ae +gov.ae +mil.ae +net.ae +org.ae +sch.ae + +// aero : https://information.aero/registration/policies/dmp +aero +// 2LDs +airline.aero +airport.aero +// 2LDs (currently not accepting registration, seemingly never have) +// As of 2024-07, these are marked as reserved for potential 3LD +// registrations (clause 11 "allocated subdomains" in the 2006 TLD +// policy), but the relevant industry partners have not opened them up +// for registration. Current status can be determined from the TLD's +// policy document: 2LDs that are open for registration must list +// their policy in the TLD's policy. Any 2LD without such a policy is +// not open for registrations. +accident-investigation.aero +accident-prevention.aero +aerobatic.aero +aeroclub.aero +aerodrome.aero +agents.aero +air-surveillance.aero +air-traffic-control.aero +aircraft.aero +airtraffic.aero +ambulance.aero +association.aero +author.aero +ballooning.aero +broker.aero +caa.aero +cargo.aero +catering.aero +certification.aero +championship.aero +charter.aero +civilaviation.aero +club.aero +conference.aero +consultant.aero +consulting.aero +control.aero +council.aero +crew.aero +design.aero +dgca.aero +educator.aero +emergency.aero +engine.aero +engineer.aero +entertainment.aero +equipment.aero +exchange.aero +express.aero +federation.aero +flight.aero +freight.aero +fuel.aero +gliding.aero +government.aero +groundhandling.aero +group.aero +hanggliding.aero +homebuilt.aero +insurance.aero +journal.aero +journalist.aero +leasing.aero +logistics.aero +magazine.aero +maintenance.aero +marketplace.aero +media.aero +microlight.aero +modelling.aero +navigation.aero +parachuting.aero +paragliding.aero +passenger-association.aero +pilot.aero +press.aero +production.aero +recreation.aero +repbody.aero +res.aero +research.aero +rotorcraft.aero +safety.aero +scientist.aero +services.aero +show.aero +skydiving.aero +software.aero +student.aero +taxi.aero +trader.aero +trading.aero +trainer.aero +union.aero +workinggroup.aero +works.aero + +// af : https://www.nic.af/domain-price +af +com.af +edu.af +gov.af +net.af +org.af + +// ag : http://www.nic.ag/prices.htm +ag +co.ag +com.ag +net.ag +nom.ag +org.ag + +// ai : https://www.nic.ai/ +ai +com.ai +net.ai +off.ai +org.ai + +// al : https://akep.al/en/domain-e-application/ -> "Regulations and Decisions" +al +com.al +edu.al +gov.al +mil.al +net.al +org.al + +// am : https://www.amnic.net/policy/en/Policy_EN.pdf +// Confirmed by ISOC AM 2024-11-18 +am +co.am +com.am +commune.am +net.am +org.am + +// ao : https://www.dns.ao/ao/ +ao +co.ao +ed.ao +edu.ao +gov.ao +gv.ao +it.ao +og.ao +org.ao +pb.ao + +// aq : https://www.iana.org/domains/root/db/aq.html +aq + +// ar : https://nic.ar/es/nic-argentina/normativa +ar +bet.ar +com.ar +coop.ar +edu.ar +gob.ar +gov.ar +int.ar +mil.ar +musica.ar +mutual.ar +net.ar +org.ar +seg.ar +senasa.ar +tur.ar + +// arpa : https://www.iana.org/domains/root/db/arpa.html +// Confirmed by registry 2008-06-18 +arpa +e164.arpa +home.arpa +in-addr.arpa +ip6.arpa +iris.arpa +uri.arpa +urn.arpa + +// as : https://www.iana.org/domains/root/db/as.html +as +gov.as + +// asia : https://www.iana.org/domains/root/db/asia.html +asia + +// at : https://www.iana.org/domains/root/db/at.html +// Confirmed by registry 2008-06-17 +at +ac.at +sth.ac.at +co.at +gv.at +or.at + +// au : https://www.iana.org/domains/root/db/au.html +// https://www.auda.org.au/ +// Confirmed by registry 2025-07-16 +au +// 2LDs +asn.au +com.au +edu.au +gov.au +id.au +net.au +org.au +// Historic 2LDs (closed to new registration, but sites still exist) +conf.au +oz.au +// CGDNs : https://www.auda.org.au/au-domain-names/the-different-au-domain-names/state-and-territory-domain-names/ +act.au +nsw.au +nt.au +qld.au +sa.au +tas.au +vic.au +wa.au +// 3LDs +act.edu.au +catholic.edu.au +// eq.edu.au - Removed at the request of the Queensland Department of Education +nsw.edu.au +nt.edu.au +qld.edu.au +sa.edu.au +tas.edu.au +vic.edu.au +wa.edu.au +// act.gov.au - Bug 984824 - Removed at request of Greg Tankard +// nsw.gov.au - Bug 547985 - Removed at request of +// nt.gov.au - Bug 940478 - Removed at request of Greg Connors +qld.gov.au +sa.gov.au +tas.gov.au +vic.gov.au +wa.gov.au +// 4LDs +// education.tas.edu.au - Removed at the request of the Department of Education Tasmania +// schools.nsw.edu.au - Removed at the request of the New South Wales Department of Education. + +// aw : https://www.iana.org/domains/root/db/aw.html +aw +com.aw + +// ax : https://www.iana.org/domains/root/db/ax.html +ax + +// az : https://www.iana.org/domains/root/db/az.html +// Confirmed via https://whois.az/?page_id=10 2024-12-11 +az +biz.az +co.az +com.az +edu.az +gov.az +info.az +int.az +mil.az +name.az +net.az +org.az +pp.az +// No longer available for registration, however domains exist as of 2024-12-11 +// see https://whois.az/?page_id=783 +pro.az + +// ba : https://www.iana.org/domains/root/db/ba.html +ba +com.ba +edu.ba +gov.ba +mil.ba +net.ba +org.ba + +// bb : https://www.iana.org/domains/root/db/bb.html +bb +biz.bb +co.bb +com.bb +edu.bb +gov.bb +info.bb +net.bb +org.bb +store.bb +tv.bb + +// bd : https://www.iana.org/domains/root/db/bd.html +// Confirmed by registry +bd +ac.bd +ai.bd +co.bd +com.bd +edu.bd +gov.bd +id.bd +info.bd +it.bd +mil.bd +net.bd +org.bd +sch.bd +tv.bd + +// be : https://www.iana.org/domains/root/db/be.html +// Confirmed by registry 2008-06-08 +be +ac.be + +// bf : https://www.iana.org/domains/root/db/bf.html +bf +gov.bf + +// bg : https://www.register.bg/ -> "Terms and Conditions" +bg +0.bg +1.bg +2.bg +3.bg +4.bg +5.bg +6.bg +7.bg +8.bg +9.bg +a.bg +b.bg +c.bg +d.bg +e.bg +f.bg +g.bg +h.bg +i.bg +j.bg +k.bg +l.bg +m.bg +n.bg +o.bg +p.bg +q.bg +r.bg +s.bg +t.bg +u.bg +v.bg +w.bg +x.bg +y.bg +z.bg + +// bh : https://www.iana.org/domains/root/db/bh.html +bh +com.bh +edu.bh +gov.bh +net.bh +org.bh + +// bi : http://whois.nic.bi/ +bi +co.bi +com.bi +edu.bi +or.bi +org.bi + +// biz : https://www.iana.org/domains/root/db/biz.html +biz + +// bj : https://nic.bj/bj-suffixes.txt +// Submitted by registry +bj +africa.bj +agro.bj +architectes.bj +assur.bj +avocats.bj +co.bj +com.bj +eco.bj +econo.bj +edu.bj +info.bj +loisirs.bj +money.bj +net.bj +org.bj +ote.bj +restaurant.bj +resto.bj +tourism.bj +univ.bj + +// bm : https://www.bermudanic.bm/domain-registration/index.php +bm +com.bm +edu.bm +gov.bm +net.bm +org.bm + +// bn : http://www.bnnic.bn/faqs +bn +com.bn +edu.bn +gov.bn +net.bn +org.bn + +// bo : https://nic.bo +// Confirmed by registry 2026-09-01 +bo +com.bo +edu.bo +gob.bo +int.bo +mil.bo +net.bo +org.bo +tv.bo +web.bo +// Social Domains +academia.bo +agro.bo +arte.bo +blog.bo +bolivia.bo +ciencia.bo +cooperativa.bo +democracia.bo +deporte.bo +ecologia.bo +economia.bo +empresa.bo +ia.bo +indigena.bo +industria.bo +info.bo +medicina.bo +movimiento.bo +musica.bo +natural.bo +nombre.bo +noticias.bo +patria.bo +plurinacional.bo +politica.bo +profesional.bo +pueblo.bo +revista.bo +salud.bo +tecnologia.bo +tksat.bo +transporte.bo +wiki.bo + +// br : http://registro.br/dominio/categoria.html +// Submitted by registry +br +9guacu.br +abc.br +adm.br +adv.br +agr.br +aju.br +am.br +anani.br +aparecida.br +api.br +app.br +arq.br +art.br +ato.br +b.br +barueri.br +belem.br +bet.br +bhz.br +bib.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinagrande.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +contagem.br +coop.br +coz.br +cri.br +cuiaba.br +curitiba.br +def.br +des.br +det.br +dev.br +ecn.br +eco.br +edu.br +emp.br +enf.br +eng.br +esp.br +etc.br +eti.br +far.br +feira.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +geo.br +ggf.br +goiania.br +gov.br +// gov.br 26 states + df https://en.wikipedia.org/wiki/States_of_Brazil +ac.gov.br +al.gov.br +am.gov.br +ap.gov.br +ba.gov.br +ce.gov.br +df.gov.br +es.gov.br +go.gov.br +ma.gov.br +mg.gov.br +ms.gov.br +mt.gov.br +pa.gov.br +pb.gov.br +pe.gov.br +pi.gov.br +pr.gov.br +rj.gov.br +rn.gov.br +ro.gov.br +rr.gov.br +rs.gov.br +sc.gov.br +se.gov.br +sp.gov.br +to.gov.br +gru.br +ia.br +imb.br +ind.br +inf.br +jab.br +jampa.br +jdf.br +joinville.br +jor.br +jus.br +leg.br +leilao.br +lel.br +log.br +londrina.br +macapa.br +maceio.br +manaus.br +maringa.br +mat.br +med.br +mil.br +morena.br +mp.br +mus.br +natal.br +net.br +niteroi.br +*.nom.br +not.br +ntr.br +odo.br +ong.br +org.br +osasco.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +pvh.br +qsl.br +radio.br +rec.br +recife.br +rep.br +ribeirao.br +rio.br +riobranco.br +riopreto.br +salvador.br +sampa.br +santamaria.br +santoandre.br +saobernardo.br +saogonca.br +seg.br +sjc.br +slg.br +slz.br +social.br +sorocaba.br +srv.br +taxi.br +tc.br +tec.br +teo.br +the.br +tmp.br +trd.br +tur.br +tv.br +udi.br +vet.br +vix.br +vlog.br +wiki.br +xyz.br +zlg.br + +// bs : http://www.register.bs/rules.html +bs +com.bs +edu.bs +gov.bs +net.bs +org.bs + +// bt : https://www.iana.org/domains/root/db/bt.html +bt +com.bt +edu.bt +gov.bt +net.bt +org.bt + +// bv : No registrations at this time. +// Submitted by registry +bv + +// bw : https://nic.net.bw/bw-name-structure +bw +ac.bw +co.bw +gov.bw +net.bw +org.bw + +// by : https://www.iana.org/domains/root/db/by.html +// http://tld.by/rules_2006_en.html +// list of other 2nd level tlds ? +by +gov.by +mil.by +// Official information does not indicate that com.by is a reserved +// second-level domain, but it's being used as one (see www.google.com.by and +// www.yahoo.com.by, for example), so we list it here for safety's sake. +com.by +// http://hoster.by/ +of.by + +// bz : http://www.belizenic.bz/ +bz +co.bz +com.bz +edu.bz +gov.bz +net.bz +org.bz + +// ca : https://www.iana.org/domains/root/db/ca.html +ca +// ca geographical names +ab.ca +bc.ca +mb.ca +nb.ca +nf.ca +nl.ca +ns.ca +nt.ca +nu.ca +on.ca +pe.ca +qc.ca +sk.ca +yk.ca +// gc.ca: https://en.wikipedia.org/wiki/.gc.ca +// see also: http://registry.gc.ca/en/SubdomainFAQ +gc.ca + +// cat : https://www.iana.org/domains/root/db/cat.html +cat + +// cc : https://www.iana.org/domains/root/db/cc.html +cc + +// cd : https://www.nic.cd +cd +gov.cd + +// cf : https://www.iana.org/domains/root/db/cf.html +cf + +// cg : https://www.iana.org/domains/root/db/cg.html +cg + +// ch : https://www.iana.org/domains/root/db/ch.html +ch + +// ci : https://www.iana.org/domains/root/db/ci.html +ci +ac.ci +aéroport.ci +asso.ci +co.ci +com.ci +ed.ci +edu.ci +go.ci +gouv.ci +int.ci +net.ci +or.ci +org.ci + +// ck : https://www.iana.org/domains/root/db/ck.html +*.ck +!www.ck + +// cl : https://www.nic.cl +// Confirmed by .CL registry +cl +co.cl +gob.cl +gov.cl +mil.cl + +// cm : https://www.iana.org/domains/root/db/cm.html plus bug 981927 +cm +co.cm +com.cm +gov.cm +net.cm + +// cn : https://www.iana.org/domains/root/db/cn.html +// Submitted by registry +cn +ac.cn +com.cn +edu.cn +gov.cn +mil.cn +net.cn +org.cn +公司.cn +網絡.cn +网络.cn +// cn geographic names +ah.cn +bj.cn +cq.cn +fj.cn +gd.cn +gs.cn +gx.cn +gz.cn +ha.cn +hb.cn +he.cn +hi.cn +hk.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +mo.cn +nm.cn +nx.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +tw.cn +xj.cn +xz.cn +yn.cn +zj.cn + +// co : https://www.iana.org/domains/root/db/co.html +// https://www.cointernet.com.co/como-funciona-un-dominio-restringido +// Confirmed by registry 2024-11-18 +co +com.co +edu.co +gov.co +mil.co +net.co +nom.co +org.co + +// com : https://www.iana.org/domains/root/db/com.html +com + +// coop : https://www.iana.org/domains/root/db/coop.html +coop + +// cr : https://nic.cr/capitulo-1-registro-de-un-nombre-de-dominio/ +cr +ac.cr +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr + +// cu : https://www.iana.org/domains/root/db/cu.html +cu +com.cu +edu.cu +gob.cu +inf.cu +nat.cu +net.cu +org.cu + +// cv : https://www.iana.org/domains/root/db/cv.html +// https://ola.cv/domain-extensions-under-cv/ +// Confirmed by registry 2024-11-26 +cv +com.cv +edu.cv +id.cv +int.cv +net.cv +nome.cv +org.cv +publ.cv + +// cw : https://www.uoc.cw/cw-registry +// Confirmed by registry 2024-11-19 +cw +com.cw +edu.cw +net.cw +org.cw + +// cx : https://www.iana.org/domains/root/db/cx.html +// list of other 2nd level tlds ? +cx +gov.cx + +// cy : http://www.nic.cy/ +// Submitted by Panayiotou Fotia +// https://nic.cy/wp-content/uploads/2024/01/Create-Request-for-domain-name-registration-1.pdf +cy +ac.cy +biz.cy +com.cy +ekloges.cy +gov.cy +ltd.cy +mil.cy +net.cy +org.cy +press.cy +pro.cy +tm.cy + +// cz : https://www.iana.org/domains/root/db/cz.html +// Confirmed by registry 2025-08-06 +cz +gov.cz + +// de : https://www.iana.org/domains/root/db/de.html +// Confirmed by registry (with technical +// reservations) 2008-07-01 +de + +// dj : https://www.iana.org/domains/root/db/dj.html +dj + +// dk : https://www.iana.org/domains/root/db/dk.html +// Confirmed by registry 2008-06-17 +dk + +// dm : https://www.iana.org/domains/root/db/dm.html +// https://nic.dm/policies/pdf/DMRulesandGuidelines2024v1.pdf +// Confirmed by registry 2024-11-19 +dm +co.dm +com.dm +edu.dm +gov.dm +net.dm +org.dm + +// do : https://www.iana.org/domains/root/db/do.html +do +art.do +com.do +edu.do +gob.do +gov.do +mil.do +net.do +org.do +sld.do +web.do + +// dz : http://www.nic.dz/images/pdf_nic/charte.pdf +dz +art.dz +asso.dz +com.dz +edu.dz +gov.dz +net.dz +org.dz +pol.dz +soc.dz +tm.dz + +// ec : https://www.nic.ec/ +// Submitted by registry +ec +abg.ec +adm.ec +agron.ec +arqt.ec +art.ec +bar.ec +chef.ec +com.ec +cont.ec +cpa.ec +cue.ec +dent.ec +dgn.ec +disco.ec +doc.ec +edu.ec +eng.ec +esm.ec +fin.ec +fot.ec +gal.ec +gob.ec +gov.ec +gye.ec +ibr.ec +info.ec +k12.ec +lat.ec +loj.ec +med.ec +mil.ec +mktg.ec +mon.ec +net.ec +ntr.ec +odont.ec +org.ec +pro.ec +prof.ec +psic.ec +psiq.ec +pub.ec +rio.ec +rrpp.ec +sal.ec +tech.ec +tul.ec +tur.ec +uio.ec +vet.ec +xxx.ec + +// edu : https://www.iana.org/domains/root/db/edu.html +edu + +// ee : https://www.internet.ee/domains/general-domains-and-procedure-for-registration-of-sub-domains-under-general-domains +ee +aip.ee +com.ee +edu.ee +fie.ee +gov.ee +lib.ee +med.ee +org.ee +pri.ee +riik.ee + +// eg : https://domain.eg/subdomain-names +eg +ac.eg +com.eg +edu.eg +eun.eg +gov.eg +info.eg +me.eg +mil.eg +name.eg +net.eg +org.eg +sci.eg +sport.eg +tv.eg + +// er : https://www.iana.org/domains/root/db/er.html +*.er + +// es : https://www.dominios.es/en +es +com.es +edu.es +gob.es +nom.es +org.es + +// et : https://www.iana.org/domains/root/db/et.html +et +biz.et +com.et +edu.et +gov.et +info.et +name.et +net.et +org.et + +// eu : https://www.iana.org/domains/root/db/eu.html +eu + +// fi : https://www.iana.org/domains/root/db/fi.html +fi +// aland.fi : https://www.iana.org/domains/root/db/ax.html +// This domain is being phased out in favor of .ax. As there are still many +// domains under aland.fi, we still keep it on the list until aland.fi is +// completely removed. +aland.fi + +// fj : https://www.iana.org/domains/root/db/fj.html +fj +ac.fj +biz.fj +com.fj +edu.fj +gov.fj +id.fj +info.fj +mil.fj +name.fj +net.fj +org.fj +pro.fj + +// fk : https://www.iana.org/domains/root/db/fk.html +*.fk + +// fm : https://www.iana.org/domains/root/db/fm.html +fm +com.fm +edu.fm +net.fm +org.fm + +// fo : https://www.iana.org/domains/root/db/fo.html +fo + +// fr : https://www.afnic.fr/ https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +fr +asso.fr +com.fr +gouv.fr +nom.fr +prd.fr +tm.fr +// Other SLDs now selfmanaged out of AFNIC range. Former "domaines sectoriels", still registration suffixes +avoues.fr +cci.fr +greta.fr +huissier-justice.fr + +// ga : https://www.iana.org/domains/root/db/ga.html +ga + +// gb : This registry is effectively dormant +// Submitted by registry +gb + +// gd : https://www.iana.org/domains/root/db/gd.html +gd +edu.gd +gov.gd + +// ge : https://nic.ge/en/administrator/the-ge-domain-regulations +// Confirmed by registry 2024-11-20 +ge +com.ge +cyb.ge +edu.ge +gov.ge +llc.ge +net.ge +online.ge +org.ge +pvt.ge +school.ge +tnx.ge + +// gf : https://www.iana.org/domains/root/db/gf.html +gf + +// gg : https://www.channelisles.net/register-1/register-direct +// Confirmed by registry 2013-11-28 +gg +co.gg +net.gg +org.gg + +// gh : https://www.iana.org/domains/root/db/gh.html +// https://www.nic.gh/ +// Although domains directly at second level are not possible at the moment, +// they have been possible for some time and may come back. +gh +biz.gh +com.gh +edu.gh +gov.gh +mil.gh +net.gh +org.gh + +// gi : https://www.nic.gi/rules.html +gi +com.gi +edu.gi +gov.gi +ltd.gi +mod.gi +org.gi + +// gl : http://nic.gl +gl +co.gl +com.gl +edu.gl +net.gl +org.gl + +// gm : https://www.nic.gm/NIC2/policies.html +gm + +// gn : http://psg.com/dns/gn/gn.txt +// Submitted by registry +gn +ac.gn +com.gn +edu.gn +gov.gn +net.gn +org.gn + +// gov : https://www.iana.org/domains/root/db/gov.html +gov + +// gp : http://www.nic.gp/index.php?lang=en +gp +asso.gp +com.gp +edu.gp +mobi.gp +net.gp +org.gp + +// gq : https://www.iana.org/domains/root/db/gq.html +gq + +// gr : https://www.iana.org/domains/root/db/gr.html +// Submitted by registry +gr +com.gr +edu.gr +gov.gr +net.gr +org.gr + +// gs : https://www.iana.org/domains/root/db/gs.html +gs + +// gt : https://www.gt/sitio/registration_policy.php?lang=en +gt +com.gt +edu.gt +gob.gt +ind.gt +mil.gt +net.gt +org.gt + +// gu : https://give.uog.edu/gu-domain-application-form/ +// University of Guam : https://www.uog.edu +// Submitted by uognoc@triton.uog.edu +gu +com.gu +edu.gu +gov.gu +guam.gu +info.gu +net.gu +org.gu +web.gu + +// gw : https://www.iana.org/domains/root/db/gw.html +// gw : https://nic.gw/regras/ +gw + +// gy : http://registry.gy/ +gy +co.gy +com.gy +edu.gy +gov.gy +net.gy +org.gy + +// hk : https://www.hkirc.hk +// Submitted by registry +hk +com.hk +edu.hk +gov.hk +idv.hk +net.hk +org.hk +个人.hk +個人.hk +公司.hk +政府.hk +敎育.hk +教育.hk +箇人.hk +組織.hk +組织.hk +網絡.hk +網络.hk +组織.hk +组织.hk +网絡.hk +网络.hk + +// hm : https://www.iana.org/domains/root/db/hm.html +hm + +// hn : https://www.iana.org/domains/root/db/hn.html +hn +com.hn +edu.hn +gob.hn +mil.hn +net.hn +org.hn + +// hr : https://domene.hr/en/portal/faq +hr +com.hr +// From.hr domene : http://from.hr/ +from.hr +iz.hr +name.hr + +// ht : http://www.nic.ht/info/charte.cfm +ht +adult.ht +art.ht +asso.ht +com.ht +coop.ht +edu.ht +firm.ht +gouv.ht +info.ht +med.ht +net.ht +org.ht +perso.ht +pol.ht +pro.ht +rel.ht +shop.ht + +// hu : https://www.iana.org/domains/root/db/hu.html +// Confirmed by registry 2008-06-12 +hu +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +co.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +info.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +org.hu +priv.hu +reklam.hu +sex.hu +shop.hu +sport.hu +suli.hu +szex.hu +tm.hu +tozsde.hu +utazas.hu +video.hu + +// id : https://www.iana.org/domains/root/db/id.html +id +ac.id +ai.id +biz.id +co.id +desa.id +go.id +kop.id +mil.id +my.id +net.id +or.id +ponpes.id +sch.id +web.id +// xn--9tfky.id (.id, Und-Bali) +ᬩᬮᬶ.id + +// ie : https://www.iana.org/domains/root/db/ie.html +ie +gov.ie + +// il : http://www.isoc.org.il/domains/ +// see also: https://en.isoc.org.il/il-cctld/registration-rules +// ISOC-IL (operated by .il Registry) +il +ac.il +co.il +gov.il +idf.il +k12.il +muni.il +net.il +org.il +// xn--4dbrk0ce ("Israel", Hebrew) : IL +ישראל +// xn--4dbgdty6c.xn--4dbrk0ce. +אקדמיה.ישראל +// xn--5dbhl8d.xn--4dbrk0ce. +ישוב.ישראל +// xn--8dbq2a.xn--4dbrk0ce. +צהל.ישראל +// xn--hebda8b.xn--4dbrk0ce. +ממשל.ישראל + +// im : https://www.nic.im/ +// Submitted by registry +im +ac.im +co.im +ltd.co.im +plc.co.im +com.im +net.im +org.im +tt.im +tv.im + +// in : https://www.iana.org/domains/root/db/in.html +// see also: https://registry.in/policies +// Please note, that nic.in is not an official eTLD, but used by most +// government institutions. +// Confirmed by Gaurav Kansal 2025-11-06 +// Added aero.in, alumni.in, school.in and ub.in by Gaurav Kansal 2026-06-25 +in +5g.in +6g.in +ac.in +aero.in +ai.in +alumni.in +am.in +bank.in +bihar.in +biz.in +business.in +ca.in +cn.in +co.in +com.in +coop.in +cs.in +delhi.in +dr.in +edu.in +er.in +fin.in +firm.in +gen.in +gov.in +gujarat.in +ind.in +info.in +int.in +internet.in +io.in +me.in +mil.in +net.in +nic.in +org.in +pg.in +post.in +pro.in +res.in +school.in +travel.in +tv.in +ub.in +uk.in +up.in +us.in + +// info : https://www.iana.org/domains/root/db/info.html +info + +// int : https://www.iana.org/domains/root/db/int.html +// Confirmed by registry 2008-06-18 +int +eu.int + +// io : http://www.nic.io/rules.htm +io +co.io +com.io +edu.io +gov.io +mil.io +net.io +nom.io +org.io + +// iq : https://cmc.iq/ +iq +com.iq +edu.iq +gov.iq +mil.iq +net.iq +org.iq + +// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules +// Also see http://www.nic.ir/Internationalized_Domain_Names +// Two .ir entries added at request of , 2010-04-16 +ir +ac.ir +co.ir +gov.ir +id.ir +net.ir +org.ir +sch.ir +// xn--mgba3a4f16a.ir (.ir, Persian YEH) +ایران.ir +// xn--mgba3a4fra.ir (.ir, Arabic YEH) +ايران.ir + +// is : http://www.isnic.is/domain/rules.php +// Confirmed by registry 2024-11-17 +is + +// it : https://www.nic.it/ +it +edu.it +gov.it +// Regions (3.3.1) +// https://www.nic.it/en/manage-your-it/forms-and-docs -> "Assignment and Management of domain names" +abr.it +abruzzo.it +aosta-valley.it +aostavalley.it +bas.it +basilicata.it +cal.it +calabria.it +cam.it +campania.it +emilia-romagna.it +emiliaromagna.it +emr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +fvg.it +laz.it +lazio.it +lig.it +liguria.it +lom.it +lombardia.it +lombardy.it +lucania.it +mar.it +marche.it +mol.it +molise.it +piedmont.it +piemonte.it +pmn.it +pug.it +puglia.it +sar.it +sardegna.it +sardinia.it +sic.it +sicilia.it +sicily.it +taa.it +tos.it +toscana.it +trentin-sud-tirol.it +trentin-süd-tirol.it +trentin-sudtirol.it +trentin-südtirol.it +trentin-sued-tirol.it +trentin-suedtirol.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-süd-tirol.it +trentino-sudtirol.it +trentino-südtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinostirol.it +trentinosud-tirol.it +trentinosüd-tirol.it +trentinosüdtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trentinsud-tirol.it +trentinsüd-tirol.it +trentinsudtirol.it +trentinsüdtirol.it +trentinsued-tirol.it +trentinsuedtirol.it +tuscany.it +umb.it +umbria.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valle-aosta.it +valle-d-aosta.it +valle-daosta.it +valleaosta.it +valled-aosta.it +valledaosta.it +vallee-aoste.it +vallée-aoste.it +vallee-d-aoste.it +vallée-d-aoste.it +valleeaoste.it +valléeaoste.it +valleedaoste.it +valléedaoste.it +vao.it +vda.it +ven.it +veneto.it +// Provinces (3.3.2) +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +balsan-sudtirol.it +balsan-südtirol.it +balsan-suedtirol.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bolzano-altoadige.it +bozen.it +bozen-sudtirol.it +bozen-südtirol.it +bozen-suedtirol.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bulsan.it +bulsan-sudtirol.it +bulsan-südtirol.it +bulsan-suedtirol.it +bz.it +ca.it +cagliari.it +caltanissetta.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesena-forlì.it +cesenaforli.it +cesenaforlì.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlì-cesena.it +forlicesena.it +forlìcesena.it +fr.it +frosinone.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +lc.it +le.it +lecce.it +lecco.it +li.it +livorno.it +lo.it +lodi.it +lt.it +lu.it +lucca.it +macerata.it +mantova.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +monza.it +monza-brianza.it +monza-e-della-brianza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +pisa.it +pistoia.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sassari.it +savona.it +si.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +su.it +sud-sardegna.it +sudsardegna.it +südtirol.it +suedtirol.it +sv.it +ta.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trentino.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tv.it +ud.it +udine.it +urbino-pesaro.it +urbinopesaro.it +va.it +varese.it +vb.it +vc.it +ve.it +venezia.it +venice.it +verbania.it +verbano-cusio-ossola.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it + +// je : https://www.iana.org/domains/root/db/je.html +// Confirmed by registry 2013-11-28 +je +co.je +net.je +org.je + +// jm : https://www.iana.org/domains/root/db/jm.html +*.jm + +// jo : https://www.dns.jo/JoFamily.aspx +// Confirmed by registry 2024-11-17 +jo +agri.jo +ai.jo +com.jo +edu.jo +eng.jo +fm.jo +gov.jo +mil.jo +net.jo +org.jo +per.jo +phd.jo +sch.jo +tv.jo + +// jobs : https://www.iana.org/domains/root/db/jobs.html +jobs + +// jp : https://www.iana.org/domains/root/db/jp.html +// http://jprs.co.jp/en/jpdomain.html +// Confirmed by registry 2024-11-22 +jp +// jp organizational type names +ac.jp +ad.jp +co.jp +ed.jp +go.jp +gr.jp +lg.jp +ne.jp +or.jp +// jp prefecture type names +aichi.jp +akita.jp +aomori.jp +chiba.jp +ehime.jp +fukui.jp +fukuoka.jp +fukushima.jp +gifu.jp +gunma.jp +hiroshima.jp +hokkaido.jp +hyogo.jp +ibaraki.jp +ishikawa.jp +iwate.jp +kagawa.jp +kagoshima.jp +kanagawa.jp +kochi.jp +kumamoto.jp +kyoto.jp +mie.jp +miyagi.jp +miyazaki.jp +nagano.jp +nagasaki.jp +nara.jp +niigata.jp +oita.jp +okayama.jp +okinawa.jp +osaka.jp +saga.jp +saitama.jp +shiga.jp +shimane.jp +shizuoka.jp +tochigi.jp +tokushima.jp +tokyo.jp +tottori.jp +toyama.jp +wakayama.jp +yamagata.jp +yamaguchi.jp +yamanashi.jp +三重.jp +京都.jp +佐賀.jp +兵庫.jp +北海道.jp +千葉.jp +和歌山.jp +埼玉.jp +大分.jp +大阪.jp +奈良.jp +宮城.jp +宮崎.jp +富山.jp +山口.jp +山形.jp +山梨.jp +岐阜.jp +岡山.jp +岩手.jp +島根.jp +広島.jp +徳島.jp +愛媛.jp +愛知.jp +新潟.jp +東京.jp +栃木.jp +沖縄.jp +滋賀.jp +熊本.jp +石川.jp +神奈川.jp +福井.jp +福岡.jp +福島.jp +秋田.jp +群馬.jp +茨城.jp +長崎.jp +長野.jp +青森.jp +静岡.jp +香川.jp +高知.jp +鳥取.jp +鹿児島.jp +// jp geographic type names +// http://jprs.jp/doc/rule/saisoku-1.html +// 2024-11-22: JPRS confirmed that jp geographic type names no longer accept new registrations. +// Once all existing registrations expire (marking full discontinuation), these suffixes +// will be removed from the PSL. +*.kawasaki.jp +!city.kawasaki.jp +*.kitakyushu.jp +!city.kitakyushu.jp +*.kobe.jp +!city.kobe.jp +*.nagoya.jp +!city.nagoya.jp +*.sapporo.jp +!city.sapporo.jp +*.sendai.jp +!city.sendai.jp +*.yokohama.jp +!city.yokohama.jp +// 4th level registration +aisai.aichi.jp +ama.aichi.jp +anjo.aichi.jp +asuke.aichi.jp +chiryu.aichi.jp +chita.aichi.jp +fuso.aichi.jp +gamagori.aichi.jp +handa.aichi.jp +hazu.aichi.jp +hekinan.aichi.jp +higashiura.aichi.jp +ichinomiya.aichi.jp +inazawa.aichi.jp +inuyama.aichi.jp +isshiki.aichi.jp +iwakura.aichi.jp +kanie.aichi.jp +kariya.aichi.jp +kasugai.aichi.jp +kira.aichi.jp +kiyosu.aichi.jp +komaki.aichi.jp +konan.aichi.jp +kota.aichi.jp +mihama.aichi.jp +miyoshi.aichi.jp +nishio.aichi.jp +nisshin.aichi.jp +obu.aichi.jp +oguchi.aichi.jp +oharu.aichi.jp +okazaki.aichi.jp +owariasahi.aichi.jp +seto.aichi.jp +shikatsu.aichi.jp +shinshiro.aichi.jp +shitara.aichi.jp +tahara.aichi.jp +takahama.aichi.jp +tobishima.aichi.jp +toei.aichi.jp +togo.aichi.jp +tokai.aichi.jp +tokoname.aichi.jp +toyoake.aichi.jp +toyohashi.aichi.jp +toyokawa.aichi.jp +toyone.aichi.jp +toyota.aichi.jp +tsushima.aichi.jp +yatomi.aichi.jp +akita.akita.jp +daisen.akita.jp +fujisato.akita.jp +gojome.akita.jp +hachirogata.akita.jp +happou.akita.jp +higashinaruse.akita.jp +honjo.akita.jp +honjyo.akita.jp +ikawa.akita.jp +kamikoani.akita.jp +kamioka.akita.jp +katagami.akita.jp +kazuno.akita.jp +kitaakita.akita.jp +kosaka.akita.jp +kyowa.akita.jp +misato.akita.jp +mitane.akita.jp +moriyoshi.akita.jp +nikaho.akita.jp +noshiro.akita.jp +odate.akita.jp +oga.akita.jp +ogata.akita.jp +semboku.akita.jp +yokote.akita.jp +yurihonjo.akita.jp +aomori.aomori.jp +gonohe.aomori.jp +hachinohe.aomori.jp +hashikami.aomori.jp +hiranai.aomori.jp +hirosaki.aomori.jp +itayanagi.aomori.jp +kuroishi.aomori.jp +misawa.aomori.jp +mutsu.aomori.jp +nakadomari.aomori.jp +noheji.aomori.jp +oirase.aomori.jp +owani.aomori.jp +rokunohe.aomori.jp +sannohe.aomori.jp +shichinohe.aomori.jp +shingo.aomori.jp +takko.aomori.jp +towada.aomori.jp +tsugaru.aomori.jp +tsuruta.aomori.jp +abiko.chiba.jp +asahi.chiba.jp +chonan.chiba.jp +chosei.chiba.jp +choshi.chiba.jp +chuo.chiba.jp +funabashi.chiba.jp +futtsu.chiba.jp +hanamigawa.chiba.jp +ichihara.chiba.jp +ichikawa.chiba.jp +ichinomiya.chiba.jp +inzai.chiba.jp +isumi.chiba.jp +kamagaya.chiba.jp +kamogawa.chiba.jp +kashiwa.chiba.jp +katori.chiba.jp +katsuura.chiba.jp +kimitsu.chiba.jp +kisarazu.chiba.jp +kozaki.chiba.jp +kujukuri.chiba.jp +kyonan.chiba.jp +matsudo.chiba.jp +midori.chiba.jp +mihama.chiba.jp +minamiboso.chiba.jp +mobara.chiba.jp +mutsuzawa.chiba.jp +nagara.chiba.jp +nagareyama.chiba.jp +narashino.chiba.jp +narita.chiba.jp +noda.chiba.jp +oamishirasato.chiba.jp +omigawa.chiba.jp +onjuku.chiba.jp +otaki.chiba.jp +sakae.chiba.jp +sakura.chiba.jp +shimofusa.chiba.jp +shirako.chiba.jp +shiroi.chiba.jp +shisui.chiba.jp +sodegaura.chiba.jp +sosa.chiba.jp +tako.chiba.jp +tateyama.chiba.jp +togane.chiba.jp +tohnosho.chiba.jp +tomisato.chiba.jp +urayasu.chiba.jp +yachimata.chiba.jp +yachiyo.chiba.jp +yokaichiba.chiba.jp +yokoshibahikari.chiba.jp +yotsukaido.chiba.jp +ainan.ehime.jp +honai.ehime.jp +ikata.ehime.jp +imabari.ehime.jp +iyo.ehime.jp +kamijima.ehime.jp +kihoku.ehime.jp +kumakogen.ehime.jp +masaki.ehime.jp +matsuno.ehime.jp +matsuyama.ehime.jp +namikata.ehime.jp +niihama.ehime.jp +ozu.ehime.jp +saijo.ehime.jp +seiyo.ehime.jp +shikokuchuo.ehime.jp +tobe.ehime.jp +toon.ehime.jp +uchiko.ehime.jp +uwajima.ehime.jp +yawatahama.ehime.jp +echizen.fukui.jp +eiheiji.fukui.jp +fukui.fukui.jp +ikeda.fukui.jp +katsuyama.fukui.jp +mihama.fukui.jp +minamiechizen.fukui.jp +obama.fukui.jp +ohi.fukui.jp +ono.fukui.jp +sabae.fukui.jp +sakai.fukui.jp +takahama.fukui.jp +tsuruga.fukui.jp +wakasa.fukui.jp +ashiya.fukuoka.jp +buzen.fukuoka.jp +chikugo.fukuoka.jp +chikuho.fukuoka.jp +chikujo.fukuoka.jp +chikushino.fukuoka.jp +chikuzen.fukuoka.jp +chuo.fukuoka.jp +dazaifu.fukuoka.jp +fukuchi.fukuoka.jp +hakata.fukuoka.jp +higashi.fukuoka.jp +hirokawa.fukuoka.jp +hisayama.fukuoka.jp +iizuka.fukuoka.jp +inatsuki.fukuoka.jp +kaho.fukuoka.jp +kasuga.fukuoka.jp +kasuya.fukuoka.jp +kawara.fukuoka.jp +keisen.fukuoka.jp +koga.fukuoka.jp +kurate.fukuoka.jp +kurogi.fukuoka.jp +kurume.fukuoka.jp +minami.fukuoka.jp +miyako.fukuoka.jp +miyama.fukuoka.jp +miyawaka.fukuoka.jp +mizumaki.fukuoka.jp +munakata.fukuoka.jp +nakagawa.fukuoka.jp +nakama.fukuoka.jp +nishi.fukuoka.jp +nogata.fukuoka.jp +ogori.fukuoka.jp +okagaki.fukuoka.jp +okawa.fukuoka.jp +oki.fukuoka.jp +omuta.fukuoka.jp +onga.fukuoka.jp +onojo.fukuoka.jp +oto.fukuoka.jp +saigawa.fukuoka.jp +sasaguri.fukuoka.jp +shingu.fukuoka.jp +shinyoshitomi.fukuoka.jp +shonai.fukuoka.jp +soeda.fukuoka.jp +sue.fukuoka.jp +tachiarai.fukuoka.jp +tagawa.fukuoka.jp +takata.fukuoka.jp +toho.fukuoka.jp +toyotsu.fukuoka.jp +tsuiki.fukuoka.jp +ukiha.fukuoka.jp +umi.fukuoka.jp +usui.fukuoka.jp +yamada.fukuoka.jp +yame.fukuoka.jp +yanagawa.fukuoka.jp +yukuhashi.fukuoka.jp +aizubange.fukushima.jp +aizumisato.fukushima.jp +aizuwakamatsu.fukushima.jp +asakawa.fukushima.jp +bandai.fukushima.jp +date.fukushima.jp +fukushima.fukushima.jp +furudono.fukushima.jp +futaba.fukushima.jp +hanawa.fukushima.jp +higashi.fukushima.jp +hirata.fukushima.jp +hirono.fukushima.jp +iitate.fukushima.jp +inawashiro.fukushima.jp +ishikawa.fukushima.jp +iwaki.fukushima.jp +izumizaki.fukushima.jp +kagamiishi.fukushima.jp +kaneyama.fukushima.jp +kawamata.fukushima.jp +kitakata.fukushima.jp +kitashiobara.fukushima.jp +koori.fukushima.jp +koriyama.fukushima.jp +kunimi.fukushima.jp +miharu.fukushima.jp +mishima.fukushima.jp +namie.fukushima.jp +nango.fukushima.jp +nishiaizu.fukushima.jp +nishigo.fukushima.jp +okuma.fukushima.jp +omotego.fukushima.jp +ono.fukushima.jp +otama.fukushima.jp +samegawa.fukushima.jp +shimogo.fukushima.jp +shirakawa.fukushima.jp +showa.fukushima.jp +soma.fukushima.jp +sukagawa.fukushima.jp +taishin.fukushima.jp +tamakawa.fukushima.jp +tanagura.fukushima.jp +tenei.fukushima.jp +yabuki.fukushima.jp +yamato.fukushima.jp +yamatsuri.fukushima.jp +yanaizu.fukushima.jp +yugawa.fukushima.jp +anpachi.gifu.jp +ena.gifu.jp +gifu.gifu.jp +ginan.gifu.jp +godo.gifu.jp +gujo.gifu.jp +hashima.gifu.jp +hichiso.gifu.jp +hida.gifu.jp +higashishirakawa.gifu.jp +ibigawa.gifu.jp +ikeda.gifu.jp +kakamigahara.gifu.jp +kani.gifu.jp +kasahara.gifu.jp +kasamatsu.gifu.jp +kawaue.gifu.jp +kitagata.gifu.jp +mino.gifu.jp +minokamo.gifu.jp +mitake.gifu.jp +mizunami.gifu.jp +motosu.gifu.jp +nakatsugawa.gifu.jp +ogaki.gifu.jp +sakahogi.gifu.jp +seki.gifu.jp +sekigahara.gifu.jp +shirakawa.gifu.jp +tajimi.gifu.jp +takayama.gifu.jp +tarui.gifu.jp +toki.gifu.jp +tomika.gifu.jp +wanouchi.gifu.jp +yamagata.gifu.jp +yaotsu.gifu.jp +yoro.gifu.jp +annaka.gunma.jp +chiyoda.gunma.jp +fujioka.gunma.jp +higashiagatsuma.gunma.jp +isesaki.gunma.jp +itakura.gunma.jp +kanna.gunma.jp +kanra.gunma.jp +katashina.gunma.jp +kawaba.gunma.jp +kiryu.gunma.jp +kusatsu.gunma.jp +maebashi.gunma.jp +meiwa.gunma.jp +midori.gunma.jp +minakami.gunma.jp +naganohara.gunma.jp +nakanojo.gunma.jp +nanmoku.gunma.jp +numata.gunma.jp +oizumi.gunma.jp +ora.gunma.jp +ota.gunma.jp +shibukawa.gunma.jp +shimonita.gunma.jp +shinto.gunma.jp +showa.gunma.jp +takasaki.gunma.jp +takayama.gunma.jp +tamamura.gunma.jp +tatebayashi.gunma.jp +tomioka.gunma.jp +tsukiyono.gunma.jp +tsumagoi.gunma.jp +ueno.gunma.jp +yoshioka.gunma.jp +asaminami.hiroshima.jp +daiwa.hiroshima.jp +etajima.hiroshima.jp +fuchu.hiroshima.jp +fukuyama.hiroshima.jp +hatsukaichi.hiroshima.jp +higashihiroshima.hiroshima.jp +hongo.hiroshima.jp +jinsekikogen.hiroshima.jp +kaita.hiroshima.jp +kui.hiroshima.jp +kumano.hiroshima.jp +kure.hiroshima.jp +mihara.hiroshima.jp +miyoshi.hiroshima.jp +naka.hiroshima.jp +onomichi.hiroshima.jp +osakikamijima.hiroshima.jp +otake.hiroshima.jp +saka.hiroshima.jp +sera.hiroshima.jp +seranishi.hiroshima.jp +shinichi.hiroshima.jp +shobara.hiroshima.jp +takehara.hiroshima.jp +abashiri.hokkaido.jp +abira.hokkaido.jp +aibetsu.hokkaido.jp +akabira.hokkaido.jp +akkeshi.hokkaido.jp +asahikawa.hokkaido.jp +ashibetsu.hokkaido.jp +ashoro.hokkaido.jp +assabu.hokkaido.jp +atsuma.hokkaido.jp +bibai.hokkaido.jp +biei.hokkaido.jp +bifuka.hokkaido.jp +bihoro.hokkaido.jp +biratori.hokkaido.jp +chippubetsu.hokkaido.jp +chitose.hokkaido.jp +date.hokkaido.jp +ebetsu.hokkaido.jp +embetsu.hokkaido.jp +eniwa.hokkaido.jp +erimo.hokkaido.jp +esan.hokkaido.jp +esashi.hokkaido.jp +fukagawa.hokkaido.jp +fukushima.hokkaido.jp +furano.hokkaido.jp +furubira.hokkaido.jp +haboro.hokkaido.jp +hakodate.hokkaido.jp +hamatonbetsu.hokkaido.jp +hidaka.hokkaido.jp +higashikagura.hokkaido.jp +higashikawa.hokkaido.jp +hiroo.hokkaido.jp +hokuryu.hokkaido.jp +hokuto.hokkaido.jp +honbetsu.hokkaido.jp +horokanai.hokkaido.jp +horonobe.hokkaido.jp +ikeda.hokkaido.jp +imakane.hokkaido.jp +ishikari.hokkaido.jp +iwamizawa.hokkaido.jp +iwanai.hokkaido.jp +kamifurano.hokkaido.jp +kamikawa.hokkaido.jp +kamishihoro.hokkaido.jp +kamisunagawa.hokkaido.jp +kamoenai.hokkaido.jp +kayabe.hokkaido.jp +kembuchi.hokkaido.jp +kikonai.hokkaido.jp +kimobetsu.hokkaido.jp +kitahiroshima.hokkaido.jp +kitami.hokkaido.jp +kiyosato.hokkaido.jp +koshimizu.hokkaido.jp +kunneppu.hokkaido.jp +kuriyama.hokkaido.jp +kuromatsunai.hokkaido.jp +kushiro.hokkaido.jp +kutchan.hokkaido.jp +kyowa.hokkaido.jp +mashike.hokkaido.jp +matsumae.hokkaido.jp +mikasa.hokkaido.jp +minamifurano.hokkaido.jp +mombetsu.hokkaido.jp +moseushi.hokkaido.jp +mukawa.hokkaido.jp +muroran.hokkaido.jp +naie.hokkaido.jp +nakagawa.hokkaido.jp +nakasatsunai.hokkaido.jp +nakatombetsu.hokkaido.jp +nanae.hokkaido.jp +nanporo.hokkaido.jp +nayoro.hokkaido.jp +nemuro.hokkaido.jp +niikappu.hokkaido.jp +niki.hokkaido.jp +nishiokoppe.hokkaido.jp +noboribetsu.hokkaido.jp +numata.hokkaido.jp +obihiro.hokkaido.jp +obira.hokkaido.jp +oketo.hokkaido.jp +okoppe.hokkaido.jp +otaru.hokkaido.jp +otobe.hokkaido.jp +otofuke.hokkaido.jp +otoineppu.hokkaido.jp +oumu.hokkaido.jp +ozora.hokkaido.jp +pippu.hokkaido.jp +rankoshi.hokkaido.jp +rebun.hokkaido.jp +rikubetsu.hokkaido.jp +rishiri.hokkaido.jp +rishirifuji.hokkaido.jp +saroma.hokkaido.jp +sarufutsu.hokkaido.jp +shakotan.hokkaido.jp +shari.hokkaido.jp +shibecha.hokkaido.jp +shibetsu.hokkaido.jp +shikabe.hokkaido.jp +shikaoi.hokkaido.jp +shimamaki.hokkaido.jp +shimizu.hokkaido.jp +shimokawa.hokkaido.jp +shinshinotsu.hokkaido.jp +shintoku.hokkaido.jp +shiranuka.hokkaido.jp +shiraoi.hokkaido.jp +shiriuchi.hokkaido.jp +sobetsu.hokkaido.jp +sunagawa.hokkaido.jp +taiki.hokkaido.jp +takasu.hokkaido.jp +takikawa.hokkaido.jp +takinoue.hokkaido.jp +teshikaga.hokkaido.jp +tobetsu.hokkaido.jp +tohma.hokkaido.jp +tomakomai.hokkaido.jp +tomari.hokkaido.jp +toya.hokkaido.jp +toyako.hokkaido.jp +toyotomi.hokkaido.jp +toyoura.hokkaido.jp +tsubetsu.hokkaido.jp +tsukigata.hokkaido.jp +urakawa.hokkaido.jp +urausu.hokkaido.jp +uryu.hokkaido.jp +utashinai.hokkaido.jp +wakkanai.hokkaido.jp +wassamu.hokkaido.jp +yakumo.hokkaido.jp +yoichi.hokkaido.jp +aioi.hyogo.jp +akashi.hyogo.jp +ako.hyogo.jp +amagasaki.hyogo.jp +aogaki.hyogo.jp +asago.hyogo.jp +ashiya.hyogo.jp +awaji.hyogo.jp +fukusaki.hyogo.jp +goshiki.hyogo.jp +harima.hyogo.jp +himeji.hyogo.jp +ichikawa.hyogo.jp +inagawa.hyogo.jp +itami.hyogo.jp +kakogawa.hyogo.jp +kamigori.hyogo.jp +kamikawa.hyogo.jp +kasai.hyogo.jp +kasuga.hyogo.jp +kawanishi.hyogo.jp +miki.hyogo.jp +minamiawaji.hyogo.jp +nishinomiya.hyogo.jp +nishiwaki.hyogo.jp +ono.hyogo.jp +sanda.hyogo.jp +sannan.hyogo.jp +sasayama.hyogo.jp +sayo.hyogo.jp +shingu.hyogo.jp +shinonsen.hyogo.jp +shiso.hyogo.jp +sumoto.hyogo.jp +taishi.hyogo.jp +taka.hyogo.jp +takarazuka.hyogo.jp +takasago.hyogo.jp +takino.hyogo.jp +tamba.hyogo.jp +tatsuno.hyogo.jp +toyooka.hyogo.jp +yabu.hyogo.jp +yashiro.hyogo.jp +yoka.hyogo.jp +yokawa.hyogo.jp +ami.ibaraki.jp +asahi.ibaraki.jp +bando.ibaraki.jp +chikusei.ibaraki.jp +daigo.ibaraki.jp +fujishiro.ibaraki.jp +hitachi.ibaraki.jp +hitachinaka.ibaraki.jp +hitachiomiya.ibaraki.jp +hitachiota.ibaraki.jp +ibaraki.ibaraki.jp +ina.ibaraki.jp +inashiki.ibaraki.jp +itako.ibaraki.jp +iwama.ibaraki.jp +joso.ibaraki.jp +kamisu.ibaraki.jp +kasama.ibaraki.jp +kashima.ibaraki.jp +kasumigaura.ibaraki.jp +koga.ibaraki.jp +miho.ibaraki.jp +mito.ibaraki.jp +moriya.ibaraki.jp +naka.ibaraki.jp +namegata.ibaraki.jp +oarai.ibaraki.jp +ogawa.ibaraki.jp +omitama.ibaraki.jp +ryugasaki.ibaraki.jp +sakai.ibaraki.jp +sakuragawa.ibaraki.jp +shimodate.ibaraki.jp +shimotsuma.ibaraki.jp +shirosato.ibaraki.jp +sowa.ibaraki.jp +suifu.ibaraki.jp +takahagi.ibaraki.jp +tamatsukuri.ibaraki.jp +tokai.ibaraki.jp +tomobe.ibaraki.jp +tone.ibaraki.jp +toride.ibaraki.jp +tsuchiura.ibaraki.jp +tsukuba.ibaraki.jp +uchihara.ibaraki.jp +ushiku.ibaraki.jp +yachiyo.ibaraki.jp +yamagata.ibaraki.jp +yawara.ibaraki.jp +yuki.ibaraki.jp +anamizu.ishikawa.jp +hakui.ishikawa.jp +hakusan.ishikawa.jp +kaga.ishikawa.jp +kahoku.ishikawa.jp +kanazawa.ishikawa.jp +kawakita.ishikawa.jp +komatsu.ishikawa.jp +nakanoto.ishikawa.jp +nanao.ishikawa.jp +nomi.ishikawa.jp +nonoichi.ishikawa.jp +noto.ishikawa.jp +shika.ishikawa.jp +suzu.ishikawa.jp +tsubata.ishikawa.jp +tsurugi.ishikawa.jp +uchinada.ishikawa.jp +wajima.ishikawa.jp +fudai.iwate.jp +fujisawa.iwate.jp +hanamaki.iwate.jp +hiraizumi.iwate.jp +hirono.iwate.jp +ichinohe.iwate.jp +ichinoseki.iwate.jp +iwaizumi.iwate.jp +iwate.iwate.jp +joboji.iwate.jp +kamaishi.iwate.jp +kanegasaki.iwate.jp +karumai.iwate.jp +kawai.iwate.jp +kitakami.iwate.jp +kuji.iwate.jp +kunohe.iwate.jp +kuzumaki.iwate.jp +miyako.iwate.jp +mizusawa.iwate.jp +morioka.iwate.jp +ninohe.iwate.jp +noda.iwate.jp +ofunato.iwate.jp +oshu.iwate.jp +otsuchi.iwate.jp +rikuzentakata.iwate.jp +shiwa.iwate.jp +shizukuishi.iwate.jp +sumita.iwate.jp +tanohata.iwate.jp +tono.iwate.jp +yahaba.iwate.jp +yamada.iwate.jp +ayagawa.kagawa.jp +higashikagawa.kagawa.jp +kanonji.kagawa.jp +kotohira.kagawa.jp +manno.kagawa.jp +marugame.kagawa.jp +mitoyo.kagawa.jp +naoshima.kagawa.jp +sanuki.kagawa.jp +tadotsu.kagawa.jp +takamatsu.kagawa.jp +tonosho.kagawa.jp +uchinomi.kagawa.jp +utazu.kagawa.jp +zentsuji.kagawa.jp +akune.kagoshima.jp +amami.kagoshima.jp +hioki.kagoshima.jp +isa.kagoshima.jp +isen.kagoshima.jp +izumi.kagoshima.jp +kagoshima.kagoshima.jp +kanoya.kagoshima.jp +kawanabe.kagoshima.jp +kinko.kagoshima.jp +kouyama.kagoshima.jp +makurazaki.kagoshima.jp +matsumoto.kagoshima.jp +minamitane.kagoshima.jp +nakatane.kagoshima.jp +nishinoomote.kagoshima.jp +satsumasendai.kagoshima.jp +soo.kagoshima.jp +tarumizu.kagoshima.jp +yusui.kagoshima.jp +aikawa.kanagawa.jp +atsugi.kanagawa.jp +ayase.kanagawa.jp +chigasaki.kanagawa.jp +ebina.kanagawa.jp +fujisawa.kanagawa.jp +hadano.kanagawa.jp +hakone.kanagawa.jp +hiratsuka.kanagawa.jp +isehara.kanagawa.jp +kaisei.kanagawa.jp +kamakura.kanagawa.jp +kiyokawa.kanagawa.jp +matsuda.kanagawa.jp +minamiashigara.kanagawa.jp +miura.kanagawa.jp +nakai.kanagawa.jp +ninomiya.kanagawa.jp +odawara.kanagawa.jp +oi.kanagawa.jp +oiso.kanagawa.jp +sagamihara.kanagawa.jp +samukawa.kanagawa.jp +tsukui.kanagawa.jp +yamakita.kanagawa.jp +yamato.kanagawa.jp +yokosuka.kanagawa.jp +yugawara.kanagawa.jp +zama.kanagawa.jp +zushi.kanagawa.jp +aki.kochi.jp +geisei.kochi.jp +hidaka.kochi.jp +higashitsuno.kochi.jp +ino.kochi.jp +kagami.kochi.jp +kami.kochi.jp +kitagawa.kochi.jp +kochi.kochi.jp +mihara.kochi.jp +motoyama.kochi.jp +muroto.kochi.jp +nahari.kochi.jp +nakamura.kochi.jp +nankoku.kochi.jp +nishitosa.kochi.jp +niyodogawa.kochi.jp +ochi.kochi.jp +okawa.kochi.jp +otoyo.kochi.jp +otsuki.kochi.jp +sakawa.kochi.jp +sukumo.kochi.jp +susaki.kochi.jp +tosa.kochi.jp +tosashimizu.kochi.jp +toyo.kochi.jp +tsuno.kochi.jp +umaji.kochi.jp +yasuda.kochi.jp +yusuhara.kochi.jp +amakusa.kumamoto.jp +arao.kumamoto.jp +aso.kumamoto.jp +choyo.kumamoto.jp +gyokuto.kumamoto.jp +kamiamakusa.kumamoto.jp +kikuchi.kumamoto.jp +kumamoto.kumamoto.jp +mashiki.kumamoto.jp +mifune.kumamoto.jp +minamata.kumamoto.jp +minamioguni.kumamoto.jp +nagasu.kumamoto.jp +nishihara.kumamoto.jp +oguni.kumamoto.jp +ozu.kumamoto.jp +sumoto.kumamoto.jp +takamori.kumamoto.jp +uki.kumamoto.jp +uto.kumamoto.jp +yamaga.kumamoto.jp +yamato.kumamoto.jp +yatsushiro.kumamoto.jp +ayabe.kyoto.jp +fukuchiyama.kyoto.jp +higashiyama.kyoto.jp +ide.kyoto.jp +ine.kyoto.jp +joyo.kyoto.jp +kameoka.kyoto.jp +kamo.kyoto.jp +kita.kyoto.jp +kizu.kyoto.jp +kumiyama.kyoto.jp +kyotamba.kyoto.jp +kyotanabe.kyoto.jp +kyotango.kyoto.jp +maizuru.kyoto.jp +minami.kyoto.jp +minamiyamashiro.kyoto.jp +miyazu.kyoto.jp +muko.kyoto.jp +nagaokakyo.kyoto.jp +nakagyo.kyoto.jp +nantan.kyoto.jp +oyamazaki.kyoto.jp +sakyo.kyoto.jp +seika.kyoto.jp +tanabe.kyoto.jp +uji.kyoto.jp +ujitawara.kyoto.jp +wazuka.kyoto.jp +yamashina.kyoto.jp +yawata.kyoto.jp +asahi.mie.jp +inabe.mie.jp +ise.mie.jp +kameyama.mie.jp +kawagoe.mie.jp +kiho.mie.jp +kisosaki.mie.jp +kiwa.mie.jp +komono.mie.jp +kumano.mie.jp +kuwana.mie.jp +matsusaka.mie.jp +meiwa.mie.jp +mihama.mie.jp +minamiise.mie.jp +misugi.mie.jp +miyama.mie.jp +nabari.mie.jp +shima.mie.jp +suzuka.mie.jp +tado.mie.jp +taiki.mie.jp +taki.mie.jp +tamaki.mie.jp +toba.mie.jp +tsu.mie.jp +udono.mie.jp +ureshino.mie.jp +watarai.mie.jp +yokkaichi.mie.jp +furukawa.miyagi.jp +higashimatsushima.miyagi.jp +ishinomaki.miyagi.jp +iwanuma.miyagi.jp +kakuda.miyagi.jp +kami.miyagi.jp +kawasaki.miyagi.jp +marumori.miyagi.jp +matsushima.miyagi.jp +minamisanriku.miyagi.jp +misato.miyagi.jp +murata.miyagi.jp +natori.miyagi.jp +ogawara.miyagi.jp +ohira.miyagi.jp +onagawa.miyagi.jp +osaki.miyagi.jp +rifu.miyagi.jp +semine.miyagi.jp +shibata.miyagi.jp +shichikashuku.miyagi.jp +shikama.miyagi.jp +shiogama.miyagi.jp +shiroishi.miyagi.jp +tagajo.miyagi.jp +taiwa.miyagi.jp +tome.miyagi.jp +tomiya.miyagi.jp +wakuya.miyagi.jp +watari.miyagi.jp +yamamoto.miyagi.jp +zao.miyagi.jp +aya.miyazaki.jp +ebino.miyazaki.jp +gokase.miyazaki.jp +hyuga.miyazaki.jp +kadogawa.miyazaki.jp +kawaminami.miyazaki.jp +kijo.miyazaki.jp +kitagawa.miyazaki.jp +kitakata.miyazaki.jp +kitaura.miyazaki.jp +kobayashi.miyazaki.jp +kunitomi.miyazaki.jp +kushima.miyazaki.jp +mimata.miyazaki.jp +miyakonojo.miyazaki.jp +miyazaki.miyazaki.jp +morotsuka.miyazaki.jp +nichinan.miyazaki.jp +nishimera.miyazaki.jp +nobeoka.miyazaki.jp +saito.miyazaki.jp +shiiba.miyazaki.jp +shintomi.miyazaki.jp +takaharu.miyazaki.jp +takanabe.miyazaki.jp +takazaki.miyazaki.jp +tsuno.miyazaki.jp +achi.nagano.jp +agematsu.nagano.jp +anan.nagano.jp +aoki.nagano.jp +asahi.nagano.jp +azumino.nagano.jp +chikuhoku.nagano.jp +chikuma.nagano.jp +chino.nagano.jp +fujimi.nagano.jp +hakuba.nagano.jp +hara.nagano.jp +hiraya.nagano.jp +iida.nagano.jp +iijima.nagano.jp +iiyama.nagano.jp +iizuna.nagano.jp +ikeda.nagano.jp +ikusaka.nagano.jp +ina.nagano.jp +karuizawa.nagano.jp +kawakami.nagano.jp +kiso.nagano.jp +kisofukushima.nagano.jp +kitaaiki.nagano.jp +komagane.nagano.jp +komoro.nagano.jp +matsukawa.nagano.jp +matsumoto.nagano.jp +miasa.nagano.jp +minamiaiki.nagano.jp +minamimaki.nagano.jp +minamiminowa.nagano.jp +minowa.nagano.jp +miyada.nagano.jp +miyota.nagano.jp +mochizuki.nagano.jp +nagano.nagano.jp +nagawa.nagano.jp +nagiso.nagano.jp +nakagawa.nagano.jp +nakano.nagano.jp +nozawaonsen.nagano.jp +obuse.nagano.jp +ogawa.nagano.jp +okaya.nagano.jp +omachi.nagano.jp +omi.nagano.jp +ookuwa.nagano.jp +ooshika.nagano.jp +otaki.nagano.jp +otari.nagano.jp +sakae.nagano.jp +sakaki.nagano.jp +saku.nagano.jp +sakuho.nagano.jp +shimosuwa.nagano.jp +shinanomachi.nagano.jp +shiojiri.nagano.jp +suwa.nagano.jp +suzaka.nagano.jp +takagi.nagano.jp +takamori.nagano.jp +takayama.nagano.jp +tateshina.nagano.jp +tatsuno.nagano.jp +togakushi.nagano.jp +togura.nagano.jp +tomi.nagano.jp +ueda.nagano.jp +wada.nagano.jp +yamagata.nagano.jp +yamanouchi.nagano.jp +yasaka.nagano.jp +yasuoka.nagano.jp +chijiwa.nagasaki.jp +futsu.nagasaki.jp +goto.nagasaki.jp +hasami.nagasaki.jp +hirado.nagasaki.jp +iki.nagasaki.jp +isahaya.nagasaki.jp +kawatana.nagasaki.jp +kuchinotsu.nagasaki.jp +matsuura.nagasaki.jp +nagasaki.nagasaki.jp +obama.nagasaki.jp +omura.nagasaki.jp +oseto.nagasaki.jp +saikai.nagasaki.jp +sasebo.nagasaki.jp +seihi.nagasaki.jp +shimabara.nagasaki.jp +shinkamigoto.nagasaki.jp +togitsu.nagasaki.jp +tsushima.nagasaki.jp +unzen.nagasaki.jp +ando.nara.jp +gose.nara.jp +heguri.nara.jp +higashiyoshino.nara.jp +ikaruga.nara.jp +ikoma.nara.jp +kamikitayama.nara.jp +kanmaki.nara.jp +kashiba.nara.jp +kashihara.nara.jp +katsuragi.nara.jp +kawai.nara.jp +kawakami.nara.jp +kawanishi.nara.jp +koryo.nara.jp +kurotaki.nara.jp +mitsue.nara.jp +miyake.nara.jp +nara.nara.jp +nosegawa.nara.jp +oji.nara.jp +ouda.nara.jp +oyodo.nara.jp +sakurai.nara.jp +sango.nara.jp +shimoichi.nara.jp +shimokitayama.nara.jp +shinjo.nara.jp +soni.nara.jp +takatori.nara.jp +tawaramoto.nara.jp +tenkawa.nara.jp +tenri.nara.jp +uda.nara.jp +yamatokoriyama.nara.jp +yamatotakada.nara.jp +yamazoe.nara.jp +yoshino.nara.jp +aga.niigata.jp +agano.niigata.jp +gosen.niigata.jp +itoigawa.niigata.jp +izumozaki.niigata.jp +joetsu.niigata.jp +kamo.niigata.jp +kariwa.niigata.jp +kashiwazaki.niigata.jp +minamiuonuma.niigata.jp +mitsuke.niigata.jp +muika.niigata.jp +murakami.niigata.jp +myoko.niigata.jp +nagaoka.niigata.jp +niigata.niigata.jp +ojiya.niigata.jp +omi.niigata.jp +sado.niigata.jp +sanjo.niigata.jp +seiro.niigata.jp +seirou.niigata.jp +sekikawa.niigata.jp +shibata.niigata.jp +tagami.niigata.jp +tainai.niigata.jp +tochio.niigata.jp +tokamachi.niigata.jp +tsubame.niigata.jp +tsunan.niigata.jp +uonuma.niigata.jp +yahiko.niigata.jp +yoita.niigata.jp +yuzawa.niigata.jp +beppu.oita.jp +bungoono.oita.jp +bungotakada.oita.jp +hasama.oita.jp +hiji.oita.jp +himeshima.oita.jp +hita.oita.jp +kamitsue.oita.jp +kokonoe.oita.jp +kuju.oita.jp +kunisaki.oita.jp +kusu.oita.jp +oita.oita.jp +saiki.oita.jp +taketa.oita.jp +tsukumi.oita.jp +usa.oita.jp +usuki.oita.jp +yufu.oita.jp +akaiwa.okayama.jp +asakuchi.okayama.jp +bizen.okayama.jp +hayashima.okayama.jp +ibara.okayama.jp +kagamino.okayama.jp +kasaoka.okayama.jp +kibichuo.okayama.jp +kumenan.okayama.jp +kurashiki.okayama.jp +maniwa.okayama.jp +misaki.okayama.jp +nagi.okayama.jp +niimi.okayama.jp +nishiawakura.okayama.jp +okayama.okayama.jp +satosho.okayama.jp +setouchi.okayama.jp +shinjo.okayama.jp +shoo.okayama.jp +soja.okayama.jp +takahashi.okayama.jp +tamano.okayama.jp +tsuyama.okayama.jp +wake.okayama.jp +yakage.okayama.jp +aguni.okinawa.jp +ginowan.okinawa.jp +ginoza.okinawa.jp +gushikami.okinawa.jp +haebaru.okinawa.jp +higashi.okinawa.jp +hirara.okinawa.jp +iheya.okinawa.jp +ishigaki.okinawa.jp +ishikawa.okinawa.jp +itoman.okinawa.jp +izena.okinawa.jp +kadena.okinawa.jp +kin.okinawa.jp +kitadaito.okinawa.jp +kitanakagusuku.okinawa.jp +kumejima.okinawa.jp +kunigami.okinawa.jp +minamidaito.okinawa.jp +motobu.okinawa.jp +nago.okinawa.jp +naha.okinawa.jp +nakagusuku.okinawa.jp +nakijin.okinawa.jp +nanjo.okinawa.jp +nishihara.okinawa.jp +ogimi.okinawa.jp +okinawa.okinawa.jp +onna.okinawa.jp +shimoji.okinawa.jp +taketomi.okinawa.jp +tarama.okinawa.jp +tokashiki.okinawa.jp +tomigusuku.okinawa.jp +tonaki.okinawa.jp +urasoe.okinawa.jp +uruma.okinawa.jp +yaese.okinawa.jp +yomitan.okinawa.jp +yonabaru.okinawa.jp +yonaguni.okinawa.jp +zamami.okinawa.jp +abeno.osaka.jp +chihayaakasaka.osaka.jp +chuo.osaka.jp +daito.osaka.jp +fujiidera.osaka.jp +habikino.osaka.jp +hannan.osaka.jp +higashiosaka.osaka.jp +higashisumiyoshi.osaka.jp +higashiyodogawa.osaka.jp +hirakata.osaka.jp +ibaraki.osaka.jp +ikeda.osaka.jp +izumi.osaka.jp +izumiotsu.osaka.jp +izumisano.osaka.jp +kadoma.osaka.jp +kaizuka.osaka.jp +kanan.osaka.jp +kashiwara.osaka.jp +katano.osaka.jp +kawachinagano.osaka.jp +kishiwada.osaka.jp +kita.osaka.jp +kumatori.osaka.jp +matsubara.osaka.jp +minato.osaka.jp +minoh.osaka.jp +misaki.osaka.jp +moriguchi.osaka.jp +neyagawa.osaka.jp +nishi.osaka.jp +nose.osaka.jp +osakasayama.osaka.jp +sakai.osaka.jp +sayama.osaka.jp +sennan.osaka.jp +settsu.osaka.jp +shijonawate.osaka.jp +shimamoto.osaka.jp +suita.osaka.jp +tadaoka.osaka.jp +taishi.osaka.jp +tajiri.osaka.jp +takaishi.osaka.jp +takatsuki.osaka.jp +tondabayashi.osaka.jp +toyonaka.osaka.jp +toyono.osaka.jp +yao.osaka.jp +ariake.saga.jp +arita.saga.jp +fukudomi.saga.jp +genkai.saga.jp +hamatama.saga.jp +hizen.saga.jp +imari.saga.jp +kamimine.saga.jp +kanzaki.saga.jp +karatsu.saga.jp +kashima.saga.jp +kitagata.saga.jp +kitahata.saga.jp +kiyama.saga.jp +kouhoku.saga.jp +kyuragi.saga.jp +nishiarita.saga.jp +ogi.saga.jp +omachi.saga.jp +ouchi.saga.jp +saga.saga.jp +shiroishi.saga.jp +taku.saga.jp +tara.saga.jp +tosu.saga.jp +yoshinogari.saga.jp +arakawa.saitama.jp +asaka.saitama.jp +chichibu.saitama.jp +fujimi.saitama.jp +fujimino.saitama.jp +fukaya.saitama.jp +hanno.saitama.jp +hanyu.saitama.jp +hasuda.saitama.jp +hatogaya.saitama.jp +hatoyama.saitama.jp +hidaka.saitama.jp +higashichichibu.saitama.jp +higashimatsuyama.saitama.jp +honjo.saitama.jp +ina.saitama.jp +iruma.saitama.jp +iwatsuki.saitama.jp +kamiizumi.saitama.jp +kamikawa.saitama.jp +kamisato.saitama.jp +kasukabe.saitama.jp +kawagoe.saitama.jp +kawaguchi.saitama.jp +kawajima.saitama.jp +kazo.saitama.jp +kitamoto.saitama.jp +koshigaya.saitama.jp +kounosu.saitama.jp +kuki.saitama.jp +kumagaya.saitama.jp +matsubushi.saitama.jp +minano.saitama.jp +misato.saitama.jp +miyashiro.saitama.jp +miyoshi.saitama.jp +moroyama.saitama.jp +nagatoro.saitama.jp +namegawa.saitama.jp +niiza.saitama.jp +ogano.saitama.jp +ogawa.saitama.jp +ogose.saitama.jp +okegawa.saitama.jp +omiya.saitama.jp +otaki.saitama.jp +ranzan.saitama.jp +ryokami.saitama.jp +saitama.saitama.jp +sakado.saitama.jp +satte.saitama.jp +sayama.saitama.jp +shiki.saitama.jp +shiraoka.saitama.jp +soka.saitama.jp +sugito.saitama.jp +toda.saitama.jp +tokigawa.saitama.jp +tokorozawa.saitama.jp +tsurugashima.saitama.jp +urawa.saitama.jp +warabi.saitama.jp +yashio.saitama.jp +yokoze.saitama.jp +yono.saitama.jp +yorii.saitama.jp +yoshida.saitama.jp +yoshikawa.saitama.jp +yoshimi.saitama.jp +aisho.shiga.jp +gamo.shiga.jp +higashiomi.shiga.jp +hikone.shiga.jp +koka.shiga.jp +konan.shiga.jp +kosei.shiga.jp +koto.shiga.jp +kusatsu.shiga.jp +maibara.shiga.jp +moriyama.shiga.jp +nagahama.shiga.jp +nishiazai.shiga.jp +notogawa.shiga.jp +omihachiman.shiga.jp +otsu.shiga.jp +ritto.shiga.jp +ryuoh.shiga.jp +takashima.shiga.jp +takatsuki.shiga.jp +torahime.shiga.jp +toyosato.shiga.jp +yasu.shiga.jp +akagi.shimane.jp +ama.shimane.jp +gotsu.shimane.jp +hamada.shimane.jp +higashiizumo.shimane.jp +hikawa.shimane.jp +hikimi.shimane.jp +izumo.shimane.jp +kakinoki.shimane.jp +masuda.shimane.jp +matsue.shimane.jp +misato.shimane.jp +nishinoshima.shimane.jp +ohda.shimane.jp +okinoshima.shimane.jp +okuizumo.shimane.jp +shimane.shimane.jp +tamayu.shimane.jp +tsuwano.shimane.jp +unnan.shimane.jp +yakumo.shimane.jp +yasugi.shimane.jp +yatsuka.shimane.jp +arai.shizuoka.jp +atami.shizuoka.jp +fuji.shizuoka.jp +fujieda.shizuoka.jp +fujikawa.shizuoka.jp +fujinomiya.shizuoka.jp +fukuroi.shizuoka.jp +gotemba.shizuoka.jp +haibara.shizuoka.jp +hamamatsu.shizuoka.jp +higashiizu.shizuoka.jp +ito.shizuoka.jp +iwata.shizuoka.jp +izu.shizuoka.jp +izunokuni.shizuoka.jp +kakegawa.shizuoka.jp +kannami.shizuoka.jp +kawanehon.shizuoka.jp +kawazu.shizuoka.jp +kikugawa.shizuoka.jp +kosai.shizuoka.jp +makinohara.shizuoka.jp +matsuzaki.shizuoka.jp +minamiizu.shizuoka.jp +mishima.shizuoka.jp +morimachi.shizuoka.jp +nishiizu.shizuoka.jp +numazu.shizuoka.jp +omaezaki.shizuoka.jp +shimada.shizuoka.jp +shimizu.shizuoka.jp +shimoda.shizuoka.jp +shizuoka.shizuoka.jp +susono.shizuoka.jp +yaizu.shizuoka.jp +yoshida.shizuoka.jp +ashikaga.tochigi.jp +bato.tochigi.jp +haga.tochigi.jp +ichikai.tochigi.jp +iwafune.tochigi.jp +kaminokawa.tochigi.jp +kanuma.tochigi.jp +karasuyama.tochigi.jp +kuroiso.tochigi.jp +mashiko.tochigi.jp +mibu.tochigi.jp +moka.tochigi.jp +motegi.tochigi.jp +nasu.tochigi.jp +nasushiobara.tochigi.jp +nikko.tochigi.jp +nishikata.tochigi.jp +nogi.tochigi.jp +ohira.tochigi.jp +ohtawara.tochigi.jp +oyama.tochigi.jp +sakura.tochigi.jp +sano.tochigi.jp +shimotsuke.tochigi.jp +shioya.tochigi.jp +takanezawa.tochigi.jp +tochigi.tochigi.jp +tsuga.tochigi.jp +ujiie.tochigi.jp +utsunomiya.tochigi.jp +yaita.tochigi.jp +aizumi.tokushima.jp +anan.tokushima.jp +ichiba.tokushima.jp +itano.tokushima.jp +kainan.tokushima.jp +komatsushima.tokushima.jp +matsushige.tokushima.jp +mima.tokushima.jp +minami.tokushima.jp +miyoshi.tokushima.jp +mugi.tokushima.jp +nakagawa.tokushima.jp +naruto.tokushima.jp +sanagochi.tokushima.jp +shishikui.tokushima.jp +tokushima.tokushima.jp +wajiki.tokushima.jp +adachi.tokyo.jp +akiruno.tokyo.jp +akishima.tokyo.jp +aogashima.tokyo.jp +arakawa.tokyo.jp +bunkyo.tokyo.jp +chiyoda.tokyo.jp +chofu.tokyo.jp +chuo.tokyo.jp +edogawa.tokyo.jp +fuchu.tokyo.jp +fussa.tokyo.jp +hachijo.tokyo.jp +hachioji.tokyo.jp +hamura.tokyo.jp +higashikurume.tokyo.jp +higashimurayama.tokyo.jp +higashiyamato.tokyo.jp +hino.tokyo.jp +hinode.tokyo.jp +hinohara.tokyo.jp +inagi.tokyo.jp +itabashi.tokyo.jp +katsushika.tokyo.jp +kita.tokyo.jp +kiyose.tokyo.jp +kodaira.tokyo.jp +koganei.tokyo.jp +kokubunji.tokyo.jp +komae.tokyo.jp +koto.tokyo.jp +kouzushima.tokyo.jp +kunitachi.tokyo.jp +machida.tokyo.jp +meguro.tokyo.jp +minato.tokyo.jp +mitaka.tokyo.jp +mizuho.tokyo.jp +musashimurayama.tokyo.jp +musashino.tokyo.jp +nakano.tokyo.jp +nerima.tokyo.jp +ogasawara.tokyo.jp +okutama.tokyo.jp +ome.tokyo.jp +oshima.tokyo.jp +ota.tokyo.jp +setagaya.tokyo.jp +shibuya.tokyo.jp +shinagawa.tokyo.jp +shinjuku.tokyo.jp +suginami.tokyo.jp +sumida.tokyo.jp +tachikawa.tokyo.jp +taito.tokyo.jp +tama.tokyo.jp +toshima.tokyo.jp +chizu.tottori.jp +hino.tottori.jp +kawahara.tottori.jp +koge.tottori.jp +kotoura.tottori.jp +misasa.tottori.jp +nanbu.tottori.jp +nichinan.tottori.jp +sakaiminato.tottori.jp +tottori.tottori.jp +wakasa.tottori.jp +yazu.tottori.jp +yonago.tottori.jp +asahi.toyama.jp +fuchu.toyama.jp +fukumitsu.toyama.jp +funahashi.toyama.jp +himi.toyama.jp +imizu.toyama.jp +inami.toyama.jp +johana.toyama.jp +kamiichi.toyama.jp +kurobe.toyama.jp +nakaniikawa.toyama.jp +namerikawa.toyama.jp +nanto.toyama.jp +nyuzen.toyama.jp +oyabe.toyama.jp +taira.toyama.jp +takaoka.toyama.jp +tateyama.toyama.jp +toga.toyama.jp +tonami.toyama.jp +toyama.toyama.jp +unazuki.toyama.jp +uozu.toyama.jp +yamada.toyama.jp +arida.wakayama.jp +aridagawa.wakayama.jp +gobo.wakayama.jp +hashimoto.wakayama.jp +hidaka.wakayama.jp +hirogawa.wakayama.jp +inami.wakayama.jp +iwade.wakayama.jp +kainan.wakayama.jp +kamitonda.wakayama.jp +katsuragi.wakayama.jp +kimino.wakayama.jp +kinokawa.wakayama.jp +kitayama.wakayama.jp +koya.wakayama.jp +koza.wakayama.jp +kozagawa.wakayama.jp +kudoyama.wakayama.jp +kushimoto.wakayama.jp +mihama.wakayama.jp +misato.wakayama.jp +nachikatsuura.wakayama.jp +shingu.wakayama.jp +shirahama.wakayama.jp +taiji.wakayama.jp +tanabe.wakayama.jp +wakayama.wakayama.jp +yuasa.wakayama.jp +yura.wakayama.jp +asahi.yamagata.jp +funagata.yamagata.jp +higashine.yamagata.jp +iide.yamagata.jp +kahoku.yamagata.jp +kaminoyama.yamagata.jp +kaneyama.yamagata.jp +kawanishi.yamagata.jp +mamurogawa.yamagata.jp +mikawa.yamagata.jp +murayama.yamagata.jp +nagai.yamagata.jp +nakayama.yamagata.jp +nanyo.yamagata.jp +nishikawa.yamagata.jp +obanazawa.yamagata.jp +oe.yamagata.jp +oguni.yamagata.jp +ohkura.yamagata.jp +oishida.yamagata.jp +sagae.yamagata.jp +sakata.yamagata.jp +sakegawa.yamagata.jp +shinjo.yamagata.jp +shirataka.yamagata.jp +shonai.yamagata.jp +takahata.yamagata.jp +tendo.yamagata.jp +tozawa.yamagata.jp +tsuruoka.yamagata.jp +yamagata.yamagata.jp +yamanobe.yamagata.jp +yonezawa.yamagata.jp +yuza.yamagata.jp +abu.yamaguchi.jp +hagi.yamaguchi.jp +hikari.yamaguchi.jp +hofu.yamaguchi.jp +iwakuni.yamaguchi.jp +kudamatsu.yamaguchi.jp +mitou.yamaguchi.jp +nagato.yamaguchi.jp +oshima.yamaguchi.jp +shimonoseki.yamaguchi.jp +shunan.yamaguchi.jp +tabuse.yamaguchi.jp +tokuyama.yamaguchi.jp +toyota.yamaguchi.jp +ube.yamaguchi.jp +yuu.yamaguchi.jp +chuo.yamanashi.jp +doshi.yamanashi.jp +fuefuki.yamanashi.jp +fujikawa.yamanashi.jp +fujikawaguchiko.yamanashi.jp +fujiyoshida.yamanashi.jp +hayakawa.yamanashi.jp +hokuto.yamanashi.jp +ichikawamisato.yamanashi.jp +kai.yamanashi.jp +kofu.yamanashi.jp +koshu.yamanashi.jp +kosuge.yamanashi.jp +minami-alps.yamanashi.jp +minobu.yamanashi.jp +nakamichi.yamanashi.jp +nanbu.yamanashi.jp +narusawa.yamanashi.jp +nirasaki.yamanashi.jp +nishikatsura.yamanashi.jp +oshino.yamanashi.jp +otsuki.yamanashi.jp +showa.yamanashi.jp +tabayama.yamanashi.jp +tsuru.yamanashi.jp +uenohara.yamanashi.jp +yamanakako.yamanashi.jp +yamanashi.yamanashi.jp + +// ke : http://www.kenic.or.ke/index.php/en/ke-domains/ke-domains +ke +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke + +// kg : http://www.domain.kg/dmn_n.html +kg +com.kg +edu.kg +gov.kg +mil.kg +net.kg +org.kg + +// kh : https://trc.gov.kh +// Submitted by khnic@trc.gov.kh +kh +com.kh +edu.kh +gov.kh +net.kh +org.kh + +// ki : https://www.iana.org/domains/root/db/ki.html +ki +biz.ki +com.ki +edu.ki +gov.ki +info.ki +net.ki +org.ki + +// km : https://www.domaine.km/ +km +ass.km +com.km +edu.km +gov.km +mil.km +nom.km +org.km +prd.km +tm.km +// These are only mentioned as proposed suggestions at domaine.km, but +// https://en.wikipedia.org/wiki/.km says they're available for registration: +asso.km +coop.km +gouv.km +medecin.km +notaires.km +pharmaciens.km +presse.km +veterinaire.km + +// kn : https://nic.kn/ +kn +edu.kn +gov.kn +net.kn +org.kn + +// kp : http://www.star.co.kp/ +kp +com.kp +edu.kp +gov.kp +org.kp +rep.kp +tra.kp + +// kr : https://www.iana.org/domains/root/db/kr.html +// see also: https://krnic.kisa.or.kr/jsp/infoboard/law/domBylawsReg.jsp +kr +ac.kr +ai.kr +co.kr +es.kr +go.kr +hs.kr +io.kr +it.kr +kg.kr +me.kr +mil.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +sc.kr +// kr geographical names +busan.kr +chungbuk.kr +chungnam.kr +daegu.kr +daejeon.kr +gangwon.kr +gwangju.kr +gyeongbuk.kr +gyeonggi.kr +gyeongnam.kr +incheon.kr +jeju.kr +jeonbuk.kr +jeonnam.kr +seoul.kr +ulsan.kr + +// kw : https://www.nic.kw/policies/ +// Confirmed by registry +kw +com.kw +edu.kw +emb.kw +gov.kw +ind.kw +net.kw +org.kw + +// ky : https://www.ofreg.ky/ict/kydomain-introduction +ky +com.ky +edu.ky +net.ky +org.ky + +// kz : https://www.iana.org/domains/root/db/kz.html +// see also: http://www.nic.kz/rules/index.jsp +kz +com.kz +edu.kz +gov.kz +mil.kz +net.kz +org.kz + +// la : https://www.iana.org/domains/root/db/la.html +// Submitted by registry +la +com.la +edu.la +gov.la +info.la +int.la +net.la +org.la +per.la + +// lb : https://www.iana.org/domains/root/db/lb.html +// Submitted by registry +lb +com.lb +edu.lb +gov.lb +net.lb +org.lb + +// lc : https://www.iana.org/domains/root/db/lc.html +// see also: http://www.nic.lc/rules.htm +lc +co.lc +com.lc +edu.lc +gov.lc +net.lc +org.lc + +// li : https://www.iana.org/domains/root/db/li.html +li + +// lk : https://www.iana.org/domains/root/db/lk.html +lk +ac.lk +assn.lk +com.lk +edu.lk +gov.lk +grp.lk +hotel.lk +int.lk +ltd.lk +net.lk +ngo.lk +org.lk +sch.lk +soc.lk +web.lk + +// lr : http://psg.com/dns/lr/lr.txt +// Submitted by registry +lr +com.lr +edu.lr +gov.lr +net.lr +org.lr + +// ls : http://www.nic.ls/ +// Confirmed by registry +ls +ac.ls +biz.ls +co.ls +edu.ls +gov.ls +info.ls +net.ls +org.ls +sc.ls + +// lt : https://www.domreg.lt/ +lt +gov.lt + +// lu : http://www.dns.lu/en/ +lu + +// lv : https://www.iana.org/domains/root/db/lv.html +lv +asn.lv +com.lv +conf.lv +edu.lv +gov.lv +id.lv +mil.lv +net.lv +org.lv + +// ly : http://www.nic.ly/regulations.php +ly +com.ly +edu.ly +gov.ly +id.ly +med.ly +net.ly +org.ly +plc.ly +sch.ly + +// ma : http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf +ma +ac.ma +co.ma +gov.ma +net.ma +org.ma +press.ma + +// mc : http://www.nic.mc/ +mc +asso.mc +tm.mc + +// md : https://www.iana.org/domains/root/db/md.html +md + +// me : https://www.iana.org/domains/root/db/me.html +me +ac.me +co.me +edu.me +gov.me +its.me +net.me +org.me +priv.me + +// mg : https://nic.mg +mg +co.mg +com.mg +edu.mg +gov.mg +mil.mg +nom.mg +org.mg +prd.mg + +// mh : https://www.iana.org/domains/root/db/mh.html +mh + +// mil : https://www.iana.org/domains/root/db/mil.html +mil + +// mk : https://marnet.mk/ -> "ПРАВИЛНИК" +mk +com.mk +edu.mk +gov.mk +inf.mk +name.mk +net.mk +org.mk + +// ml : https://www.iana.org/domains/root/db/ml.html +// Confirmed by Boubacar NDIAYE 2024-12-31 +ml +ac.ml +art.ml +asso.ml +com.ml +edu.ml +gouv.ml +gov.ml +info.ml +inst.ml +net.ml +org.ml +pr.ml +presse.ml + +// mm : https://www.iana.org/domains/root/db/mm.html +*.mm + +// mn : https://www.iana.org/domains/root/db/mn.html +mn +edu.mn +gov.mn +org.mn + +// mo : https://www.monic.mo/ +mo +com.mo +edu.mo +gov.mo +net.mo +org.mo + +// mobi : https://www.iana.org/domains/root/db/mobi.html +mobi + +// mp : http://get.mp/ +mp + +// mq : https://www.iana.org/domains/root/db/mq.html +mq + +// mr : https://www.iana.org/domains/root/db/mr.html +mr +gov.mr + +// ms : https://www.iana.org/domains/root/db/ms.html +ms +com.ms +edu.ms +gov.ms +net.ms +org.ms + +// mt : https://www.nic.org.mt/go/policy +// Submitted by registry +mt +com.mt +edu.mt +net.mt +org.mt + +// mu : https://www.iana.org/domains/root/db/mu.html +mu +ac.mu +co.mu +com.mu +gov.mu +net.mu +or.mu +org.mu + +// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/ +museum + +// mv : https://www.iana.org/domains/root/db/mv.html +// "mv" included because, contra Wikipedia, google.mv exists. +mv +aero.mv +biz.mv +com.mv +coop.mv +edu.mv +gov.mv +info.mv +int.mv +mil.mv +museum.mv +name.mv +net.mv +org.mv +pro.mv + +// mw : http://www.registrar.mw/ +mw +ac.mw +biz.mw +co.mw +com.mw +coop.mw +edu.mw +gov.mw +int.mw +net.mw +org.mw + +// mx : http://www.nic.mx/ +// Submitted by registry +mx +com.mx +edu.mx +gob.mx +net.mx +org.mx + +// my : http://www.mynic.my/ +// Available strings: https://mynic.my/resources/domains/buying-a-domain/ +my +biz.my +com.my +edu.my +gov.my +mil.my +name.my +net.my +org.my + +// mz : http://www.uem.mz/ +// Submitted by registry +mz +ac.mz +adv.mz +co.mz +edu.mz +gov.mz +mil.mz +net.mz +org.mz + +// na : http://www.na-nic.com.na/ +na +alt.na +co.na +com.na +gov.na +net.na +org.na + +// name : http://www.nic.name/ +// Regarding 2LDs: https://github.com/publicsuffix/list/issues/2306 +name + +// nc : http://www.cctld.nc/ +nc +asso.nc +nom.nc + +// ne : https://www.iana.org/domains/root/db/ne.html +ne + +// net : https://www.iana.org/domains/root/db/net.html +net + +// nf : https://www.iana.org/domains/root/db/nf.html +nf +arts.nf +com.nf +firm.nf +info.nf +net.nf +other.nf +per.nf +rec.nf +store.nf +web.nf + +// ng : https://www.nira.org.ng/ +ng +com.ng +edu.ng +gov.ng +i.ng +mil.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng + +// ni : https://www.nic.ni/ +ni +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni + +// nl : https://www.sidn.nl/ +nl + +// no : https://www.norid.no/en/om-domenenavn/regelverk-for-no/ +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +// RSS feed: https://teknisk.norid.no/en/feed/ +no +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +fhs.no +folkebibl.no +fylkesbibl.no +gielda.no +herad.no +idrett.no +kommune.no +museum.no +priv.no +suohkan.no +tjielte.no +uenorge.no +vgs.no +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +dep.no +mil.no +stat.no +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// counties +aa.no +ah.no +bu.no +fm.no +hl.no +hm.no +jan-mayen.no +mr.no +nl.no +nt.no +of.no +ol.no +oslo.no +rl.no +sf.no +st.no +svalbard.no +tm.no +tr.no +va.no +vf.no +// primary and lower secondary schools per county +gs.aa.no +gs.ah.no +gs.bu.no +gs.fm.no +gs.hl.no +gs.hm.no +gs.jan-mayen.no +gs.mr.no +gs.nl.no +gs.nt.no +gs.of.no +gs.ol.no +gs.oslo.no +gs.rl.no +gs.sf.no +gs.st.no +gs.svalbard.no +gs.tm.no +gs.tr.no +gs.va.no +gs.vf.no +// cities +akrehamn.no +åkrehamn.no +algard.no +ålgård.no +arna.no +bronnoysund.no +brønnøysund.no +brumunddal.no +bryne.no +drobak.no +drøbak.no +egersund.no +fetsund.no +floro.no +florø.no +fredrikstad.no +hokksund.no +honefoss.no +hønefoss.no +jessheim.no +jorpeland.no +jørpeland.no +kirkenes.no +kopervik.no +krokstadelva.no +langevag.no +langevåg.no +leirvik.no +mjondalen.no +mjøndalen.no +mo-i-rana.no +mosjoen.no +mosjøen.no +nesoddtangen.no +orkanger.no +osoyro.no +osøyro.no +raholt.no +råholt.no +sandnessjoen.no +sandnessjøen.no +skedsmokorset.no +slattum.no +spjelkavik.no +stathelle.no +stavern.no +stjordalshalsen.no +stjørdalshalsen.no +tananger.no +tranby.no +vossevangen.no +// communities +aarborte.no +aejrie.no +afjord.no +åfjord.no +agdenes.no +nes.akershus.no +aknoluokta.no +ákŋoluokta.no +al.no +ål.no +alaheadju.no +álaheadju.no +alesund.no +ålesund.no +alstahaug.no +alta.no +áltá.no +alvdal.no +amli.no +åmli.no +amot.no +åmot.no +andasuolo.no +andebu.no +andoy.no +andøy.no +ardal.no +årdal.no +aremark.no +arendal.no +ås.no +aseral.no +åseral.no +asker.no +askim.no +askoy.no +askøy.no +askvoll.no +asnes.no +åsnes.no +audnedal.no +aukra.no +aure.no +aurland.no +aurskog-holand.no +aurskog-høland.no +austevoll.no +austrheim.no +averoy.no +averøy.no +badaddja.no +bådåddjå.no +bærum.no +bahcavuotna.no +báhcavuotna.no +bahccavuotna.no +báhccavuotna.no +baidar.no +báidár.no +bajddar.no +bájddar.no +balat.no +bálát.no +balestrand.no +ballangen.no +balsfjord.no +bamble.no +bardu.no +barum.no +batsfjord.no +båtsfjord.no +bearalvahki.no +bearalváhki.no +beardu.no +beiarn.no +berg.no +bergen.no +berlevag.no +berlevåg.no +bievat.no +bievát.no +bindal.no +birkenes.no +bjerkreim.no +bjugn.no +bodo.no +bodø.no +bokn.no +bomlo.no +bømlo.no +bremanger.no +bronnoy.no +brønnøy.no +budejju.no +nes.buskerud.no +bygland.no +bykle.no +cahcesuolo.no +čáhcesuolo.no +davvenjarga.no +davvenjárga.no +davvesiida.no +deatnu.no +dielddanuorri.no +divtasvuodna.no +divttasvuotna.no +donna.no +dønna.no +dovre.no +drammen.no +drangedal.no +dyroy.no +dyrøy.no +eid.no +eidfjord.no +eidsberg.no +eidskog.no +eidsvoll.no +eigersund.no +elverum.no +enebakk.no +engerdal.no +etne.no +etnedal.no +evenassi.no +evenášši.no +evenes.no +evje-og-hornnes.no +farsund.no +fauske.no +fedje.no +fet.no +finnoy.no +finnøy.no +fitjar.no +fjaler.no +fjell.no +fla.no +flå.no +flakstad.no +flatanger.no +flekkefjord.no +flesberg.no +flora.no +folldal.no +forde.no +førde.no +forsand.no +fosnes.no +fræna.no +frana.no +frogn.no +froland.no +frosta.no +froya.no +frøya.no +fuoisku.no +fuossko.no +fusa.no +fyresdal.no +gaivuotna.no +gáivuotna.no +galsa.no +gálsá.no +gamvik.no +gangaviika.no +gáŋgaviika.no +gaular.no +gausdal.no +giehtavuoatna.no +gildeskal.no +gildeskål.no +giske.no +gjemnes.no +gjerdrum.no +gjerstad.no +gjesdal.no +gjovik.no +gjøvik.no +gloppen.no +gol.no +gran.no +grane.no +granvin.no +gratangen.no +grimstad.no +grong.no +grue.no +gulen.no +guovdageaidnu.no +ha.no +hå.no +habmer.no +hábmer.no +hadsel.no +hægebostad.no +hagebostad.no +halden.no +halsa.no +hamar.no +hamaroy.no +hamarøy.no +hammarfeasta.no +hámmárfeasta.no +hammerfest.no +hapmir.no +hápmir.no +haram.no +hareid.no +harstad.no +hasvik.no +hattfjelldal.no +haugesund.no +os.hedmark.no +valer.hedmark.no +våler.hedmark.no +hemne.no +hemnes.no +hemsedal.no +hitra.no +hjartdal.no +hjelmeland.no +hobol.no +hobøl.no +hof.no +hol.no +hole.no +holmestrand.no +holtalen.no +holtålen.no +os.hordaland.no +hornindal.no +horten.no +hoyanger.no +høyanger.no +hoylandet.no +høylandet.no +hurdal.no +hurum.no +hvaler.no +hyllestad.no +ibestad.no +inderoy.no +inderøy.no +iveland.no +ivgu.no +jevnaker.no +jolster.no +jølster.no +jondal.no +kafjord.no +kåfjord.no +karasjohka.no +kárášjohka.no +karasjok.no +karlsoy.no +karlsøy.no +karmoy.no +karmøy.no +kautokeino.no +klabu.no +klæbu.no +klepp.no +kongsberg.no +kongsvinger.no +kraanghke.no +kråanghke.no +kragero.no +kragerø.no +kristiansand.no +kristiansund.no +krodsherad.no +krødsherad.no +kvæfjord.no +kvænangen.no +kvafjord.no +kvalsund.no +kvam.no +kvanangen.no +kvinesdal.no +kvinnherad.no +kviteseid.no +kvitsoy.no +kvitsøy.no +laakesvuemie.no +lærdal.no +lahppi.no +láhppi.no +lardal.no +larvik.no +lavagis.no +lavangen.no +leangaviika.no +leaŋgaviika.no +lebesby.no +leikanger.no +leirfjord.no +leka.no +leksvik.no +lenvik.no +lerdal.no +lesja.no +levanger.no +lier.no +lierne.no +lillehammer.no +lillesand.no +lindas.no +lindås.no +lindesnes.no +loabat.no +loabát.no +lodingen.no +lødingen.no +lom.no +loppa.no +lorenskog.no +lørenskog.no +loten.no +løten.no +lund.no +lunner.no +luroy.no +lurøy.no +luster.no +lyngdal.no +lyngen.no +malatvuopmi.no +málatvuopmi.no +malselv.no +målselv.no +malvik.no +mandal.no +marker.no +marnardal.no +masfjorden.no +masoy.no +måsøy.no +matta-varjjat.no +mátta-várjjat.no +meland.no +meldal.no +melhus.no +meloy.no +meløy.no +meraker.no +meråker.no +midsund.no +midtre-gauldal.no +moareke.no +moåreke.no +modalen.no +modum.no +molde.no +heroy.more-og-romsdal.no +sande.more-og-romsdal.no +herøy.møre-og-romsdal.no +sande.møre-og-romsdal.no +moskenes.no +moss.no +muosat.no +muosát.no +naamesjevuemie.no +nååmesjevuemie.no +nærøy.no +namdalseid.no +namsos.no +namsskogan.no +nannestad.no +naroy.no +narviika.no +narvik.no +naustdal.no +navuotna.no +návuotna.no +nedre-eiker.no +nesna.no +nesodden.no +nesseby.no +nesset.no +nissedal.no +nittedal.no +nord-aurdal.no +nord-fron.no +nord-odal.no +norddal.no +nordkapp.no +bo.nordland.no +bø.nordland.no +heroy.nordland.no +herøy.nordland.no +nordre-land.no +nordreisa.no +nore-og-uvdal.no +notodden.no +notteroy.no +nøtterøy.no +odda.no +oksnes.no +øksnes.no +omasvuotna.no +oppdal.no +oppegard.no +oppegård.no +orkdal.no +orland.no +ørland.no +orskog.no +ørskog.no +orsta.no +ørsta.no +osen.no +osteroy.no +osterøy.no +valer.ostfold.no +våler.østfold.no +ostre-toten.no +østre-toten.no +overhalla.no +ovre-eiker.no +øvre-eiker.no +oyer.no +øyer.no +oygarden.no +øygarden.no +oystre-slidre.no +øystre-slidre.no +porsanger.no +porsangu.no +porsáŋgu.no +porsgrunn.no +rade.no +råde.no +radoy.no +radøy.no +rælingen.no +rahkkeravju.no +ráhkkerávju.no +raisa.no +ráisa.no +rakkestad.no +ralingen.no +rana.no +randaberg.no +rauma.no +re.no +rendalen.no +rennebu.no +rennesoy.no +rennesøy.no +rindal.no +ringebu.no +ringerike.no +ringsaker.no +risor.no +risør.no +rissa.no +roan.no +rodoy.no +rødøy.no +rollag.no +romsa.no +romskog.no +rømskog.no +roros.no +røros.no +rost.no +røst.no +royken.no +røyken.no +royrvik.no +røyrvik.no +ruovat.no +rygge.no +salangen.no +salat.no +sálat.no +sálát.no +saltdal.no +samnanger.no +sandefjord.no +sandnes.no +sandoy.no +sandøy.no +sarpsborg.no +sauda.no +sauherad.no +sel.no +selbu.no +selje.no +seljord.no +siellak.no +sigdal.no +siljan.no +sirdal.no +skanit.no +skánit.no +skanland.no +skånland.no +skaun.no +skedsmo.no +ski.no +skien.no +skierva.no +skiervá.no +skiptvet.no +skjak.no +skjåk.no +skjervoy.no +skjervøy.no +skodje.no +smola.no +smøla.no +snaase.no +snåase.no +snasa.no +snåsa.no +snillfjord.no +snoasa.no +sogndal.no +sogne.no +søgne.no +sokndal.no +sola.no +solund.no +somna.no +sømna.no +sondre-land.no +søndre-land.no +songdalen.no +sor-aurdal.no +sør-aurdal.no +sor-fron.no +sør-fron.no +sor-odal.no +sør-odal.no +sor-varanger.no +sør-varanger.no +sorfold.no +sørfold.no +sorreisa.no +sørreisa.no +sortland.no +sorum.no +sørum.no +spydeberg.no +stange.no +stavanger.no +steigen.no +steinkjer.no +stjordal.no +stjørdal.no +stokke.no +stor-elvdal.no +stord.no +stordal.no +storfjord.no +strand.no +stranda.no +stryn.no +sula.no +suldal.no +sund.no +sunndal.no +surnadal.no +sveio.no +svelvik.no +sykkylven.no +tana.no +bo.telemark.no +bø.telemark.no +time.no +tingvoll.no +tinn.no +tjeldsund.no +tjome.no +tjøme.no +tokke.no +tolga.no +tonsberg.no +tønsberg.no +torsken.no +træna.no +trana.no +tranoy.no +tranøy.no +troandin.no +trogstad.no +trøgstad.no +tromsa.no +tromso.no +tromsø.no +trondheim.no +trysil.no +tvedestrand.no +tydal.no +tynset.no +tysfjord.no +tysnes.no +tysvær.no +tysvar.no +ullensaker.no +ullensvang.no +ulstein.no +ulvik.no +unjarga.no +unjárga.no +utsira.no +vaapste.no +vadso.no +vadsø.no +værøy.no +vaga.no +vågå.no +vagan.no +vågan.no +vagsoy.no +vågsøy.no +vaksdal.no +valle.no +vang.no +vanylven.no +vardo.no +vardø.no +varggat.no +várggát.no +varoy.no +vefsn.no +vega.no +vegarshei.no +vegårshei.no +vennesla.no +verdal.no +verran.no +vestby.no +sande.vestfold.no +vestnes.no +vestre-slidre.no +vestre-toten.no +vestvagoy.no +vestvågøy.no +vevelstad.no +vik.no +vikna.no +vindafjord.no +voagat.no +volda.no +voss.no + +// np : https://www.mos.com.np/ +*.np + +// nr : http://cenpac.net.nr/dns/index.html +// Submitted by registry +nr +biz.nr +com.nr +edu.nr +gov.nr +info.nr +net.nr +org.nr + +// nu : https://www.iana.org/domains/root/db/nu.html +nu + +// nz : https://www.iana.org/domains/root/db/nz.html +// Submitted by registry +nz +ac.nz +co.nz +cri.nz +geek.nz +gen.nz +govt.nz +health.nz +iwi.nz +kiwi.nz +maori.nz +māori.nz +mil.nz +net.nz +org.nz +parliament.nz +school.nz + +// om : https://www.iana.org/domains/root/db/om.html +om +co.om +com.om +edu.om +gov.om +med.om +museum.om +net.om +org.om +pro.om + +// onion : https://tools.ietf.org/html/rfc7686 +onion + +// org : https://www.iana.org/domains/root/db/org.html +org + +// pa : http://www.nic.pa/ +// Some additional second level "domains" resolve directly as hostnames, such as +// pannet.pa, so we add a rule for "pa". +pa +abo.pa +ac.pa +com.pa +edu.pa +gob.pa +ing.pa +med.pa +net.pa +nom.pa +org.pa +sld.pa + +// pe : https://punto.pe/policy.php +pe +com.pe +edu.pe +gob.pe +mil.pe +net.pe +nom.pe +org.pe + +// pf : https://www.iana.org/domains/root/db/pf.html +pf +com.pf +edu.pf +org.pf + +// pg : https://www.iana.org/domains/root/db/pg.html +*.pg + +// ph : https://www.iana.org/domains/root/db/ph.html +// Submitted by registry +ph +com.ph +edu.ph +gov.ph +i.ph +mil.ph +net.ph +ngo.ph +org.ph + +// pk : https://www.pknic.net.pk/domain-structure.html +// Contact Email: staff@pknic.net.pk +pk +ac.pk +biz.pk +com.pk +edu.pk +fam.pk +gkp.pk +gob.pk +gog.pk +gok.pk +gop.pk +gos.pk +gov.pk +net.pk +org.pk +web.pk + +// pl : https://www.dns.pl/en/ +// Confirmed by registry 2024-11-18 +pl +com.pl +net.pl +org.pl +// pl functional domains : https://www.dns.pl/en/list_of_functional_domain_names +agro.pl +aid.pl +atm.pl +auto.pl +biz.pl +edu.pl +gmina.pl +gsm.pl +info.pl +mail.pl +media.pl +miasta.pl +mil.pl +nieruchomosci.pl +nom.pl +pc.pl +powiat.pl +priv.pl +realestate.pl +rel.pl +sex.pl +shop.pl +sklep.pl +sos.pl +szkola.pl +targi.pl +tm.pl +tourism.pl +travel.pl +turystyka.pl +// Government domains : https://www.dns.pl/informacje_o_rejestracji_domen_gov_pl +// In accordance with the .gov.pl Domain Name Regulations : https://www.dns.pl/regulamin_gov_pl +gov.pl +ap.gov.pl +griw.gov.pl +ic.gov.pl +is.gov.pl +kmpsp.gov.pl +konsulat.gov.pl +kppsp.gov.pl +kwp.gov.pl +kwpsp.gov.pl +mup.gov.pl +mw.gov.pl +oia.gov.pl +oirm.gov.pl +oke.gov.pl +oow.gov.pl +oschr.gov.pl +oum.gov.pl +pa.gov.pl +pinb.gov.pl +piw.gov.pl +po.gov.pl +pr.gov.pl +psp.gov.pl +psse.gov.pl +pup.gov.pl +rzgw.gov.pl +sa.gov.pl +sdn.gov.pl +sko.gov.pl +so.gov.pl +sr.gov.pl +starostwo.gov.pl +ug.gov.pl +ugim.gov.pl +um.gov.pl +umig.gov.pl +upow.gov.pl +uppo.gov.pl +us.gov.pl +uw.gov.pl +uzs.gov.pl +wif.gov.pl +wiih.gov.pl +winb.gov.pl +wios.gov.pl +witd.gov.pl +wiw.gov.pl +wkz.gov.pl +wsa.gov.pl +wskr.gov.pl +wsse.gov.pl +wuoz.gov.pl +wzmiuw.gov.pl +zp.gov.pl +zpisdn.gov.pl +// pl regional domains : https://www.dns.pl/en/list_of_regional_domain_names +augustow.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +czeladz.pl +czest.pl +dlugoleka.pl +elblag.pl +elk.pl +glogow.pl +gniezno.pl +gorlice.pl +grajewo.pl +ilawa.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kazimierz-dolny.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +mielec.pl +mielno.pl +mragowo.pl +naklo.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorskie.pl +pomorze.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +skoczow.pl +slask.pl +slupsk.pl +sosnowiec.pl +stalowa-wola.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +tarnobrzeg.pl +tgory.pl +turek.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl + +// pm : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +pm + +// pn : https://www.iana.org/domains/root/db/pn.html +pn +co.pn +edu.pn +gov.pn +net.pn +org.pn + +// post : https://www.iana.org/domains/root/db/post.html +post + +// pr : https://www.domains.pr/ +pr +ac.pr +biz.pr +com.pr +edu.pr +est.pr +gov.pr +info.pr +isla.pr +name.pr +net.pr +org.pr +pro.pr +prof.pr + +// pro : http://registry.pro/get-pro +pro +aaa.pro +aca.pro +acct.pro +avocat.pro +bar.pro +cpa.pro +eng.pro +jur.pro +law.pro +med.pro +recht.pro + +// ps : https://www.pnina.ps/registration-policy/ +ps +com.ps +edu.ps +gov.ps +net.ps +org.ps +plo.ps +sec.ps + +// pt : https://www.dns.pt/en/domain/pt-terms-and-conditions-registration-rules/ +pt +com.pt +edu.pt +gov.pt +int.pt +net.pt +nome.pt +org.pt +publ.pt + +// pw : https://www.iana.org/domains/root/db/pw.html +// Confirmed by registry in private correspondence with @dnsguru 2024-12-09 +pw +gov.pw + +// py : https://www.iana.org/domains/root/db/py.html +// Submitted by registry +py +com.py +coop.py +edu.py +gov.py +mil.py +net.py +org.py + +// qa : http://domains.qa/en/ +qa +com.qa +edu.qa +gov.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa + +// re : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +// Confirmed by registry 2024-11-18 +re +// Closed for registration on 2013-03-15 but domains are still maintained +asso.re +com.re + +// ro : http://www.rotld.ro/ +ro +arts.ro +com.ro +firm.ro +info.ro +nom.ro +nt.ro +org.ro +rec.ro +store.ro +tm.ro +www.ro + +// rs : https://www.rnids.rs/en/domains/national-domains +rs +ac.rs +co.rs +edu.rs +gov.rs +in.rs +org.rs + +// ru : https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +ru + +// rw : https://www.iana.org/domains/root/db/rw.html +rw +ac.rw +co.rw +coop.rw +gov.rw +mil.rw +net.rw +org.rw + +// sa : http://www.nic.net.sa/ +sa +com.sa +edu.sa +gov.sa +med.sa +net.sa +org.sa +pub.sa +sch.sa + +// sb : http://www.nic.net.sb/ +sb +com.sb +edu.sb +gov.sb +net.sb +org.sb + +// sc : https://www.nic.sc/en/policies.html +sc +com.sc +edu.sc +gov.sc +net.sc +org.sc + +// sd : https://www.iana.org/domains/root/db/sd.html +// Submitted by registry +sd +com.sd +edu.sd +gov.sd +info.sd +med.sd +net.sd +org.sd +tv.sd + +// se : https://www.iana.org/domains/root/db/se.html +// https://data.internetstiftelsen.se/barred_domains_list.txt -> Second level domains & Sub-domains +// Confirmed by Registry Services 2024-11-20 +se +a.se +ac.se +b.se +bd.se +brand.se +c.se +d.se +e.se +f.se +fh.se +fhsk.se +fhv.se +g.se +h.se +i.se +k.se +komforb.se +kommunalforbund.se +komvux.se +l.se +lanbib.se +m.se +n.se +naturbruksgymn.se +o.se +org.se +p.se +parti.se +pp.se +press.se +r.se +s.se +t.se +tm.se +u.se +w.se +x.se +y.se +z.se + +// sg : https://www.sgnic.sg/domain-registration/sg-categories-rules +// Confirmed by registry 2024-11-19 +sg +com.sg +edu.sg +gov.sg +net.sg +org.sg + +// sh : http://nic.sh/rules.htm +sh +com.sh +gov.sh +mil.sh +net.sh +org.sh + +// si : https://www.iana.org/domains/root/db/si.html +si + +// sj : No registrations at this time. +// Submitted by registry +sj + +// sk : https://sk-nic.sk/ +sk +org.sk + +// sl : http://www.nic.sl +// Submitted by registry +sl +com.sl +edu.sl +gov.sl +net.sl +org.sl + +// sm : https://www.iana.org/domains/root/db/sm.html +sm + +// sn : https://www.iana.org/domains/root/db/sn.html +sn +art.sn +com.sn +edu.sn +gouv.sn +org.sn +univ.sn + +// so : https://sonic.so/policies/ +so +com.so +edu.so +gov.so +me.so +net.so +org.so + +// sr : https://www.iana.org/domains/root/db/sr.html +sr + +// ss : https://registry.nic.ss/ +// Submitted by registry +ss +biz.ss +co.ss +com.ss +edu.ss +gov.ss +me.ss +net.ss +org.ss +sch.ss + +// st : http://www.nic.st/html/policyrules/ +st +co.st +com.st +consulado.st +edu.st +embaixada.st +mil.st +net.st +org.st +principe.st +saotome.st +store.st + +// su : https://www.iana.org/domains/root/db/su.html +su + +// sv : https://www.iana.org/domains/root/db/sv.html +sv +com.sv +edu.sv +gob.sv +org.sv +red.sv + +// sx : https://www.iana.org/domains/root/db/sx.html +// Submitted by registry +sx +gov.sx + +// sy : https://www.iana.org/domains/root/db/sy.html +sy +com.sy +edu.sy +gov.sy +mil.sy +net.sy +org.sy + +// sz : http://www.sispa.org.sz/ +sz +ac.sz +co.sz +org.sz + +// tc : https://www.iana.org/domains/root/db/tc.html +tc + +// td : https://www.iana.org/domains/root/db/td.html +td + +// tel : http://www.telnic.org/ +tel + +// tf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +tf + +// tg : http://www.nic.tg/ +tg + +// th : https://www.iana.org/domains/root/db/th.html +// Submitted by registry +th +ac.th +co.th +go.th +in.th +mi.th +net.th +or.th + +// tj : http://www.nic.tj/policy.html +tj +biz.tj +co.tj +com.tj +edu.tj +go.tj +gov.tj +int.tj +mil.tj +name.tj +net.tj +nic.tj +org.tj +test.tj +web.tj + +// tk : https://www.iana.org/domains/root/db/tk.html +tk + +// tl : https://www.iana.org/domains/root/db/tl.html +tl +gov.tl + +// tm : https://www.nic.tm/local.html +// Confirmed by registry 2024-11-19 +tm +co.tm +com.tm +edu.tm +gov.tm +mil.tm +net.tm +nom.tm +org.tm + +// tn : http://www.registre.tn/fr/ +// https://whois.ati.tn/ +tn +com.tn +ens.tn +fin.tn +gov.tn +ind.tn +info.tn +intl.tn +mincom.tn +nat.tn +net.tn +org.tn +perso.tn +tourism.tn + +// to : https://www.iana.org/domains/root/db/to.html +// Submitted by registry +to +com.to +edu.to +gov.to +mil.to +net.to +org.to + +// tr : https://nic.tr/ +// https://nic.tr/forms/eng/policies.pdf +// https://nic.tr/index.php?USRACTN=PRICELST +tr +av.tr +bbs.tr +bel.tr +biz.tr +com.tr +dr.tr +edu.tr +gen.tr +gov.tr +info.tr +k12.tr +kep.tr +mil.tr +name.tr +net.tr +org.tr +pol.tr +tel.tr +tsk.tr +tv.tr +web.tr +// Used by Northern Cyprus +nc.tr +// Used by government agencies of Northern Cyprus +gov.nc.tr + +// tt : https://www.nic.tt/ +// Confirmed by registry 2024-11-19 +tt +biz.tt +co.tt +com.tt +edu.tt +gov.tt +info.tt +mil.tt +name.tt +net.tt +org.tt +pro.tt + +// tv : https://www.iana.org/domains/root/db/tv.html +// Not listing any 2LDs as reserved since none seem to exist in practice, +// Wikipedia notwithstanding. +tv + +// tw : https://www.iana.org/domains/root/db/tw.html +// https://twnic.tw/dnservice_catag.php +// Confirmed by registry 2024-11-26 +tw +club.tw +com.tw +ebiz.tw +edu.tw +game.tw +gov.tw +idv.tw +mil.tw +net.tw +org.tw + +// tz : https://karibu.tz/regulations +tz +ac.tz +co.tz +go.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz + +// ua : https://hostmaster.ua/policy/?ua +// Submitted by registry +ua +// ua 2LD +com.ua +edu.ua +gov.ua +in.ua +net.ua +org.ua +// ua geographic names +// https://hostmaster.ua/2ld/ +cherkassy.ua +cherkasy.ua +chernigov.ua +chernihiv.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +cr.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +donetsk.ua +dp.ua +if.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +khmelnytskyi.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +kropyvnytskyi.ua +krym.ua +ks.ua +kv.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +luhansk.ua +lutsk.ua +lv.ua +lviv.ua +mk.ua +mykolaiv.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +pl.ua +poltava.ua +rivne.ua +rovno.ua +rv.ua +sb.ua +sebastopol.ua +sevastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +uzhhorod.ua +vinnica.ua +vinnytsia.ua +vn.ua +volyn.ua +yalta.ua +zakarpattia.ua +zaporizhzhe.ua +zaporizhzhia.ua +zhitomir.ua +zhytomyr.ua +zp.ua +zt.ua + +// ug : https://www.registry.co.ug/ +// https://www.registry.co.ug, https://whois.co.ug +// Confirmed by registry 2025-01-20 +ug +ac.ug +co.ug +com.ug +edu.ug +go.ug +gov.ug +mil.ug +ne.ug +or.ug +org.ug +sc.ug +us.ug + +// uk : https://www.iana.org/domains/root/db/uk.html +// Submitted by registry +uk +ac.uk +co.uk +gov.uk +ltd.uk +me.uk +net.uk +nhs.uk +org.uk +plc.uk +police.uk +*.sch.uk + +// us : https://www.iana.org/domains/root/db/us.html +// Confirmed via the .us zone file by William Harrison 2024-12-10 +us +dni.us +isa.us +nsn.us +// Geographic Names +ak.us +al.us +ar.us +as.us +az.us +ca.us +co.us +ct.us +dc.us +de.us +fl.us +ga.us +gu.us +hi.us +ia.us +id.us +il.us +in.us +ks.us +ky.us +la.us +ma.us +md.us +me.us +mi.us +mn.us +mo.us +ms.us +mt.us +nc.us +nd.us +ne.us +nh.us +nj.us +nm.us +nv.us +ny.us +oh.us +ok.us +or.us +pa.us +pr.us +ri.us +sc.us +sd.us +tn.us +tx.us +ut.us +va.us +vi.us +vt.us +wa.us +wi.us +wv.us +wy.us +// The registrar notes several more specific domains available in each state, +// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat +// haphazard; in some states these domains resolve as addresses, while in others +// only subdomains are available, or even nothing at all. We include the +// most common ones where it's clear that different sites are different +// entities. +k12.ak.us +k12.al.us +k12.ar.us +k12.as.us +k12.az.us +k12.ca.us +k12.co.us +k12.ct.us +k12.dc.us +k12.fl.us +k12.ga.us +k12.gu.us +// k12.hi.us - Bug 614565 - Hawaii has a state-wide DOE login +k12.ia.us +k12.id.us +k12.il.us +k12.in.us +k12.ks.us +k12.ky.us +k12.la.us +k12.ma.us +k12.md.us +k12.me.us +k12.mi.us +k12.mn.us +k12.mo.us +k12.ms.us +k12.mt.us +k12.nc.us +k12.ne.us +k12.nh.us +k12.nj.us +k12.nm.us +k12.nv.us +k12.ny.us +k12.oh.us +k12.ok.us +k12.or.us +k12.pa.us +k12.pr.us +// k12.ri.us - Removed at request of Kim Cournoyer +k12.sc.us +// k12.sd.us - Bug 934131 - Removed at request of James Booze +k12.tn.us +k12.tx.us +k12.ut.us +k12.va.us +k12.vi.us +k12.vt.us +k12.wa.us +k12.wi.us +// k12.wv.us - Bug 947705 - Removed at request of Verne Britton +cc.ak.us +lib.ak.us +cc.al.us +lib.al.us +cc.ar.us +lib.ar.us +cc.as.us +lib.as.us +cc.az.us +lib.az.us +cc.ca.us +lib.ca.us +cc.co.us +lib.co.us +cc.ct.us +lib.ct.us +cc.dc.us +lib.dc.us +cc.de.us +cc.fl.us +lib.fl.us +cc.ga.us +lib.ga.us +cc.gu.us +lib.gu.us +cc.hi.us +lib.hi.us +cc.ia.us +lib.ia.us +cc.id.us +lib.id.us +cc.il.us +lib.il.us +cc.in.us +lib.in.us +cc.ks.us +lib.ks.us +cc.ky.us +lib.ky.us +cc.la.us +lib.la.us +cc.ma.us +lib.ma.us +cc.md.us +lib.md.us +cc.me.us +lib.me.us +cc.mi.us +lib.mi.us +cc.mn.us +lib.mn.us +cc.mo.us +lib.mo.us +cc.ms.us +cc.mt.us +lib.mt.us +cc.nc.us +lib.nc.us +cc.ne.us +lib.ne.us +cc.nh.us +lib.nh.us +cc.nj.us +lib.nj.us +cc.nm.us +lib.nm.us +cc.nv.us +lib.nv.us +cc.ny.us +lib.ny.us +cc.oh.us +lib.oh.us +cc.ok.us +lib.ok.us +cc.or.us +lib.or.us +cc.pa.us +lib.pa.us +cc.pr.us +lib.pr.us +cc.ri.us +lib.ri.us +cc.sc.us +lib.sc.us +cc.sd.us +lib.sd.us +cc.tn.us +lib.tn.us +cc.tx.us +lib.tx.us +cc.ut.us +lib.ut.us +cc.va.us +lib.va.us +cc.vi.us +lib.vi.us +cc.vt.us +lib.vt.us +cc.wa.us +lib.wa.us +cc.wi.us +lib.wi.us +cc.wv.us +cc.wy.us +k12.wy.us +// lib.wv.us - Bug 941670 - Removed at request of Larry W Arnold +lib.wy.us +// k12.ma.us contains school districts in Massachusetts. The 4LDs are +// managed independently except for private (PVT), charter (CHTR) and +// parochial (PAROCH) schools. Those are delegated directly to the +// 5LD operators. +chtr.k12.ma.us +paroch.k12.ma.us +pvt.k12.ma.us +// Merit Network, Inc. maintains the registry for =~ /(k12|cc|lib).mi.us/ and the following +// see also: https://domreg.merit.edu : domreg@merit.edu +// see also: whois -h whois.domreg.merit.edu help +ann-arbor.mi.us +cog.mi.us +dst.mi.us +eaton.mi.us +gen.mi.us +mus.mi.us +tec.mi.us +washtenaw.mi.us + +// uy : http://www.nic.org.uy/ +uy +com.uy +edu.uy +gub.uy +mil.uy +net.uy +org.uy + +// uz : http://www.reg.uz/ +uz +co.uz +com.uz +net.uz +org.uz + +// va : https://www.iana.org/domains/root/db/va.html +va + +// vc : https://www.iana.org/domains/root/db/vc.html +// Submitted by registry +vc +com.vc +edu.vc +gov.vc +mil.vc +net.vc +org.vc + +// ve : https://nic.ve/ +// https://nic.ve/site/user-agreement -> under "III. Clasificación de Nombres de Dominio" +// Submitted by registry nic@nic.ve and nicve@conatel.gob.ve +ve +arts.ve +bib.ve +co.ve +com.ve +e12.ve +edu.ve +emprende.ve +firm.ve +gob.ve +gov.ve +ia.ve +info.ve +int.ve +mil.ve +net.ve +nom.ve +org.ve +rar.ve +rec.ve +store.ve +tec.ve +web.ve + +// vg : https://www.iana.org/domains/root/db/vg.html +// Confirmed by registry 2025-01-10 +vg +edu.vg + +// vi : https://www.iana.org/domains/root/db/vi.html +vi +co.vi +com.vi +k12.vi +net.vi +org.vi + +// vn : https://vnnic.vn/en/domain-name-vn/domain-name/cctldvn +vn +ac.vn +ai.vn +biz.vn +com.vn +edu.vn +gov.vn +health.vn +id.vn +info.vn +int.vn +io.vn +name.vn +net.vn +org.vn +pro.vn + +// vn geographical names +angiang.vn +bacgiang.vn +backan.vn +baclieu.vn +bacninh.vn +baria-vungtau.vn +bentre.vn +binhdinh.vn +binhduong.vn +binhphuoc.vn +binhthuan.vn +camau.vn +cantho.vn +caobang.vn +daklak.vn +daknong.vn +danang.vn +dienbien.vn +dongnai.vn +dongthap.vn +gialai.vn +hagiang.vn +haiduong.vn +haiphong.vn +hanam.vn +hanoi.vn +hatinh.vn +haugiang.vn +hoabinh.vn +hue.vn +hungyen.vn +khanhhoa.vn +kiengiang.vn +kontum.vn +laichau.vn +lamdong.vn +langson.vn +laocai.vn +longan.vn +namdinh.vn +nghean.vn +ninhbinh.vn +ninhthuan.vn +phutho.vn +phuyen.vn +quangbinh.vn +quangnam.vn +quangngai.vn +quangninh.vn +quangtri.vn +soctrang.vn +sonla.vn +tayninh.vn +thaibinh.vn +thainguyen.vn +thanhhoa.vn +thanhphohochiminh.vn +thuathienhue.vn +tiengiang.vn +travinh.vn +tuyenquang.vn +vinhlong.vn +vinhphuc.vn +yenbai.vn + +// vu : https://www.iana.org/domains/root/db/vu.html +// http://www.vunic.vu/ +vu +com.vu +edu.vu +net.vu +org.vu + +// wf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +wf + +// ws : https://www.iana.org/domains/root/db/ws.html +// http://samoanic.ws/index.dhtml +ws +com.ws +edu.ws +gov.ws +net.ws +org.ws + +// yt : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +yt + +// IDN ccTLDs +// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then +// U-label, and follow this format: +// // A-Label ("", [, variant info]) : +// // [sponsoring org] +// U-Label + +// xn--mgbaam7a8h ("Emerat", Arabic) : AE +// http://aeda.ae/ +امارات + +// xn--y9a3aq ("hye", Armenian) : AM +// ISOC AM (operated by .am Registry) +հայ + +// xn--54b7fta0cc ("Bangla", Bangla) : BD +বাংলা + +// xn--90ae ("bg", Bulgarian) : BG +бг + +// xn--mgbcpq6gpa1a ("albahrain", Arabic) : BH +البحرين + +// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY +// Operated by .by registry +бел + +// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN +// CNNIC +// https://www.cnnic.cn/11/192/index.html +中国 + +// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN +// CNNIC +// https://www.cnnic.com.cn/AU/MediaC/Announcement/201609/t20160905_54470.htm +中國 + +// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ +الجزائر + +// xn--wgbh1c ("Egypt/Masr", Arabic) : EG +// http://www.dotmasr.eg/ +مصر + +// xn--e1a4c ("eu", Cyrillic) : EU +// https://eurid.eu +ею + +// xn--qxa6a ("eu", Greek) : EU +// https://eurid.eu +ευ + +// xn--mgbah1a3hjkrd ("Mauritania", Arabic) : MR +موريتانيا + +// xn--node ("ge", Georgian Mkhedruli) : GE +გე + +// xn--qxam ("el", Greek) : GR +// Hellenic Ministry of Infrastructure, Transport, and Networks +ελ + +// xn--j6w193g ("Hong Kong", Chinese) : HK +// https://www.hkirc.hk +// Submitted by registry +// https://www.hkirc.hk/content.jsp?id=30#!/34 +香港 +個人.香港 +公司.香港 +政府.香港 +教育.香港 +組織.香港 +網絡.香港 + +// xn--2scrj9c ("Bharat", Kannada) : IN +// India +ಭಾರತ + +// xn--3hcrj9c ("Bharat", Oriya) : IN +// India +ଭାରତ + +// xn--45br5cyl ("Bharatam", Assamese) : IN +// India +ভাৰত + +// xn--h2breg3eve ("Bharatam", Sanskrit) : IN +// India +भारतम् + +// xn--h2brj9c8c ("Bharot", Santali) : IN +// India +भारोत + +// xn--mgbgu82a ("Bharat", Sindhi) : IN +// India +ڀارت + +// xn--rvc1e0am3e ("Bharatam", Malayalam) : IN +// India +ഭാരതം + +// xn--h2brj9c ("Bharat", Devanagari) : IN +// India +भारत + +// xn--mgbbh1a ("Bharat", Kashmiri) : IN +// India +بارت + +// xn--mgbbh1a71e ("Bharat", Arabic) : IN +// India +بھارت + +// xn--fpcrj9c3d ("Bharat", Telugu) : IN +// India +భారత్ + +// xn--gecrj9c ("Bharat", Gujarati) : IN +// India +ભારત + +// xn--s9brj9c ("Bharat", Gurmukhi) : IN +// India +ਭਾਰਤ + +// xn--45brj9c ("Bharat", Bengali) : IN +// India +ভারত + +// xn--xkc2dl3a5ee0h ("India", Tamil) : IN +// India +இந்தியா + +// xn--mgba3a4f16a ("Iran", Persian) : IR +ایران + +// xn--mgba3a4fra ("Iran", Arabic) : IR +ايران + +// xn--mgbtx2b ("Iraq", Arabic) : IQ +// Communications and Media Commission +عراق + +// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO +// National Information Technology Center (NITC) +// Royal Scientific Society, Al-Jubeiha +الاردن + +// xn--3e0b707e ("Republic of Korea", Hangul) : KR +한국 + +// xn--80ao21a ("Kaz", Kazakh) : KZ +қаз + +// xn--q7ce6a ("Lao", Lao) : LA +ລາວ + +// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK +// http://www.domains.lk/ +ලංකා + +// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK +// http://www.domains.lk/ +இலங்கை + +// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA +المغرب + +// xn--d1alf ("mkd", Macedonian) : MK +// MARnet +мкд + +// xn--l1acc ("mon", Mongolian) : MN +мон + +// xn--mix891f ("Macao", Chinese, Traditional) : MO +// MONIC / HNET Asia (Registry Operator for .mo) +澳門 + +// xn--mix082f ("Macao", Chinese, Simplified) : MO +澳门 + +// xn--mgbx4cd0ab ("Malaysia", Malay) : MY +مليسيا + +// xn--mgb9awbf ("Oman", Arabic) : OM +عمان + +// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK +پاکستان + +// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK +پاكستان + +// xn--ygbi2ammx ("Falasteen", Arabic) : PS +// The Palestinian National Internet Naming Authority (PNINA) +// http://www.pnina.ps +فلسطين + +// xn--90a3ac ("srb", Cyrillic) : RS +// https://www.rnids.rs/en/domains/national-domains +срб +ак.срб +обр.срб +од.срб +орг.срб +пр.срб +упр.срб + +// xn--p1ai ("rf", Russian-Cyrillic) : RU +// https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +рф + +// xn--wgbl6a ("Qatar", Arabic) : QA +// https://www.cra.gov.qa/ +قطر + +// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA +// http://www.nic.net.sa/ +السعودية + +// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant): SA +السعودیة + +// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA +السعودیۃ + +// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA +السعوديه + +// xn--mgbpl2fh ("sudan", Arabic) : SD +// Operated by .sd registry +سودان + +// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG +新加坡 + +// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG +சிங்கப்பூர் + +// xn--ogbpf8fl ("Syria", Arabic) : SY +سورية + +// xn--mgbtf8fl ("Syria", Arabic, variant) : SY +سوريا + +// xn--o3cw4h ("Thai", Thai) : TH +// http://www.thnic.co.th +ไทย +ทหาร.ไทย +ธุรกิจ.ไทย +เน็ต.ไทย +รัฐบาล.ไทย +ศึกษา.ไทย +องค์กร.ไทย + +// xn--pgbs0dh ("Tunisia", Arabic) : TN +// http://nic.tn +تونس + +// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW +// https://twnic.tw/dnservice_catag.php +台灣 + +// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台湾 + +// xn--nnx388a ("Taiwan", Chinese, variant) : TW +臺灣 + +// xn--j1amh ("ukr", Cyrillic) : UA +укр + +// xn--mgb2ddes ("AlYemen", Arabic) : YE +اليمن + +// xxx : https://icmregistry.biz/ +xxx + +// ye : https://www.iana.org/domains/root/db/ye.html +ye +com.ye +edu.ye +gov.ye +mil.ye +net.ye +org.ye + +// za : https://www.iana.org/domains/root/db/za.html +ac.za +agric.za +alt.za +co.za +edu.za +gov.za +grondar.za +law.za +mil.za +net.za +ngo.za +nic.za +nis.za +nom.za +org.za +school.za +tm.za +web.za + +// zm : https://zicta.zm/ +zm +ac.zm +biz.zm +co.zm +com.zm +edu.zm +gov.zm +info.zm +mil.zm +net.zm +org.zm +sch.zm + +// zw : https://www.potraz.gov.zw/ +// Confirmed by registry 2017-01-25 +zw +ac.zw +co.zw +gov.zw +mil.zw +org.zw + +// newGTLDs + +// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2026-07-24T16:40:16Z +// This list is auto-generated, don't edit it manually. +// aaa : American Automobile Association, Inc. +// https://www.iana.org/domains/root/db/aaa.html +aaa + +// aarp : AARP +// https://www.iana.org/domains/root/db/aarp.html +aarp + +// abb : ABB Ltd +// https://www.iana.org/domains/root/db/abb.html +abb + +// abbott : Abbott Laboratories, Inc. +// https://www.iana.org/domains/root/db/abbott.html +abbott + +// abbvie : AbbVie Inc. +// https://www.iana.org/domains/root/db/abbvie.html +abbvie + +// abc : Disney Enterprises, Inc. +// https://www.iana.org/domains/root/db/abc.html +abc + +// able : Able Inc. +// https://www.iana.org/domains/root/db/able.html +able + +// abogado : Registry Services, LLC +// https://www.iana.org/domains/root/db/abogado.html +abogado + +// abudhabi : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/abudhabi.html +abudhabi + +// academy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/academy.html +academy + +// accenture : Accenture plc +// https://www.iana.org/domains/root/db/accenture.html +accenture + +// accountant : dot Accountant Limited +// https://www.iana.org/domains/root/db/accountant.html +accountant + +// accountants : Binky Moon, LLC +// https://www.iana.org/domains/root/db/accountants.html +accountants + +// aco : ACO Severin Ahlmann GmbH & Co. KG +// https://www.iana.org/domains/root/db/aco.html +aco + +// actor : Dog Beach, LLC +// https://www.iana.org/domains/root/db/actor.html +actor + +// ads : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ads.html +ads + +// adult : ICM Registry AD LLC +// https://www.iana.org/domains/root/db/adult.html +adult + +// aeg : Aktiebolaget Electrolux +// https://www.iana.org/domains/root/db/aeg.html +aeg + +// aetna : Aetna Life Insurance Company +// https://www.iana.org/domains/root/db/aetna.html +aetna + +// afl : Australian Football League +// https://www.iana.org/domains/root/db/afl.html +afl + +// africa : ZA Central Registry NPC trading as Registry.Africa +// https://www.iana.org/domains/root/db/africa.html +africa + +// agakhan : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/agakhan.html +agakhan + +// agency : Binky Moon, LLC +// https://www.iana.org/domains/root/db/agency.html +agency + +// aig : American International Group, Inc. +// https://www.iana.org/domains/root/db/aig.html +aig + +// airbus : Airbus S.A.S. +// https://www.iana.org/domains/root/db/airbus.html +airbus + +// airforce : Dog Beach, LLC +// https://www.iana.org/domains/root/db/airforce.html +airforce + +// airtel : Bharti Airtel Limited +// https://www.iana.org/domains/root/db/airtel.html +airtel + +// akdn : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/akdn.html +akdn + +// alibaba : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alibaba.html +alibaba + +// alipay : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alipay.html +alipay + +// allfinanz : Allfinanz Deutsche Vermögensberatung Aktiengesellschaft +// https://www.iana.org/domains/root/db/allfinanz.html +allfinanz + +// allstate : Allstate Fire and Casualty Insurance Company +// https://www.iana.org/domains/root/db/allstate.html +allstate + +// ally : Ally Financial Inc. +// https://www.iana.org/domains/root/db/ally.html +ally + +// alsace : Region Grand Est +// https://www.iana.org/domains/root/db/alsace.html +alsace + +// alstom : ALSTOM +// https://www.iana.org/domains/root/db/alstom.html +alstom + +// amazon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/amazon.html +amazon + +// americanexpress : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/americanexpress.html +americanexpress + +// americanfamily : AmFam, Inc. +// https://www.iana.org/domains/root/db/americanfamily.html +americanfamily + +// amex : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/amex.html +amex + +// amfam : AmFam, Inc. +// https://www.iana.org/domains/root/db/amfam.html +amfam + +// amica : Amica Mutual Insurance Company +// https://www.iana.org/domains/root/db/amica.html +amica + +// amsterdam : Gemeente Amsterdam +// https://www.iana.org/domains/root/db/amsterdam.html +amsterdam + +// analytics : Campus IP LLC +// https://www.iana.org/domains/root/db/analytics.html +analytics + +// android : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/android.html +android + +// anquan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/anquan.html +anquan + +// anz : Australia and New Zealand Banking Group Limited +// https://www.iana.org/domains/root/db/anz.html +anz + +// aol : AOL Media LLC +// https://www.iana.org/domains/root/db/aol.html +aol + +// apartments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/apartments.html +apartments + +// app : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/app.html +app + +// apple : Apple Inc. +// https://www.iana.org/domains/root/db/apple.html +apple + +// aquarelle : Aquarelle.com +// https://www.iana.org/domains/root/db/aquarelle.html +aquarelle + +// arab : League of Arab States +// https://www.iana.org/domains/root/db/arab.html +arab + +// aramco : Aramco Services Company +// https://www.iana.org/domains/root/db/aramco.html +aramco + +// archi : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/archi.html +archi + +// army : Dog Beach, LLC +// https://www.iana.org/domains/root/db/army.html +army + +// art : UK Creative Ideas Limited +// https://www.iana.org/domains/root/db/art.html +art + +// arte : Association Relative à la Télévision Européenne G.E.I.E. +// https://www.iana.org/domains/root/db/arte.html +arte + +// asda : Asda Stores Limited +// https://www.iana.org/domains/root/db/asda.html +asda + +// associates : Binky Moon, LLC +// https://www.iana.org/domains/root/db/associates.html +associates + +// athleta : The Gap, Inc. +// https://www.iana.org/domains/root/db/athleta.html +athleta + +// attorney : Dog Beach, LLC +// https://www.iana.org/domains/root/db/attorney.html +attorney + +// auction : Dog Beach, LLC +// https://www.iana.org/domains/root/db/auction.html +auction + +// audi : AUDI Aktiengesellschaft +// https://www.iana.org/domains/root/db/audi.html +audi + +// audible : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/audible.html +audible + +// audio : XYZ.COM LLC +// https://www.iana.org/domains/root/db/audio.html +audio + +// auspost : Australian Postal Corporation +// https://www.iana.org/domains/root/db/auspost.html +auspost + +// author : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/author.html +author + +// auto : XYZ.COM LLC +// https://www.iana.org/domains/root/db/auto.html +auto + +// autos : XYZ.COM LLC +// https://www.iana.org/domains/root/db/autos.html +autos + +// aws : AWS Registry LLC +// https://www.iana.org/domains/root/db/aws.html +aws + +// axa : AXA Group Operations SAS +// https://www.iana.org/domains/root/db/axa.html +axa + +// azure : Microsoft Corporation +// https://www.iana.org/domains/root/db/azure.html +azure + +// baby : XYZ.COM LLC +// https://www.iana.org/domains/root/db/baby.html +baby + +// baidu : Baidu, Inc. +// https://www.iana.org/domains/root/db/baidu.html +baidu + +// banamex : Citigroup Inc. +// https://www.iana.org/domains/root/db/banamex.html +banamex + +// band : Dog Beach, LLC +// https://www.iana.org/domains/root/db/band.html +band + +// bank : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/bank.html +bank + +// bar : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/bar.html +bar + +// barcelona : Municipi de Barcelona +// https://www.iana.org/domains/root/db/barcelona.html +barcelona + +// barclaycard : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclaycard.html +barclaycard + +// barclays : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclays.html +barclays + +// barefoot : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/barefoot.html +barefoot + +// bargains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bargains.html +bargains + +// baseball : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/baseball.html +baseball + +// basketball : Fédération Internationale de Basketball (FIBA) +// https://www.iana.org/domains/root/db/basketball.html +basketball + +// bauhaus : Werkhaus GmbH +// https://www.iana.org/domains/root/db/bauhaus.html +bauhaus + +// bayern : Bayern Connect GmbH +// https://www.iana.org/domains/root/db/bayern.html +bayern + +// bbc : British Broadcasting Corporation +// https://www.iana.org/domains/root/db/bbc.html +bbc + +// bbt : BB&T Corporation +// https://www.iana.org/domains/root/db/bbt.html +bbt + +// bbva : BANCO BILBAO VIZCAYA ARGENTARIA, S.A. +// https://www.iana.org/domains/root/db/bbva.html +bbva + +// bcg : The Boston Consulting Group, Inc. +// https://www.iana.org/domains/root/db/bcg.html +bcg + +// bcn : Municipi de Barcelona +// https://www.iana.org/domains/root/db/bcn.html +bcn + +// beats : Beats Electronics, LLC +// https://www.iana.org/domains/root/db/beats.html +beats + +// beauty : XYZ.COM LLC +// https://www.iana.org/domains/root/db/beauty.html +beauty + +// beer : Registry Services, LLC +// https://www.iana.org/domains/root/db/beer.html +beer + +// berlin : dotBERLIN GmbH & Co. KG +// https://www.iana.org/domains/root/db/berlin.html +berlin + +// best : BestTLD Pty Ltd +// https://www.iana.org/domains/root/db/best.html +best + +// bestbuy : BBY Solutions, Inc. +// https://www.iana.org/domains/root/db/bestbuy.html +bestbuy + +// bet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bet.html +bet + +// bharti : Bharti Enterprises (Holding) Private Limited +// https://www.iana.org/domains/root/db/bharti.html +bharti + +// bible : American Bible Society +// https://www.iana.org/domains/root/db/bible.html +bible + +// bid : dot Bid Limited +// https://www.iana.org/domains/root/db/bid.html +bid + +// bike : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bike.html +bike + +// bing : Microsoft Corporation +// https://www.iana.org/domains/root/db/bing.html +bing + +// bingo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bingo.html +bingo + +// bio : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bio.html +bio + +// black : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/black.html +black + +// blackfriday : Registry Services, LLC +// https://www.iana.org/domains/root/db/blackfriday.html +blackfriday + +// blockbuster : Dish DBS Corporation +// https://www.iana.org/domains/root/db/blockbuster.html +blockbuster + +// blog : Knock Knock WHOIS There, LLC +// https://www.iana.org/domains/root/db/blog.html +blog + +// bloomberg : Bloomberg IP Holdings LLC +// https://www.iana.org/domains/root/db/bloomberg.html +bloomberg + +// blue : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/blue.html +blue + +// bms : Bristol-Myers Squibb Company +// https://www.iana.org/domains/root/db/bms.html +bms + +// bmw : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/bmw.html +bmw + +// bnpparibas : BNP Paribas +// https://www.iana.org/domains/root/db/bnpparibas.html +bnpparibas + +// boats : XYZ.COM LLC +// https://www.iana.org/domains/root/db/boats.html +boats + +// boehringer : Boehringer Ingelheim International GmbH +// https://www.iana.org/domains/root/db/boehringer.html +boehringer + +// bofa : Bank of America Corporation +// https://www.iana.org/domains/root/db/bofa.html +bofa + +// bom : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/bom.html +bom + +// bond : ShortDot SA +// https://www.iana.org/domains/root/db/bond.html +bond + +// boo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/boo.html +boo + +// book : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/book.html +book + +// booking : Booking.com B.V. +// https://www.iana.org/domains/root/db/booking.html +booking + +// bosch : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/bosch.html +bosch + +// bostik : Bostik SA +// https://www.iana.org/domains/root/db/bostik.html +bostik + +// boston : Registry Services, LLC +// https://www.iana.org/domains/root/db/boston.html +boston + +// bot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/bot.html +bot + +// boutique : Binky Moon, LLC +// https://www.iana.org/domains/root/db/boutique.html +boutique + +// box : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/box.html +box + +// bradesco : Banco Bradesco S.A. +// https://www.iana.org/domains/root/db/bradesco.html +bradesco + +// bridgestone : Bridgestone Corporation +// https://www.iana.org/domains/root/db/bridgestone.html +bridgestone + +// broadway : Celebrate Broadway, Inc. +// https://www.iana.org/domains/root/db/broadway.html +broadway + +// broker : Dog Beach, LLC +// https://www.iana.org/domains/root/db/broker.html +broker + +// brother : Brother Industries, Ltd. +// https://www.iana.org/domains/root/db/brother.html +brother + +// brussels : DNS.be vzw +// https://www.iana.org/domains/root/db/brussels.html +brussels + +// build : Plan Bee LLC +// https://www.iana.org/domains/root/db/build.html +build + +// builders : Binky Moon, LLC +// https://www.iana.org/domains/root/db/builders.html +builders + +// business : Binky Moon, LLC +// https://www.iana.org/domains/root/db/business.html +business + +// buy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/buy.html +buy + +// buzz : DOTSTRATEGY CO. +// https://www.iana.org/domains/root/db/buzz.html +buzz + +// bzh : Association www.bzh +// https://www.iana.org/domains/root/db/bzh.html +bzh + +// cab : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cab.html +cab + +// cafe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cafe.html +cafe + +// cal : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/cal.html +cal + +// call : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/call.html +call + +// calvinklein : PVH gTLD Holdings LLC +// https://www.iana.org/domains/root/db/calvinklein.html +calvinklein + +// cam : Cam Connecting SARL +// https://www.iana.org/domains/root/db/cam.html +cam + +// camera : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camera.html +camera + +// camp : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camp.html +camp + +// canon : Canon Inc. +// https://www.iana.org/domains/root/db/canon.html +canon + +// capetown : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/capetown.html +capetown + +// capital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/capital.html +capital + +// capitalone : Capital One Financial Corporation +// https://www.iana.org/domains/root/db/capitalone.html +capitalone + +// car : XYZ.COM LLC +// https://www.iana.org/domains/root/db/car.html +car + +// caravan : Caravan International, Inc. +// https://www.iana.org/domains/root/db/caravan.html +caravan + +// cards : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cards.html +cards + +// care : Binky Moon, LLC +// https://www.iana.org/domains/root/db/care.html +care + +// career : dotCareer LLC +// https://www.iana.org/domains/root/db/career.html +career + +// careers : Binky Moon, LLC +// https://www.iana.org/domains/root/db/careers.html +careers + +// cars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/cars.html +cars + +// casa : Registry Services, LLC +// https://www.iana.org/domains/root/db/casa.html +casa + +// case : Digity, LLC +// https://www.iana.org/domains/root/db/case.html +case + +// cash : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cash.html +cash + +// casino : Binky Moon, LLC +// https://www.iana.org/domains/root/db/casino.html +casino + +// catering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/catering.html +catering + +// catholic : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/catholic.html +catholic + +// cba : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/cba.html +cba + +// cbn : The Christian Broadcasting Network, Inc. +// https://www.iana.org/domains/root/db/cbn.html +cbn + +// cbre : CBRE, Inc. +// https://www.iana.org/domains/root/db/cbre.html +cbre + +// center : Binky Moon, LLC +// https://www.iana.org/domains/root/db/center.html +center + +// ceo : XYZ.COM LLC +// https://www.iana.org/domains/root/db/ceo.html +ceo + +// cern : European Organization for Nuclear Research ("CERN") +// https://www.iana.org/domains/root/db/cern.html +cern + +// cfa : CFA Institute +// https://www.iana.org/domains/root/db/cfa.html +cfa + +// cfd : ShortDot SA +// https://www.iana.org/domains/root/db/cfd.html +cfd + +// chanel : Chanel International B.V. +// https://www.iana.org/domains/root/db/chanel.html +chanel + +// channel : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/channel.html +channel + +// charity : Public Interest Registry +// https://www.iana.org/domains/root/db/charity.html +charity + +// chase : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/chase.html +chase + +// chat : Binky Moon, LLC +// https://www.iana.org/domains/root/db/chat.html +chat + +// cheap : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cheap.html +cheap + +// chintai : CHINTAI Corporation +// https://www.iana.org/domains/root/db/chintai.html +chintai + +// christmas : XYZ.COM LLC +// https://www.iana.org/domains/root/db/christmas.html +christmas + +// chrome : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/chrome.html +chrome + +// church : Binky Moon, LLC +// https://www.iana.org/domains/root/db/church.html +church + +// cipriani : Hotel Cipriani Srl +// https://www.iana.org/domains/root/db/cipriani.html +cipriani + +// circle : Jolly Host, LLC +// https://www.iana.org/domains/root/db/circle.html +circle + +// cisco : Cisco Technology, Inc. +// https://www.iana.org/domains/root/db/cisco.html +cisco + +// citadel : Citadel Domain LLC +// https://www.iana.org/domains/root/db/citadel.html +citadel + +// citi : Citigroup Inc. +// https://www.iana.org/domains/root/db/citi.html +citi + +// citic : CITIC Group Corporation +// https://www.iana.org/domains/root/db/citic.html +citic + +// city : Binky Moon, LLC +// https://www.iana.org/domains/root/db/city.html +city + +// claims : Binky Moon, LLC +// https://www.iana.org/domains/root/db/claims.html +claims + +// cleaning : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cleaning.html +cleaning + +// click : Waterford Limited +// https://www.iana.org/domains/root/db/click.html +click + +// clinic : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clinic.html +clinic + +// clinique : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/clinique.html +clinique + +// clothing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clothing.html +clothing + +// cloud : Aruba PEC S.p.A. +// https://www.iana.org/domains/root/db/cloud.html +cloud + +// club : Registry Services, LLC +// https://www.iana.org/domains/root/db/club.html +club + +// clubmed : Club Méditerranée S.A. +// https://www.iana.org/domains/root/db/clubmed.html +clubmed + +// coach : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coach.html +coach + +// codes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/codes.html +codes + +// coffee : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coffee.html +coffee + +// college : XYZ.COM LLC +// https://www.iana.org/domains/root/db/college.html +college + +// cologne : dotKoeln GmbH +// https://www.iana.org/domains/root/db/cologne.html +cologne + +// commbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/commbank.html +commbank + +// community : Binky Moon, LLC +// https://www.iana.org/domains/root/db/community.html +community + +// company : Binky Moon, LLC +// https://www.iana.org/domains/root/db/company.html +company + +// compare : Registry Services, LLC +// https://www.iana.org/domains/root/db/compare.html +compare + +// computer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/computer.html +computer + +// comsec : VeriSign, Inc. +// https://www.iana.org/domains/root/db/comsec.html +comsec + +// condos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/condos.html +condos + +// construction : Binky Moon, LLC +// https://www.iana.org/domains/root/db/construction.html +construction + +// consulting : Dog Beach, LLC +// https://www.iana.org/domains/root/db/consulting.html +consulting + +// contact : Dog Beach, LLC +// https://www.iana.org/domains/root/db/contact.html +contact + +// contractors : Binky Moon, LLC +// https://www.iana.org/domains/root/db/contractors.html +contractors + +// cooking : Registry Services, LLC +// https://www.iana.org/domains/root/db/cooking.html +cooking + +// cool : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cool.html +cool + +// corsica : Collectivité de Corse +// https://www.iana.org/domains/root/db/corsica.html +corsica + +// country : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/country.html +country + +// coupon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/coupon.html +coupon + +// coupons : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coupons.html +coupons + +// courses : Registry Services, LLC +// https://www.iana.org/domains/root/db/courses.html +courses + +// cpa : American Institute of Certified Public Accountants +// https://www.iana.org/domains/root/db/cpa.html +cpa + +// credit : Binky Moon, LLC +// https://www.iana.org/domains/root/db/credit.html +credit + +// creditcard : Binky Moon, LLC +// https://www.iana.org/domains/root/db/creditcard.html +creditcard + +// creditunion : DotCooperation LLC +// https://www.iana.org/domains/root/db/creditunion.html +creditunion + +// cricket : dot Cricket Limited +// https://www.iana.org/domains/root/db/cricket.html +cricket + +// crown : Crown Equipment Corporation +// https://www.iana.org/domains/root/db/crown.html +crown + +// crs : Federated Co-operatives Limited +// https://www.iana.org/domains/root/db/crs.html +crs + +// cruise : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/cruise.html +cruise + +// cruises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cruises.html +cruises + +// cuisinella : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/cuisinella.html +cuisinella + +// cymru : Nominet UK +// https://www.iana.org/domains/root/db/cymru.html +cymru + +// cyou : ShortDot SA +// https://www.iana.org/domains/root/db/cyou.html +cyou + +// dad : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dad.html +dad + +// dance : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dance.html +dance + +// data : Dish DBS Corporation +// https://www.iana.org/domains/root/db/data.html +data + +// date : dot Date Limited +// https://www.iana.org/domains/root/db/date.html +date + +// dating : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dating.html +dating + +// datsun : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/datsun.html +datsun + +// day : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/day.html +day + +// dclk : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dclk.html +dclk + +// dds : Registry Services, LLC +// https://www.iana.org/domains/root/db/dds.html +dds + +// deal : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/deal.html +deal + +// dealer : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/dealer.html +dealer + +// deals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/deals.html +deals + +// degree : Dog Beach, LLC +// https://www.iana.org/domains/root/db/degree.html +degree + +// delivery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/delivery.html +delivery + +// dell : Dell Inc. +// https://www.iana.org/domains/root/db/dell.html +dell + +// deloitte : Deloitte Touche Tohmatsu +// https://www.iana.org/domains/root/db/deloitte.html +deloitte + +// delta : Delta Air Lines, Inc. +// https://www.iana.org/domains/root/db/delta.html +delta + +// democrat : Dog Beach, LLC +// https://www.iana.org/domains/root/db/democrat.html +democrat + +// dental : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dental.html +dental + +// dentist : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dentist.html +dentist + +// desi +// https://www.iana.org/domains/root/db/desi.html +desi + +// design : Registry Services, LLC +// https://www.iana.org/domains/root/db/design.html +design + +// dev : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dev.html +dev + +// dhl : Deutsche Post AG +// https://www.iana.org/domains/root/db/dhl.html +dhl + +// diamonds : Binky Moon, LLC +// https://www.iana.org/domains/root/db/diamonds.html +diamonds + +// diet : XYZ.COM LLC +// https://www.iana.org/domains/root/db/diet.html +diet + +// digital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/digital.html +digital + +// direct : Binky Moon, LLC +// https://www.iana.org/domains/root/db/direct.html +direct + +// directory : Binky Moon, LLC +// https://www.iana.org/domains/root/db/directory.html +directory + +// discount : Binky Moon, LLC +// https://www.iana.org/domains/root/db/discount.html +discount + +// discover : Discover Financial Services +// https://www.iana.org/domains/root/db/discover.html +discover + +// dish : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dish.html +dish + +// diy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/diy.html +diy + +// dnp : Dai Nippon Printing Co., Ltd. +// https://www.iana.org/domains/root/db/dnp.html +dnp + +// docs : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/docs.html +docs + +// doctor : Binky Moon, LLC +// https://www.iana.org/domains/root/db/doctor.html +doctor + +// dog : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dog.html +dog + +// domains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/domains.html +domains + +// dot : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dot.html +dot + +// download : dot Support Limited +// https://www.iana.org/domains/root/db/download.html +download + +// drive : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/drive.html +drive + +// dtv : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dtv.html +dtv + +// dubai : Dubai Smart Government Department +// https://www.iana.org/domains/root/db/dubai.html +dubai + +// dupont : DuPont Specialty Products USA, LLC +// https://www.iana.org/domains/root/db/dupont.html +dupont + +// durban : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/durban.html +durban + +// dvag : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/dvag.html +dvag + +// dvr : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/dvr.html +dvr + +// earth : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/earth.html +earth + +// eat : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/eat.html +eat + +// eco : Big Room Inc. +// https://www.iana.org/domains/root/db/eco.html +eco + +// edeka : EDEKA Verband kaufmännischer Genossenschaften e.V. +// https://www.iana.org/domains/root/db/edeka.html +edeka + +// education : Binky Moon, LLC +// https://www.iana.org/domains/root/db/education.html +education + +// email : Binky Moon, LLC +// https://www.iana.org/domains/root/db/email.html +email + +// emerck : Merck KGaA +// https://www.iana.org/domains/root/db/emerck.html +emerck + +// energy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/energy.html +energy + +// engineer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/engineer.html +engineer + +// engineering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/engineering.html +engineering + +// enterprises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/enterprises.html +enterprises + +// epson : Seiko Epson Corporation +// https://www.iana.org/domains/root/db/epson.html +epson + +// equipment : Binky Moon, LLC +// https://www.iana.org/domains/root/db/equipment.html +equipment + +// ericsson : Telefonaktiebolaget L M Ericsson +// https://www.iana.org/domains/root/db/ericsson.html +ericsson + +// erni : ERNI Group Holding AG +// https://www.iana.org/domains/root/db/erni.html +erni + +// esq : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/esq.html +esq + +// estate : Binky Moon, LLC +// https://www.iana.org/domains/root/db/estate.html +estate + +// eurovision : European Broadcasting Union (EBU) +// https://www.iana.org/domains/root/db/eurovision.html +eurovision + +// eus : Puntueus Fundazioa +// https://www.iana.org/domains/root/db/eus.html +eus + +// events : Binky Moon, LLC +// https://www.iana.org/domains/root/db/events.html +events + +// exchange : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exchange.html +exchange + +// expert : Binky Moon, LLC +// https://www.iana.org/domains/root/db/expert.html +expert + +// exposed : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exposed.html +exposed + +// express : Binky Moon, LLC +// https://www.iana.org/domains/root/db/express.html +express + +// extraspace : Extra Space Storage LLC +// https://www.iana.org/domains/root/db/extraspace.html +extraspace + +// fage : Fage International S.A. +// https://www.iana.org/domains/root/db/fage.html +fage + +// fail : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fail.html +fail + +// fairwinds : FairWinds Partners, LLC +// https://www.iana.org/domains/root/db/fairwinds.html +fairwinds + +// faith : dot Faith Limited +// https://www.iana.org/domains/root/db/faith.html +faith + +// family : Dog Beach, LLC +// https://www.iana.org/domains/root/db/family.html +family + +// fan : Dog Beach, LLC +// https://www.iana.org/domains/root/db/fan.html +fan + +// fans : ZDNS International Limited +// https://www.iana.org/domains/root/db/fans.html +fans + +// farm : Binky Moon, LLC +// https://www.iana.org/domains/root/db/farm.html +farm + +// farmers : Farmers Insurance Exchange +// https://www.iana.org/domains/root/db/farmers.html +farmers + +// fashion : Registry Services, LLC +// https://www.iana.org/domains/root/db/fashion.html +fashion + +// fast : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fast.html +fast + +// fedex : Federal Express Corporation +// https://www.iana.org/domains/root/db/fedex.html +fedex + +// feedback : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/feedback.html +feedback + +// ferrari : Fiat Chrysler Automobiles N.V. +// https://www.iana.org/domains/root/db/ferrari.html +ferrari + +// ferrero : Ferrero Trading Lux S.A. +// https://www.iana.org/domains/root/db/ferrero.html +ferrero + +// fidelity : Fidelity Brokerage Services LLC +// https://www.iana.org/domains/root/db/fidelity.html +fidelity + +// fido : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/fido.html +fido + +// film : Motion Picture Domain Registry Pty Ltd +// https://www.iana.org/domains/root/db/film.html +film + +// final : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/final.html +final + +// finance : Binky Moon, LLC +// https://www.iana.org/domains/root/db/finance.html +finance + +// financial : Binky Moon, LLC +// https://www.iana.org/domains/root/db/financial.html +financial + +// fire : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fire.html +fire + +// firestone : Bridgestone Licensing Services, Inc +// https://www.iana.org/domains/root/db/firestone.html +firestone + +// firmdale : Firmdale Holdings Limited +// https://www.iana.org/domains/root/db/firmdale.html +firmdale + +// fish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fish.html +fish + +// fishing : Registry Services, LLC +// https://www.iana.org/domains/root/db/fishing.html +fishing + +// fit : Registry Services, LLC +// https://www.iana.org/domains/root/db/fit.html +fit + +// fitness : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fitness.html +fitness + +// flickr : Flickr, Inc. +// https://www.iana.org/domains/root/db/flickr.html +flickr + +// flights : Binky Moon, LLC +// https://www.iana.org/domains/root/db/flights.html +flights + +// flir : FLIR Systems, Inc. +// https://www.iana.org/domains/root/db/flir.html +flir + +// florist : Binky Moon, LLC +// https://www.iana.org/domains/root/db/florist.html +florist + +// flowers : XYZ.COM LLC +// https://www.iana.org/domains/root/db/flowers.html +flowers + +// fly : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/fly.html +fly + +// foo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/foo.html +foo + +// food : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/food.html +food + +// football : Binky Moon, LLC +// https://www.iana.org/domains/root/db/football.html +football + +// ford : Ford Motor Company +// https://www.iana.org/domains/root/db/ford.html +ford + +// forex : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forex.html +forex + +// forsale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forsale.html +forsale + +// forum : Waterford Limited +// https://www.iana.org/domains/root/db/forum.html +forum + +// foundation : Public Interest Registry +// https://www.iana.org/domains/root/db/foundation.html +foundation + +// fox : FOX Registry, LLC +// https://www.iana.org/domains/root/db/fox.html +fox + +// free : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/free.html +free + +// fresenius : Fresenius Immobilien-Verwaltungs-GmbH +// https://www.iana.org/domains/root/db/fresenius.html +fresenius + +// frl : FRLregistry B.V. +// https://www.iana.org/domains/root/db/frl.html +frl + +// frogans : OP3FT +// https://www.iana.org/domains/root/db/frogans.html +frogans + +// frontier : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/frontier.html +frontier + +// ftr : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/ftr.html +ftr + +// fujitsu : Fujitsu Limited +// https://www.iana.org/domains/root/db/fujitsu.html +fujitsu + +// fun : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/fun.html +fun + +// fund : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fund.html +fund + +// furniture : Binky Moon, LLC +// https://www.iana.org/domains/root/db/furniture.html +furniture + +// futbol : Dog Beach, LLC +// https://www.iana.org/domains/root/db/futbol.html +futbol + +// fyi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fyi.html +fyi + +// gal : Asociación puntoGAL +// https://www.iana.org/domains/root/db/gal.html +gal + +// gallery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gallery.html +gallery + +// gallo : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/gallo.html +gallo + +// gallup : Gallup, Inc. +// https://www.iana.org/domains/root/db/gallup.html +gallup + +// game : XYZ.COM LLC +// https://www.iana.org/domains/root/db/game.html +game + +// games : Dog Beach, LLC +// https://www.iana.org/domains/root/db/games.html +games + +// gap : The Gap, Inc. +// https://www.iana.org/domains/root/db/gap.html +gap + +// garden : Registry Services, LLC +// https://www.iana.org/domains/root/db/garden.html +garden + +// gay : Registry Services, LLC +// https://www.iana.org/domains/root/db/gay.html +gay + +// gbiz : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gbiz.html +gbiz + +// gdn : Joint Stock Company "Navigation-information systems" +// https://www.iana.org/domains/root/db/gdn.html +gdn + +// gea : GEA Group Aktiengesellschaft +// https://www.iana.org/domains/root/db/gea.html +gea + +// gent : Easyhost BV +// https://www.iana.org/domains/root/db/gent.html +gent + +// genting : Resorts World Inc Pte. Ltd. +// https://www.iana.org/domains/root/db/genting.html +genting + +// george : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/george.html +george + +// ggee : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/ggee.html +ggee + +// gift : DotGift, LLC +// https://www.iana.org/domains/root/db/gift.html +gift + +// gifts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gifts.html +gifts + +// gives : Public Interest Registry +// https://www.iana.org/domains/root/db/gives.html +gives + +// giving : Public Interest Registry +// https://www.iana.org/domains/root/db/giving.html +giving + +// glass : Binky Moon, LLC +// https://www.iana.org/domains/root/db/glass.html +glass + +// gle : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gle.html +gle + +// global : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/global.html +global + +// globo : Globo Comunicação e Participações S.A +// https://www.iana.org/domains/root/db/globo.html +globo + +// gmail : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gmail.html +gmail + +// gmbh : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gmbh.html +gmbh + +// gmo : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/gmo.html +gmo + +// gmx : 1&1 Mail & Media GmbH +// https://www.iana.org/domains/root/db/gmx.html +gmx + +// godaddy : Go Daddy East, LLC +// https://www.iana.org/domains/root/db/godaddy.html +godaddy + +// gold : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gold.html +gold + +// goldpoint : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/goldpoint.html +goldpoint + +// golf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/golf.html +golf + +// goodyear : The Goodyear Tire & Rubber Company +// https://www.iana.org/domains/root/db/goodyear.html +goodyear + +// goog : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/goog.html +goog + +// google : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/google.html +google + +// gop : Republican State Leadership Committee, Inc. +// https://www.iana.org/domains/root/db/gop.html +gop + +// got : Jolly Host, LLC +// https://www.iana.org/domains/root/db/got.html +got + +// grainger : Grainger Registry Services, LLC +// https://www.iana.org/domains/root/db/grainger.html +grainger + +// graphics : Binky Moon, LLC +// https://www.iana.org/domains/root/db/graphics.html +graphics + +// gratis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gratis.html +gratis + +// green : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/green.html +green + +// gripe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gripe.html +gripe + +// grocery : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/grocery.html +grocery + +// group : Binky Moon, LLC +// https://www.iana.org/domains/root/db/group.html +group + +// gucci : Guccio Gucci S.p.a. +// https://www.iana.org/domains/root/db/gucci.html +gucci + +// guge : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/guge.html +guge + +// guide : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guide.html +guide + +// guitars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/guitars.html +guitars + +// guru : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guru.html +guru + +// hair : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hair.html +hair + +// hamburg : Hamburg Top-Level-Domain GmbH +// https://www.iana.org/domains/root/db/hamburg.html +hamburg + +// hangout : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/hangout.html +hangout + +// haus : Dog Beach, LLC +// https://www.iana.org/domains/root/db/haus.html +haus + +// hbo : HBO Registry Services, Inc. +// https://www.iana.org/domains/root/db/hbo.html +hbo + +// hdfc : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfc.html +hdfc + +// hdfcbank : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfcbank.html +hdfcbank + +// health : Registry Services, LLC +// https://www.iana.org/domains/root/db/health.html +health + +// healthcare : Binky Moon, LLC +// https://www.iana.org/domains/root/db/healthcare.html +healthcare + +// help : Innovation service Limited +// https://www.iana.org/domains/root/db/help.html +help + +// helsinki : City of Helsinki +// https://www.iana.org/domains/root/db/helsinki.html +helsinki + +// here : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/here.html +here + +// hermes : HERMES INTERNATIONAL +// https://www.iana.org/domains/root/db/hermes.html +hermes + +// hiphop : Dot Hip Hop, LLC +// https://www.iana.org/domains/root/db/hiphop.html +hiphop + +// hisamitsu : Hisamitsu Pharmaceutical Co.,Inc. +// https://www.iana.org/domains/root/db/hisamitsu.html +hisamitsu + +// hitachi : Hitachi, Ltd. +// https://www.iana.org/domains/root/db/hitachi.html +hitachi + +// hiv : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/hiv.html +hiv + +// hkt : PCCW-HKT DataCom Services Limited +// https://www.iana.org/domains/root/db/hkt.html +hkt + +// hockey : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hockey.html +hockey + +// holdings : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holdings.html +holdings + +// holiday : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holiday.html +holiday + +// homedepot : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/homedepot.html +homedepot + +// homegoods : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homegoods.html +homegoods + +// homes : XYZ.COM LLC +// https://www.iana.org/domains/root/db/homes.html +homes + +// homesense : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homesense.html +homesense + +// honda : Honda Motor Co., Ltd. +// https://www.iana.org/domains/root/db/honda.html +honda + +// horse : Registry Services, LLC +// https://www.iana.org/domains/root/db/horse.html +horse + +// hospital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hospital.html +hospital + +// host : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/host.html +host + +// hosting : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hosting.html +hosting + +// hot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/hot.html +hot + +// hotel : HOTEL Top-Level-Domain S.a.r.l +// https://www.iana.org/domains/root/db/hotel.html +hotel + +// hotels : Booking.com B.V. +// https://www.iana.org/domains/root/db/hotels.html +hotels + +// hotmail : Microsoft Corporation +// https://www.iana.org/domains/root/db/hotmail.html +hotmail + +// house : Binky Moon, LLC +// https://www.iana.org/domains/root/db/house.html +house + +// how : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/how.html +how + +// hsbc : HSBC Global Services (UK) Limited +// https://www.iana.org/domains/root/db/hsbc.html +hsbc + +// hughes : Hughes Satellite Systems Corporation +// https://www.iana.org/domains/root/db/hughes.html +hughes + +// hyatt : Hyatt GTLD, L.L.C. +// https://www.iana.org/domains/root/db/hyatt.html +hyatt + +// hyundai : Hyundai Motor Company +// https://www.iana.org/domains/root/db/hyundai.html +hyundai + +// ibm : International Business Machines Corporation +// https://www.iana.org/domains/root/db/ibm.html +ibm + +// icbc : Industrial and Commercial Bank of China Limited +// https://www.iana.org/domains/root/db/icbc.html +icbc + +// ice : IntercontinentalExchange, Inc. +// https://www.iana.org/domains/root/db/ice.html +ice + +// icu : ShortDot SA +// https://www.iana.org/domains/root/db/icu.html +icu + +// ieee : IEEE Global LLC +// https://www.iana.org/domains/root/db/ieee.html +ieee + +// ifm : ifm electronic gmbh +// https://www.iana.org/domains/root/db/ifm.html +ifm + +// ikano : Ikano S.A. +// https://www.iana.org/domains/root/db/ikano.html +ikano + +// imamat : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/imamat.html +imamat + +// imdb : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/imdb.html +imdb + +// immo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/immo.html +immo + +// immobilien : Dog Beach, LLC +// https://www.iana.org/domains/root/db/immobilien.html +immobilien + +// inc : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/inc.html +inc + +// industries : Binky Moon, LLC +// https://www.iana.org/domains/root/db/industries.html +industries + +// infiniti : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/infiniti.html +infiniti + +// ing : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ing.html +ing + +// ink : Registry Services, LLC +// https://www.iana.org/domains/root/db/ink.html +ink + +// institute : Binky Moon, LLC +// https://www.iana.org/domains/root/db/institute.html +institute + +// insurance : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/insurance.html +insurance + +// insure : Binky Moon, LLC +// https://www.iana.org/domains/root/db/insure.html +insure + +// international : Binky Moon, LLC +// https://www.iana.org/domains/root/db/international.html +international + +// intuit : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/intuit.html +intuit + +// investments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/investments.html +investments + +// ipiranga : Ipiranga Produtos de Petroleo S.A. +// https://www.iana.org/domains/root/db/ipiranga.html +ipiranga + +// irish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/irish.html +irish + +// ismaili : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/ismaili.html +ismaili + +// ist : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/ist.html +ist + +// istanbul : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/istanbul.html +istanbul + +// itau : Itau Unibanco Holding S.A. +// https://www.iana.org/domains/root/db/itau.html +itau + +// itv : ITV Services Limited +// https://www.iana.org/domains/root/db/itv.html +itv + +// jaguar : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/jaguar.html +jaguar + +// java : Oracle Corporation +// https://www.iana.org/domains/root/db/java.html +java + +// jcb : JCB Co., Ltd. +// https://www.iana.org/domains/root/db/jcb.html +jcb + +// jeep : FCA US LLC. +// https://www.iana.org/domains/root/db/jeep.html +jeep + +// jetzt : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jetzt.html +jetzt + +// jewelry : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jewelry.html +jewelry + +// jio : Reliance Industries Limited +// https://www.iana.org/domains/root/db/jio.html +jio + +// jll : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/jll.html +jll + +// jmp : Matrix IP LLC +// https://www.iana.org/domains/root/db/jmp.html +jmp + +// jnj : Johnson & Johnson Services, Inc. +// https://www.iana.org/domains/root/db/jnj.html +jnj + +// joburg : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/joburg.html +joburg + +// jot : Jolly Host, LLC +// https://www.iana.org/domains/root/db/jot.html +jot + +// joy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/joy.html +joy + +// jpmorgan : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/jpmorgan.html +jpmorgan + +// jprs : Japan Registry Services Co., Ltd. +// https://www.iana.org/domains/root/db/jprs.html +jprs + +// juegos : Dog Beach, LLC +// https://www.iana.org/domains/root/db/juegos.html +juegos + +// juniper : JUNIPER NETWORKS, INC. +// https://www.iana.org/domains/root/db/juniper.html +juniper + +// kaufen : Dog Beach, LLC +// https://www.iana.org/domains/root/db/kaufen.html +kaufen + +// kddi : KDDI CORPORATION +// https://www.iana.org/domains/root/db/kddi.html +kddi + +// kerryhotels : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryhotels.html +kerryhotels + +// kerryproperties : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryproperties.html +kerryproperties + +// kfh : Kuwait Finance House +// https://www.iana.org/domains/root/db/kfh.html +kfh + +// kia : KIA MOTORS CORPORATION +// https://www.iana.org/domains/root/db/kia.html +kia + +// kids : DotKids Foundation Limited +// https://www.iana.org/domains/root/db/kids.html +kids + +// kim : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/kim.html +kim + +// kindle : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/kindle.html +kindle + +// kitchen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/kitchen.html +kitchen + +// kiwi : DOT KIWI LIMITED +// https://www.iana.org/domains/root/db/kiwi.html +kiwi + +// koeln : dotKoeln GmbH +// https://www.iana.org/domains/root/db/koeln.html +koeln + +// komatsu : Komatsu Ltd. +// https://www.iana.org/domains/root/db/komatsu.html +komatsu + +// kosher : Kosher Marketing Assets LLC +// https://www.iana.org/domains/root/db/kosher.html +kosher + +// kpmg : KPMG International Cooperative (KPMG International Genossenschaft) +// https://www.iana.org/domains/root/db/kpmg.html +kpmg + +// kpn : Koninklijke KPN N.V. +// https://www.iana.org/domains/root/db/kpn.html +kpn + +// krd : KRG Department of Information Technology +// https://www.iana.org/domains/root/db/krd.html +krd + +// kred : KredTLD Pty Ltd +// https://www.iana.org/domains/root/db/kred.html +kred + +// kuokgroup : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kuokgroup.html +kuokgroup + +// kyoto : Academic Institution: The University of Informatics +// https://www.iana.org/domains/root/db/kyoto.html +kyoto + +// lacaixa : Fundación Bancaria Caixa d’Estalvis i Pensions de Barcelona, “la Caixa” +// https://www.iana.org/domains/root/db/lacaixa.html +lacaixa + +// lamborghini : Automobili Lamborghini S.p.A. +// https://www.iana.org/domains/root/db/lamborghini.html +lamborghini + +// lamer : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/lamer.html +lamer + +// land : Binky Moon, LLC +// https://www.iana.org/domains/root/db/land.html +land + +// landrover : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/landrover.html +landrover + +// lanxess : LANXESS Corporation +// https://www.iana.org/domains/root/db/lanxess.html +lanxess + +// lasalle : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/lasalle.html +lasalle + +// lat : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lat.html +lat + +// latino : Dish DBS Corporation +// https://www.iana.org/domains/root/db/latino.html +latino + +// latrobe : La Trobe University +// https://www.iana.org/domains/root/db/latrobe.html +latrobe + +// law : Registry Services, LLC +// https://www.iana.org/domains/root/db/law.html +law + +// lawyer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/lawyer.html +lawyer + +// lds : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/lds.html +lds + +// lease : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lease.html +lease + +// leclerc : A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc +// https://www.iana.org/domains/root/db/leclerc.html +leclerc + +// lefrak : LeFrak Organization, Inc. +// https://www.iana.org/domains/root/db/lefrak.html +lefrak + +// legal : Binky Moon, LLC +// https://www.iana.org/domains/root/db/legal.html +legal + +// lego : LEGO Juris A/S +// https://www.iana.org/domains/root/db/lego.html +lego + +// lexus : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/lexus.html +lexus + +// lgbt : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lgbt.html +lgbt + +// lidl : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/lidl.html +lidl + +// life : Binky Moon, LLC +// https://www.iana.org/domains/root/db/life.html +life + +// lifeinsurance : American Council of Life Insurers +// https://www.iana.org/domains/root/db/lifeinsurance.html +lifeinsurance + +// lifestyle : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/lifestyle.html +lifestyle + +// lighting : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lighting.html +lighting + +// like : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/like.html +like + +// lilly : Eli Lilly and Company +// https://www.iana.org/domains/root/db/lilly.html +lilly + +// limited : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limited.html +limited + +// limo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limo.html +limo + +// lincoln : Ford Motor Company +// https://www.iana.org/domains/root/db/lincoln.html +lincoln + +// link : Nova Registry Ltd +// https://www.iana.org/domains/root/db/link.html +link + +// live : Dog Beach, LLC +// https://www.iana.org/domains/root/db/live.html +live + +// living : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/living.html +living + +// llc : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/llc.html +llc + +// llp : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/llp.html +llp + +// loan : dot Loan Limited +// https://www.iana.org/domains/root/db/loan.html +loan + +// loans : Binky Moon, LLC +// https://www.iana.org/domains/root/db/loans.html +loans + +// locker : Orange Domains LLC +// https://www.iana.org/domains/root/db/locker.html +locker + +// locus : Locus Analytics LLC +// https://www.iana.org/domains/root/db/locus.html +locus + +// lol : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lol.html +lol + +// london : Dot London Domains Limited +// https://www.iana.org/domains/root/db/london.html +london + +// lotte : Lotte Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/lotte.html +lotte + +// lotto : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lotto.html +lotto + +// love : Waterford Limited +// https://www.iana.org/domains/root/db/love.html +love + +// lpl : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lpl.html +lpl + +// lplfinancial : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lplfinancial.html +lplfinancial + +// ltd : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ltd.html +ltd + +// ltda : InterNetX, Corp +// https://www.iana.org/domains/root/db/ltda.html +ltda + +// lundbeck : H. Lundbeck A/S +// https://www.iana.org/domains/root/db/lundbeck.html +lundbeck + +// luxe : Registry Services, LLC +// https://www.iana.org/domains/root/db/luxe.html +luxe + +// luxury : Luxury Partners, LLC +// https://www.iana.org/domains/root/db/luxury.html +luxury + +// madrid : Comunidad de Madrid +// https://www.iana.org/domains/root/db/madrid.html +madrid + +// maif : Mutuelle Assurance Instituteur France (MAIF) +// https://www.iana.org/domains/root/db/maif.html +maif + +// maison : Binky Moon, LLC +// https://www.iana.org/domains/root/db/maison.html +maison + +// makeup : XYZ.COM LLC +// https://www.iana.org/domains/root/db/makeup.html +makeup + +// man : MAN Truck & Bus SE +// https://www.iana.org/domains/root/db/man.html +man + +// management : Binky Moon, LLC +// https://www.iana.org/domains/root/db/management.html +management + +// mango : PUNTO FA S.L. +// https://www.iana.org/domains/root/db/mango.html +mango + +// map : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/map.html +map + +// market : Dog Beach, LLC +// https://www.iana.org/domains/root/db/market.html +market + +// marketing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/marketing.html +marketing + +// markets : Dog Beach, LLC +// https://www.iana.org/domains/root/db/markets.html +markets + +// marriott : Marriott Worldwide Corporation +// https://www.iana.org/domains/root/db/marriott.html +marriott + +// marshalls : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/marshalls.html +marshalls + +// mattel : Mattel IT Services, Inc. +// https://www.iana.org/domains/root/db/mattel.html +mattel + +// mba : Binky Moon, LLC +// https://www.iana.org/domains/root/db/mba.html +mba + +// mckinsey : McKinsey Holdings, Inc. +// https://www.iana.org/domains/root/db/mckinsey.html +mckinsey + +// med : Medistry LLC +// https://www.iana.org/domains/root/db/med.html +med + +// media : Binky Moon, LLC +// https://www.iana.org/domains/root/db/media.html +media + +// meet : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meet.html +meet + +// melbourne : The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation +// https://www.iana.org/domains/root/db/melbourne.html +melbourne + +// meme : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meme.html +meme + +// memorial : Dog Beach, LLC +// https://www.iana.org/domains/root/db/memorial.html +memorial + +// men : Exclusive Registry Limited +// https://www.iana.org/domains/root/db/men.html +men + +// menu : Dot Menu Registry, LLC +// https://www.iana.org/domains/root/db/menu.html +menu + +// merck : Merck Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merck.html +merck + +// merckmsd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merckmsd.html +merckmsd + +// miami : Registry Services, LLC +// https://www.iana.org/domains/root/db/miami.html +miami + +// microsoft : Microsoft Corporation +// https://www.iana.org/domains/root/db/microsoft.html +microsoft + +// mini : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/mini.html +mini + +// mint : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/mint.html +mint + +// mit : Massachusetts Institute of Technology +// https://www.iana.org/domains/root/db/mit.html +mit + +// mitsubishi : Mitsubishi Corporation +// https://www.iana.org/domains/root/db/mitsubishi.html +mitsubishi + +// mlb : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/mlb.html +mlb + +// mls : The Canadian Real Estate Association +// https://www.iana.org/domains/root/db/mls.html +mls + +// mma : MMA IARD +// https://www.iana.org/domains/root/db/mma.html +mma + +// mobile : Dish DBS Corporation +// https://www.iana.org/domains/root/db/mobile.html +mobile + +// moda : Dog Beach, LLC +// https://www.iana.org/domains/root/db/moda.html +moda + +// moe : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/moe.html +moe + +// moi : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/moi.html +moi + +// mom : XYZ.COM LLC +// https://www.iana.org/domains/root/db/mom.html +mom + +// monash : Monash University +// https://www.iana.org/domains/root/db/monash.html +monash + +// money : Binky Moon, LLC +// https://www.iana.org/domains/root/db/money.html +money + +// monster : XYZ.COM LLC +// https://www.iana.org/domains/root/db/monster.html +monster + +// mormon : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/mormon.html +mormon + +// mortgage : Dog Beach, LLC +// https://www.iana.org/domains/root/db/mortgage.html +mortgage + +// moscow : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/moscow.html +moscow + +// moto : Motorola Trademark Holdings, LLC +// https://www.iana.org/domains/root/db/moto.html +moto + +// motorcycles : XYZ.COM LLC +// https://www.iana.org/domains/root/db/motorcycles.html +motorcycles + +// mov : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/mov.html +mov + +// movie : Binky Moon, LLC +// https://www.iana.org/domains/root/db/movie.html +movie + +// msd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/msd.html +msd + +// mtn : MTN Dubai Limited +// https://www.iana.org/domains/root/db/mtn.html +mtn + +// mtr : MTR Corporation Limited +// https://www.iana.org/domains/root/db/mtr.html +mtr + +// music : DotMusic Limited +// https://www.iana.org/domains/root/db/music.html +music + +// nab : National Australia Bank Limited +// https://www.iana.org/domains/root/db/nab.html +nab + +// nagoya : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/nagoya.html +nagoya + +// navy : Dog Beach, LLC +// https://www.iana.org/domains/root/db/navy.html +navy + +// nba : NBA REGISTRY, LLC +// https://www.iana.org/domains/root/db/nba.html +nba + +// nec : NEC Corporation +// https://www.iana.org/domains/root/db/nec.html +nec + +// netbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/netbank.html +netbank + +// netflix : Netflix, Inc. +// https://www.iana.org/domains/root/db/netflix.html +netflix + +// network : Binky Moon, LLC +// https://www.iana.org/domains/root/db/network.html +network + +// neustar : NeuStar, Inc. +// https://www.iana.org/domains/root/db/neustar.html +neustar + +// new : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/new.html +new + +// news : Dog Beach, LLC +// https://www.iana.org/domains/root/db/news.html +news + +// next : Next plc +// https://www.iana.org/domains/root/db/next.html +next + +// nextdirect : Next plc +// https://www.iana.org/domains/root/db/nextdirect.html +nextdirect + +// nexus : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/nexus.html +nexus + +// nfl : NFL Reg Ops LLC +// https://www.iana.org/domains/root/db/nfl.html +nfl + +// ngo : Public Interest Registry +// https://www.iana.org/domains/root/db/ngo.html +ngo + +// nhk : Japan Broadcasting Corporation (NHK) +// https://www.iana.org/domains/root/db/nhk.html +nhk + +// nico : DWANGO Co., Ltd. +// https://www.iana.org/domains/root/db/nico.html +nico + +// nike : NIKE, Inc. +// https://www.iana.org/domains/root/db/nike.html +nike + +// nikon : NIKON CORPORATION +// https://www.iana.org/domains/root/db/nikon.html +nikon + +// ninja : Dog Beach, LLC +// https://www.iana.org/domains/root/db/ninja.html +ninja + +// nissan : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/nissan.html +nissan + +// nissay : Nippon Life Insurance Company +// https://www.iana.org/domains/root/db/nissay.html +nissay + +// nokia : Nokia Corporation +// https://www.iana.org/domains/root/db/nokia.html +nokia + +// norton : Gen Digital Inc. +// https://www.iana.org/domains/root/db/norton.html +norton + +// now : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/now.html +now + +// nowruz +// https://www.iana.org/domains/root/db/nowruz.html +nowruz + +// nowtv : Starbucks (HK) Limited +// https://www.iana.org/domains/root/db/nowtv.html +nowtv + +// nra : National Rifle Association of America +// https://www.iana.org/domains/root/db/nra.html +nra + +// nrw : Minds + Machines GmbH +// https://www.iana.org/domains/root/db/nrw.html +nrw + +// ntt : NIPPON TELEGRAPH AND TELEPHONE CORPORATION +// https://www.iana.org/domains/root/db/ntt.html +ntt + +// nyc : The City of New York by and through the New York City Department of Information Technology & Telecommunications +// https://www.iana.org/domains/root/db/nyc.html +nyc + +// obi : OBI Group Holding SE & Co. KGaA +// https://www.iana.org/domains/root/db/obi.html +obi + +// observer : Fegistry, LLC +// https://www.iana.org/domains/root/db/observer.html +observer + +// office : Microsoft Corporation +// https://www.iana.org/domains/root/db/office.html +office + +// okinawa : BRregistry, Inc. +// https://www.iana.org/domains/root/db/okinawa.html +okinawa + +// olayan : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayan.html +olayan + +// olayangroup : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayangroup.html +olayangroup + +// ollo : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ollo.html +ollo + +// omega : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/omega.html +omega + +// one : One.com A/S +// https://www.iana.org/domains/root/db/one.html +one + +// ong : Public Interest Registry +// https://www.iana.org/domains/root/db/ong.html +ong + +// onl : Jolly Host, LLC +// https://www.iana.org/domains/root/db/onl.html +onl + +// online : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/online.html +online + +// ooo : INFIBEAM AVENUES LIMITED +// https://www.iana.org/domains/root/db/ooo.html +ooo + +// open : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/open.html +open + +// oracle : Oracle Corporation +// https://www.iana.org/domains/root/db/oracle.html +oracle + +// orange : Orange Brand Services Limited +// https://www.iana.org/domains/root/db/orange.html +orange + +// organic : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/organic.html +organic + +// origins : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/origins.html +origins + +// osaka : Osaka Registry Co., Ltd. +// https://www.iana.org/domains/root/db/osaka.html +osaka + +// otsuka : Otsuka Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/otsuka.html +otsuka + +// ott : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ott.html +ott + +// ovh : MédiaBC +// https://www.iana.org/domains/root/db/ovh.html +ovh + +// page : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/page.html +page + +// panasonic : Panasonic Holdings Corporation +// https://www.iana.org/domains/root/db/panasonic.html +panasonic + +// paris : City of Paris +// https://www.iana.org/domains/root/db/paris.html +paris + +// pars +// https://www.iana.org/domains/root/db/pars.html +pars + +// partners : Binky Moon, LLC +// https://www.iana.org/domains/root/db/partners.html +partners + +// parts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/parts.html +parts + +// party : Blue Sky Registry Limited +// https://www.iana.org/domains/root/db/party.html +party + +// pay : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pay.html +pay + +// pccw : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/pccw.html +pccw + +// pet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pet.html +pet + +// pfizer : Pfizer Inc. +// https://www.iana.org/domains/root/db/pfizer.html +pfizer + +// pharmacy : National Association of Boards of Pharmacy +// https://www.iana.org/domains/root/db/pharmacy.html +pharmacy + +// phd : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/phd.html +phd + +// philips : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/philips.html +philips + +// phone : Dish DBS Corporation +// https://www.iana.org/domains/root/db/phone.html +phone + +// photo : Registry Services, LLC +// https://www.iana.org/domains/root/db/photo.html +photo + +// photography : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photography.html +photography + +// photos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photos.html +photos + +// physio : PhysBiz Pty Ltd +// https://www.iana.org/domains/root/db/physio.html +physio + +// pics : XYZ.COM LLC +// https://www.iana.org/domains/root/db/pics.html +pics + +// pictet : Banque Pictet & Cie SA +// https://www.iana.org/domains/root/db/pictet.html +pictet + +// pictures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pictures.html +pictures + +// pid : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/pid.html +pid + +// pin : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pin.html +pin + +// ping : Ping Registry Provider, Inc. +// https://www.iana.org/domains/root/db/ping.html +ping + +// pink : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pink.html +pink + +// pioneer : Pioneer Corporation +// https://www.iana.org/domains/root/db/pioneer.html +pioneer + +// pizza : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pizza.html +pizza + +// place : Binky Moon, LLC +// https://www.iana.org/domains/root/db/place.html +place + +// play : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/play.html +play + +// playstation : Sony Interactive Entertainment Inc. +// https://www.iana.org/domains/root/db/playstation.html +playstation + +// plumbing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plumbing.html +plumbing + +// plus : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plus.html +plus + +// pnc : PNC Domain Co., LLC +// https://www.iana.org/domains/root/db/pnc.html +pnc + +// pohl : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/pohl.html +pohl + +// poker : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/poker.html +poker + +// politie : Politie Nederland +// https://www.iana.org/domains/root/db/politie.html +politie + +// porn : ICM Registry PN LLC +// https://www.iana.org/domains/root/db/porn.html +porn + +// praxi : Praxi S.p.A. +// https://www.iana.org/domains/root/db/praxi.html +praxi + +// press : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/press.html +press + +// prime : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/prime.html +prime + +// prod : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prod.html +prod + +// productions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/productions.html +productions + +// prof : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prof.html +prof + +// progressive : Progressive Casualty Insurance Company +// https://www.iana.org/domains/root/db/progressive.html +progressive + +// promo : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/promo.html +promo + +// properties : Binky Moon, LLC +// https://www.iana.org/domains/root/db/properties.html +properties + +// property : Digital Property Infrastructure Limited +// https://www.iana.org/domains/root/db/property.html +property + +// protection : XYZ.COM LLC +// https://www.iana.org/domains/root/db/protection.html +protection + +// pru : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/pru.html +pru + +// prudential : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/prudential.html +prudential + +// pub : Dog Beach, LLC +// https://www.iana.org/domains/root/db/pub.html +pub + +// pwc : PricewaterhouseCoopers LLP +// https://www.iana.org/domains/root/db/pwc.html +pwc + +// qpon : dotQPON LLC +// https://www.iana.org/domains/root/db/qpon.html +qpon + +// quebec : PointQuébec Inc +// https://www.iana.org/domains/root/db/quebec.html +quebec + +// quest : XYZ.COM LLC +// https://www.iana.org/domains/root/db/quest.html +quest + +// racing : Premier Registry Limited +// https://www.iana.org/domains/root/db/racing.html +racing + +// radio : Digity, LLC +// https://www.iana.org/domains/root/db/radio.html +radio + +// read : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/read.html +read + +// realestate : dotRealEstate LLC +// https://www.iana.org/domains/root/db/realestate.html +realestate + +// realtor : Real Estate Domains LLC +// https://www.iana.org/domains/root/db/realtor.html +realtor + +// realty : Waterford Limited +// https://www.iana.org/domains/root/db/realty.html +realty + +// recipes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/recipes.html +recipes + +// red : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/red.html +red + +// redumbrella : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/redumbrella.html +redumbrella + +// rehab : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rehab.html +rehab + +// reise : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reise.html +reise + +// reisen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reisen.html +reisen + +// reit : National Association of Real Estate Investment Trusts, Inc. +// https://www.iana.org/domains/root/db/reit.html +reit + +// reliance : Reliance Industries Limited +// https://www.iana.org/domains/root/db/reliance.html +reliance + +// ren : ZDNS International Limited +// https://www.iana.org/domains/root/db/ren.html +ren + +// rent : XYZ.COM LLC +// https://www.iana.org/domains/root/db/rent.html +rent + +// rentals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/rentals.html +rentals + +// repair : Binky Moon, LLC +// https://www.iana.org/domains/root/db/repair.html +repair + +// report : Binky Moon, LLC +// https://www.iana.org/domains/root/db/report.html +report + +// republican : Dog Beach, LLC +// https://www.iana.org/domains/root/db/republican.html +republican + +// rest : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/rest.html +rest + +// restaurant : Binky Moon, LLC +// https://www.iana.org/domains/root/db/restaurant.html +restaurant + +// review : dot Review Limited +// https://www.iana.org/domains/root/db/review.html +review + +// reviews : Dog Beach, LLC +// https://www.iana.org/domains/root/db/reviews.html +reviews + +// rexroth : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/rexroth.html +rexroth + +// rich : iRegistry GmbH +// https://www.iana.org/domains/root/db/rich.html +rich + +// richardli : Pacific Century Asset Management (HK) Limited +// https://www.iana.org/domains/root/db/richardli.html +richardli + +// ricoh : Ricoh Company, Ltd. +// https://www.iana.org/domains/root/db/ricoh.html +ricoh + +// ril : Reliance Industries Limited +// https://www.iana.org/domains/root/db/ril.html +ril + +// rio : Empresa Municipal de Informática SA - IPLANRIO +// https://www.iana.org/domains/root/db/rio.html +rio + +// rip : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rip.html +rip + +// rocks : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rocks.html +rocks + +// rodeo : Registry Services, LLC +// https://www.iana.org/domains/root/db/rodeo.html +rodeo + +// rogers : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/rogers.html +rogers + +// room : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/room.html +room + +// rsvp : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/rsvp.html +rsvp + +// rugby : World Rugby Strategic Developments Limited +// https://www.iana.org/domains/root/db/rugby.html +rugby + +// ruhr : dotSaarland GmbH +// https://www.iana.org/domains/root/db/ruhr.html +ruhr + +// run : Binky Moon, LLC +// https://www.iana.org/domains/root/db/run.html +run + +// rwe : RWE AG +// https://www.iana.org/domains/root/db/rwe.html +rwe + +// ryukyu : BRregistry, Inc. +// https://www.iana.org/domains/root/db/ryukyu.html +ryukyu + +// saarland : dotSaarland GmbH +// https://www.iana.org/domains/root/db/saarland.html +saarland + +// safe : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/safe.html +safe + +// safety : Jolly Host, LLC +// https://www.iana.org/domains/root/db/safety.html +safety + +// sakura : SAKURA Internet Inc. +// https://www.iana.org/domains/root/db/sakura.html +sakura + +// sale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/sale.html +sale + +// salon : Binky Moon, LLC +// https://www.iana.org/domains/root/db/salon.html +salon + +// samsclub : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/samsclub.html +samsclub + +// samsung : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/samsung.html +samsung + +// sandvik : Sandvik AB +// https://www.iana.org/domains/root/db/sandvik.html +sandvik + +// sandvikcoromant : Sandvik AB +// https://www.iana.org/domains/root/db/sandvikcoromant.html +sandvikcoromant + +// sanofi : Sanofi +// https://www.iana.org/domains/root/db/sanofi.html +sanofi + +// sap : SAP AG +// https://www.iana.org/domains/root/db/sap.html +sap + +// sarl : Binky Moon, LLC +// https://www.iana.org/domains/root/db/sarl.html +sarl + +// sas : Research IP LLC +// https://www.iana.org/domains/root/db/sas.html +sas + +// save : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/save.html +save + +// saxo : Saxo Bank A/S +// https://www.iana.org/domains/root/db/saxo.html +saxo + +// sbi : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/sbi.html +sbi + +// sbs : ShortDot SA +// https://www.iana.org/domains/root/db/sbs.html +sbs + +// scb : The Siam Commercial Bank Public Company Limited ("SCB") +// https://www.iana.org/domains/root/db/scb.html +scb + +// schaeffler : Schaeffler Technologies AG & Co. KG +// https://www.iana.org/domains/root/db/schaeffler.html +schaeffler + +// schmidt : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/schmidt.html +schmidt + +// scholarships : Scholarships.com, LLC +// https://www.iana.org/domains/root/db/scholarships.html +scholarships + +// school : Binky Moon, LLC +// https://www.iana.org/domains/root/db/school.html +school + +// schule : Binky Moon, LLC +// https://www.iana.org/domains/root/db/schule.html +schule + +// schwarz : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/schwarz.html +schwarz + +// science : dot Science Limited +// https://www.iana.org/domains/root/db/science.html +science + +// scot : Dot Scot Registry Limited +// https://www.iana.org/domains/root/db/scot.html +scot + +// search : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/search.html +search + +// seat : SEAT, S.A. (Sociedad Unipersonal) +// https://www.iana.org/domains/root/db/seat.html +seat + +// secure : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/secure.html +secure + +// security : XYZ.COM LLC +// https://www.iana.org/domains/root/db/security.html +security + +// seek : Seek Limited +// https://www.iana.org/domains/root/db/seek.html +seek + +// select : Registry Services, LLC +// https://www.iana.org/domains/root/db/select.html +select + +// sener : Sener Ingeniería y Sistemas, S.A. +// https://www.iana.org/domains/root/db/sener.html +sener + +// services : Binky Moon, LLC +// https://www.iana.org/domains/root/db/services.html +services + +// seven : Seven West Media Ltd +// https://www.iana.org/domains/root/db/seven.html +seven + +// sew : SEW-EURODRIVE GmbH & Co KG +// https://www.iana.org/domains/root/db/sew.html +sew + +// sex : ICM Registry SX LLC +// https://www.iana.org/domains/root/db/sex.html +sex + +// sexy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/sexy.html +sexy + +// sfr : Societe Francaise du Radiotelephone - SFR +// https://www.iana.org/domains/root/db/sfr.html +sfr + +// shangrila : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/shangrila.html +shangrila + +// sharp : Sharp Corporation +// https://www.iana.org/domains/root/db/sharp.html +sharp + +// shell : Shell Information Technology International Inc +// https://www.iana.org/domains/root/db/shell.html +shell + +// shia +// https://www.iana.org/domains/root/db/shia.html +shia + +// shiksha : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/shiksha.html +shiksha + +// shoes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shoes.html +shoes + +// shop : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/shop.html +shop + +// shopping : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shopping.html +shopping + +// shouji : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/shouji.html +shouji + +// show : Binky Moon, LLC +// https://www.iana.org/domains/root/db/show.html +show + +// silk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/silk.html +silk + +// sina : Sina Corporation +// https://www.iana.org/domains/root/db/sina.html +sina + +// singles : Binky Moon, LLC +// https://www.iana.org/domains/root/db/singles.html +singles + +// site : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/site.html +site + +// ski : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/ski.html +ski + +// skin : XYZ.COM LLC +// https://www.iana.org/domains/root/db/skin.html +skin + +// sky : Sky UK Limited +// https://www.iana.org/domains/root/db/sky.html +sky + +// skype : Microsoft Corporation +// https://www.iana.org/domains/root/db/skype.html +skype + +// sling : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/sling.html +sling + +// smart : Smart Communications, Inc. (SMART) +// https://www.iana.org/domains/root/db/smart.html +smart + +// smile : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/smile.html +smile + +// sncf : Société Nationale SNCF +// https://www.iana.org/domains/root/db/sncf.html +sncf + +// soccer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/soccer.html +soccer + +// social : Dog Beach, LLC +// https://www.iana.org/domains/root/db/social.html +social + +// softbank : SoftBank Group Corp. +// https://www.iana.org/domains/root/db/softbank.html +softbank + +// software : Dog Beach, LLC +// https://www.iana.org/domains/root/db/software.html +software + +// sohu : Sohu.com Limited +// https://www.iana.org/domains/root/db/sohu.html +sohu + +// solar : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solar.html +solar + +// solutions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solutions.html +solutions + +// song : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/song.html +song + +// sony : Sony Group Corporation +// https://www.iana.org/domains/root/db/sony.html +sony + +// soy : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/soy.html +soy + +// spa : Asia Spa and Wellness Promotion Council Limited +// https://www.iana.org/domains/root/db/spa.html +spa + +// space : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/space.html +space + +// sport : SportAccord +// https://www.iana.org/domains/root/db/sport.html +sport + +// spot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/spot.html +spot + +// srl : InterNetX, Corp +// https://www.iana.org/domains/root/db/srl.html +srl + +// stada : STADA Arzneimittel AG +// https://www.iana.org/domains/root/db/stada.html +stada + +// staples : Staples, Inc. +// https://www.iana.org/domains/root/db/staples.html +staples + +// star : Star India Private Limited +// https://www.iana.org/domains/root/db/star.html +star + +// statebank : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/statebank.html +statebank + +// statefarm : State Farm Mutual Automobile Insurance Company +// https://www.iana.org/domains/root/db/statefarm.html +statefarm + +// stc : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stc.html +stc + +// stcgroup : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stcgroup.html +stcgroup + +// stockholm : Stockholms kommun +// https://www.iana.org/domains/root/db/stockholm.html +stockholm + +// storage : XYZ.COM LLC +// https://www.iana.org/domains/root/db/storage.html +storage + +// store : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/store.html +store + +// stream : dot Stream Limited +// https://www.iana.org/domains/root/db/stream.html +stream + +// studio : Dog Beach, LLC +// https://www.iana.org/domains/root/db/studio.html +studio + +// study : Registry Services, LLC +// https://www.iana.org/domains/root/db/study.html +study + +// style : Binky Moon, LLC +// https://www.iana.org/domains/root/db/style.html +style + +// sucks : Vox Populi Registry Ltd. +// https://www.iana.org/domains/root/db/sucks.html +sucks + +// supplies : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supplies.html +supplies + +// supply : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supply.html +supply + +// support : Binky Moon, LLC +// https://www.iana.org/domains/root/db/support.html +support + +// surf : Registry Services, LLC +// https://www.iana.org/domains/root/db/surf.html +surf + +// surgery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/surgery.html +surgery + +// suzuki : SUZUKI MOTOR CORPORATION +// https://www.iana.org/domains/root/db/suzuki.html +suzuki + +// swatch : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/swatch.html +swatch + +// swiss : Swiss Confederation +// https://www.iana.org/domains/root/db/swiss.html +swiss + +// sydney : State of New South Wales, Department of Premier and Cabinet +// https://www.iana.org/domains/root/db/sydney.html +sydney + +// systems : Binky Moon, LLC +// https://www.iana.org/domains/root/db/systems.html +systems + +// tab : Tabcorp Holdings Limited +// https://www.iana.org/domains/root/db/tab.html +tab + +// taipei : Taipei City Government +// https://www.iana.org/domains/root/db/taipei.html +taipei + +// talk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/talk.html +talk + +// taobao : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/taobao.html +taobao + +// target : Target Domain Holdings, LLC +// https://www.iana.org/domains/root/db/target.html +target + +// tatamotors : Tata Motors Ltd +// https://www.iana.org/domains/root/db/tatamotors.html +tatamotors + +// tatar : Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" +// https://www.iana.org/domains/root/db/tatar.html +tatar + +// tattoo : Registry Services, LLC +// https://www.iana.org/domains/root/db/tattoo.html +tattoo + +// tax : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tax.html +tax + +// taxi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/taxi.html +taxi + +// tci +// https://www.iana.org/domains/root/db/tci.html +tci + +// tdk : TDK Corporation +// https://www.iana.org/domains/root/db/tdk.html +tdk + +// team : Binky Moon, LLC +// https://www.iana.org/domains/root/db/team.html +team + +// tech : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/tech.html +tech + +// technology : Binky Moon, LLC +// https://www.iana.org/domains/root/db/technology.html +technology + +// temasek : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/temasek.html +temasek + +// tennis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tennis.html +tennis + +// teva : Teva Pharmaceutical Industries Limited +// https://www.iana.org/domains/root/db/teva.html +teva + +// thd : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/thd.html +thd + +// theater : Binky Moon, LLC +// https://www.iana.org/domains/root/db/theater.html +theater + +// theatre : XYZ.COM LLC +// https://www.iana.org/domains/root/db/theatre.html +theatre + +// tiaa : Teachers Insurance and Annuity Association of America +// https://www.iana.org/domains/root/db/tiaa.html +tiaa + +// tickets : XYZ.COM LLC +// https://www.iana.org/domains/root/db/tickets.html +tickets + +// tienda : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tienda.html +tienda + +// tips : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tips.html +tips + +// tires : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tires.html +tires + +// tirol : punkt Tirol GmbH +// https://www.iana.org/domains/root/db/tirol.html +tirol + +// tjmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjmaxx.html +tjmaxx + +// tjx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjx.html +tjx + +// tkmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tkmaxx.html +tkmaxx + +// tmall : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/tmall.html +tmall + +// today : Binky Moon, LLC +// https://www.iana.org/domains/root/db/today.html +today + +// tokyo : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/tokyo.html +tokyo + +// tools : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tools.html +tools + +// top : Hong Kong Zhongze International Limited +// https://www.iana.org/domains/root/db/top.html +top + +// toray : Toray Industries, Inc. +// https://www.iana.org/domains/root/db/toray.html +toray + +// toshiba : TOSHIBA Corporation +// https://www.iana.org/domains/root/db/toshiba.html +toshiba + +// total : TotalEnergies SE +// https://www.iana.org/domains/root/db/total.html +total + +// tours : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tours.html +tours + +// town : Binky Moon, LLC +// https://www.iana.org/domains/root/db/town.html +town + +// toyota : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/toyota.html +toyota + +// toys : Binky Moon, LLC +// https://www.iana.org/domains/root/db/toys.html +toys + +// trade : Elite Registry Limited +// https://www.iana.org/domains/root/db/trade.html +trade + +// trading : Dog Beach, LLC +// https://www.iana.org/domains/root/db/trading.html +trading + +// training : Binky Moon, LLC +// https://www.iana.org/domains/root/db/training.html +training + +// travel : Dog Beach, LLC +// https://www.iana.org/domains/root/db/travel.html +travel + +// travelers : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelers.html +travelers + +// travelersinsurance : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelersinsurance.html +travelersinsurance + +// trust : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/trust.html +trust + +// trv : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/trv.html +trv + +// tube : Latin American Telecom LLC +// https://www.iana.org/domains/root/db/tube.html +tube + +// tui : TUI AG +// https://www.iana.org/domains/root/db/tui.html +tui + +// tunes : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tunes.html +tunes + +// tushu : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tushu.html +tushu + +// tvs : T V SUNDRAM IYENGAR & SONS LIMITED +// https://www.iana.org/domains/root/db/tvs.html +tvs + +// ubank : National Australia Bank Limited +// https://www.iana.org/domains/root/db/ubank.html +ubank + +// ubs : UBS AG +// https://www.iana.org/domains/root/db/ubs.html +ubs + +// unicom : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/unicom.html +unicom + +// university : Binky Moon, LLC +// https://www.iana.org/domains/root/db/university.html +university + +// uno : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/uno.html +uno + +// uol : UBN INTERNET LTDA. +// https://www.iana.org/domains/root/db/uol.html +uol + +// ups : UPS Market Driver, Inc. +// https://www.iana.org/domains/root/db/ups.html +ups + +// vacations : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vacations.html +vacations + +// vana : D3 Registry LLC +// https://www.iana.org/domains/root/db/vana.html +vana + +// vanguard : The Vanguard Group, Inc. +// https://www.iana.org/domains/root/db/vanguard.html +vanguard + +// vegas : Dot Vegas, Inc. +// https://www.iana.org/domains/root/db/vegas.html +vegas + +// ventures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ventures.html +ventures + +// verisign : VeriSign, Inc. +// https://www.iana.org/domains/root/db/verisign.html +verisign + +// versicherung : tldbox GmbH +// https://www.iana.org/domains/root/db/versicherung.html +versicherung + +// vet : Dog Beach, LLC +// https://www.iana.org/domains/root/db/vet.html +vet + +// viajes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/viajes.html +viajes + +// video : Dog Beach, LLC +// https://www.iana.org/domains/root/db/video.html +video + +// vig : VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe +// https://www.iana.org/domains/root/db/vig.html +vig + +// viking : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/viking.html +viking + +// villas : Binky Moon, LLC +// https://www.iana.org/domains/root/db/villas.html +villas + +// vin : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vin.html +vin + +// vip : Registry Services, LLC +// https://www.iana.org/domains/root/db/vip.html +vip + +// virgin : Virgin Enterprises Limited +// https://www.iana.org/domains/root/db/virgin.html +virgin + +// visa : Visa Worldwide Pte. Limited +// https://www.iana.org/domains/root/db/visa.html +visa + +// vision : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vision.html +vision + +// viva : Saudi Telecom Company +// https://www.iana.org/domains/root/db/viva.html +viva + +// vivo : Telefonica Brasil S.A. +// https://www.iana.org/domains/root/db/vivo.html +vivo + +// vlaanderen : DNS.be vzw +// https://www.iana.org/domains/root/db/vlaanderen.html +vlaanderen + +// vodka : Registry Services, LLC +// https://www.iana.org/domains/root/db/vodka.html +vodka + +// volvo : Volvo Holding Sverige Aktiebolag +// https://www.iana.org/domains/root/db/volvo.html +volvo + +// vote : Monolith Registry LLC +// https://www.iana.org/domains/root/db/vote.html +vote + +// voting : Valuetainment Corp. +// https://www.iana.org/domains/root/db/voting.html +voting + +// voto : Monolith Registry LLC +// https://www.iana.org/domains/root/db/voto.html +voto + +// voyage : Binky Moon, LLC +// https://www.iana.org/domains/root/db/voyage.html +voyage + +// wales : Nominet UK +// https://www.iana.org/domains/root/db/wales.html +wales + +// walmart : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/walmart.html +walmart + +// walter : Sandvik AB +// https://www.iana.org/domains/root/db/walter.html +walter + +// wang : Zodiac Wang Limited +// https://www.iana.org/domains/root/db/wang.html +wang + +// wanggou : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wanggou.html +wanggou + +// watch : Binky Moon, LLC +// https://www.iana.org/domains/root/db/watch.html +watch + +// watches : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/watches.html +watches + +// weather : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weather.html +weather + +// weatherchannel : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weatherchannel.html +weatherchannel + +// web : VeriSign, Inc. +// https://www.iana.org/domains/root/db/web.html +web + +// webcam : dot Webcam Limited +// https://www.iana.org/domains/root/db/webcam.html +webcam + +// weber : Saint-Gobain Weber SA +// https://www.iana.org/domains/root/db/weber.html +weber + +// website : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/website.html +website + +// wed +// https://www.iana.org/domains/root/db/wed.html +wed + +// wedding : Registry Services, LLC +// https://www.iana.org/domains/root/db/wedding.html +wedding + +// weibo : Sina Corporation +// https://www.iana.org/domains/root/db/weibo.html +weibo + +// weir : Weir Group IP Limited +// https://www.iana.org/domains/root/db/weir.html +weir + +// whoswho : Who's Who Registry +// https://www.iana.org/domains/root/db/whoswho.html +whoswho + +// wien : domainworx Service & Management GmbH +// https://www.iana.org/domains/root/db/wien.html +wien + +// wiki : Registry Services, LLC +// https://www.iana.org/domains/root/db/wiki.html +wiki + +// williamhill : William Hill Organization Limited +// https://www.iana.org/domains/root/db/williamhill.html +williamhill + +// win : First Registry Limited +// https://www.iana.org/domains/root/db/win.html +win + +// windows : Microsoft Corporation +// https://www.iana.org/domains/root/db/windows.html +windows + +// wine : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wine.html +wine + +// winners : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/winners.html +winners + +// wme : William Morris Endeavor Entertainment, LLC +// https://www.iana.org/domains/root/db/wme.html +wme + +// woodside : Woodside Petroleum Limited +// https://www.iana.org/domains/root/db/woodside.html +woodside + +// work : Registry Services, LLC +// https://www.iana.org/domains/root/db/work.html +work + +// works : Binky Moon, LLC +// https://www.iana.org/domains/root/db/works.html +works + +// world : Binky Moon, LLC +// https://www.iana.org/domains/root/db/world.html +world + +// wow : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wow.html +wow + +// wtc : World Trade Centers Association, Inc. +// https://www.iana.org/domains/root/db/wtc.html +wtc + +// wtf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wtf.html +wtf + +// xbox : Microsoft Corporation +// https://www.iana.org/domains/root/db/xbox.html +xbox + +// xerox : Xerox DNHC LLC +// https://www.iana.org/domains/root/db/xerox.html +xerox + +// xihuan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/xihuan.html +xihuan + +// xin : Elegant Leader Limited +// https://www.iana.org/domains/root/db/xin.html +xin + +// xn--11b4c3d : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--11b4c3d.html +कॉम + +// xn--1ck2e1b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--1ck2e1b.html +セール + +// xn--1qqw23a : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--1qqw23a.html +佛山 + +// xn--30rr7y : Excellent First Limited +// https://www.iana.org/domains/root/db/xn--30rr7y.html +慈善 + +// xn--3bst00m : Eagle Horizon Limited +// https://www.iana.org/domains/root/db/xn--3bst00m.html +集团 + +// xn--3ds443g : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--3ds443g.html +在线 + +// xn--3pxu8k : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--3pxu8k.html +点看 + +// xn--42c2d9a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--42c2d9a.html +คอม + +// xn--45q11c : Zodiac Gemini Ltd +// https://www.iana.org/domains/root/db/xn--45q11c.html +八卦 + +// xn--4gbrim : Helium TLDs Ltd +// https://www.iana.org/domains/root/db/xn--4gbrim.html +موقع + +// xn--55qw42g : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--55qw42g.html +公益 + +// xn--55qx5d : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--55qx5d.html +公司 + +// xn--5su34j936bgsg : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/xn--5su34j936bgsg.html +香格里拉 + +// xn--5tzm5g : Jolly Host, LLC +// https://www.iana.org/domains/root/db/xn--5tzm5g.html +网站 + +// xn--6frz82g : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/xn--6frz82g.html +移动 + +// xn--6qq986b3xl : Tycoon Treasure Limited +// https://www.iana.org/domains/root/db/xn--6qq986b3xl.html +我爱你 + +// xn--80adxhks : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/xn--80adxhks.html +москва + +// xn--80aqecdr1a : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--80aqecdr1a.html +католик + +// xn--80asehdb : CORE Association +// https://www.iana.org/domains/root/db/xn--80asehdb.html +онлайн + +// xn--80aswg : CORE Association +// https://www.iana.org/domains/root/db/xn--80aswg.html +сайт + +// xn--8y0a063a : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/xn--8y0a063a.html +联通 + +// xn--9dbq2a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--9dbq2a.html +קום + +// xn--9et52u : RISE VICTORY LIMITED +// https://www.iana.org/domains/root/db/xn--9et52u.html +时尚 + +// xn--9krt00a : Sina Corporation +// https://www.iana.org/domains/root/db/xn--9krt00a.html +微博 + +// xn--b4w605ferd : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/xn--b4w605ferd.html +淡马锡 + +// xn--bck1b9a5dre4c : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--bck1b9a5dre4c.html +ファッション + +// xn--c1avg : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--c1avg.html +орг + +// xn--c2br7g : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--c2br7g.html +नेट + +// xn--cck2b3b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cck2b3b.html +ストア + +// xn--cckwcxetd : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cckwcxetd.html +アマゾン + +// xn--cg4bki : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/xn--cg4bki.html +삼성 + +// xn--czr694b : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--czr694b.html +商标 + +// xn--czrs0t : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--czrs0t.html +商店 + +// xn--czru2d : Zodiac Aquarius Limited +// https://www.iana.org/domains/root/db/xn--czru2d.html +商城 + +// xn--d1acj3b : The Foundation for Network Initiatives “The Smart Internet” +// https://www.iana.org/domains/root/db/xn--d1acj3b.html +дети + +// xn--eckvdtc9d : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--eckvdtc9d.html +ポイント + +// xn--efvy88h : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--efvy88h.html +新闻 + +// xn--fct429k : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--fct429k.html +家電 + +// xn--fhbei : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--fhbei.html +كوم + +// xn--fiq228c5hs : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--fiq228c5hs.html +中文网 + +// xn--fiq64b : CITIC Group Corporation +// https://www.iana.org/domains/root/db/xn--fiq64b.html +中信 + +// xn--fjq720a : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--fjq720a.html +娱乐 + +// xn--flw351e : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--flw351e.html +谷歌 + +// xn--fzys8d69uvgm : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/xn--fzys8d69uvgm.html +電訊盈科 + +// xn--g2xx48c : Nawang Heli(Xiamen) Network Service Co., LTD. +// https://www.iana.org/domains/root/db/xn--g2xx48c.html +购物 + +// xn--gckr3f0f : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gckr3f0f.html +クラウド + +// xn--gk3at1e : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gk3at1e.html +通販 + +// xn--hxt814e : Zodiac Taurus Limited +// https://www.iana.org/domains/root/db/xn--hxt814e.html +网店 + +// xn--i1b6b1a6a2e : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--i1b6b1a6a2e.html +संगठन + +// xn--imr513n : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--imr513n.html +餐厅 + +// xn--io0a7i : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--io0a7i.html +网络 + +// xn--j1aef : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--j1aef.html +ком + +// xn--jlq480n2rg : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jlq480n2rg.html +亚马逊 + +// xn--jvr189m : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jvr189m.html +食品 + +// xn--kcrx77d1x4a : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/xn--kcrx77d1x4a.html +飞利浦 + +// xn--kput3i : Beijing RITT-Net Technology Development Co., Ltd +// https://www.iana.org/domains/root/db/xn--kput3i.html +手机 + +// xn--mgba3a3ejt : Aramco Services Company +// https://www.iana.org/domains/root/db/xn--mgba3a3ejt.html +ارامكو + +// xn--mgba7c0bbn0a : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/xn--mgba7c0bbn0a.html +العليان + +// xn--mgbab2bd : CORE Association +// https://www.iana.org/domains/root/db/xn--mgbab2bd.html +بازار + +// xn--mgbca7dzdo : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/xn--mgbca7dzdo.html +ابوظبي + +// xn--mgbi4ecexp : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--mgbi4ecexp.html +كاثوليك + +// xn--mgbt3dhd +// https://www.iana.org/domains/root/db/xn--mgbt3dhd.html +همراه + +// xn--mk1bu44c : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--mk1bu44c.html +닷컴 + +// xn--mxtq1m : Net-Chinese Co., Ltd. +// https://www.iana.org/domains/root/db/xn--mxtq1m.html +政府 + +// xn--ngbc5azd : International Domain Registry Pty. Ltd. +// https://www.iana.org/domains/root/db/xn--ngbc5azd.html +شبكة + +// xn--ngbe9e0a : Kuwait Finance House +// https://www.iana.org/domains/root/db/xn--ngbe9e0a.html +بيتك + +// xn--ngbrx : League of Arab States +// https://www.iana.org/domains/root/db/xn--ngbrx.html +عرب + +// xn--nqv7f : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7f.html +机构 + +// xn--nqv7fs00ema : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7fs00ema.html +组织机构 + +// xn--nyqy26a : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--nyqy26a.html +健康 + +// xn--otu796d : Jiang Yu Liang Cai Technology Company Limited +// https://www.iana.org/domains/root/db/xn--otu796d.html +招聘 + +// xn--p1acf : Rusnames Limited +// https://www.iana.org/domains/root/db/xn--p1acf.html +рус + +// xn--pssy2u : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--pssy2u.html +大拿 + +// xn--q9jyb4c : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--q9jyb4c.html +みんな + +// xn--qcka1pmc : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--qcka1pmc.html +グーグル + +// xn--rhqv96g : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--rhqv96g.html +世界 + +// xn--rovu88b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--rovu88b.html +書籍 + +// xn--ses554g : KNET Co., Ltd. +// https://www.iana.org/domains/root/db/xn--ses554g.html +网址 + +// xn--t60b56a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--t60b56a.html +닷넷 + +// xn--tckwe : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--tckwe.html +コム + +// xn--tiq49xqyj : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--tiq49xqyj.html +天主教 + +// xn--unup4y : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--unup4y.html +游戏 + +// xn--vermgensberater-ctb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberater-ctb.html +vermögensberater + +// xn--vermgensberatung-pwb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberatung-pwb.html +vermögensberatung + +// xn--vhquv : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--vhquv.html +企业 + +// xn--vuq861b : Beijing Tele-info Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--vuq861b.html +信息 + +// xn--w4r85el8fhu5dnra : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4r85el8fhu5dnra.html +嘉里大酒店 + +// xn--w4rs40l : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4rs40l.html +嘉里 + +// xn--xhq521b : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--xhq521b.html +广东 + +// xn--zfr164b : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--zfr164b.html +政务 + +// xyz : XYZ.COM LLC +// https://www.iana.org/domains/root/db/xyz.html +xyz + +// yachts : XYZ.COM LLC +// https://www.iana.org/domains/root/db/yachts.html +yachts + +// yahoo : Yahoo Inc. +// https://www.iana.org/domains/root/db/yahoo.html +yahoo + +// yamaxun : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/yamaxun.html +yamaxun + +// yandex : YANDEX, LLC +// https://www.iana.org/domains/root/db/yandex.html +yandex + +// yodobashi : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/yodobashi.html +yodobashi + +// yoga : Registry Services, LLC +// https://www.iana.org/domains/root/db/yoga.html +yoga + +// yokohama : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/yokohama.html +yokohama + +// you : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/you.html +you + +// youtube : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/youtube.html +youtube + +// yun : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/yun.html +yun + +// zappos : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zappos.html +zappos + +// zara : Industria de Diseño Textil, S.A. (INDITEX, S.A.) +// https://www.iana.org/domains/root/db/zara.html +zara + +// zero : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zero.html +zero + +// zip : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/zip.html +zip + +// zone : Binky Moon, LLC +// https://www.iana.org/domains/root/db/zone.html +zone + +// zuerich : Kanton Zürich (Canton of Zurich) +// https://www.iana.org/domains/root/db/zuerich.html +zuerich + +// ===END ICANN DOMAINS=== + +// ===BEGIN PRIVATE DOMAINS=== + +// (Note: these are in alphabetical order by company name) + +// .KRD : https://nic.krd +co.krd +edu.krd + +// .pl domains (grandfathered) +art.pl +gliwice.pl +krakow.pl +poznan.pl +wroc.pl +zakopane.pl + +// 1GB LLC : https://www.1gb.ua/ +// Submitted by 1GB LLC +cc.ua +inf.ua +ltd.ua + +// 611 blockchain domain name system : https://sixone.one/ +611.to + +// A2 Hosting +// Submitted by Tyler Hall +a2hosted.com +cpserver.com + +// ActiveTrail : https://www.activetrail.biz/ +// Submitted by Ofer Kalaora +activetrail.biz + +// addr.tools : https://addr.tools/ +// Submitted by Brian Shea +myaddr.dev +myaddr.io +dyn.addr.tools +myaddr.tools + +// Adobe : https://www.adobe.com/ +// Submitted by Ian Boston and Lars Trieloff +adobeaemcloud.com +*.dev.adobeaemcloud.com +aem.live +hlx.live +adobeaemcloud.net +aem.network +aem.page +hlx.page +aem.reviews + +// Adobe Developer Platform : https://developer.adobe.com +// Submitted by Jesse MacFadyen +adobeio-static.net +adobeioruntime.net + +// Africa.com Web Solutions Ltd : https://registry.africa.com +// Submitted by Gavin Brown +africa.com + +// AgentbaseAI Inc. : https://assistant-ui.com +// Submitted by Simon Farshid +*.auiusercontent.com + +// Agnat sp. z o.o. : https://domena.pl +// Submitted by Przemyslaw Plewa +beep.pl + +// Aiven : https://aiven.io/ +// Submitted by Aiven Security Team +aiven.app +*.aivencloud.com + +// Akamai : https://www.akamai.com/ +// Submitted by Akamai Team +akadns.net +akamai.net +akamai-staging.net +akamaiedge.net +akamaiedge-staging.net +akamaihd.net +akamaihd-staging.net +akamaiorigin.net +akamaiorigin-staging.net +akamaized.net +akamaized-staging.net +edgekey.net +edgekey-staging.net +edgesuite.net +edgesuite-staging.net + +// alboto.ca : http://alboto.ca +// Submitted by Anton Avramov +barsy.ca + +// Alces Software Ltd : http://alces-software.com +// Submitted by Mark J. Titorenko +*.compute.estate +*.alces.network + +// Alibaba Cloud API Gateway +// Submitted by Alibaba Cloud Security +alibabacloudcs.com +ms.fun +ms.show + +// all-inkl.com : https://all-inkl.com +// Submitted by Werner Kaltofen +kasserver.com + +// Altervista : https://www.altervista.org +// Submitted by Carlo Cannas +altervista.org + +// alwaysdata : https://www.alwaysdata.com +// Submitted by Cyril +alwaysdata.net + +// Amaze Software : https://amaze.co +// Submitted by Domain Admin +myamaze.net + +// Amazon : https://www.amazon.com/ +// Submitted by AWS Security +// Subsections of Amazon/subsidiaries will appear until "concludes" tag + +// Amazon API Gateway +// Submitted by AWS Security +// Reference: 6a4f5a95-8c7d-4077-a7af-9cf1abec0a53 +execute-api.cn-north-1.amazonaws.com.cn +execute-api.cn-northwest-1.amazonaws.com.cn +execute-api.af-south-1.amazonaws.com +execute-api.ap-east-1.amazonaws.com +execute-api.ap-northeast-1.amazonaws.com +execute-api.ap-northeast-2.amazonaws.com +execute-api.ap-northeast-3.amazonaws.com +execute-api.ap-south-1.amazonaws.com +execute-api.ap-south-2.amazonaws.com +execute-api.ap-southeast-1.amazonaws.com +execute-api.ap-southeast-2.amazonaws.com +execute-api.ap-southeast-3.amazonaws.com +execute-api.ap-southeast-4.amazonaws.com +execute-api.ap-southeast-5.amazonaws.com +execute-api.ca-central-1.amazonaws.com +execute-api.ca-west-1.amazonaws.com +execute-api.eu-central-1.amazonaws.com +execute-api.eu-central-2.amazonaws.com +execute-api.eu-north-1.amazonaws.com +execute-api.eu-south-1.amazonaws.com +execute-api.eu-south-2.amazonaws.com +execute-api.eu-west-1.amazonaws.com +execute-api.eu-west-2.amazonaws.com +execute-api.eu-west-3.amazonaws.com +execute-api.il-central-1.amazonaws.com +execute-api.me-central-1.amazonaws.com +execute-api.me-south-1.amazonaws.com +execute-api.sa-east-1.amazonaws.com +execute-api.us-east-1.amazonaws.com +execute-api.us-east-2.amazonaws.com +execute-api.us-gov-east-1.amazonaws.com +execute-api.us-gov-west-1.amazonaws.com +execute-api.us-west-1.amazonaws.com +execute-api.us-west-2.amazonaws.com + +// Amazon CloudFront +// Submitted by Donavan Miller +// Reference: 54144616-fd49-4435-8535-19c6a601bdb3 +cloudfront.net + +// Amazon Cognito +// Submitted by AWS Security +// Reference: d7d4a954-976e-403e-a010-de9ed0cfbbd1 +auth.af-south-1.amazoncognito.com +auth.ap-east-1.amazoncognito.com +auth.ap-northeast-1.amazoncognito.com +auth.ap-northeast-2.amazoncognito.com +auth.ap-northeast-3.amazoncognito.com +auth.ap-south-1.amazoncognito.com +auth.ap-south-2.amazoncognito.com +auth.ap-southeast-1.amazoncognito.com +auth.ap-southeast-2.amazoncognito.com +auth.ap-southeast-3.amazoncognito.com +auth.ap-southeast-4.amazoncognito.com +auth.ap-southeast-5.amazoncognito.com +auth.ap-southeast-7.amazoncognito.com +auth.ca-central-1.amazoncognito.com +auth.ca-west-1.amazoncognito.com +auth.eu-central-1.amazoncognito.com +auth.eu-central-2.amazoncognito.com +auth.eu-north-1.amazoncognito.com +auth.eu-south-1.amazoncognito.com +auth.eu-south-2.amazoncognito.com +auth.eu-west-1.amazoncognito.com +auth.eu-west-2.amazoncognito.com +auth.eu-west-3.amazoncognito.com +auth.il-central-1.amazoncognito.com +auth.me-central-1.amazoncognito.com +auth.me-south-1.amazoncognito.com +auth.mx-central-1.amazoncognito.com +auth.sa-east-1.amazoncognito.com +auth.us-east-1.amazoncognito.com +auth-fips.us-east-1.amazoncognito.com +auth.us-east-2.amazoncognito.com +auth-fips.us-east-2.amazoncognito.com +auth-fips.us-gov-east-1.amazoncognito.com +auth-fips.us-gov-west-1.amazoncognito.com +auth.us-west-1.amazoncognito.com +auth-fips.us-west-1.amazoncognito.com +auth.us-west-2.amazoncognito.com +auth-fips.us-west-2.amazoncognito.com +auth.cognito-idp.eusc-de-east-1.on.amazonwebservices.eu + +// Amazon EC2 +// Submitted by Luke Wells +// Reference: 4c38fa71-58ac-4768-99e5-689c1767e537 +*.compute.amazonaws.com.cn +*.compute.amazonaws.com +*.compute-1.amazonaws.com +us-east-1.amazonaws.com + +// Amazon EMR +// Submitted by AWS Security +// Reference: 82f43f9f-bbb8-400e-8349-854f5a62f20d +emrappui-prod.cn-north-1.amazonaws.com.cn +emrnotebooks-prod.cn-north-1.amazonaws.com.cn +emrstudio-prod.cn-north-1.amazonaws.com.cn +emrappui-prod.cn-northwest-1.amazonaws.com.cn +emrnotebooks-prod.cn-northwest-1.amazonaws.com.cn +emrstudio-prod.cn-northwest-1.amazonaws.com.cn +emrappui-prod.af-south-1.amazonaws.com +emrnotebooks-prod.af-south-1.amazonaws.com +emrstudio-prod.af-south-1.amazonaws.com +emrappui-prod.ap-east-1.amazonaws.com +emrnotebooks-prod.ap-east-1.amazonaws.com +emrstudio-prod.ap-east-1.amazonaws.com +emrappui-prod.ap-northeast-1.amazonaws.com +emrnotebooks-prod.ap-northeast-1.amazonaws.com +emrstudio-prod.ap-northeast-1.amazonaws.com +emrappui-prod.ap-northeast-2.amazonaws.com +emrnotebooks-prod.ap-northeast-2.amazonaws.com +emrstudio-prod.ap-northeast-2.amazonaws.com +emrappui-prod.ap-northeast-3.amazonaws.com +emrnotebooks-prod.ap-northeast-3.amazonaws.com +emrstudio-prod.ap-northeast-3.amazonaws.com +emrappui-prod.ap-south-1.amazonaws.com +emrnotebooks-prod.ap-south-1.amazonaws.com +emrstudio-prod.ap-south-1.amazonaws.com +emrappui-prod.ap-south-2.amazonaws.com +emrnotebooks-prod.ap-south-2.amazonaws.com +emrstudio-prod.ap-south-2.amazonaws.com +emrappui-prod.ap-southeast-1.amazonaws.com +emrnotebooks-prod.ap-southeast-1.amazonaws.com +emrstudio-prod.ap-southeast-1.amazonaws.com +emrappui-prod.ap-southeast-2.amazonaws.com +emrnotebooks-prod.ap-southeast-2.amazonaws.com +emrstudio-prod.ap-southeast-2.amazonaws.com +emrappui-prod.ap-southeast-3.amazonaws.com +emrnotebooks-prod.ap-southeast-3.amazonaws.com +emrstudio-prod.ap-southeast-3.amazonaws.com +emrappui-prod.ap-southeast-4.amazonaws.com +emrnotebooks-prod.ap-southeast-4.amazonaws.com +emrstudio-prod.ap-southeast-4.amazonaws.com +emrappui-prod.ca-central-1.amazonaws.com +emrnotebooks-prod.ca-central-1.amazonaws.com +emrstudio-prod.ca-central-1.amazonaws.com +emrappui-prod.ca-west-1.amazonaws.com +emrnotebooks-prod.ca-west-1.amazonaws.com +emrstudio-prod.ca-west-1.amazonaws.com +emrappui-prod.eu-central-1.amazonaws.com +emrnotebooks-prod.eu-central-1.amazonaws.com +emrstudio-prod.eu-central-1.amazonaws.com +emrappui-prod.eu-central-2.amazonaws.com +emrnotebooks-prod.eu-central-2.amazonaws.com +emrstudio-prod.eu-central-2.amazonaws.com +emrappui-prod.eu-north-1.amazonaws.com +emrnotebooks-prod.eu-north-1.amazonaws.com +emrstudio-prod.eu-north-1.amazonaws.com +emrappui-prod.eu-south-1.amazonaws.com +emrnotebooks-prod.eu-south-1.amazonaws.com +emrstudio-prod.eu-south-1.amazonaws.com +emrappui-prod.eu-south-2.amazonaws.com +emrnotebooks-prod.eu-south-2.amazonaws.com +emrstudio-prod.eu-south-2.amazonaws.com +emrappui-prod.eu-west-1.amazonaws.com +emrnotebooks-prod.eu-west-1.amazonaws.com +emrstudio-prod.eu-west-1.amazonaws.com +emrappui-prod.eu-west-2.amazonaws.com +emrnotebooks-prod.eu-west-2.amazonaws.com +emrstudio-prod.eu-west-2.amazonaws.com +emrappui-prod.eu-west-3.amazonaws.com +emrnotebooks-prod.eu-west-3.amazonaws.com +emrstudio-prod.eu-west-3.amazonaws.com +emrappui-prod.il-central-1.amazonaws.com +emrnotebooks-prod.il-central-1.amazonaws.com +emrstudio-prod.il-central-1.amazonaws.com +emrappui-prod.me-central-1.amazonaws.com +emrnotebooks-prod.me-central-1.amazonaws.com +emrstudio-prod.me-central-1.amazonaws.com +emrappui-prod.me-south-1.amazonaws.com +emrnotebooks-prod.me-south-1.amazonaws.com +emrstudio-prod.me-south-1.amazonaws.com +emrappui-prod.sa-east-1.amazonaws.com +emrnotebooks-prod.sa-east-1.amazonaws.com +emrstudio-prod.sa-east-1.amazonaws.com +emrappui-prod.us-east-1.amazonaws.com +emrnotebooks-prod.us-east-1.amazonaws.com +emrstudio-prod.us-east-1.amazonaws.com +emrappui-prod.us-east-2.amazonaws.com +emrnotebooks-prod.us-east-2.amazonaws.com +emrstudio-prod.us-east-2.amazonaws.com +emrappui-prod.us-gov-east-1.amazonaws.com +emrnotebooks-prod.us-gov-east-1.amazonaws.com +emrstudio-prod.us-gov-east-1.amazonaws.com +emrappui-prod.us-gov-west-1.amazonaws.com +emrnotebooks-prod.us-gov-west-1.amazonaws.com +emrstudio-prod.us-gov-west-1.amazonaws.com +emrappui-prod.us-west-1.amazonaws.com +emrnotebooks-prod.us-west-1.amazonaws.com +emrstudio-prod.us-west-1.amazonaws.com +emrappui-prod.us-west-2.amazonaws.com +emrnotebooks-prod.us-west-2.amazonaws.com +emrstudio-prod.us-west-2.amazonaws.com + +// Amazon Managed Workflows for Apache Airflow +// Submitted by AWS Security +// Reference: bfd043cc-2816-451d-894e-612c6b61a438 +*.airflow.af-south-1.on.aws +*.airflow.ap-east-1.on.aws +*.airflow.ap-northeast-1.on.aws +*.airflow.ap-northeast-2.on.aws +*.airflow.ap-northeast-3.on.aws +*.airflow.ap-south-1.on.aws +*.airflow.ap-south-2.on.aws +*.airflow.ap-southeast-1.on.aws +*.airflow.ap-southeast-2.on.aws +*.airflow.ap-southeast-3.on.aws +*.airflow.ap-southeast-4.on.aws +*.airflow.ap-southeast-5.on.aws +*.airflow.ca-central-1.on.aws +*.airflow.ca-west-1.on.aws +*.airflow.eu-central-1.on.aws +*.airflow.eu-central-2.on.aws +*.airflow.eu-north-1.on.aws +*.airflow.eu-south-1.on.aws +*.airflow.eu-south-2.on.aws +*.airflow.eu-west-1.on.aws +*.airflow.eu-west-2.on.aws +*.airflow.eu-west-3.on.aws +*.airflow.il-central-1.on.aws +*.airflow.me-central-1.on.aws +*.airflow.me-south-1.on.aws +*.airflow.sa-east-1.on.aws +*.airflow.us-east-1.on.aws +*.airflow.us-east-2.on.aws +*.airflow.us-west-1.on.aws +*.airflow.us-west-2.on.aws +*.cn-north-1.airflow.amazonaws.com.cn +*.cn-northwest-1.airflow.amazonaws.com.cn +*.airflow.cn-north-1.on.amazonwebservices.com.cn +*.airflow.cn-northwest-1.on.amazonwebservices.com.cn +*.af-south-1.airflow.amazonaws.com +*.ap-east-1.airflow.amazonaws.com +*.ap-northeast-1.airflow.amazonaws.com +*.ap-northeast-2.airflow.amazonaws.com +*.ap-northeast-3.airflow.amazonaws.com +*.ap-south-1.airflow.amazonaws.com +*.ap-south-2.airflow.amazonaws.com +*.ap-southeast-1.airflow.amazonaws.com +*.ap-southeast-2.airflow.amazonaws.com +*.ap-southeast-3.airflow.amazonaws.com +*.ap-southeast-4.airflow.amazonaws.com +*.ap-southeast-5.airflow.amazonaws.com +*.ap-southeast-7.airflow.amazonaws.com +*.ca-central-1.airflow.amazonaws.com +*.ca-west-1.airflow.amazonaws.com +*.eu-central-1.airflow.amazonaws.com +*.eu-central-2.airflow.amazonaws.com +*.eu-north-1.airflow.amazonaws.com +*.eu-south-1.airflow.amazonaws.com +*.eu-south-2.airflow.amazonaws.com +*.eu-west-1.airflow.amazonaws.com +*.eu-west-2.airflow.amazonaws.com +*.eu-west-3.airflow.amazonaws.com +*.il-central-1.airflow.amazonaws.com +*.me-central-1.airflow.amazonaws.com +*.me-south-1.airflow.amazonaws.com +*.sa-east-1.airflow.amazonaws.com +*.us-east-1.airflow.amazonaws.com +*.us-east-2.airflow.amazonaws.com +*.us-west-1.airflow.amazonaws.com +*.us-west-2.airflow.amazonaws.com + +// Amazon Relational Database Service +// Submitted by: AWS Security +// Reference: 5aa87906-fd4f-4831-8727-4ffca6094159 +*.rds.cn-north-1.amazonaws.com.cn +*.rds.cn-northwest-1.amazonaws.com.cn +*.af-south-1.rds.amazonaws.com +*.ap-east-1.rds.amazonaws.com +*.ap-east-2.rds.amazonaws.com +*.ap-northeast-1.rds.amazonaws.com +*.ap-northeast-2.rds.amazonaws.com +*.ap-northeast-3.rds.amazonaws.com +*.ap-south-1.rds.amazonaws.com +*.ap-south-2.rds.amazonaws.com +*.ap-southeast-1.rds.amazonaws.com +*.ap-southeast-2.rds.amazonaws.com +*.ap-southeast-3.rds.amazonaws.com +*.ap-southeast-4.rds.amazonaws.com +*.ap-southeast-5.rds.amazonaws.com +*.ap-southeast-6.rds.amazonaws.com +*.ap-southeast-7.rds.amazonaws.com +*.ca-central-1.rds.amazonaws.com +*.ca-west-1.rds.amazonaws.com +*.eu-central-1.rds.amazonaws.com +*.eu-central-2.rds.amazonaws.com +*.eu-west-1.rds.amazonaws.com +*.eu-west-2.rds.amazonaws.com +*.eu-west-3.rds.amazonaws.com +*.il-central-1.rds.amazonaws.com +*.me-central-1.rds.amazonaws.com +*.me-south-1.rds.amazonaws.com +*.mx-central-1.rds.amazonaws.com +*.sa-east-1.rds.amazonaws.com +*.us-east-1.rds.amazonaws.com +*.us-east-2.rds.amazonaws.com +*.us-gov-east-1.rds.amazonaws.com +*.us-gov-west-1.rds.amazonaws.com +*.us-northeast-1.rds.amazonaws.com +*.us-west-1.rds.amazonaws.com +*.us-west-2.rds.amazonaws.com + +// Amazon S3 +// Submitted by AWS Security +// Reference: 6f374c1c-1cc9-47de-8b2a-69ca56a3a3b6 +s3.dualstack.cn-north-1.amazonaws.com.cn +s3-accesspoint.dualstack.cn-north-1.amazonaws.com.cn +s3-website.dualstack.cn-north-1.amazonaws.com.cn +s3.cn-north-1.amazonaws.com.cn +s3-accesspoint.cn-north-1.amazonaws.com.cn +s3-deprecated.cn-north-1.amazonaws.com.cn +s3-object-lambda.cn-north-1.amazonaws.com.cn +s3-website.cn-north-1.amazonaws.com.cn +s3.dualstack.cn-northwest-1.amazonaws.com.cn +s3-accesspoint.dualstack.cn-northwest-1.amazonaws.com.cn +s3.cn-northwest-1.amazonaws.com.cn +s3-accesspoint.cn-northwest-1.amazonaws.com.cn +s3-object-lambda.cn-northwest-1.amazonaws.com.cn +s3-website.cn-northwest-1.amazonaws.com.cn +s3.dualstack.af-south-1.amazonaws.com +s3-accesspoint.dualstack.af-south-1.amazonaws.com +s3-website.dualstack.af-south-1.amazonaws.com +s3.af-south-1.amazonaws.com +s3-accesspoint.af-south-1.amazonaws.com +s3-object-lambda.af-south-1.amazonaws.com +s3-website.af-south-1.amazonaws.com +s3.dualstack.ap-east-1.amazonaws.com +s3-accesspoint.dualstack.ap-east-1.amazonaws.com +s3.ap-east-1.amazonaws.com +s3-accesspoint.ap-east-1.amazonaws.com +s3-object-lambda.ap-east-1.amazonaws.com +s3-website.ap-east-1.amazonaws.com +s3.dualstack.ap-northeast-1.amazonaws.com +s3-accesspoint.dualstack.ap-northeast-1.amazonaws.com +s3-website.dualstack.ap-northeast-1.amazonaws.com +s3.ap-northeast-1.amazonaws.com +s3-accesspoint.ap-northeast-1.amazonaws.com +s3-object-lambda.ap-northeast-1.amazonaws.com +s3-website.ap-northeast-1.amazonaws.com +s3.dualstack.ap-northeast-2.amazonaws.com +s3-accesspoint.dualstack.ap-northeast-2.amazonaws.com +s3-website.dualstack.ap-northeast-2.amazonaws.com +s3.ap-northeast-2.amazonaws.com +s3-accesspoint.ap-northeast-2.amazonaws.com +s3-object-lambda.ap-northeast-2.amazonaws.com +s3-website.ap-northeast-2.amazonaws.com +s3.dualstack.ap-northeast-3.amazonaws.com +s3-accesspoint.dualstack.ap-northeast-3.amazonaws.com +s3-website.dualstack.ap-northeast-3.amazonaws.com +s3.ap-northeast-3.amazonaws.com +s3-accesspoint.ap-northeast-3.amazonaws.com +s3-object-lambda.ap-northeast-3.amazonaws.com +s3-website.ap-northeast-3.amazonaws.com +s3.dualstack.ap-south-1.amazonaws.com +s3-accesspoint.dualstack.ap-south-1.amazonaws.com +s3-website.dualstack.ap-south-1.amazonaws.com +s3.ap-south-1.amazonaws.com +s3-accesspoint.ap-south-1.amazonaws.com +s3-object-lambda.ap-south-1.amazonaws.com +s3-website.ap-south-1.amazonaws.com +s3.dualstack.ap-south-2.amazonaws.com +s3-accesspoint.dualstack.ap-south-2.amazonaws.com +s3-website.dualstack.ap-south-2.amazonaws.com +s3.ap-south-2.amazonaws.com +s3-accesspoint.ap-south-2.amazonaws.com +s3-object-lambda.ap-south-2.amazonaws.com +s3-website.ap-south-2.amazonaws.com +s3.dualstack.ap-southeast-1.amazonaws.com +s3-accesspoint.dualstack.ap-southeast-1.amazonaws.com +s3-website.dualstack.ap-southeast-1.amazonaws.com +s3.ap-southeast-1.amazonaws.com +s3-accesspoint.ap-southeast-1.amazonaws.com +s3-object-lambda.ap-southeast-1.amazonaws.com +s3-website.ap-southeast-1.amazonaws.com +s3.dualstack.ap-southeast-2.amazonaws.com +s3-accesspoint.dualstack.ap-southeast-2.amazonaws.com +s3-website.dualstack.ap-southeast-2.amazonaws.com +s3.ap-southeast-2.amazonaws.com +s3-accesspoint.ap-southeast-2.amazonaws.com +s3-object-lambda.ap-southeast-2.amazonaws.com +s3-website.ap-southeast-2.amazonaws.com +s3.dualstack.ap-southeast-3.amazonaws.com +s3-accesspoint.dualstack.ap-southeast-3.amazonaws.com +s3-website.dualstack.ap-southeast-3.amazonaws.com +s3.ap-southeast-3.amazonaws.com +s3-accesspoint.ap-southeast-3.amazonaws.com +s3-object-lambda.ap-southeast-3.amazonaws.com +s3-website.ap-southeast-3.amazonaws.com +s3.dualstack.ap-southeast-4.amazonaws.com +s3-accesspoint.dualstack.ap-southeast-4.amazonaws.com +s3-website.dualstack.ap-southeast-4.amazonaws.com +s3.ap-southeast-4.amazonaws.com +s3-accesspoint.ap-southeast-4.amazonaws.com +s3-object-lambda.ap-southeast-4.amazonaws.com +s3-website.ap-southeast-4.amazonaws.com +s3.dualstack.ap-southeast-5.amazonaws.com +s3-accesspoint.dualstack.ap-southeast-5.amazonaws.com +s3-website.dualstack.ap-southeast-5.amazonaws.com +s3.ap-southeast-5.amazonaws.com +s3-accesspoint.ap-southeast-5.amazonaws.com +s3-deprecated.ap-southeast-5.amazonaws.com +s3-object-lambda.ap-southeast-5.amazonaws.com +s3-website.ap-southeast-5.amazonaws.com +s3.dualstack.ca-central-1.amazonaws.com +s3-accesspoint.dualstack.ca-central-1.amazonaws.com +s3-accesspoint-fips.dualstack.ca-central-1.amazonaws.com +s3-fips.dualstack.ca-central-1.amazonaws.com +s3-website.dualstack.ca-central-1.amazonaws.com +s3.ca-central-1.amazonaws.com +s3-accesspoint.ca-central-1.amazonaws.com +s3-accesspoint-fips.ca-central-1.amazonaws.com +s3-fips.ca-central-1.amazonaws.com +s3-object-lambda.ca-central-1.amazonaws.com +s3-website.ca-central-1.amazonaws.com +s3.dualstack.ca-west-1.amazonaws.com +s3-accesspoint.dualstack.ca-west-1.amazonaws.com +s3-accesspoint-fips.dualstack.ca-west-1.amazonaws.com +s3-fips.dualstack.ca-west-1.amazonaws.com +s3-website.dualstack.ca-west-1.amazonaws.com +s3.ca-west-1.amazonaws.com +s3-accesspoint.ca-west-1.amazonaws.com +s3-accesspoint-fips.ca-west-1.amazonaws.com +s3-fips.ca-west-1.amazonaws.com +s3-object-lambda.ca-west-1.amazonaws.com +s3-website.ca-west-1.amazonaws.com +s3.dualstack.eu-central-1.amazonaws.com +s3-accesspoint.dualstack.eu-central-1.amazonaws.com +s3-website.dualstack.eu-central-1.amazonaws.com +s3.eu-central-1.amazonaws.com +s3-accesspoint.eu-central-1.amazonaws.com +s3-object-lambda.eu-central-1.amazonaws.com +s3-website.eu-central-1.amazonaws.com +s3.dualstack.eu-central-2.amazonaws.com +s3-accesspoint.dualstack.eu-central-2.amazonaws.com +s3-website.dualstack.eu-central-2.amazonaws.com +s3.eu-central-2.amazonaws.com +s3-accesspoint.eu-central-2.amazonaws.com +s3-object-lambda.eu-central-2.amazonaws.com +s3-website.eu-central-2.amazonaws.com +s3.dualstack.eu-north-1.amazonaws.com +s3-accesspoint.dualstack.eu-north-1.amazonaws.com +s3.eu-north-1.amazonaws.com +s3-accesspoint.eu-north-1.amazonaws.com +s3-object-lambda.eu-north-1.amazonaws.com +s3-website.eu-north-1.amazonaws.com +s3.dualstack.eu-south-1.amazonaws.com +s3-accesspoint.dualstack.eu-south-1.amazonaws.com +s3-website.dualstack.eu-south-1.amazonaws.com +s3.eu-south-1.amazonaws.com +s3-accesspoint.eu-south-1.amazonaws.com +s3-object-lambda.eu-south-1.amazonaws.com +s3-website.eu-south-1.amazonaws.com +s3.dualstack.eu-south-2.amazonaws.com +s3-accesspoint.dualstack.eu-south-2.amazonaws.com +s3-website.dualstack.eu-south-2.amazonaws.com +s3.eu-south-2.amazonaws.com +s3-accesspoint.eu-south-2.amazonaws.com +s3-object-lambda.eu-south-2.amazonaws.com +s3-website.eu-south-2.amazonaws.com +s3.dualstack.eu-west-1.amazonaws.com +s3-accesspoint.dualstack.eu-west-1.amazonaws.com +s3-website.dualstack.eu-west-1.amazonaws.com +s3.eu-west-1.amazonaws.com +s3-accesspoint.eu-west-1.amazonaws.com +s3-deprecated.eu-west-1.amazonaws.com +s3-object-lambda.eu-west-1.amazonaws.com +s3-website.eu-west-1.amazonaws.com +s3.dualstack.eu-west-2.amazonaws.com +s3-accesspoint.dualstack.eu-west-2.amazonaws.com +s3.eu-west-2.amazonaws.com +s3-accesspoint.eu-west-2.amazonaws.com +s3-object-lambda.eu-west-2.amazonaws.com +s3-website.eu-west-2.amazonaws.com +s3.dualstack.eu-west-3.amazonaws.com +s3-accesspoint.dualstack.eu-west-3.amazonaws.com +s3-website.dualstack.eu-west-3.amazonaws.com +s3.eu-west-3.amazonaws.com +s3-accesspoint.eu-west-3.amazonaws.com +s3-object-lambda.eu-west-3.amazonaws.com +s3-website.eu-west-3.amazonaws.com +s3.dualstack.il-central-1.amazonaws.com +s3-accesspoint.dualstack.il-central-1.amazonaws.com +s3-website.dualstack.il-central-1.amazonaws.com +s3.il-central-1.amazonaws.com +s3-accesspoint.il-central-1.amazonaws.com +s3-object-lambda.il-central-1.amazonaws.com +s3-website.il-central-1.amazonaws.com +s3.dualstack.me-central-1.amazonaws.com +s3-accesspoint.dualstack.me-central-1.amazonaws.com +s3-website.dualstack.me-central-1.amazonaws.com +s3.me-central-1.amazonaws.com +s3-accesspoint.me-central-1.amazonaws.com +s3-object-lambda.me-central-1.amazonaws.com +s3-website.me-central-1.amazonaws.com +s3.dualstack.me-south-1.amazonaws.com +s3-accesspoint.dualstack.me-south-1.amazonaws.com +s3.me-south-1.amazonaws.com +s3-accesspoint.me-south-1.amazonaws.com +s3-object-lambda.me-south-1.amazonaws.com +s3-website.me-south-1.amazonaws.com +s3.amazonaws.com +s3-1.amazonaws.com +s3-ap-east-1.amazonaws.com +s3-ap-northeast-1.amazonaws.com +s3-ap-northeast-2.amazonaws.com +s3-ap-northeast-3.amazonaws.com +s3-ap-south-1.amazonaws.com +s3-ap-southeast-1.amazonaws.com +s3-ap-southeast-2.amazonaws.com +s3-ca-central-1.amazonaws.com +s3-eu-central-1.amazonaws.com +s3-eu-north-1.amazonaws.com +s3-eu-west-1.amazonaws.com +s3-eu-west-2.amazonaws.com +s3-eu-west-3.amazonaws.com +s3-external-1.amazonaws.com +s3-fips-us-gov-east-1.amazonaws.com +s3-fips-us-gov-west-1.amazonaws.com +mrap.accesspoint.s3-global.amazonaws.com +s3-me-south-1.amazonaws.com +s3-sa-east-1.amazonaws.com +s3-us-east-2.amazonaws.com +s3-us-gov-east-1.amazonaws.com +s3-us-gov-west-1.amazonaws.com +s3-us-west-1.amazonaws.com +s3-us-west-2.amazonaws.com +s3-website-ap-northeast-1.amazonaws.com +s3-website-ap-southeast-1.amazonaws.com +s3-website-ap-southeast-2.amazonaws.com +s3-website-eu-west-1.amazonaws.com +s3-website-sa-east-1.amazonaws.com +s3-website-us-east-1.amazonaws.com +s3-website-us-gov-west-1.amazonaws.com +s3-website-us-west-1.amazonaws.com +s3-website-us-west-2.amazonaws.com +s3.dualstack.sa-east-1.amazonaws.com +s3-accesspoint.dualstack.sa-east-1.amazonaws.com +s3-website.dualstack.sa-east-1.amazonaws.com +s3.sa-east-1.amazonaws.com +s3-accesspoint.sa-east-1.amazonaws.com +s3-object-lambda.sa-east-1.amazonaws.com +s3-website.sa-east-1.amazonaws.com +s3.dualstack.us-east-1.amazonaws.com +s3-accesspoint.dualstack.us-east-1.amazonaws.com +s3-accesspoint-fips.dualstack.us-east-1.amazonaws.com +s3-fips.dualstack.us-east-1.amazonaws.com +s3-website.dualstack.us-east-1.amazonaws.com +s3.us-east-1.amazonaws.com +s3-accesspoint.us-east-1.amazonaws.com +s3-accesspoint-fips.us-east-1.amazonaws.com +s3-deprecated.us-east-1.amazonaws.com +s3-fips.us-east-1.amazonaws.com +s3-object-lambda.us-east-1.amazonaws.com +s3-website.us-east-1.amazonaws.com +s3.dualstack.us-east-2.amazonaws.com +s3-accesspoint.dualstack.us-east-2.amazonaws.com +s3-accesspoint-fips.dualstack.us-east-2.amazonaws.com +s3-fips.dualstack.us-east-2.amazonaws.com +s3-website.dualstack.us-east-2.amazonaws.com +s3.us-east-2.amazonaws.com +s3-accesspoint.us-east-2.amazonaws.com +s3-accesspoint-fips.us-east-2.amazonaws.com +s3-deprecated.us-east-2.amazonaws.com +s3-fips.us-east-2.amazonaws.com +s3-object-lambda.us-east-2.amazonaws.com +s3-website.us-east-2.amazonaws.com +s3.dualstack.us-gov-east-1.amazonaws.com +s3-accesspoint.dualstack.us-gov-east-1.amazonaws.com +s3-accesspoint-fips.dualstack.us-gov-east-1.amazonaws.com +s3-fips.dualstack.us-gov-east-1.amazonaws.com +s3-website.dualstack.us-gov-east-1.amazonaws.com +s3.us-gov-east-1.amazonaws.com +s3-accesspoint.us-gov-east-1.amazonaws.com +s3-accesspoint-fips.us-gov-east-1.amazonaws.com +s3-fips.us-gov-east-1.amazonaws.com +s3-object-lambda.us-gov-east-1.amazonaws.com +s3-website.us-gov-east-1.amazonaws.com +s3.dualstack.us-gov-west-1.amazonaws.com +s3-accesspoint.dualstack.us-gov-west-1.amazonaws.com +s3-accesspoint-fips.dualstack.us-gov-west-1.amazonaws.com +s3-fips.dualstack.us-gov-west-1.amazonaws.com +s3-website.dualstack.us-gov-west-1.amazonaws.com +s3.us-gov-west-1.amazonaws.com +s3-accesspoint.us-gov-west-1.amazonaws.com +s3-accesspoint-fips.us-gov-west-1.amazonaws.com +s3-fips.us-gov-west-1.amazonaws.com +s3-object-lambda.us-gov-west-1.amazonaws.com +s3-website.us-gov-west-1.amazonaws.com +s3.dualstack.us-west-1.amazonaws.com +s3-accesspoint.dualstack.us-west-1.amazonaws.com +s3-accesspoint-fips.dualstack.us-west-1.amazonaws.com +s3-fips.dualstack.us-west-1.amazonaws.com +s3-website.dualstack.us-west-1.amazonaws.com +s3.us-west-1.amazonaws.com +s3-accesspoint.us-west-1.amazonaws.com +s3-accesspoint-fips.us-west-1.amazonaws.com +s3-fips.us-west-1.amazonaws.com +s3-object-lambda.us-west-1.amazonaws.com +s3-website.us-west-1.amazonaws.com +s3.dualstack.us-west-2.amazonaws.com +s3-accesspoint.dualstack.us-west-2.amazonaws.com +s3-accesspoint-fips.dualstack.us-west-2.amazonaws.com +s3-fips.dualstack.us-west-2.amazonaws.com +s3-website.dualstack.us-west-2.amazonaws.com +s3.us-west-2.amazonaws.com +s3-accesspoint.us-west-2.amazonaws.com +s3-accesspoint-fips.us-west-2.amazonaws.com +s3-deprecated.us-west-2.amazonaws.com +s3-fips.us-west-2.amazonaws.com +s3-object-lambda.us-west-2.amazonaws.com +s3-website.us-west-2.amazonaws.com + +// Amazon SageMaker Ground Truth +// Submitted by AWS Security +// Reference: 98dbfde4-7802-48c3-8751-b60f204e0d9c +labeling.ap-northeast-1.sagemaker.aws +labeling.ap-northeast-2.sagemaker.aws +labeling.ap-south-1.sagemaker.aws +labeling.ap-southeast-1.sagemaker.aws +labeling.ap-southeast-2.sagemaker.aws +labeling.ca-central-1.sagemaker.aws +labeling.eu-central-1.sagemaker.aws +labeling.eu-west-1.sagemaker.aws +labeling.eu-west-2.sagemaker.aws +labeling.us-east-1.sagemaker.aws +labeling.us-east-2.sagemaker.aws +labeling.us-west-2.sagemaker.aws + +// Amazon SageMaker Notebook Instances +// Submitted by AWS Security +// Reference: b5ea56df-669e-43cc-9537-14aa172f5dfc +notebook.af-south-1.sagemaker.aws +notebook.ap-east-1.sagemaker.aws +notebook.ap-northeast-1.sagemaker.aws +notebook.ap-northeast-2.sagemaker.aws +notebook.ap-northeast-3.sagemaker.aws +notebook.ap-south-1.sagemaker.aws +notebook.ap-south-2.sagemaker.aws +notebook.ap-southeast-1.sagemaker.aws +notebook.ap-southeast-2.sagemaker.aws +notebook.ap-southeast-3.sagemaker.aws +notebook.ap-southeast-4.sagemaker.aws +notebook.ca-central-1.sagemaker.aws +notebook-fips.ca-central-1.sagemaker.aws +notebook.ca-west-1.sagemaker.aws +notebook-fips.ca-west-1.sagemaker.aws +notebook.eu-central-1.sagemaker.aws +notebook.eu-central-2.sagemaker.aws +notebook.eu-north-1.sagemaker.aws +notebook.eu-south-1.sagemaker.aws +notebook.eu-south-2.sagemaker.aws +notebook.eu-west-1.sagemaker.aws +notebook.eu-west-2.sagemaker.aws +notebook.eu-west-3.sagemaker.aws +notebook.il-central-1.sagemaker.aws +notebook.me-central-1.sagemaker.aws +notebook.me-south-1.sagemaker.aws +notebook.sa-east-1.sagemaker.aws +notebook.us-east-1.sagemaker.aws +notebook-fips.us-east-1.sagemaker.aws +notebook.us-east-2.sagemaker.aws +notebook-fips.us-east-2.sagemaker.aws +notebook.us-gov-east-1.sagemaker.aws +notebook-fips.us-gov-east-1.sagemaker.aws +notebook.us-gov-west-1.sagemaker.aws +notebook-fips.us-gov-west-1.sagemaker.aws +notebook.us-west-1.sagemaker.aws +notebook-fips.us-west-1.sagemaker.aws +notebook.us-west-2.sagemaker.aws +notebook-fips.us-west-2.sagemaker.aws +notebook.cn-north-1.sagemaker.com.cn +notebook.cn-northwest-1.sagemaker.com.cn + +// Amazon SageMaker Studio +// Submitted by AWS Security +// Reference: 475f237e-ab88-4041-9f41-7cfccdf66aeb +studio.af-south-1.sagemaker.aws +studio.ap-east-1.sagemaker.aws +studio.ap-northeast-1.sagemaker.aws +studio.ap-northeast-2.sagemaker.aws +studio.ap-northeast-3.sagemaker.aws +studio.ap-south-1.sagemaker.aws +studio.ap-southeast-1.sagemaker.aws +studio.ap-southeast-2.sagemaker.aws +studio.ap-southeast-3.sagemaker.aws +studio.ca-central-1.sagemaker.aws +studio.eu-central-1.sagemaker.aws +studio.eu-central-2.sagemaker.aws +studio.eu-north-1.sagemaker.aws +studio.eu-south-1.sagemaker.aws +studio.eu-south-2.sagemaker.aws +studio.eu-west-1.sagemaker.aws +studio.eu-west-2.sagemaker.aws +studio.eu-west-3.sagemaker.aws +studio.il-central-1.sagemaker.aws +studio.me-central-1.sagemaker.aws +studio.me-south-1.sagemaker.aws +studio.sa-east-1.sagemaker.aws +studio.us-east-1.sagemaker.aws +studio.us-east-2.sagemaker.aws +studio.us-gov-east-1.sagemaker.aws +studio-fips.us-gov-east-1.sagemaker.aws +studio.us-gov-west-1.sagemaker.aws +studio-fips.us-gov-west-1.sagemaker.aws +studio.us-west-1.sagemaker.aws +studio.us-west-2.sagemaker.aws +studio.cn-north-1.sagemaker.com.cn +studio.cn-northwest-1.sagemaker.com.cn + +// Amazon SageMaker with MLflow +// Submited by: AWS Security +// Reference: c19f92b3-a82a-452d-8189-831b572eea7e +*.experiments.sagemaker.aws + +// Analytics on AWS +// Submitted by AWS Security +// Reference: 955f9f40-a495-4e73-ae85-67b77ac9cadd +analytics-gateway.ap-northeast-1.amazonaws.com +analytics-gateway.ap-northeast-2.amazonaws.com +analytics-gateway.ap-south-1.amazonaws.com +analytics-gateway.ap-southeast-1.amazonaws.com +analytics-gateway.ap-southeast-2.amazonaws.com +analytics-gateway.eu-central-1.amazonaws.com +analytics-gateway.eu-west-1.amazonaws.com +analytics-gateway.us-east-1.amazonaws.com +analytics-gateway.us-east-2.amazonaws.com +analytics-gateway.us-west-2.amazonaws.com + +// AWS Amplify +// Submitted by AWS Security +// Reference: c35bed18-6f4f-424f-9298-5756f2f7d72b +amplifyapp.com + +// AWS App Runner +// Submitted by AWS Security +// Reference: 6828c008-ba5d-442f-ade5-48da4e7c2316 +*.awsapprunner.com + +// AWS Cloud9 +// Submitted by: AWS Security +// Reference: 30717f72-4007-4f0f-8ed4-864c6f2efec9 +webview-assets.aws-cloud9.af-south-1.amazonaws.com +vfs.cloud9.af-south-1.amazonaws.com +webview-assets.cloud9.af-south-1.amazonaws.com +webview-assets.aws-cloud9.ap-east-1.amazonaws.com +vfs.cloud9.ap-east-1.amazonaws.com +webview-assets.cloud9.ap-east-1.amazonaws.com +webview-assets.aws-cloud9.ap-northeast-1.amazonaws.com +vfs.cloud9.ap-northeast-1.amazonaws.com +webview-assets.cloud9.ap-northeast-1.amazonaws.com +webview-assets.aws-cloud9.ap-northeast-2.amazonaws.com +vfs.cloud9.ap-northeast-2.amazonaws.com +webview-assets.cloud9.ap-northeast-2.amazonaws.com +webview-assets.aws-cloud9.ap-northeast-3.amazonaws.com +vfs.cloud9.ap-northeast-3.amazonaws.com +webview-assets.cloud9.ap-northeast-3.amazonaws.com +webview-assets.aws-cloud9.ap-south-1.amazonaws.com +vfs.cloud9.ap-south-1.amazonaws.com +webview-assets.cloud9.ap-south-1.amazonaws.com +webview-assets.aws-cloud9.ap-southeast-1.amazonaws.com +vfs.cloud9.ap-southeast-1.amazonaws.com +webview-assets.cloud9.ap-southeast-1.amazonaws.com +webview-assets.aws-cloud9.ap-southeast-2.amazonaws.com +vfs.cloud9.ap-southeast-2.amazonaws.com +webview-assets.cloud9.ap-southeast-2.amazonaws.com +webview-assets.aws-cloud9.ca-central-1.amazonaws.com +vfs.cloud9.ca-central-1.amazonaws.com +webview-assets.cloud9.ca-central-1.amazonaws.com +webview-assets.aws-cloud9.eu-central-1.amazonaws.com +vfs.cloud9.eu-central-1.amazonaws.com +webview-assets.cloud9.eu-central-1.amazonaws.com +webview-assets.aws-cloud9.eu-north-1.amazonaws.com +vfs.cloud9.eu-north-1.amazonaws.com +webview-assets.cloud9.eu-north-1.amazonaws.com +webview-assets.aws-cloud9.eu-south-1.amazonaws.com +vfs.cloud9.eu-south-1.amazonaws.com +webview-assets.cloud9.eu-south-1.amazonaws.com +webview-assets.aws-cloud9.eu-west-1.amazonaws.com +vfs.cloud9.eu-west-1.amazonaws.com +webview-assets.cloud9.eu-west-1.amazonaws.com +webview-assets.aws-cloud9.eu-west-2.amazonaws.com +vfs.cloud9.eu-west-2.amazonaws.com +webview-assets.cloud9.eu-west-2.amazonaws.com +webview-assets.aws-cloud9.eu-west-3.amazonaws.com +vfs.cloud9.eu-west-3.amazonaws.com +webview-assets.cloud9.eu-west-3.amazonaws.com +webview-assets.aws-cloud9.il-central-1.amazonaws.com +vfs.cloud9.il-central-1.amazonaws.com +webview-assets.aws-cloud9.me-south-1.amazonaws.com +vfs.cloud9.me-south-1.amazonaws.com +webview-assets.cloud9.me-south-1.amazonaws.com +webview-assets.aws-cloud9.sa-east-1.amazonaws.com +vfs.cloud9.sa-east-1.amazonaws.com +webview-assets.cloud9.sa-east-1.amazonaws.com +webview-assets.aws-cloud9.us-east-1.amazonaws.com +vfs.cloud9.us-east-1.amazonaws.com +webview-assets.cloud9.us-east-1.amazonaws.com +webview-assets.aws-cloud9.us-east-2.amazonaws.com +vfs.cloud9.us-east-2.amazonaws.com +webview-assets.cloud9.us-east-2.amazonaws.com +webview-assets.aws-cloud9.us-west-1.amazonaws.com +vfs.cloud9.us-west-1.amazonaws.com +webview-assets.cloud9.us-west-1.amazonaws.com +webview-assets.aws-cloud9.us-west-2.amazonaws.com +vfs.cloud9.us-west-2.amazonaws.com +webview-assets.cloud9.us-west-2.amazonaws.com + +// AWS Directory Service +// Submitted by AWS Security +// Reference: a13203e8-42dc-4045-a0d2-2ee67bed1068 +awsapps.com + +// AWS Elastic Beanstalk +// Submitted by AWS Security +// Reference: e4e02a54-eaf9-4fe7-b662-39ccbc011a04 +cn-north-1.eb.amazonaws.com.cn +cn-northwest-1.eb.amazonaws.com.cn +elasticbeanstalk.com +af-south-1.elasticbeanstalk.com +ap-east-1.elasticbeanstalk.com +ap-northeast-1.elasticbeanstalk.com +ap-northeast-2.elasticbeanstalk.com +ap-northeast-3.elasticbeanstalk.com +ap-south-1.elasticbeanstalk.com +ap-southeast-1.elasticbeanstalk.com +ap-southeast-2.elasticbeanstalk.com +ap-southeast-3.elasticbeanstalk.com +ap-southeast-5.elasticbeanstalk.com +ap-southeast-7.elasticbeanstalk.com +ca-central-1.elasticbeanstalk.com +eu-central-1.elasticbeanstalk.com +eu-north-1.elasticbeanstalk.com +eu-south-1.elasticbeanstalk.com +eu-south-2.elasticbeanstalk.com +eu-west-1.elasticbeanstalk.com +eu-west-2.elasticbeanstalk.com +eu-west-3.elasticbeanstalk.com +il-central-1.elasticbeanstalk.com +me-central-1.elasticbeanstalk.com +me-south-1.elasticbeanstalk.com +sa-east-1.elasticbeanstalk.com +us-east-1.elasticbeanstalk.com +us-east-2.elasticbeanstalk.com +us-gov-east-1.elasticbeanstalk.com +us-gov-west-1.elasticbeanstalk.com +us-west-1.elasticbeanstalk.com +us-west-2.elasticbeanstalk.com + +// (AWS) Elastic Load Balancing +// Submitted by Luke Wells +// Reference: 12a3d528-1bac-4433-a359-a395867ffed2 +*.elb.amazonaws.com.cn +*.elb.amazonaws.com + +// AWS Global Accelerator +// Submitted by Daniel Massaguer +// Reference: d916759d-a08b-4241-b536-4db887383a6a +awsglobalaccelerator.com + +// AWS Lambda Function URLs +// Submitted by AWS Security +// Reference: 57df74ca-0820-46a5-89ea-0f0d0c4714b7 +lambda-url.af-south-1.on.aws +lambda-url.ap-east-1.on.aws +lambda-url.ap-northeast-1.on.aws +lambda-url.ap-northeast-2.on.aws +lambda-url.ap-northeast-3.on.aws +lambda-url.ap-south-1.on.aws +lambda-url.ap-southeast-1.on.aws +lambda-url.ap-southeast-2.on.aws +lambda-url.ap-southeast-3.on.aws +lambda-url.ca-central-1.on.aws +lambda-url.eu-central-1.on.aws +lambda-url.eu-north-1.on.aws +lambda-url.eu-south-1.on.aws +lambda-url.eu-west-1.on.aws +lambda-url.eu-west-2.on.aws +lambda-url.eu-west-3.on.aws +lambda-url.me-south-1.on.aws +lambda-url.sa-east-1.on.aws +lambda-url.us-east-1.on.aws +lambda-url.us-east-2.on.aws +lambda-url.us-west-1.on.aws +lambda-url.us-west-2.on.aws + +// AWS re:Post Private +// Submitted by AWS Security +// Reference: 83385945-225f-416e-9aa0-ad0632bfdcee +*.private.repost.aws + +// AWS Transfer Family web apps +// Submitted by AWS Security +// Reference: 9265cdd3-f017-42ab-98bb-08bf427d3fc9 +transfer-webapp.af-south-1.on.aws +transfer-webapp.ap-east-1.on.aws +transfer-webapp.ap-northeast-1.on.aws +transfer-webapp.ap-northeast-2.on.aws +transfer-webapp.ap-northeast-3.on.aws +transfer-webapp.ap-south-1.on.aws +transfer-webapp.ap-south-2.on.aws +transfer-webapp.ap-southeast-1.on.aws +transfer-webapp.ap-southeast-2.on.aws +transfer-webapp.ap-southeast-3.on.aws +transfer-webapp.ap-southeast-4.on.aws +transfer-webapp.ap-southeast-5.on.aws +transfer-webapp.ap-southeast-7.on.aws +transfer-webapp.ca-central-1.on.aws +transfer-webapp.ca-west-1.on.aws +transfer-webapp.eu-central-1.on.aws +transfer-webapp.eu-central-2.on.aws +transfer-webapp.eu-north-1.on.aws +transfer-webapp.eu-south-1.on.aws +transfer-webapp.eu-south-2.on.aws +transfer-webapp.eu-west-1.on.aws +transfer-webapp.eu-west-2.on.aws +transfer-webapp.eu-west-3.on.aws +transfer-webapp.il-central-1.on.aws +transfer-webapp.me-central-1.on.aws +transfer-webapp.me-south-1.on.aws +transfer-webapp.mx-central-1.on.aws +transfer-webapp.sa-east-1.on.aws +transfer-webapp.us-east-1.on.aws +transfer-webapp.us-east-2.on.aws +transfer-webapp.us-gov-east-1.on.aws +transfer-webapp-fips.us-gov-east-1.on.aws +transfer-webapp.us-gov-west-1.on.aws +transfer-webapp-fips.us-gov-west-1.on.aws +transfer-webapp.us-west-1.on.aws +transfer-webapp.us-west-2.on.aws +transfer-webapp.cn-north-1.on.amazonwebservices.com.cn +transfer-webapp.cn-northwest-1.on.amazonwebservices.com.cn + +// eero +// Submitted by Yue Kang +// Reference: 264afe70-f62c-4c02-8ab9-b5281ed24461 +eero.online +eero-stage.online + +// concludes Amazon + +// Anomaly : https://opencode.ai +// Submitted by Dax Raad +opentunnel.xyz + +// Antagonist B.V. : https://www.antagonist.nl/ +// Submitted by Sander Hoentjen +antagonist.cloud + +// Anthropic : https://www.anthropic.com/ +// Submitted by Sid Bidasaria +claude.app +claudeusercontent.com +frame.claudeusercontent.com + +// Anysphere Inc : https://cursor.com +// Submitted by Benson Liu +*.cursorusercontent.com + +// Apigee : https://apigee.com/ +// Submitted by Apigee Security Team +apigee.io + +// Apis Networks : https://apisnetworks.com +// Submitted by Matt Saladna +panel.dev + +// Apphud : https://apphud.com +// Submitted by Alexander Selivanov +siiites.com + +// Apple : https://www.apple.com +// Submitted by Apple DNS +int.apple +*.cloud.int.apple +*.r.cloud.int.apple +*.ap-north-1.r.cloud.int.apple +*.ap-south-1.r.cloud.int.apple +*.ap-south-2.r.cloud.int.apple +*.eu-central-1.r.cloud.int.apple +*.eu-north-1.r.cloud.int.apple +*.us-central-1.r.cloud.int.apple +*.us-central-2.r.cloud.int.apple +*.us-east-1.r.cloud.int.apple +*.us-east-2.r.cloud.int.apple +*.us-west-1.r.cloud.int.apple +*.us-west-2.r.cloud.int.apple +*.us-west-3.r.cloud.int.apple + +// Appspace : https://www.appspace.com +// Submitted by Appspace Security Team +appspacehosted.com +appspaceusercontent.com + +// Appudo UG (haftungsbeschränkt) : https://www.appudo.com +// Submitted by Alexander Hochbaum +appudo.net + +// Appwrite : https://appwrite.io +// Submitted by Steven Nguyen +appwrite.global +appwrite.network +*.appwrite.run + +// Aptible : https://www.aptible.com/ +// Submitted by Thomas Orozco +on-aptible.com + +// Aquapal : https://aquapal.net/ +// Submitted by Aki Ueno +f5.si + +// ArvanCloud EdgeCompute +// Submitted by ArvanCloud CDN +arvanedge.ir + +// ASEINet : https://www.aseinet.com/ +// Submitted by Asei SEKIGUCHI +user.aseinet.ne.jp +gv.vc +d.gv.vc + +// Asociación Amigos de la Informática "Euskalamiga" : http://encounter.eus/ +// Submitted by Hector Martin +user.party.eus + +// Association potager.org : https://potager.org/ +// Submitted by Lunar +pimienta.org +poivron.org +potager.org +sweetpepper.org + +// ASUSTOR Inc. : http://www.asustor.com +// Submitted by Vincent Tseng +myasustor.com + +// Atlassian : https://atlassian.com +// Submitted by Benjamin McAlary +*.atlassian-3p.com +*.atlassian-3p-us-gov-mod.com +*.atlassian-isolated-3p.com +cdn.prod.atlassian-dev.net + +// AVM : https://avm.de +// Submitted by Andreas Weise +myfritz.link +myfritz.net + +// AW AdvisorWebsites.com Software Inc : https://advisorwebsites.com +// Submitted by James Kennedy +*.awdev.ca +*.advisor.ws + +// AZ.pl sp. z.o.o : https://az.pl +// Submitted by Krzysztof Wolski +ecommerce-shop.pl + +// b-data GmbH : https://www.b-data.io +// Submitted by Olivier Benz +b-data.io + +// Balena : https://www.balena.io +// Submitted by Petros Angelatos +balena-devices.com + +// BASE, Inc. : https://binc.jp +// Submitted by Yuya NAGASAWA +base.ec +official.ec +buyshop.jp +fashionstore.jp +handcrafted.jp +kawaiishop.jp +supersale.jp +theshop.jp +shopselect.net +base.shop + +// BeagleBoard.org Foundation : https://beagleboard.org +// Submitted by Jason Kridner +beagleboard.io + +// Bear Blog : https://bearblog.dev +// Submitted by Herman Martinus +bearblog.dev + +// Beget LLC : https://beget.com +// Submitted by Lev Nekrasov & Nikita Radchenko +*.beget.app +*.begetcdn.cloud + +// Besties : https://besties.house +// Submitted by Hazel Cora +pages.gay + +// BinaryLane : http://www.binarylane.com +// Submitted by Nathan O'Sullivan +bnr.la + +// Bitbucket : http://bitbucket.org +// Submitted by Andy Ortlieb +bitbucket.io + +// Blackbaud, Inc. : https://www.blackbaud.com +// Submitted by Paul Crowder +blackbaudcdn.net + +// Blatech : http://www.blatech.net +// Submitted by Luke Bratch +of.je + +// Block, Inc. : https://block.xyz +// Submitted by Jonathan Boice +square.site + +// Blue Bite, LLC : https://bluebite.com +// Submitted by Joshua Weiss +bluebite.io + +// Boomla : https://boomla.com +// Submitted by Tibor Halter +boomla.net + +// Boutir : https://www.boutir.com +// Submitted by Eric Ng Ka Ka +boutir.com + +// Boxfuse : https://boxfuse.com +// Submitted by Axel Fontaine +boxfuse.io + +// bplaced : https://www.bplaced.net/ +// Submitted by Miroslav Bozic +square7.ch +bplaced.com +bplaced.de +square7.de +bplaced.net +square7.net + +// Brave : https://brave.com +// Submitted by Andrea Brancaleoni +brave.app +*.s.brave.app +brave.dev +*.s.brave.dev +brave.io +*.s.brave.io + +// Brendly : https://brendly.rs +// Submitted by Dusan Radovanovic +shop.brendly.ba +shop.brendly.hr +shop.brendly.rs + +// BrowserSafetyMark +// Submitted by Dave Tharp +browsersafetymark.io + +// BRS Media : https://brsmedia.com/ +// Submitted by Gavin Brown +radio.am +radio.fm + +// Bubble : https://bubble.io/ +// Submitted by Merlin Zhao +cdn.bubble.io +bubbleapps.io + +// bwCloud-OS : https://bwcloud-os.de/ +// Submitted by Klara Mall +*.bwcloud-os-instance.de + +// Caf.js Labs LLC : https://www.cafjs.com +// Submitted by Antonio Lain +cafjs.com + +// Canva Pty Ltd : https://canva.com/ +// Submitted by Joel Aquilina +canva-apps.cn +canva-code.cn +my.canvasite.cn +khsj.cn +canva-apps.com +canva-hosted-embed.com +canvacode.com +rice-labs.com +canva.link +canva.run +my.canva.site + +// Carrd : https://carrd.co +// Submitted by AJ +drr.ac +uwu.ai +carrd.co +crd.co +ju.mp + +// CDDO : https://www.gov.uk/guidance/get-an-api-domain-on-govuk +// Submitted by Jamie Tanna +api.gov.uk + +// CDN77.com : http://www.cdn77.com +// Submitted by Jan Krpes +cdn77-storage.com +rsc.contentproxy9.cz +r.cdn77.net +cdn77-ssl.net +c.cdn77.org +rsc.cdn77.org +ssl.origin.cdn77-secure.org + +// CentralNic : https://teaminternet.com/ +// Submitted by registry +za.bz +br.com +cn.com +de.com +eu.com +jpn.com +mex.com +ru.com +sa.com +uk.com +us.com +za.com +com.de +gb.net +hu.net +jp.net +se.net +uk.net +ae.org +com.se + +// Cityhost LLC : https://cityhost.ua +// Submitted by Maksym Rivtin +cx.ua + +// Civilized Discourse Construction Kit, Inc. : https://www.discourse.org/ +// Submitted by Rishabh Nambiar, Michael Brown, Rafael dos Santos Silva +discourse.diy +discourse.group +discourse.team + +// Clerk : https://www.clerk.dev +// Submitted by Colin Sidoti +clerk.app +clerkstage.app +*.lcl.dev +*.lclstage.dev +*.stg.dev +*.stgstage.dev + +// Clever Cloud : https://www.clever-cloud.com/ +// Submitted by Quentin Adam +cleverapps.cc +*.services.clever-cloud.com +cleverapps.io +cleverapps.tech + +// ClickRising : https://clickrising.com/ +// Submitted by Umut Gumeli +clickrising.net + +// Cloud DNS Ltd : http://www.cloudns.net +// Submitted by Aleksander Hristov & Boyan Peychev +cloudns.asia +cloudns.be +cloud-ip.biz +cloudns.biz +cloud-ip.cc +cloudns.cc +cloudns.ch +cloudns.cl +cloudns.club +abrdns.com +dnsabr.com +ip-ddns.com +cloudns.cx +cloudns.eu +cloudns.in +cloudns.info +ddns-ip.net +dns-cloud.net +dns-dynamic.net +cloudns.nz +cloudns.org +ip-dynamic.org +cloudns.ph +cloudns.pro +cloudns.pw +cloudns.us + +// Cloud66 : https://www.cloud66.com/ +// Submitted by Khash Sajadi +c66.me +cloud66.ws + +// CloudAccess.net : https://www.cloudaccess.net/ +// Submitted by Pawel Panek +jdevcloud.com +wpdevcloud.com +cloudaccess.host +freesite.host +cloudaccess.net + +// Cloudbees, Inc. : https://www.cloudbees.com/ +// Submitted by Mohideen Shajith +cloudbeesusercontent.io + +// Cloudera, Inc. : https://www.cloudera.com/ +// Submitted by Kedarnath Waikar +*.cloudera.site + +// Cloudflare, Inc. : https://www.cloudflare.com/ +// Submitted by Cloudflare Team +cloudflare.app +cf-ipfs.com +cloudflare-ipfs.com +trycloudflare.com +pages.dev +r2.dev +workers.dev +cloudflare.net +cdn.cloudflare.net +cdn.cloudflareanycast.net +cdn.cloudflarecn.net +cdn.cloudflareglobal.net + +// cloudscale.ch AG : https://www.cloudscale.ch/ +// Submitted by Gaudenz Steinlin +cust.cloudscale.ch +objects.lpg.cloudscale.ch +objects.rma.cloudscale.ch +lpg.objectstorage.ch +rma.objectstorage.ch + +// Clovyr : https://clovyr.io +// Submitted by Patrick Nielsen +wnext.app + +// CNPY : https://cnpy.gdn +// Submitted by Angelo Gladding +cnpy.gdn + +// Co & Co : https://co-co.nl/ +// Submitted by Govert Versluis +*.otap.co + +// co.ca : http://registry.co.ca/ +co.ca + +// co.com Registry, LLC : https://registry.co.com +// Submitted by Gavin Brown +co.com + +// Code For Host Inc Ltd : https://codeforhost.com +// Submitted by Mehedi Hasan +sch.ac +dev.cv +store.cv + +// Codeberg e. V. : https://codeberg.org +// Submitted by Moritz Marquardt +codeberg.page + +// CodePen : https://codepen.io +// Submitted by Stephen Shaw +codepen.app +codepen.dev + +// CodeSandbox B.V. : https://codesandbox.io +// Submitted by Ives van Hoorne +csb.app +preview.csb.app + +// CoDNS B.V. +co.nl +co.no + +// Cognition AI, Inc. : https://cognition.ai +// Submitted by Philip Papurt +*.devinapps.com + +// Combell.com : https://www.combell.com +// Submitted by Combell Team +webhosting.be +prvw.eu +hosting-cluster.nl + +// Contentful GmbH : https://www.contentful.com +// Submitted by Contentful Developer Experience Team +ctfcloud.net + +// Convex : https://convex.dev/ +// Submitted by James Cowling +convex.app +convex.cloud +eu-west-1.convex.cloud +us-east-1.convex.cloud +convex.site +eu-west-1.convex.site +us-east-1.convex.site + +// Coordination Center for TLD RU and XN--P1AI : https://cctld.ru/en/domains/domens_ru/reserved/ +// Submitted by George Georgievsky +ac.ru +edu.ru +gov.ru +int.ru +mil.ru + +// CoreSpeed, Inc. : https://corespeed.io +// Submitted by CoreSpeed Team +corespeed.app + +// COSIMO GmbH : http://www.cosimo.de +// Submitted by Rene Marticke +dyn.cosidns.de +dnsupdater.de +dynamisches-dns.de +internet-dns.de +l-o-g-i-n.de +dynamic-dns.info +feste-ip.net +knx-server.net +static-access.net + +// Craft Docs Ltd : https://www.craft.do/ +// Submitted by Zsombor Fuszenecker +craft.me + +// Craynic, s.r.o. : http://www.craynic.com/ +// Submitted by Ales Krajnik +realm.cz + +// cyber_Folks S.A. : https://cyberfolks.pl +// Submitted by Bartlomiej Kida +cfolks.pl + +// cyon GmbH : https://www.cyon.ch/ +// Submitted by Dominic Luechinger +cyon.link +cyon.site + +// Dansk.net : http://www.dansk.net/ +// Submitted by Anani Voule +biz.dk +co.dk +firm.dk +reg.dk +store.dk + +// dappnode.io : https://dappnode.io/ +// Submitted by Abel Boldu / DAppNode Team +dyndns.dappnode.io + +// Dark, Inc. : https://darklang.com +// Submitted by Paul Biggar +builtwithdark.com +darklang.io + +// DataDetect, LLC. : https://datadetect.com +// Submitted by Andrew Banchich +demo.datadetect.com +instance.datadetect.com + +// Datawire, Inc : https://www.datawire.io +// Submitted by Richard Li +edgestack.me + +// Datto, Inc. : https://www.datto.com/ +// Submitted by Philipp Heckel +dattolocal.com +dattorelay.com +dattoweb.com +mydatto.com +dattolocal.net +mydatto.net + +// ddnss.de : https://www.ddnss.de/ +// Submitted by Robert Niedziela +ddnss.de +dyn.ddnss.de +dyndns.ddnss.de +dyn-ip24.de +dyndns1.de +home-webserver.de +dyn.home-webserver.de +myhome-server.de +ddnss.org + +// Debian : https://www.debian.org/ +// Submitted by Peter Palfrader / Debian Sysadmin Team +debian.net + +// Definima : http://www.definima.com/ +// Submitted by Maxence Bitterli +definima.io +definima.net + +// Deno Land Inc : https://deno.com/ +// Submitted by Luca Casonato +deno.dev +deno-staging.dev +deno.net +sandbox.deno.net + +// DeployAgent : https://deployagent.com +// Submitted by Danny +deployagent.com +piebox.site +deployagent.space + +// deSEC : https://desec.io/ +// Submitted by Peter Thomassen +dedyn.io + +// Deuxfleurs : https://deuxfleurs.fr +// Submitted by Aeddis Desauw +deuxfleurs.eu +deuxfleurs.page + +// Developed Methods LLC : https://methods.dev +// Submitted by Patrick Lorio +*.at.ply.gg +d6.ply.gg +joinmc.link +playit.plus +*.at.playit.plus +with.playit.plus + +// Dfinity Foundation: https://dfinity.org/ +// Submitted by Dfinity Team +icp0.io +*.raw.icp0.io +icp1.io +*.raw.icp1.io +opencloud.me +*.icp.net +caffeine.site +caffeine.xyz + +// dhosting.pl Sp. z o.o. : https://dhosting.pl/ +// Submitted by Szczepan Redzioch +mybox.company +intouch.email +mybox.me +mybox.page +dfirma.pl +dkonto.pl +you2.pl + +// DigitalOcean App Platform : https://www.digitalocean.com/products/app-platform/ +// Submitted by Braxton Huggins +ondigitalocean.app + +// DigitalOcean Spaces : https://www.digitalocean.com/products/spaces/ +// Submitted by Robin H. Johnson +*.digitaloceanspaces.com + +// DigitalPlat : https://www.digitalplat.org/ +// Submitted by Edward Hsing +qzz.io +us.kg +xx.kg +dpdns.org + +// Discord Inc : https://discord.com +// Submitted by Sahn Lam +discordsays.com +discordsez.com + +// DNS Africa Ltd : https://dns.business +// Submitted by Calvin Browne +jozi.biz + +// DNSHE : https://www.dnshe.com +// Submitted by DNSHE Team +ccwu.cc +cc.cd +us.ci +de5.net + +// dnsHome : https://www.dnshome.de/ +// Submitted by Norbert Auler +dnshome.at +resolve.bar +ddns.berlin +dnshome.cloud +ddnssec.de +dnshome.de +dyndnssec.de +heimdns.de +srvdns.de +dnshome.eu +dnshome.it +dyn.now +heimdns.online +ddns.wtf + +// DotArai : https://www.dotarai.com/ +// Submitted by Atsadawat Netcharadsang +online.th +shop.th + +// dotScot Domains : https://domains.scot/ +// Submitted by DNS Team +co.scot +me.scot +org.scot + +// DrayTek Corp. : https://www.draytek.com/ +// Submitted by Paul Fang +drayddns.com + +// DreamCommerce : https://shoper.pl/ +// Submitted by Konrad Kotarba +shoparena.pl + +// DreamHost : http://www.dreamhost.com/ +// Submitted by Andrew Farmer +dreamhosters.com + +// Dreamyoungs, Inc. : https://durumis.com +// Submitted by Infra Team +durumis.com + +// DuckDNS : http://www.duckdns.org/ +// Submitted by Richard Harper +duckdns.org + +// dy.fi : http://dy.fi/ +// Submitted by Heikki Hannikainen +dy.fi +tunk.org + +// DynDNS.com : http://www.dyndns.com/services/dns/dyndns/ +dyndns.biz +for-better.biz +for-more.biz +for-some.biz +for-the.biz +selfip.biz +webhop.biz +ftpaccess.cc +game-server.cc +myphotos.cc +scrapping.cc +blogdns.com +cechire.com +dnsalias.com +dnsdojo.com +doesntexist.com +dontexist.com +doomdns.com +dyn-o-saur.com +dynalias.com +dyndns-at-home.com +dyndns-at-work.com +dyndns-blog.com +dyndns-free.com +dyndns-home.com +dyndns-ip.com +dyndns-mail.com +dyndns-office.com +dyndns-pics.com +dyndns-remote.com +dyndns-server.com +dyndns-web.com +dyndns-wiki.com +dyndns-work.com +est-a-la-maison.com +est-a-la-masion.com +est-le-patron.com +est-mon-blogueur.com +from-ak.com +from-al.com +from-ar.com +from-ca.com +from-ct.com +from-dc.com +from-de.com +from-fl.com +from-ga.com +from-hi.com +from-ia.com +from-id.com +from-il.com +from-in.com +from-ks.com +from-ky.com +from-ma.com +from-md.com +from-mi.com +from-mn.com +from-mo.com +from-ms.com +from-mt.com +from-nc.com +from-nd.com +from-ne.com +from-nh.com +from-nj.com +from-nm.com +from-nv.com +from-oh.com +from-ok.com +from-or.com +from-pa.com +from-pr.com +from-ri.com +from-sc.com +from-sd.com +from-tn.com +from-tx.com +from-ut.com +from-va.com +from-vt.com +from-wa.com +from-wi.com +from-wv.com +from-wy.com +getmyip.com +gotdns.com +hobby-site.com +homelinux.com +homeunix.com +iamallama.com +is-a-anarchist.com +is-a-blogger.com +is-a-bookkeeper.com +is-a-bulls-fan.com +is-a-caterer.com +is-a-chef.com +is-a-conservative.com +is-a-cpa.com +is-a-cubicle-slave.com +is-a-democrat.com +is-a-designer.com +is-a-doctor.com +is-a-financialadvisor.com +is-a-geek.com +is-a-green.com +is-a-guru.com +is-a-hard-worker.com +is-a-hunter.com +is-a-landscaper.com +is-a-lawyer.com +is-a-liberal.com +is-a-libertarian.com +is-a-llama.com +is-a-musician.com +is-a-nascarfan.com +is-a-nurse.com +is-a-painter.com +is-a-personaltrainer.com +is-a-photographer.com +is-a-player.com +is-a-republican.com +is-a-rockstar.com +is-a-socialist.com +is-a-student.com +is-a-teacher.com +is-a-techie.com +is-a-therapist.com +is-an-accountant.com +is-an-actor.com +is-an-actress.com +is-an-anarchist.com +is-an-artist.com +is-an-engineer.com +is-an-entertainer.com +is-certified.com +is-gone.com +is-into-anime.com +is-into-cars.com +is-into-cartoons.com +is-into-games.com +is-leet.com +is-not-certified.com +is-slick.com +is-uberleet.com +is-with-theband.com +isa-geek.com +isa-hockeynut.com +issmarterthanyou.com +likes-pie.com +likescandy.com +neat-url.com +saves-the-whales.com +selfip.com +sells-for-less.com +sells-for-u.com +servebbs.com +simple-url.com +space-to-rent.com +teaches-yoga.com +writesthisblog.com +ath.cx +fuettertdasnetz.de +isteingeek.de +istmein.de +lebtimnetz.de +leitungsen.de +traeumtgerade.de +barrel-of-knowledge.info +barrell-of-knowledge.info +dyndns.info +for-our.info +groks-the.info +groks-this.info +here-for-more.info +knowsitall.info +selfip.info +webhop.info +forgot.her.name +forgot.his.name +at-band-camp.net +blogdns.net +broke-it.net +buyshouses.net +dnsalias.net +dnsdojo.net +does-it.net +dontexist.net +dynalias.net +dynathome.net +endofinternet.net +from-az.net +from-co.net +from-la.net +from-ny.net +gets-it.net +ham-radio-op.net +homeftp.net +homeip.net +homelinux.net +homeunix.net +in-the-band.net +is-a-chef.net +is-a-geek.net +isa-geek.net +kicks-ass.net +office-on-the.net +podzone.net +scrapper-site.net +selfip.net +sells-it.net +servebbs.net +serveftp.net +thruhere.net +webhop.net +merseine.nu +mine.nu +shacknet.nu +blogdns.org +blogsite.org +boldlygoingnowhere.org +dnsalias.org +dnsdojo.org +doesntexist.org +dontexist.org +doomdns.org +dvrdns.org +dynalias.org +dyndns.org +go.dyndns.org +home.dyndns.org +endofinternet.org +endoftheinternet.org +from-me.org +game-host.org +gotdns.org +hobby-site.org +homedns.org +homeftp.org +homelinux.org +homeunix.org +is-a-bruinsfan.org +is-a-candidate.org +is-a-celticsfan.org +is-a-chef.org +is-a-geek.org +is-a-knight.org +is-a-linux-user.org +is-a-patsfan.org +is-a-soxfan.org +is-found.org +is-lost.org +is-saved.org +is-very-bad.org +is-very-evil.org +is-very-good.org +is-very-nice.org +is-very-sweet.org +isa-geek.org +kicks-ass.org +misconfused.org +podzone.org +readmyblog.org +selfip.org +sellsyourhome.org +servebbs.org +serveftp.org +servegame.org +stuff-4-sale.org +webhop.org +better-than.tv +dyndns.tv +on-the-web.tv +worse-than.tv +is-by.us +land-4-sale.us +stuff-4-sale.us +dyndns.ws +mypets.ws + +// Dynu.com : https://www.dynu.com/ +// Submitted by Sue Ye +1cooldns.com +bumbleshrimp.com +ddnsfree.com +ddnsgeek.com +ddnsguru.com +dynuddns.com +dynuhosting.com +giize.com +gleeze.com +kozow.com +loseyourip.com +ooguy.com +pivohosting.com +theworkpc.com +wiredbladehosting.com +casacam.net +dynu.net +dynuddns.net +mysynology.net +opik.net +spryt.net +accesscam.org +camdvr.org +freeddns.org +mywire.org +roxa.org +webredirect.org +myddns.rocks + +// dynv6 : https://dynv6.com +// Submitted by Dominik Menke +dynv6.net + +// E4YOU spol. s.r.o. : https://e4you.cz/ +// Submitted by Vladimir Dudr +e4.cz + +// Easypanel : https://easypanel.io +// Submitted by Andrei Canta +easypanel.app +easypanel.host + +// EasyWP : https://www.easywp.com +// Submitted by +*.ewp.live + +// eDirect Corp. : https://hosting.url.com.tw/ +// Submitted by C.S. chang +twmail.cc +twmail.net +twmail.org +mymailer.com.tw +url.tw + +// Electromagnetic Field : https://www.emfcamp.org +// Submitted by +at.emf.camp + +// Elefunc, Inc. : https://elefunc.com +// Submitted by Cetin Sert +rt.ht + +// Elementor : Elementor Ltd. +// Submitted by Anton Barkan +elementor.cloud +elementor.cool + +// Emergent : https://emergent.sh +// Submitted by Emergent Security Team +emergent.cloud +preview.emergentagent.com +emergent.host + +// Enalean SAS : https://www.enalean.com +// Submitted by Enalean Security Team +mytuleap.com +tuleap-partners.com + +// Encoretivity AB : https://encore.cloud +// Submitted by André Eriksson +encr.app +frontend.encr.app +encoreapi.com +lp.dev +api.lp.dev +objects.lp.dev + +// encoway GmbH : https://www.encoway.de +// Submitted by Marcel Daus +eu.encoway.cloud + +// EU.org : https://eu.org/ +// Submitted by Pierre Beyssac +eu.org +al.eu.org +asso.eu.org +at.eu.org +au.eu.org +be.eu.org +bg.eu.org +ca.eu.org +cd.eu.org +ch.eu.org +cn.eu.org +cy.eu.org +cz.eu.org +de.eu.org +dk.eu.org +edu.eu.org +ee.eu.org +es.eu.org +fi.eu.org +fr.eu.org +gr.eu.org +hr.eu.org +hu.eu.org +ie.eu.org +il.eu.org +in.eu.org +int.eu.org +is.eu.org +it.eu.org +jp.eu.org +kr.eu.org +lt.eu.org +lu.eu.org +lv.eu.org +me.eu.org +mk.eu.org +mt.eu.org +my.eu.org +net.eu.org +ng.eu.org +nl.eu.org +no.eu.org +nz.eu.org +pl.eu.org +pt.eu.org +ro.eu.org +ru.eu.org +se.eu.org +si.eu.org +sk.eu.org +tr.eu.org +uk.eu.org +us.eu.org + +// Eurobyte : https://eurobyte.ru +// Submitted by Evgeniy Subbotin +eurodir.ru + +// Evennode : http://www.evennode.com/ +// Submitted by Michal Kralik +eu-1.evennode.com +eu-2.evennode.com +eu-3.evennode.com +eu-4.evennode.com +us-1.evennode.com +us-2.evennode.com +us-3.evennode.com +us-4.evennode.com + +// Evervault : https://evervault.com +// Submitted by Hannah Neary +relay.evervault.app +relay.evervault.dev + +// Exe : https://exe.dev +// Submitted by Josh Bleecher Snyder +exe.xyz + +// Expo : https://expo.dev/ +// Submitted by Phil Pluckthun +expo.app +on.expo.app +staging.expo.app +on.staging.expo.app + +// fachschaften.org: https://fachschaften.org/ +// Submitted by Felix Schäfer +fspages.org + +// FAITID : https://faitid.org/ +// Submitted by Maxim Alzoba +// https://www.flexireg.net/stat_info +ru.net +adygeya.ru +bashkiria.ru +bir.ru +cbg.ru +com.ru +dagestan.ru +grozny.ru +kalmykia.ru +kustanai.ru +marine.ru +mordovia.ru +msk.ru +mytis.ru +nalchik.ru +nov.ru +pyatigorsk.ru +spb.ru +vladikavkaz.ru +vladimir.ru +abkhazia.su +adygeya.su +aktyubinsk.su +arkhangelsk.su +armenia.su +ashgabad.su +azerbaijan.su +balashov.su +bashkiria.su +bryansk.su +bukhara.su +chimkent.su +dagestan.su +east-kazakhstan.su +exnet.su +georgia.su +grozny.su +ivanovo.su +jambyl.su +kalmykia.su +kaluga.su +karacol.su +karaganda.su +karelia.su +khakassia.su +krasnodar.su +kurgan.su +kustanai.su +lenug.su +mangyshlak.su +mordovia.su +msk.su +murmansk.su +nalchik.su +navoi.su +north-kazakhstan.su +nov.su +obninsk.su +penza.su +pokrovsk.su +sochi.su +spb.su +tashkent.su +termez.su +togliatti.su +troitsk.su +tselinograd.su +tula.su +tuva.su +vladikavkaz.su +vladimir.su +vologda.su + +// Fancy Bits, LLC : http://getchannels.com +// Submitted by Aman Gupta +channelsdvr.net +u.channelsdvr.net + +// Fastly Inc. : http://www.fastly.com/ +// Submitted by Fastly Security +edgecompute.app +fastly-edge.com +fastly-terrarium.com +freetls.fastly.net +map.fastly.net +a.prod.fastly.net +global.prod.fastly.net +a.ssl.fastly.net +b.ssl.fastly.net +global.ssl.fastly.net +fastlylb.net +map.fastlylb.net + +// Fastmail : https://www.fastmail.com/ +// Submitted by Marc Bradshaw +*.user.fm + +// FASTVPS EESTI OU : https://fastvps.ru/ +// Submitted by Likhachev Vasiliy +fastvps-server.com +fastvps.host +myfast.host +fastvps.site +myfast.space + +// FearWorks Media Ltd. : https://fearworksmedia.co.uk +// Submitted by Keith Fairley +conn.uk +copro.uk +hosp.uk + +// Fedora : https://fedoraproject.org/ +// Submitted by Patrick Uiterwijk +fedorainfracloud.org +fedorapeople.org +cloud.fedoraproject.org +app.os.fedoraproject.org +app.os.stg.fedoraproject.org + +// Fermax : https://fermax.com/ +// Submitted by Koen Van Isterdael +mydobiss.com + +// FH Muenster : https://www.fh-muenster.de +// Submitted by Robin Naundorf +fh-muenster.io + +// Figma : https://www.figma.com +// Submitted by Nick Frost +payload.dev +figma.site +figma-gov.site +preview.site + +// Filegear Inc. : https://www.filegear.com +// Submitted by Jason Zhu +filegear.me + +// Firebase, Inc. +// Submitted by Chris Raynor +firebaseapp.com + +// FlashDrive : https://flashdrive.io +// Submitted by Eric Chan +fldrv.com + +// Fleek Labs Inc : https://fleek.xyz +// Submitted by Parsa Ghadimi +on-fleek.app + +// FlutterFlow : https://flutterflow.io +// Submitted by Anton Emelyanov +flutterflow.app + +// fly.io : https://fly.io +// Submitted by Kurt Mackey +sprites.app +fly.dev + +// FoundryLabs, Inc : https://e2b.dev/ +// Submitted by Jiri Sveceny +e2b.app + +// Framer : https://www.framer.com +// Submitted by Koen Rouwhorst +framer.ai +framer.app +framercanvas.com +framer.media +framer.photos +framer.website +framer.wiki + +// Frederik Braun : https://frederik-braun.com +// Submitted by Frederik Braun +*.0e.vc + +// Freebox : http://www.freebox.fr +// Submitted by Romain Fliedel +freebox-os.com +freeboxos.com +fbx-os.fr +fbxos.fr +freebox-os.fr +freeboxos.fr + +// freedesktop.org : https://www.freedesktop.org +// Submitted by Daniel Stone +freedesktop.org + +// freemyip.com : https://freemyip.com +// Submitted by Cadence +freemyip.com + +// Frusky MEDIA&PR : https://www.frusky.de +// Submitted by Victor Pupynin +*.frusky.de + +// FunkFeuer - Verein zur Förderung freier Netze : https://www.funkfeuer.at +// Submitted by Daniel A. Maierhofer +wien.funkfeuer.at + +// Future Versatile Group. : https://www.fvg-on.net/ +// T.Kabu +daemon.asia +dix.asia +mydns.bz +0am.jp +0g0.jp +0j0.jp +0t0.jp +mydns.jp +pgw.jp +wjg.jp +keyword-on.net +live-on.net +server-on.net +mydns.tw +mydns.vc + +// Futureweb GmbH : https://www.futureweb.at +// Submitted by Andreas Schnederle-Wagner +*.futurecms.at +*.ex.futurecms.at +*.in.futurecms.at +futurehosting.at +futuremailing.at +*.ex.ortsinfo.at +*.kunden.ortsinfo.at +*.statics.cloud + +// Gadget Software Inc. : https://gadget.dev +// Submitted by Harry Brundage +gadget.app +gadget.host + +// GCom Internet : https://www.gcom.net.au +// Submitted by Leo Julius +aliases121.com + +// GDS : https://www.gov.uk/service-manual/technology/managing-domain-names +// Submitted by Stephen Ford +campaign.gov.uk +service.gov.uk +independent-commission.uk +independent-inquest.uk +independent-inquiry.uk +independent-panel.uk +independent-review.uk +public-inquiry.uk +royal-commission.uk + +// Gehirn Inc. : https://www.gehirn.co.jp/ +// Submitted by Kohei YOSHIDA +gehirn.ne.jp +usercontent.jp + +// Gentlent, Inc. : https://www.gentlent.com +// Submitted by Tom Klein +gentapps.com +gentlentapis.com +cdn-edges.net + +// GignoSystemJapan : http://gsj.bz +// Submitted by GignoSystemJapan +gsj.bz + +// GitBook Inc. : https://www.gitbook.com/ +// Submitted by Samy Pesse +gitbook.io + +// GitHub, Inc. +// Submitted by Patrick Toomey +github.app +githubusercontent.com +githubpreview.dev +github.io + +// GitLab, Inc. : https://about.gitlab.com/ +// Submitted by Alex Hanselka +gitlab.io + +// Gitplac.si : https://gitplac.si +// Submitted by Aljaž Starc +gitapp.si +gitpage.si + +// Global NOG Alliance : https://nogalliance.org/ +// Submitted by Sander Steffann +nog.community + +// Globe Hosting SRL : https://www.globehosting.com/ +// Submitted by Gavin Brown +co.ro +shop.ro + +// GMO Pepabo, Inc. : https://pepabo.com/ +// Submitted by Hosting Div +lolipop.io +angry.jp +babyblue.jp +babymilk.jp +backdrop.jp +bambina.jp +bitter.jp +blush.jp +boo.jp +boy.jp +boyfriend.jp +but.jp +candypop.jp +capoo.jp +catfood.jp +cheap.jp +chicappa.jp +chillout.jp +chips.jp +chowder.jp +chu.jp +ciao.jp +cocotte.jp +coolblog.jp +cranky.jp +cutegirl.jp +daa.jp +deca.jp +deci.jp +digick.jp +egoism.jp +fakefur.jp +fem.jp +flier.jp +floppy.jp +fool.jp +frenchkiss.jp +girlfriend.jp +girly.jp +gloomy.jp +gonna.jp +greater.jp +hacca.jp +heavy.jp +her.jp +hiho.jp +hippy.jp +holy.jp +hungry.jp +icurus.jp +itigo.jp +jellybean.jp +kikirara.jp +kill.jp +kilo.jp +kuron.jp +littlestar.jp +lolipopmc.jp +lolitapunk.jp +lomo.jp +lovepop.jp +lovesick.jp +main.jp +mods.jp +mond.jp +mongolian.jp +moo.jp +namaste.jp +nikita.jp +nobushi.jp +noor.jp +oops.jp +parallel.jp +parasite.jp +pecori.jp +peewee.jp +penne.jp +pepper.jp +perma.jp +pigboat.jp +pinoko.jp +punyu.jp +pupu.jp +pussycat.jp +pya.jp +raindrop.jp +readymade.jp +sadist.jp +schoolbus.jp +secret.jp +staba.jp +stripper.jp +sub.jp +sunnyday.jp +thick.jp +tonkotsu.jp +under.jp +upper.jp +velvet.jp +verse.jp +versus.jp +vivian.jp +watson.jp +weblike.jp +whitesnow.jp +zombie.jp +heteml.net + +// GNTC, Inc. : https://gntc.com/ +// Submitted by VibeHost Security +vibehost.space + +// GoDaddy Registry : https://registry.godaddy +// Submitted by Rohan Durrant +graphic.design + +// GoIP DNS Services : http://www.goip.de +// Submitted by Christian Poulter +goip.de + +// Google, Inc. +// Submitted by Shannon McCabe +*.hosted.app +*.run.app +*.mtls.run.app +web.app +*.0emm.com +appspot.com +*.r.appspot.com +blogspot.com +codespot.com +googleapis.com +googlecode.com +pagespeedmobilizer.com +withgoogle.com +withyoutube.com +*.gateway.dev +cloud.goog +translate.goog +*.usercontent.goog +cloudfunctions.net +cloud.run +ai.studio + +// Goupile : https://goupile.fr +// Submitted by Niels Martignene +goupile.fr + +// GOV.UK Pay : https://www.payments.service.gov.uk/ +// Submitted by Richard Baker +pymnt.uk + +// Government of the Netherlands : https://www.government.nl +// Submitted by +gov.nl + +// Grafana Labs : https://grafana.com/ +// Submitted by Platform Engineering +grafana-dev.net + +// GrayJay Web Solutions Inc. : https://grayjaysports.ca +// Submitted by Matt Yamkowy +grayjayleagues.com + +// Grebedoc : https://grebedoc.dev +// Submitted by Catherine Zotova +grebedoc.dev + +// GünstigBestellen : https://günstigbestellen.de +// Submitted by Furkan Akkoc +günstigbestellen.de +günstigliefern.de + +// GV.UY : https://nic.gv.uy +// Submitted by cheng +gv.uy + +// Hackclub Nest : https://hackclub.app +// Submitted by Cyteon +hackclub.app + +// Häkkinen.fi : https://www.häkkinen.fi/ +// Submitted by Eero Häkkinen +häkkinen.fi + +// Hashbang : https://hashbang.sh +hashbang.sh + +// Hasura : https://hasura.io +// Submitted by Shahidh K Muhammed +hasura.app +hasura-app.io + +// Hatena Co., Ltd. : https://hatena.co.jp +// Submitted by Masato Nakamura +hatenablog.com +hatenadiary.com +hateblo.jp +hatenablog.jp +hatenadiary.jp +hatenadiary.org + +// Heilbronn University of Applied Sciences - Faculty Informatics (GitLab Pages) : https://www.hs-heilbronn.de +// Submitted by Richard Zowalla +pages.it.hs-heilbronn.de +pages-research.it.hs-heilbronn.de + +// HeiyuSpace : https://lazycat.cloud +// Submitted by Xia Bin +heiyu.space + +// Helio Networks : https://heliohost.org +// Submitted by Ben Frede +helioho.st +heliohost.us + +// Hepforge : https://www.hepforge.org +// Submitted by David Grellscheid +hepforge.org + +// Hercules : https://hercules.app +// Submitted by Brendan Falk +onhercules.app +hercules-app.com +hercules-dev.com + +// here.now : https://here.now/ +// Submitted by Adam Ludwin +here.now + +// Heroku : https://www.heroku.com/ +// Submitted by Shumon Huque +herokuapp.com + +// Heyflow : https://www.heyflow.com +// Submitted by Mirko Nitschke +heyflow.page +heyflow.site + +// Hibernating Rhinos +// Submitted by Oren Eini +ravendb.cloud +ravendb.community +development.run +ravendb.run + +// HiDNS : https://www.hidoha.net +// Submitted by ifeng +hidns.co +hidns.vip + +// home.pl S.A. : https://home.pl +// Submitted by Krzysztof Wolski +homesklep.pl + +// Homebase : https://homebase.id/ +// Submitted by Jason Babo +*.kin.one +*.id.pub +*.kin.pub + +// HOOC AG : https://www.hooc.ch +// Submitted by Fabrizio Steiner +seprox.hooc.me + +// Hoplix : https://www.hoplix.com +// Submitted by Danilo De Franco +hoplix.shop + +// HOSTBIP REGISTRY : https://www.hostbip.com/ +// Submitted by Atanunu Igbunuroghene +orx.biz +biz.ng +co.biz.ng +dl.biz.ng +go.biz.ng +lg.biz.ng +on.biz.ng +col.ng +firm.ng +gen.ng +ltd.ng +ngo.ng +plc.ng + +// Hostinger : https://hostinger.com +// Submitted by Valentinas Cirba +hstgr.cloud + +// HostyHosting : https://hostyhosting.com +hostyhosting.io + +// Hugging Face : https://huggingface.co +// Submitted by Eliott Coyac +hf.space +static.hf.space + +// Hypernode B.V. : https://www.hypernode.com/ +// Submitted by Cipriano Groenendal +hypernode.io + +// I-O DATA DEVICE, INC. : http://www.iodata.com/ +// Submitted by Yuji Minagawa +iobb.net + +// i-registry s.r.o. : http://www.i-registry.cz/ +// Submitted by Martin Semrad +co.cz + +// Ici la Lune : http://www.icilalune.com/ +// Submitted by Simon Morvan +*.moonscale.io +moonscale.net + +// iDOT Services Limited : http://www.domain.gr.com +// Submitted by Gavin Brown +gr.com + +// iki.fi +// Submitted by Hannu Aronsson +iki.fi + +// iliad italia : https://www.iliad.it +// Submitted by Marios Makassikis +ibxos.it +iliadboxos.it + +// Imagine : https://imagine.dev +// Submitted by Steven Nguyen +imagine.diy +imagine-proxy.work + +// Incsub, LLC : https://incsub.com/ +// Submitted by Aaron Edwards +smushcdn.com +wphostedmail.com +wpmucdn.com +tempurl.host +wpmudev.host + +// Individual Network Berlin e.V. : https://www.in-berlin.de/ +// Submitted by Christian Seitz +dyn-berlin.de +in-berlin.de +in-brb.de +in-butter.de +in-dsl.de +in-vpn.de +in-dsl.net +in-vpn.net +in-dsl.org +in-vpn.org + +// Inferno Communications : https://inferno.co.uk +// Submitted by Connor McFarlane +oninferno.net + +// info.cx : http://info.cx +// Submitted by June Slater +info.cx + +// Interlegis : http://www.interlegis.leg.br +// Submitted by Gabriel Ferreira +ac.leg.br +al.leg.br +am.leg.br +ap.leg.br +ba.leg.br +ce.leg.br +df.leg.br +es.leg.br +go.leg.br +ma.leg.br +mg.leg.br +ms.leg.br +mt.leg.br +pa.leg.br +pb.leg.br +pe.leg.br +pi.leg.br +pr.leg.br +rj.leg.br +rn.leg.br +ro.leg.br +rr.leg.br +rs.leg.br +sc.leg.br +se.leg.br +sp.leg.br +to.leg.br + +// intermetrics GmbH : https://pixolino.com/ +// Submitted by Wolfgang Schwarz +pixolino.com + +// Internet-Pro, LLP : https://netangels.ru/ +// Submitted by Vasiliy Sheredeko +na4u.ru + +// Inventor Services : https://inventor.gg/ +// Submitted by Inventor Team +botdash.app +botdash.dev +botdash.gg +botdash.net +botda.sh +botdash.xyz + +// IONOS SE : https://www.ionos.com/ +// IONOS Group SE : https://www.ionos-group.com/ +// Submitted by Anton Mehlmann +online-server.cloud +apps-1and1.com +live-website.com +webspace-host.com +apps-1and1.net +websitebuilder.online +app-ionos.space + +// iopsys software solutions AB : https://iopsys.eu/ +// Submitted by Roman Azarenko +iopsys.se + +// IPFS Project : https://ipfs.tech/ +// Submitted by Interplanetary Shipyard +*.inbrowser.dev +*.dweb.link +*.inbrowser.link + +// IPiFony Systems, Inc. : https://www.ipifony.com/ +// Submitted by Matthew Hardeman +ipifony.net + +// IPv64.net : https://ipv64.net/ +// Submitted by Dennis Schröder +home64.de +ipv64.de +ipv64.net + +// ir.md : https://nic.ir.md +// Submitted by Ali Soizi +ir.md + +// is-a-good.dev : https://is-a-good.dev +// Submitted by William Harrison +is-a-good.dev + +// IServ GmbH : https://iserv.de +// Submitted by Kim Brodowski +iservschule.de +mein-iserv.de +schuldock.de +schulplattform.de +schulserver.de +test-iserv.de +iserv.dev +iserv.host + +// Ispmanager : https://www.ispmanager.com/ +// Submitted by Ispmanager infrastructure team +ispmanager.name + +// Jelastic, Inc. : https://jelastic.com/ +// Submitted by Ihor Kolodyuk +mel.cloudlets.com.au +cloud.interhostsolutions.be +alp1.ae.flow.ch +appengine.flow.ch +es-1.axarnet.cloud +diadem.cloud +vip.jelastic.cloud +jele.cloud +it1.eur.aruba.jenv-aruba.cloud +it1.jenv-aruba.cloud +keliweb.cloud +cs.keliweb.cloud +oxa.cloud +tn.oxa.cloud +uk.oxa.cloud +primetel.cloud +uk.primetel.cloud +ca.reclaim.cloud +uk.reclaim.cloud +us.reclaim.cloud +ch.trendhosting.cloud +de.trendhosting.cloud +jele.club +dopaas.com +paas.hosted-by-previder.com +rag-cloud.hosteur.com +rag-cloud-ch.hosteur.com +jcloud.ik-server.com +jcloud-ver-jpc.ik-server.com +demo.jelastic.com +paas.massivegrid.com +jed.wafaicloud.com +ryd.wafaicloud.com +j.scaleforce.com.cy +jelastic.dogado.eu +fi.cloudplatform.fi +jele.host +mircloud.host +paas.beebyte.io +sekd1.beebyteapp.io +jele.io +jc.neen.it +jcloud.kz +cloudjiffy.net +fra1-de.cloudjiffy.net +west1-us.cloudjiffy.net +jls-sto1.elastx.net +jls-sto2.elastx.net +jls-sto3.elastx.net +fr-1.paas.massivegrid.net +lon-1.paas.massivegrid.net +lon-2.paas.massivegrid.net +ny-1.paas.massivegrid.net +ny-2.paas.massivegrid.net +sg-1.paas.massivegrid.net +jelastic.saveincloud.net +nordeste-idc.saveincloud.net +j.scaleforce.net +sdscloud.pl +unicloud.pl +mircloud.ru +enscaled.sg +jele.site +jelastic.team +orangecloud.tn +j.layershift.co.uk +phx.enscaled.us +mircloud.us + +// Jino : https://www.jino.ru +// Submitted by Sergey Ulyashin +myjino.ru +*.hosting.myjino.ru +*.landing.myjino.ru +*.spectrum.myjino.ru +*.vps.myjino.ru + +// Jotelulu S.L. : https://jotelulu.com +// Submitted by Daniel Fariña +jote.cloud +jotelulu.cloud +eu1-plenit.com +la1-plenit.com +us1-plenit.com + +// JouwWeb B.V. : https://www.jouwweb.nl +// Submitted by Camilo Sperberg +webadorsite.com +jouwweb.site + +// JS.ORG : http://dns.js.org +// Submitted by Stefan Keim +js.org + +// K2 Cloud : https://k2.cloud/ +// Submitted by K2 Cloud +elastic.k2.cloud +lb.ru-msk.k2.cloud +s3.ru-msk.k2.cloud +website.ru-msk.k2.cloud +lb.ru-spb.k2.cloud +s3.ru-spb.k2.cloud +website.ru-spb.k2.cloud +s3.k2.cloud +website.k2.cloud + +// KaasHosting : http://www.kaashosting.nl/ +// Submitted by Wouter Bakker +kaas.gg +khplay.nl + +// Kapsi : https://kapsi.fi +// Submitted by Tomi Juntunen +kapsi.fi + +// KataBump : https://katabump.com +// Submitted by Thibault Lapeyre +kdns.fr + +// Katholieke Universiteit Leuven : https://www.kuleuven.be +// Submitted by Abuse KU Leuven +ezproxy.kuleuven.be +kuleuven.cloud + +// Keenetic : https://keenetic.com +// Submitted by Alexey Nikitin +keenetic.io +keenetic.link +keenetic.name +keenetic.pro + +// Kevin Service : https://kevsrv.me +// Submitted by Kevin Service Team +ae.kg + +// Keyweb AG : https://www.keyweb.de +// Submitted by Martin Dannehl +keymachine.de + +// Kilo Code, Inc. : https://kilo.ai +// Submitted by Remon Oldenbeuving +kiloapps.ai +kiloapps.io + +// KingHost : https://king.host +// Submitted by Felipe Keller Braz +kinghost.net +uni5.net + +// KnightPoint Systems, LLC : http://www.knightpoint.com/ +// Submitted by Roy Keene +knightpoint.systems + +// KoobinEvent, SL : https://www.koobin.com +// Submitted by Iván Oliva +koobin.events + +// Krellian Ltd. : https://krellian.com +// Submitted by Ben Francis +webthings.io +krellian.net + +// KUROKU LTD : https://kuroku.ltd/ +// Submitted by DisposaBoy +oya.to + +// KV GmbH : https://www.nic.co.de +// Submitted by KV GmbH +// Abuse reports to +co.de + +// Laravel Holdings, Inc. : https://laravel.com +// Submitted by André Valentin & James Brooks +shiptoday.app +shiptoday.build +laravel.cloud +on-forge.com +on-vapor.com + +// Last Mile Labs, Inc : https://eth.limo +// Submitted by eth.limo team +*.eth.limo +*.eth.link + +// LCube - Professional hosting e.K. : https://www.lcube-webhosting.de +// Submitted by Lars Laehn +git-repos.de +lcube-server.de +svn-repos.de + +// Leadpages : https://www.leadpages.net +// Submitted by Greg Dallavalle +leadpages.co +lpages.co +lpusercontent.com + +// Leapcell : https://leapcell.io/ +// Submitted by Leapcell Team +leapcell.app +leapcell.dev +leapcell.online + +// Liara : https://liara.ir +// Submitted by Amirhossein Badinloo +liara.run +iran.liara.run + +// libp2p project : https://libp2p.io +// Submitted by Interplanetary Shipyard +libp2p.direct + +// Libre IT Ltd : https://libre.nz +// Submitted by Tomas Maggio +runcontainers.dev + +// Lifetime Hosting : https://Lifetime.Hosting/ +// Submitted by Mike Fillator +co.business +co.education +co.events +co.financial +co.network +co.place +co.technology + +// linkyard ldt : https://www.linkyard.ch/ +// Submitted by Mario Siegenthaler +linkyard-cloud.ch +linkyard.cloud + +// Linode : https://linode.com +// Submitted by +members.linode.com +*.nodebalancer.linode.com +*.linodeobjects.com +ip.linodeusercontent.com + +// LiquidNet Ltd : http://www.liquidnetlimited.com/ +// Submitted by Victor Velchev +we.bs + +// Listen53 : https://www.l53.net +// Submitted by Gerry Keh +filegear-sg.me +ggff.net + +// Localcert : https://localcert.dev +// Submitted by Lann Martin +*.user.localcert.dev + +// Localtonet : https://localtonet.com/ +// Submitted by Burak Isleyici +localtonet.com +*.localto.net + +// Lodz University of Technology LODMAN regional domains : https://www.man.lodz.pl/dns +// Submitted by Piotr Wilk +lodz.pl +pabianice.pl +plock.pl +sieradz.pl +skierniewice.pl +zgierz.pl + +// Log'in Line : https://www.loginline.com/ +// Submitted by Rémi Mach +loginline.app +loginline.dev +loginline.io +loginline.services +loginline.site + +// Lõhmus Family, The : https://lohmus.me/ +// Submitted by Heiki Lõhmus +lohmus.me + +// Lovable : https://lovable.dev +// Submitted by Fabian Hedin +lovable.app +lovableproject.com +lovable.run +lovable.sh + +// LubMAN UMCS Sp. z o.o : https://lubman.pl/ +// Submitted by Ireneusz Maliszewski +krasnik.pl +leczna.pl +lubartow.pl +lublin.pl +poniatowa.pl +swidnik.pl + +// Lug.org.uk : https://lug.org.uk +// Submitted by Jon Spriggs +glug.org.uk +lug.org.uk +lugs.org.uk + +// Lukanet Ltd : https://lukanet.com +// Submitted by Anton Avramov +barsy.bg +barsy.club +barsycenter.com +barsyonline.com +barsy.de +barsy.dev +barsy.eu +barsy.gr +barsy.in +barsy.info +barsy.io +barsy.me +barsy.menu +barsyonline.menu +barsy.mobi +barsy.net +barsy.online +barsy.org +barsy.pro +barsy.pub +barsy.ro +barsy.rs +barsy.shop +barsyonline.shop +barsy.site +barsy.store +barsy.support +barsy.uk +barsy.co.uk +barsyonline.co.uk + +// Lutra : https://lutra.ai +// Submitted by Joshua Newman +*.lutrausercontent.com + +// Luyani Inc. : https://luyani.com/ +// Submitted by Umut Gumeli +luyani.app +luyani.net + +// Magento Commerce +// Submitted by Damien Tournoud +*.magentosite.cloud + +// Magic Patterns : https://www.magicpatterns.com +// Submitted by Teddy Ni +magicpatterns.app +magicpatternsapp.com + +// Mail.Ru Group : https://hb.cldmail.ru +// Submitted by Ilya Zaretskiy +hb.cldmail.ru + +// MathWorks : https://www.mathworks.com/ +// Submitted by Emily Reed +matlab.cloud +modelscape.com +mwcloudnonprod.com +polyspace.com + +// May First - People Link : https://mayfirst.org/ +// Submitted by Jamie McClelland +mayfirst.info + +// McHost : https://mchost.ru +// Submitted by Evgeniy Subbotin +mcdir.me +mcdir.ru +vps.mcdir.ru +mcpre.ru + +// Mediatech : https://mediatech.by +// Submitted by Evgeniy Kozhuhovskiy +mediatech.by +mediatech.dev + +// Medicom Health : https://medicomhealth.com +// Submitted by Michael Olson +hra.health + +// MedusaJS, Inc : https://medusajs.com/ +// Submitted by Stevche Radevski +medusajs.app + +// Memset hosting : https://www.memset.com +// Submitted by Tom Whitwell +miniserver.com +memset.net + +// Messerli Informatik AG : https://www.messerli.ch/ +// Submitted by Ruben Schmidmeister +messerli.app + +// Meta Platforms, Inc. : https://meta.com/ +// Submitted by Jacob Cordero +atmeta.com +apps.fbsbx.com +*.metaaiusercontent.com + +// MetaCentrum, CESNET z.s.p.o. : https://www.metacentrum.cz/en/ +// Submitted by Zdeněk Šustr and Radim Janča +*.cloud.metacentrum.cz +custom.metacentrum.cz +flt.cloud.muni.cz +usr.cloud.muni.cz + +// Meteor Development Group : https://www.meteor.com/hosting +// Submitted by Pierre Carrier +meteorapp.com +eu.meteorapp.com + +// Michau Enterprises Limited : http://www.co.pl/ +co.pl + +// Microsoft Corporation : http://microsoft.com +// Submitted by Public Suffix List Admin +// Managed by Corporate Domains +// Microsoft Azure : https://home.azure +*.azurecontainer.io +azure-api.net +azure-mobile.net +azureedge.net +azurefd.net +azurestaticapps.net +1.azurestaticapps.net +2.azurestaticapps.net +3.azurestaticapps.net +4.azurestaticapps.net +5.azurestaticapps.net +6.azurestaticapps.net +7.azurestaticapps.net +centralus.azurestaticapps.net +eastasia.azurestaticapps.net +eastus2.azurestaticapps.net +westeurope.azurestaticapps.net +westus2.azurestaticapps.net +azurewebsites.net +australiacentral-01.azurewebsites.net +australiacentral2-01.azurewebsites.net +australiaeast-01.azurewebsites.net +australiasoutheast-01.azurewebsites.net +austriaeast-01.azurewebsites.net +belgiumcentral-01.azurewebsites.net +brazilsouth-01.azurewebsites.net +brazilsoutheast-01.azurewebsites.net +canadacentral-01.azurewebsites.net +canadaeast-01.azurewebsites.net +centralindia-01.azurewebsites.net +centralus-01.azurewebsites.net +centraluseuap-01.azurewebsites.net +chilecentral-01.azurewebsites.net +denmarkeast-01.azurewebsites.net +eastasia-01.azurewebsites.net +eastasiastage-01.azurewebsites.net +eastus-01.azurewebsites.net +eastus2-01.azurewebsites.net +eastus2euap-01.azurewebsites.net +eastus3-01.azurewebsites.net +francecentral-01.azurewebsites.net +francesouth-01.azurewebsites.net +germanynorth-01.azurewebsites.net +germanywestcentral-01.azurewebsites.net +indiasouthcentral-01.azurewebsites.net +indonesiacentral-01.azurewebsites.net +israelcentral-01.azurewebsites.net +israelnorthwest-01.azurewebsites.net +italynorth-01.azurewebsites.net +japaneast-01.azurewebsites.net +japanwest-01.azurewebsites.net +jioindiacentral-01.azurewebsites.net +jioindiawest-01.azurewebsites.net +koreacentral-01.azurewebsites.net +koreasouth-01.azurewebsites.net +malaysiawest-01.azurewebsites.net +mexicocentral-01.azurewebsites.net +newzealandnorth-01.azurewebsites.net +northcentralus-01.azurewebsites.net +northcentralusstage-01.azurewebsites.net +northeastus5-01.azurewebsites.net +northeurope-01.azurewebsites.net +norwayeast-01.azurewebsites.net +norwaywest-01.azurewebsites.net +*.p.azurewebsites.net +polandcentral-01.azurewebsites.net +qatarcentral-01.azurewebsites.net +southafricanorth-01.azurewebsites.net +southafricawest-01.azurewebsites.net +southcentralus-01.azurewebsites.net +southcentralus2-01.azurewebsites.net +southeastasia-01.azurewebsites.net +southeastus5-01.azurewebsites.net +southindia-01.azurewebsites.net +spaincentral-01.azurewebsites.net +swedencentral-01.azurewebsites.net +swedensouth-01.azurewebsites.net +switzerlandnorth-01.azurewebsites.net +switzerlandwest-01.azurewebsites.net +taiwannorth-01.azurewebsites.net +taiwannorthwest-01.azurewebsites.net +uaecentral-01.azurewebsites.net +uaenorth-01.azurewebsites.net +uksouth-01.azurewebsites.net +ukwest-01.azurewebsites.net +westcentralus-01.azurewebsites.net +westeurope-01.azurewebsites.net +westindia-01.azurewebsites.net +westus-01.azurewebsites.net +westus2-01.azurewebsites.net +westus3-01.azurewebsites.net +cloudapp.net +trafficmanager.net +blob.core.usgovcloudapi.net +file.core.usgovcloudapi.net +web.core.usgovcloudapi.net +servicebus.usgovcloudapi.net +usgovcloudapp.net +usgovtrafficmanager.net +blob.core.windows.net +file.core.windows.net +web.core.windows.net +servicebus.windows.net +azure-api.us +azurewebsites.us + +// MikroTik : https://mikrotik.com +// Submitted by MikroTik SysAdmin Team +routingthecloud.com +sn.mynetname.net +routingthecloud.net +routingthecloud.org + +// Million Software, Inc : https://million.dev/ +// Submitted by Rayhan Noufal Arayilakath +same-app.com +same-preview.com + +// minion.systems : http://minion.systems +// Submitted by Robert Böttinger +csx.cc + +// Miren, Inc. : https://miren.dev +// Submitted by Miren Product Team +miren.app +miren.systems + +// Mittwald CM Service GmbH & Co. KG : https://mittwald.de +// Submitted by Marco Rieger +mydbserver.com +webspaceconfig.de +mittwald.info +mittwaldserver.info +typo3server.info +project.space + +// MKM : https://mkm.fan/ +// Submitted by Kashi Ahmer +mkm.fan + +// Mocha : https://getmocha.com +// Submitted by Ben Reinhart +mocha.app +mochausercontent.com +mocha-sandbox.dev + +// MODX Systems LLC : https://modx.com +// Submitted by Elizabeth Southwell +modx.dev + +// Mozilla Foundation : https://mozilla.org/ +// Submitted by glob +bmoattachments.org + +// MSK-IX : https://www.msk-ix.ru/ +// Submitted by Khannanov Roman +net.ru +org.ru +pp.ru + +// MyOwn srl : https://www.myown.eu/ +// Submitted by Stephane Bouvard +my.be + +// Mythic Beasts : https://www.mythic-beasts.com +// Submitted by Paul Cammish +hostedpi.com +caracal.mythic-beasts.com +customer.mythic-beasts.com +fentiger.mythic-beasts.com +lynx.mythic-beasts.com +ocelot.mythic-beasts.com +oncilla.mythic-beasts.com +onza.mythic-beasts.com +sphinx.mythic-beasts.com +vs.mythic-beasts.com +x.mythic-beasts.com +yali.mythic-beasts.com +cust.retrosnub.co.uk + +// Nabu Casa : https://www.nabucasa.com +// Submitted by Paulus Schoutsen +ui.nabu.casa + +// Needle Tools GmbH : https://needle.tools +// Submitted by Felix Herbst +needle.run + +// Neo : https://www.neo.space +// Submitted by Ankit Kulkarni +co.site + +// Net at Work Gmbh : https://www.netatwork.de +// Submitted by Jan Jaeschke +cloud.nospamproxy.com +o365.cloud.nospamproxy.com + +// Net libre : https://www.netlib.re +// Submitted by Philippe PITTOLI +netlib.re + +// Netlify : https://www.netlify.com +// Submitted by Jessica Parsons +netlify.app + +// Neustar Inc. +// Submitted by Trung Tran +4u.com + +// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/ +// Submitted by Jeff Wheelhouse +nfshost.com + +// NFT.Storage : https://nft.storage/ +// Submitted by Vasco Santos or +ipfs.nftstorage.link + +// NGO.US Registry : https://nic.ngo.us +// Submitted by Alstra Solutions Ltd. Networking Team +ngo.us + +// ngrok : https://ngrok.com/ +// Submitted by Alan Shreve +ngrok.app +ngrok-free.app +ngrok.dev +ngrok-free.dev +ngrok.io +ap.ngrok.io +au.ngrok.io +eu.ngrok.io +in.ngrok.io +jp.ngrok.io +sa.ngrok.io +us.ngrok.io +ngrok.pizza +ngrok.pro + +// Nicolaus Copernicus University in Torun - MSK TORMAN : https://www.man.torun.pl +torun.pl + +// Nimbus Hosting Ltd. : https://www.nimbushosting.co.uk/ +// Submitted by Nicholas Ford +nh-serv.co.uk +nimsite.uk + +// No-IP.com : https://noip.com/ +// Submitted by Deven Reza +mmafan.biz +myftp.biz +no-ip.biz +no-ip.ca +fantasyleague.cc +gotdns.ch +3utilities.com +blogsyte.com +ciscofreak.com +damnserver.com +ddnsking.com +ditchyourip.com +dnsiskinky.com +dynns.com +geekgalaxy.com +health-carereform.com +homesecuritymac.com +homesecuritypc.com +myactivedirectory.com +mysecuritycamera.com +myvnc.com +net-freaks.com +onthewifi.com +point2this.com +quicksytes.com +securitytactics.com +servebeer.com +servecounterstrike.com +serveexchange.com +serveftp.com +servegame.com +servehalflife.com +servehttp.com +servehumour.com +serveirc.com +servemp3.com +servep2p.com +servepics.com +servequake.com +servesarcasm.com +stufftoread.com +unusualperson.com +workisboring.com +dvrcam.info +ilovecollege.info +no-ip.info +brasilia.me +ddns.me +dnsfor.me +hopto.me +loginto.me +noip.me +webhop.me +bounceme.net +ddns.net +eating-organic.net +mydissent.net +myeffect.net +mymediapc.net +mypsx.net +mysecuritycamera.net +nhlfan.net +no-ip.net +pgafan.net +privatizehealthinsurance.net +redirectme.net +serveblog.net +serveminecraft.net +sytes.net +cable-modem.org +collegefan.org +couchpotatofries.org +hopto.org +mlbfan.org +myftp.org +mysecuritycamera.org +nflfan.org +no-ip.org +read-books.org +ufcfan.org +zapto.org +no-ip.co.uk +golffan.us +noip.us +pointto.us + +// NodeArt : https://nodeart.io +// Submitted by Konstantin Nosov +stage.nodeart.io + +// Noop : https://noop.app +// Submitted by Nathaniel Schweinberg +*.developer.app +noop.app + +// Northflank Ltd. : https://northflank.com/ +// Submitted by Marco Suter +*.northflank.app +*.build.run +*.code.run +*.database.run +*.migration.run + +// Northwest Nexus dba NuOz : https://nuoz.net/ +// An RFC 1480 locality domain delegate host +// Submitted by Peter Briggs on behalf of NuOz +aberdeen.wa.us +bainbridge-isl.wa.us +bellevue.wa.us +bremerton.wa.us +centralia.wa.us +chehalis.wa.us +forks.wa.us +gig-harbor.wa.us +hoquiam.wa.us +keyport.wa.us +kingston.wa.us +olympia.wa.us +port-angeles.wa.us +port-ludlow.wa.us +port-orchard.wa.us +port-townsend.wa.us +poulsbo.wa.us +redmond.wa.us +renton.wa.us +sea.wa.us +seattle.wa.us +sequim.wa.us +shelton.wa.us +silverdale.wa.us +yarrow-point.wa.us + +// Noticeable : https://noticeable.io +// Submitted by Laurent Pellegrino +noticeable.news + +// Notion Labs, Inc : https://www.notion.so/ +// Submitted by Jess Yao +notion.site + +// Now-DNS : https://now-dns.com +// Submitted by Steve Russell +dnsking.ch +mypi.co +myiphost.com +forumz.info +soundcast.me +tcp4.me +dnsup.net +hicam.net +now-dns.net +ownip.net +vpndns.net +dynserv.org +now-dns.org +x443.pw +ntdll.top +freeddns.us + +// nsupdate.info : https://www.nsupdate.info/ +// Submitted by Thomas Waldmann +nsupdate.info +nerdpol.ovh + +// O3O.Foundation : https://o3o.foundation/ +// Submitted by the prvcy.page Registry Team +prvcy.page + +// Observable, Inc. : https://observablehq.com +// Submitted by Mike Bostock +observablehq.cloud +static.observableusercontent.com + +// OMG.LOL : https://omg.lol +// Submitted by Adam Newbold +omg.lol + +// Omnibond Systems, LLC. : https://www.omnibond.com +// Submitted by Cole Estep +cloudycluster.net + +// OmniWe Limited : https://omniwe.com +// Submitted by Vicary Archangel +omniwe.site + +// One.com : https://www.one.com/ +// Submitted by Jacob Bunk Nielsen +123webseite.at +123website.be +simplesite.com.br +123website.ch +simplesite.com +123webseite.de +123hjemmeside.dk +123miweb.es +123kotisivu.fi +123siteweb.fr +simplesite.gr +123homepage.it +123website.lu +123website.nl +123hjemmeside.no +service.one +website.one +simplesite.pl +123paginaweb.pt +123minsida.se + +// ONID : https://get.onid.ca +// Submitted by ONID Engineering Team +onid.ca + +// Open Domains : https://open-domains.net +// Submitted by William Harrison +is-a-fullstack.dev +is-cool.dev +is-not-a.dev +localplayer.dev +is-local.org + +// Open Social : https://www.getopensocial.com/ +// Submitted by Alexander Varwijk +opensocial.site + +// OpenAI : https://openai.com +// Submitted by Thomas Shadwell +*.oaiusercontent.com +chatgpt.site + +// OpenCraft GmbH : http://opencraft.com/ +// Submitted by Sven Marnach +opencraft.hosting + +// OpenHost : https://registry.openhost.uk +// Submitted by OpenHost Registry Team +16-b.it +32-b.it +64-b.it + +// OpenResearch GmbH : https://openresearch.com/ +// Submitted by Philipp Schmid +orsites.com + +// Opera Software, A.S.A. +// Submitted by Yngve Pettersen +operaunite.com + +// Oracle Dyn : https://cloud.oracle.com/home https://dyn.com/dns/ +// Submitted by Gregory Drake +// Note: This is intended to also include customer-oci.com due to wildcards implicitly including the current label +*.customer-oci.com +*.oci.customer-oci.com +*.ocp.customer-oci.com +*.ocs.customer-oci.com +*.oraclecloudapps.com +*.oraclegovcloudapps.com +*.oraclegovcloudapps.uk + +// Orange : https://www.orange.com +// Submitted by Alexandre Linte +tech.orange + +// OsSav Technology Ltd. : https://ossav.com/ +// Submitted by OsSav Technology Ltd. +// https://nic.can.re +can.re + +// Oursky Limited : https://authgear.com/ +// Submitted by Authgear Team & Skygear Developer +authgear-staging.com +authgearapps.com + +// OutSystems +// Submitted by Duarte Santos +outsystemscloud.com + +// OVHcloud : https://ovhcloud.com +// Submitted by Vincent Cassé +*.hosting.ovh.net +*.webpaas.ovh.net + +// OwnProvider GmbH : http://www.ownprovider.com +// Submitted by Jan Moennich +ownprovider.com +own.pm + +// OwO : https://whats-th.is/ +// Submitted by Dean Sheather +*.owo.codes + +// OX : http://www.ox.rs +// Submitted by Adam Grand +ox.rs + +// oy.lc +// Submitted by Charly Coste +oy.lc + +// Pagefog : https://pagefog.com/ +// Submitted by Derek Myers +pgfog.com + +// Pantheon Systems, Inc. : https://pantheon.io/ +// Submitted by Gary Dylina +gotpantheon.com +pantheonsite.io + +// Paywhirl, Inc : https://paywhirl.com/ +// Submitted by Daniel Netzer +*.paywhirl.com + +// pcarrier.ca Software Inc : https://pcarrier.ca/ +// Submitted by Pierre Carrier +*.xmit.co +xmit.dev +madethis.site +srv.us +gh.srv.us +gl.srv.us + +// Peplink | Pepwave : http://peplink.com/ +// Submitted by Steve Leung +mypep.link + +// Perplexity AI : https://www.perplexity.ai/ +// Submitted by Alec Xiang +pplx.app + +// Perspecta : https://perspecta.com/ +// Submitted by Kenneth Van Alstyne +perspecta.cloud + +// Ping Identity : https://www.pingidentity.com +// Submitted by Ping Identity +forgeblocks.com +id.forgerock.io + +// Plain : https://www.plain.com/ +// Submitted by Jesús Hernández +support.site + +// Planet-Work : https://www.planet-work.com/ +// Submitted by Frédéric VANNIÈRE +on-web.fr + +// Platform.sh : https://platform.sh +// Submitted by Nikola Kotur +*.upsun.app +upsunapp.com +ent.platform.sh +eu.platform.sh +us.platform.sh +*.platformsh.site +*.tst.site + +// Playcode : https://playcode.io +// Submitted by Ruslan Ianberdin +playcode.site + +// Pley AB : https://www.pley.com/ +// Submitted by Henning Pohl +pley.games + +// Porter : https://porter.run/ +// Submitted by Rudraksh MK +onporter.run + +// Positive Codes Technology Company : http://co.bn/faq.html +// Submitted by Zulfais +co.bn + +// Postman, Inc : https://postman.com +// Submitted by Rahul Dhawan +postman-echo.com +pstmn.io +mock.pstmn.io +httpbin.org + +// prequalifyme.today : https://prequalifyme.today +// Submitted by DeepakTiwari deepak@ivylead.io +prequalifyme.today + +// prgmr.com : https://prgmr.com/ +// Submitted by Sarah Newman +xen.prgmr.com + +// priv.at : http://www.nic.priv.at/ +// Submitted by registry +priv.at + +// PROJECT ELIV : https://eliv.kr/ +// Submitted by PROJECT ELIV DomainName Team +c01.kr +eliv-api.kr +eliv-cdn.kr +eliv-dns.kr +mmv.kr +vki.kr + +// project-study : https://project-study.com +// Submitted by yumenewa +dev.project-study.com + +// PSL Sandbox : https://github.com/groundcat/PSL-Sandbox +// Submitted by groundcat +platter-app.dev + +// PT Ekossistim Indo Digital : https://e.id +// Submitted by Eid Team +e.id + +// Publication Presse Communication SARL : https://ppcom.fr +// Submitted by Yaacov Akiba Slama +chirurgiens-dentistes-en-france.fr +byen.site + +// PublicZone : https://publiczone.org/ +// Submitted by PublicZone NOC Team +nyc.mn +*.cn.st + +// pubtls.org : https://www.pubtls.org +// Submitted by Kor Nielsen +pubtls.org + +// Puter : https://puter.com +// Submitted by Puter Security Team +puter.app +puter.site +puter.work + +// PythonAnywhere LLP : https://www.pythonanywhere.com +// Submitted by Giles Thomas +pythonanywhere.com +eu.pythonanywhere.com + +// QA2 +// Submitted by Daniel Dent : https://www.danieldent.com/ +qa2.com + +// QCX +// Submitted by Cassandra Beelen +qcx.io +*.sys.qcx.io + +// QNAP System Inc : https://www.qnap.com +// Submitted by Nick Chang +myqnapcloud.cn +alpha-myqnapcloud.com +dev-myqnapcloud.com +mycloudnas.com +mynascloud.com +myqnapcloud.com + +// QOTO, Org. +// Submitted by Jeffrey Phillips Freeman +qoto.io + +// Qualifio : https://qualifio.com/ +// Submitted by Xavier De Cock +qualifioapp.com + +// Quality Unit : https://qualityunit.com +// Submitted by Vasyl Tsalko +ladesk.com + +// Qualy : https://qualyhq.com +// Submitted by Raphael Arias +*.qualyhqpartner.com +*.qualyhqportal.com + +// QuickBackend : https://www.quickbackend.com +// Submitted by Dani Biro +qbuser.com + +// Quip : https://quip.com +// Submitted by Patrick Linehan +*.quipelements.com + +// Qutheory LLC : http://qutheory.io +// Submitted by Jonas Schwartz +vapor.cloud +vaporcloud.io + +// Rackmaze LLC : https://www.rackmaze.com +// Submitted by Kirill Pertsev +rackmaze.com +rackmaze.net + +// Rad Web Hosting : https://radwebhosting.com +// Submitted by Scott Claeys +cloudsite.builders +myradweb.net +servername.us + +// Radix FZC : http://domains.in.net +// Submitted by Gavin Brown +web.in +in.net + +// Raidboxes GmbH : https://raidboxes.de +// Submitted by Auke Tembrink +myrdbx.io +site.rb-hosting.io + +// Railway Corporation : https://railway.com +// Submitted by Phineas Walton +up.railway.app + +// Rancher Labs, Inc : https://rancher.com +// Submitted by Vincent Fiduccia +*.on-rancher.cloud +*.on-k3s.io +*.on-rio.io + +// RavPage : https://www.ravpage.co.il +// Submitted by Roni Horowitz +ravpage.co.il + +// Read The Docs, Inc : https://www.readthedocs.org +// Submitted by David Fischer +readthedocs-hosted.com +readthedocs.io + +// Red Hat, Inc. OpenShift : https://openshift.redhat.com/ +// Submitted by Tim Kramer +rhcloud.com + +// Redgate Software : https://red-gate.com +// Submitted by Andrew Farries +instances.spawn.cc + +// Redpanda Data : https://redpanda.com +// Submitted by Infrastructure Team +*.clusters.rdpa.co +*.srvrless.rdpa.co + +// Render : https://render.com +// Submitted by Anurag Goel +onrender.com +app.render.com + +// Repl.it : https://repl.it +// Submitted by Lincoln Bergeson +replit.app +id.replit.app +firewalledreplit.co +id.firewalledreplit.co +repl.co +id.repl.co +replit.dev +archer.replit.dev +bones.replit.dev +canary.replit.dev +global.replit.dev +hacker.replit.dev +id.replit.dev +janeway.replit.dev +kim.replit.dev +kira.replit.dev +kirk.replit.dev +odo.replit.dev +paris.replit.dev +picard.replit.dev +pike.replit.dev +prerelease.replit.dev +reed.replit.dev +riker.replit.dev +sisko.replit.dev +spock.replit.dev +staging.replit.dev +sulu.replit.dev +tarpit.replit.dev +teams.replit.dev +tucker.replit.dev +wesley.replit.dev +worf.replit.dev +repl.run + +// Resin.io : https://resin.io +// Submitted by Tim Perry +resindevice.io +devices.resinstaging.io + +// Rico Developments Limited : https://adimo.co +// Submitted by Colin Brown +adimo.co.uk + +// Riseup Networks : https://riseup.net +// Submitted by Micah Anderson +itcouldbewor.se + +// Roar Domains LLC : https://roar.basketball/ +// Submitted by Gavin Brown +aus.basketball +nz.basketball + +// ROBOT PAYMENT INC. : https://www.robotpayment.co.jp/ +// Submitted by Kentaro Takamori +subsc-pay.com +subsc-pay.net + +// Rochester Institute of Technology : http://www.rit.edu/ +// Submitted by Jennifer Herting +git-pages.rit.edu + +// Rocket : https://rocket.new +// Submitted by Rahul Shingala +rocketpreview.app +*.builtwithrocket.new + +// Rocky Enterprise Software Foundation : https://resf.org +// Submitted by Neil Hanlon +rocky.page + +// Ruhr University Bochum : https://www.ruhr-uni-bochum.de/ +// Submitted by Andreas Jobs +rub.de +ruhr-uni-bochum.de +io.noc.ruhr-uni-bochum.de + +// Rusnames Limited : http://rusnames.ru/ +// Submitted by Sergey Zotov +биз.рус +ком.рус +крым.рус +мир.рус +мск.рус +орг.рус +самара.рус +сочи.рус +спб.рус +я.рус + +// Russian Academy of Sciences +// Submitted by Tech Support +ras.ru + +// Sakura Frp : https://www.natfrp.com +// Submitted by Bobo Liu +nyat.app + +// SAKURA Internet Inc. : https://www.sakura.ad.jp/ +// Submitted by Internet Service Department +180r.com +dojin.com +sakuratan.com +sakuraweb.com +x0.com +2-d.jp +bona.jp +crap.jp +daynight.jp +eek.jp +flop.jp +halfmoon.jp +jeez.jp +matrix.jp +mimoza.jp +ivory.ne.jp +mail-box.ne.jp +mints.ne.jp +mokuren.ne.jp +opal.ne.jp +sakura.ne.jp +sumomo.ne.jp +topaz.ne.jp +netgamers.jp +nyanta.jp +o0o0.jp +rdy.jp +rgr.jp +rulez.jp +s3.isk01.sakurastorage.jp +s3.isk02.sakurastorage.jp +saloon.jp +sblo.jp +skr.jp +tank.jp +uh-oh.jp +undo.jp +rs.webaccel.jp +user.webaccel.jp +websozai.jp +xii.jp +squares.net +jpn.org +kirara.st +x0.to +from.tv +sakura.tv + +// Salesforce.com, Inc. : https://salesforce.com/ +// Submitted by Salesforce Public Suffix List Team +*.builder.code.com +*.dev-builder.code.com +*.stg-builder.code.com +*.001.test.code-builder-stg.platform.salesforce.com +*.aa.crm.dev +*.ab.crm.dev +*.ac.crm.dev +*.ad.crm.dev +*.ae.crm.dev +*.af.crm.dev +*.ci.crm.dev +*.d.crm.dev +*.pa.crm.dev +*.pb.crm.dev +*.pc.crm.dev +*.pd.crm.dev +*.pe.crm.dev +*.pf.crm.dev +*.w.crm.dev +*.wa.crm.dev +*.wb.crm.dev +*.wc.crm.dev +*.wd.crm.dev +*.we.crm.dev +*.wf.crm.dev + +// Sandstorm Development Group, Inc. : https://sandcats.io/ +// Submitted by Asheesh Laroia +sandcats.io + +// Sav.com, LLC : https://marketing.sav.com/ +// Submitted by Mukul Kudegave +sav.case + +// SBE network solutions GmbH : https://www.sbe.de/ +// Submitted by Norman Meilick +logoip.com +logoip.de + +// Scaleway : https://www.scaleway.com/ +// Submitted by Scaleway PSL Maintainer +fr-par-1.baremetal.scw.cloud +fr-par-2.baremetal.scw.cloud +nl-ams-1.baremetal.scw.cloud +cockpit.fr-par.scw.cloud +ddl.fr-par.scw.cloud +dtwh.fr-par.scw.cloud +fnc.fr-par.scw.cloud +functions.fnc.fr-par.scw.cloud +ifr.fr-par.scw.cloud +k8s.fr-par.scw.cloud +nodes.k8s.fr-par.scw.cloud +kafk.fr-par.scw.cloud +mgdb.fr-par.scw.cloud +rdb.fr-par.scw.cloud +s3.fr-par.scw.cloud +s3-website.fr-par.scw.cloud +scbl.fr-par.scw.cloud +whm.fr-par.scw.cloud +priv.instances.scw.cloud +pub.instances.scw.cloud +k8s.scw.cloud +cockpit.nl-ams.scw.cloud +ddl.nl-ams.scw.cloud +dtwh.nl-ams.scw.cloud +ifr.nl-ams.scw.cloud +k8s.nl-ams.scw.cloud +nodes.k8s.nl-ams.scw.cloud +kafk.nl-ams.scw.cloud +mgdb.nl-ams.scw.cloud +rdb.nl-ams.scw.cloud +s3.nl-ams.scw.cloud +s3-website.nl-ams.scw.cloud +scbl.nl-ams.scw.cloud +whm.nl-ams.scw.cloud +cockpit.pl-waw.scw.cloud +ddl.pl-waw.scw.cloud +dtwh.pl-waw.scw.cloud +ifr.pl-waw.scw.cloud +k8s.pl-waw.scw.cloud +nodes.k8s.pl-waw.scw.cloud +kafk.pl-waw.scw.cloud +mgdb.pl-waw.scw.cloud +rdb.pl-waw.scw.cloud +s3.pl-waw.scw.cloud +s3-website.pl-waw.scw.cloud +scbl.pl-waw.scw.cloud +scalebook.scw.cloud +smartlabeling.scw.cloud +dedibox.fr +scw.site +ams.scw.site +waw.scw.site + +// schokokeks.org GbR : https://schokokeks.org/ +// Submitted by Hanno Böck +schokokeks.net + +// Scottish Government : https://www.gov.scot +// Submitted by Martin Ellis +gov.scot +service.gov.scot +mygov.scot + +// Scry Security : http://www.scrysec.com +// Submitted by Shante Adam +scrysec.com + +// Scrypted : https://scrypted.app +// Submitted by Koushik Dutta +client.scrypted.io + +// Securepoint GmbH : https://www.securepoint.de +// Submitted by Erik Anders +firewall-gateway.com +firewall-gateway.de +my-gateway.de +my-router.de +spdns.de +spdns.eu +firewall-gateway.net +my-firewall.org +myfirewall.org +spdns.org + +// Seidat : https://www.seidat.com +// Submitted by Artem Kondratev +seidat.net + +// Sellfy : https://sellfy.com +// Submitted by Yuriy Romadin +sellfy.store + +// Sendmsg : https://www.sendmsg.co.il +// Submitted by Assaf Stern +minisite.ms + +// Senseering GmbH : https://www.senseering.de +// Submitted by Felix Mönckemeyer +senseering.net + +// Servebolt AS : https://servebolt.com +// Submitted by Daniel Kjeserud +servebolt.cloud + +// Service Online LLC : http://drs.ua/ +// Submitted by Serhii Bulakh +biz.ua +co.ua +pp.ua + +// Shanghai Accounting Society : https://www.sasf.org.cn +// Submitted by Information Administration +as.sh.cn + +// Shanghai Oray Information Technology Co., Ltd.: https://www.oray.com/ +// Submitted by: Shanghai Oray Information Technology Co., Ltd. +vicp.fun +yicp.fun +zicp.fun + +// Sheezy.Art : https://sheezy.art +// Submitted by Nyoom +sheezy.games + +// Shopblocks : http://www.shopblocks.com/ +// Submitted by Alex Bowers +myshopblocks.com + +// Shopify : https://www.shopify.com +// Submitted by Alex Richter +myshopify.com + +// Shopit : https://www.shopitcommerce.com/ +// Submitted by Craig McMahon +shopitsite.com + +// shopware AG : https://shopware.com +// Submitted by Jens Küper +shopware.shop +shopware.store + +// Siemens Mobility GmbH +// Submitted by Oliver Graebner +mo-siemens.io + +// SinaAppEngine : http://sae.sina.com.cn/ +// Submitted by SinaAppEngine +1kapp.com +appchizi.com +applinzi.com +sinaapp.com +vipsinaapp.com + +// Siteleaf : https://www.siteleaf.com/ +// Submitted by Skylar Challand +siteleaf.net + +// Small Technology Foundation : https://small-tech.org +// Submitted by Aral Balkan +small-web.org + +// Smallregistry by Promopixel SARL : https://www.smallregistry.net +// Former AFNIC's SLDs +// Submitted by Jérôme Lipowicz +aeroport.fr +avocat.fr +chambagri.fr +chirurgiens-dentistes.fr +experts-comptables.fr +medecin.fr +notaires.fr +pharmacien.fr +port.fr +veterinaire.fr + +// Smoove.io : https://www.smoove.io/ +// Submitted by Dan Kozak +vp4.me + +// Snowflake Inc : https://www.snowflake.com/ +// Submitted by Sam Haar +*.snowflake.app +*.privatelink.snowflake.app +streamlit.app +streamlitapp.com + +// Snowplow Analytics : https://snowplowanalytics.com/ +// Submitted by Ian Streeter +try-snowplow.com + +// Software Consulting Michal Zalewski : https://www.mafelo.com +// Submitted by Michal Zalewski +mafelo.net + +// Solana Name Service : https://sns.id +// Submitted by Solana Name Service +sol.site + +// Sony Interactive Entertainment LLC : https://sie.com/ +// Submitted by David Coles +playstation-cloud.com + +// SourceHut : https://sourcehut.org +// Submitted by Drew DeVault +srht.site + +// SourceLair PC : https://www.sourcelair.com +// Submitted by Antonis Kalipetis +apps.lair.io +*.stolos.io + +// sourceWAY GmbH : https://sourceway.de +// Submitted by Richard Reiber +4.at +my.at +my.de +*.nxa.eu +nx.gw + +// Spawnbase : https://spawnbase.ai +// Submitted by Alexander Zuev +spawnbase.app + +// SpeedPartner GmbH : https://www.speedpartner.de/ +// Submitted by Stefan Neufeind +customer.speedpartner.de + +// Spreadshop (sprd.net AG) : https://www.spreadshop.com/ +// Submitted by Martin Breest +myspreadshop.at +myspreadshop.com.au +myspreadshop.be +myspreadshop.ca +myspreadshop.ch +myspreadshop.com +myspreadshop.de +myspreadshop.dk +myspreadshop.es +myspreadshop.fi +myspreadshop.fr +myspreadshop.ie +myspreadshop.it +myspreadshop.net +myspreadshop.nl +myspreadshop.no +myspreadshop.pl +myspreadshop.se +myspreadshop.co.uk + +// StackBlitz : https://stackblitz.com +// Submitted by Dominic Elm & Albert Pai +w-corp-staticblitz.com +w-credentialless-staticblitz.com +w-staticblitz.com +bolt.host + +// Stackhero : https://www.stackhero.io +// Submitted by Adrien Gillon +stackhero-network.com + +// STACKIT GmbH & Co. KG : https://www.stackit.de/en/ +// Submitted by STACKIT-DNS Team (Simon Stier) +runs.onstackit.cloud +stackit.gg +stackit.rocks +stackit.run +stackit.zone + +// Stackryze : https://stackryze.com +// Submitted by Sudheer Bhuvana +sryze.cc +indevs.in + +// Staclar : https://staclar.com +// Submitted by Q Misell +// Submitted by Matthias Merkel +musician.io +novecore.site + +// statichost.eu : https://www.statichost.eu +// Submitted by Eric Selin +statichost.page + +// stereosense GmbH : https://www.involve.me +// Submitted by Florian Burmann +feedback.ac +forms.ac +assessments.cx +calculators.cx +funnels.cx +paynow.cx +quizzes.cx +researched.cx +tests.cx +surveys.so + +// Storacha Network : https://storacha.network +// Submitted by Alan Shaw +ipfs.storacha.link +ipfs.w3s.link + +// Storebase : https://www.storebase.io +// Submitted by Tony Schirmer +storebase.store + +// Strapi : https://strapi.io/ +// Submitted by Florent Baldino +strapiapp.com +media.strapiapp.com + +// Strategic System Consulting (eApps Hosting) : https://www.eapps.com/ +// Submitted by Alex Oancea +vps-host.net +atl.jelastic.vps-host.net +njs.jelastic.vps-host.net +ric.jelastic.vps-host.net + +// Streak : https://streak.com +// Submitted by Blake Kadatz +streak-link.com +streaklinks.com +streakusercontent.com + +// Student-Run Computing Facility : https://www.srcf.net/ +// Submitted by Edwin Balani +soc.srcf.net +user.srcf.net + +// Studenten Net Twente : http://www.snt.utwente.nl/ +// Submitted by Silke Hofstra +utwente.io + +// Sub 6 Limited : http://www.sub6.com +// Submitted by Dan Miller +temp-dns.com + +// Supabase : https://supabase.io +// Submitted by Supabase Security +supabase.co +realtime.supabase.co +storage.supabase.co +supabase.in +supabase.net + +// Syncloud : https://syncloud.org +// Submitted by Boris Rybalkin +syncloud.it + +// Synology, Inc. : https://www.synology.com/ +// Submitted by Rony Weng +dscloud.biz +direct.quickconnect.cn +dsmynas.com +familyds.com +diskstation.me +dscloud.me +i234.me +myds.me +synology.me +dscloud.mobi +dsmynas.net +familyds.net +dsmynas.org +familyds.org +direct.quickconnect.to +vpnplus.to + +// Tabit Technologies Ltd. : https://tabit.cloud/ +// Submitted by Oren Agiv +mytabit.com +mytabit.co.il +tabitorder.co.il + +// TAIFUN Software AG : http://taifun-software.de +// Submitted by Bjoern Henke +taifun-dns.de + +// Tailor Inc. : https://www.tailor.tech +// Submitted by Ryuzo Yamamoto +erp.dev +web.erp.dev + +// Tailscale Inc. : https://www.tailscale.com +// Submitted by David Anderson +ts.net +*.c.ts.net + +// TASK geographical domains : https://task.gda.pl/en/services/for-entrepreneurs/ +gda.pl +gdansk.pl +gdynia.pl +med.pl +sopot.pl + +// Tave Creative Corp : https://tave.com/ +// Submitted by Adrian Ziemkowski +taveusercontent.com + +// tawk.to, Inc : https://www.tawk.to +// Submitted by tawk.to developer team +p.tawk.email +p.tawkto.email + +// Tche.br : https://tche.br +// Submitted by Bruno Lorensi +tche.br + +// team.blue : https://team.blue +// Submitted by Cedric Dubois +site.tb-hosting.com +directwp.eu + +// TechEdge Limited: https://www.nic.uk.cc/ +// Submitted by TechEdge Developer +ec.cc +eu.cc +gu.cc +uk.cc +us.cc + +// Teckids e.V. : https://www.teckids.org +// Submitted by Dominik George +edugit.io +s3.teckids.org + +// Telebit : https://telebit.cloud +// Submitted by AJ ONeal +telebit.app +telebit.io +*.telebit.xyz + +// Teleport : https://goteleport.com +// Submitted by Rob Picard +teleport.sh + +// Thingdust AG : https://thingdust.com/ +// Submitted by Adrian Imboden +*.firenet.ch +*.svc.firenet.ch +reservd.com +thingdustdata.com +cust.dev.thingdust.io +reservd.dev.thingdust.io +cust.disrec.thingdust.io +reservd.disrec.thingdust.io +cust.prod.thingdust.io +cust.testing.thingdust.io +reservd.testing.thingdust.io + +// ticket i/O GmbH : https://ticket.io +// Submitted by Christian Franke +tickets.io + +// Tigris Data, Inc. : https://www.tigrisdata.com +// Submitted by Bo Cao +t3.storage.dev +t3.storageapi.dev + +// Tlon.io : https://tlon.io +// Submitted by Mark Staarink +arvo.network +azimuth.network +tlon.network + +// Tor Project, Inc. : https://torproject.org +// Submitted by Antoine Beaupré +torproject.net +pages.torproject.net + +// TownNews.com : http://www.townnews.com +// Submitted by Dustin Ward +townnews-staging.com + +// TrafficPlex GmbH : https://www.trafficplex.de/ +// Submitted by Phillipp Röll +12hp.at +2ix.at +4lima.at +lima-city.at +12hp.ch +2ix.ch +4lima.ch +lima-city.ch +trafficplex.cloud +de.cool +12hp.de +2ix.de +4lima.de +lima-city.de +1337.pictures +clan.rip +lima-city.rocks +webspace.rocks +lima.zone + +// TransIP : https://www.transip.nl +// Submitted by Rory Breuk and Cedric Dubois +*.transurl.be +*.transurl.eu +site.transip.me +*.transurl.nl + +// Triton Data Center project : https://tritondatacenter.com +// Submitted by Triton Data Center staff +*.triton.zone + +// Tunnelmole: https://tunnelmole.com +// Submitted by Robbie Cahill +tunnelmole.net + +// TuxFamily : http://tuxfamily.org +// Submitted by TuxFamily administrators +tuxfamily.org + +// Typedream : https://typedream.com +// Submitted by Putri Karunia +typedream.app + +// Typeform : https://www.typeform.com +// Submitted by Typeform +pro.typeform.com + +// Uberspace : https://uberspace.de +// Submitted by Moritz Werner +uber.space + +// UDR Limited : http://www.udr.hk.com +// Submitted by registry +hk.com +inc.hk +ltd.hk +hk.org + +// UK Intis Telecom LTD : https://it.com +// Submitted by ITComdomains +it.com + +// Umso Software Inc. : https://www.umso.com +// Submitted by Alexis Taylor +umso.co + +// Unison Computing, PBC : https://unison.cloud +// Submitted by Simon Højberg +unison-services.cloud + +// United Gameserver GmbH : https://united-gameserver.de +// Submitted by Stefan Schwarz +virtual-user.de +virtualuser.de + +// United States Writing Corporation : https://uswriting.co +// Submitted by Andrew Sampson +obj.ag + +// UNIVERSAL DOMAIN REGISTRY : https://www.udr.org.yt/ +// see also: whois -h whois.udr.org.yt help +// Submitted by Atanunu Igbunuroghene +name.pm +sch.tf +biz.wf +sch.wf +org.yt + +// University of Banja Luka : https://unibl.org +// Domains for Republic of Srpska administrative entity. +// Submitted by Marko Ivanovic +rs.ba + +// University of Bielsko-Biala regional domain : http://dns.bielsko.pl/ +// Submitted by Marcin +bielsko.pl + +// urown.net : https://urown.net +// Submitted by Hostmaster +urown.cloud +dnsupdate.info + +// US REGISTRY LLC : http://us.org +// Submitted by Gavin Brown +us.org + +// V.UA Domain Registry: https://www.v.ua/ +// Submitted by Serhii Rostilo +v.ua + +// Val Town, Inc : https://val.town/ +// Submitted by Tom MacWright +val.run +web.val.run + +// Vercel, Inc : https://vercel.com/ +// Submitted by Thibault Miranda de Oliveira +vercel.app +v0.build +vercel.dev +vusercontent.net +tmp.now +vercel.run +now.sh + +// VeryPositive SIA : http://very.lv +// Submitted by Danko Aleksejevs +2038.io + +// Virtual-Info : https://www.virtual-info.info/ +// Submitted by Adnan RIHAN +v-info.info + +// VistaBlog : https://vistablog.ir/ +// Submitted by Hossein Piri +vistablog.ir + +// Viva Republica, Inc. : https://toss.im/ +// Submitted by Deus Team +deus-canvas.com + +// vivenu GmbH : https://vivenu.com/ +// Submitted by Marvin Frick +vivenushop.com +vivenushop.dev + +// Voorloper.com : https://voorloper.com +// Submitted by Nathan van Bakel +voorloper.cloud + +// Vultr Objects : https://www.vultr.com/products/object-storage/ +// Submitted by Niels Maumenee +*.vultrobjects.com + +// Waffle Computer Inc., Ltd. : https://docs.waffleinfo.com +// Submitted by Masayuki Note +wafflecell.com + +// Walrus : https://walrus.xyz +// Submitted by Max Spector +wal.app + +// Wasmer: https://wasmer.io +// Submitted by Lorentz Kinde +wasmer.app + +// Webflow, Inc. : https://www.webflow.com +// Submitted by Webflow Security Team +webflow.io +webflowtest.io + +// WebHare bv : https://www.webhare.com/ +// Submitted by Arnold Hendriks +*.webhare.dev + +// WebHotelier Technologies Ltd : https://www.webhotelier.net/ +// Submitted by Apostolos Tsakpinis +hotelwithflight.com +reserve-online.net +book.online + +// WebPros International, LLC : https://webpros.com/ +// Submitted by Nicolas Rochelemagne +cprapid.com +pleskns.com +wp2.host +pdns.page +plesk.page +cpanel.site +wpsquared.site + +// WebWaddle Ltd : https://webwaddle.com/ +// Submitted by Merlin Glander +*.wadl.top + +// Western Digital Technologies, Inc : https://www.wdc.com +// Submitted by Jung Jin +remotewd.com + +// Whatbox Inc. : https://whatbox.ca/ +// Submitted by Anthony Ryan +box.ca + +// WIARD Enterprises : https://wiardweb.com +// Submitted by Kidd Hustle +pages.wiardweb.com + +// Wikimedia Foundation : https://wikitech.wikimedia.org +// Submitted by Timo Tijhof +toolforge.org +wmcloud.org +beta.wmcloud.org +wmflabs.org + +// William Harrison : https://wharrison.com.au +// Submitted by William Harrison +hrsn.dev +is-a.dev +vps.hrsn.net +localcert.net + +// Windsurf : https://windsurf.com +// Submitted by Douglas Chen +windsurf.app +windsurf.build + +// WirelessCar : https://wirelesscar.com +// Submitted by Martin Lindberg +drive-platform.com +drive-platform.io + +// WISP : https://wisp.gg +// Submitted by Stepan Fedotov +panel.gg +daemon.panel.gg + +// Wix.com, Inc. : https://www.wix.com +// Submitted by Shahar Talmi / Alon Kochba +base44.app +base44-sandbox.com +wixsite.com +wixstudio.com +editorx.io +wixstudio.io +wix.run + +// Wizard Zines : https://wizardzines.com +// Submitted by Julia Evans +messwithdns.com + +// WoltLab GmbH : https://www.woltlab.com +// Submitted by Tim Düsterhus +woltlab-demo.com +myforum.community +community-pro.de +diskussionsbereich.de +community-pro.net +meinforum.net + +// Woods Valldata : https://www.woodsvalldata.co.uk/ +// Submitted by Chris Whittle +affinitylottery.org.uk +raffleentry.org.uk +weeklylottery.org.uk + +// WP Engine : https://wpengine.com/ +// Submitted by Michael Smith +// Submitted by Brandon DuRette +wpenginepowered.com +js.wpenginepowered.com + +// xAI : https://x.ai/ +// Submitted by Asim Shrestha +grok.me + +// XenonCloud GbR : https://xenoncloud.net +// Submitted by Julian Uphoff +*.xenonconnect.de +half.host + +// XS4ALL Internet bv : https://www.xs4all.nl/ +// Submitted by Daniel Mostertman +cistron.nl +demon.nl +xs4all.space + +// xTool : https://xtool.com +// Submitted by Echo +xtooldevice.com + +// Yandex.Cloud LLC : https://cloud.yandex.com +// Submitted by Alexander Lodin +yandexcloud.net +storage.yandexcloud.net +website.yandexcloud.net +sourcecraft.site + +// YesCourse Pty Ltd : https://yescourse.com +// Submitted by Atul Bhouraskar +official.academy + +// Yola : https://www.yola.com/ +// Submitted by Stefano Rivera +yolasite.com + +// Yunohost : https://yunohost.org +// Submitted by Valentin Grimaud +ynh.fr +nohost.me +noho.st + +// ZaNiC : http://www.za.net/ +// Submitted by registry +za.net +za.org + +// ZAP-Hosting GmbH & Co. KG : https://zap-hosting.com +// Submitted by Julian Alker +zap.cloud + +// Zeabur : https://zeabur.com/ +// Submitted by Zeabur Team +zeabur.app + +// Zerops : https://zerops.io/ +// Submitted by Zerops Team +*.zerops.app +prg1-zerops.zone +*.zerops.zone + +// Zine EOOD : https://zine.bg/ +// Submitted by Martin Angelov +bss.design + +// Zitcom A/S : https://www.zitcom.dk +// Submitted by Emil Stahl +basicserver.io +virtualserver.io +enterprisecloud.nu + +// Zone.ID: https://zone.id +// Submitted by Gx1.org +zone.id +nett.to + +// ZoneABC : https://zoneabc.net +// Submitted by ZoneABC Team +zabc.net + +// ===END PRIVATE DOMAINS=== diff --git a/engine/wpeqt/CMakeLists.txt b/engine/wpeqt/CMakeLists.txt index 66fb65e..d20d4da 100644 --- a/engine/wpeqt/CMakeLists.txt +++ b/engine/wpeqt/CMakeLists.txt @@ -41,3 +41,20 @@ target_compile_options(rmweb-wpeqt PRIVATE $<$:-D_FORTIFY_SOURCE=2>) # Full RELRO + bind-now: GOT is read-only after relocation (root process, no lazy-binding window). target_link_options(rmweb-wpeqt PRIVATE -Wl,-z,relro,-z,now) + +# Optional authentication executables use their own patched WPE runtime. Keep +# them out of normal browser builds, which use the existing release runtime. +add_executable(rmweb-auth-entry EXCLUDE_FROM_ALL auth-entry.cpp) +set_target_properties(rmweb-auth-entry PROPERTIES AUTOMOC OFF) +target_compile_options(rmweb-auth-entry PRIVATE -Wall -Wextra -Werror -fstack-protector-strong) +target_link_options(rmweb-auth-entry PRIVATE -Wl,-z,relro,-z,now) + +qt_add_executable(rmweb-auth-browser EXCLUDE_FROM_ALL + auth-main.cpp auth-policy.cpp auth-surface.cpp auth-passkey.cpp qtfbclient.cpp) +target_link_libraries(rmweb-auth-browser PRIVATE Qt6::Core Qt6::Gui PkgConfig::WPE) +target_compile_definitions(rmweb-auth-browser PRIVATE QT_NO_KEYWORDS) +target_compile_options(rmweb-auth-browser PRIVATE -Wall -Wextra -Werror -fstack-protector-strong) +target_link_options(rmweb-auth-browser PRIVATE -Wl,-z,relro,-z,now) +set_target_properties(rmweb-auth-browser PROPERTIES + INSTALL_RPATH "$ORIGIN/../runtime/lib" + BUILD_WITH_INSTALL_RPATH TRUE) diff --git a/engine/wpeqt/auth-entry.cpp b/engine/wpeqt/auth-entry.cpp new file mode 100644 index 0000000..5ece7cc --- /dev/null +++ b/engine/wpeqt/auth-entry.cpp @@ -0,0 +1,207 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +namespace { +// Locate this owned installation from the executable, never caller input. +bool installationPaths(std::string &browser, std::string &helpers, std::string &libraries) { + char buffer[PATH_MAX]; + const ssize_t size = readlink("/proc/self/exe", buffer, sizeof(buffer)); + if (size <= 0 || size >= static_cast(sizeof(buffer))) return false; + const std::string executable(buffer, static_cast(size)); + const size_t separator = executable.rfind('/'); + if (separator == std::string::npos) return false; + const std::string bin = executable.substr(0, separator); + if (bin.size() <= 4 || bin.substr(bin.size() - 4) != "/bin") return false; + browser = bin + "/rmweb-auth-browser"; + const std::string runtime = bin.substr(0, bin.size() - 4) + "/runtime"; + helpers = runtime + "/libexec"; + libraries = runtime + "/lib"; + return true; +} +constexpr const char *MountPoint = "/usr/libexec"; + +class Fd { +public: + explicit Fd(int value = -1) : value(value) {} + ~Fd() { if (value >= 0) ::close(value); } + Fd(const Fd &) = delete; + Fd &operator=(const Fd &) = delete; + int value; +}; + +int fail(const char *message) { + // No URLs, keys, or private paths from caller input are logged. + std::fprintf(stderr, "rmweb-auth-entry: %s\n", message); + return 1; +} + +bool owned(const struct stat &info, bool directory) { + return info.st_uid == 0 && !(info.st_mode & (S_IWGRP | S_IWOTH | S_ISUID | S_ISGID)) + && (directory ? S_ISDIR(info.st_mode) + : S_ISREG(info.st_mode) && (info.st_mode & 0111)); +} + +// Walk every component without following symlinks. The installation's parent +// directories cannot be exchanged by an unprivileged process after validation. +int openOwned(const char *path, bool directory) { + if (!path || path[0] != '/') return -1; + int current = ::open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + struct stat info{}; + if (current < 0) return -1; + if (fstat(current, &info) != 0 || !owned(info, true)) { + ::close(current); + return -1; + } + const std::string input(path); + size_t start = 1; + while (start < input.size()) { + const size_t end = input.find('/', start); + const bool last = end == std::string::npos; + const std::string part = input.substr(start, last ? end : end - start); + if (part.empty() || part == "." || part == "..") { ::close(current); return -1; } + const int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK + | ((!last || directory) ? O_DIRECTORY : 0); + const int next = openat(current, part.c_str(), flags); + ::close(current); + current = next; + if (current < 0) return -1; + if (fstat(current, &info) != 0 || !owned(info, !last || directory)) { + ::close(current); + return -1; + } + if (last) return current; + start = end + 1; + } + ::close(current); + return -1; +} + +bool validUrl(const char *url) { + const size_t length = strnlen(url, 8193); + if (!length || length > 8192) return false; + size_t scheme = 0; + if (std::strncmp(url, "https://", 8) == 0) scheme = 8; + else return false; + if (length <= scheme || url[scheme] == '/' || url[scheme] == '?' || url[scheme] == '#') + return false; + for (size_t i = 0; i < length; ++i) { + const unsigned char c = static_cast(url[i]); + if (c <= 0x20 || c == 0x7f) return false; + } + return true; +} + +bool validKey() { + const char *key = std::getenv("QTFB_KEY"); + if (!key) return false; + const size_t length = strnlen(key, 11); + if (!length || length > 10) return false; + unsigned long number = 0; + for (size_t i = 0; i < length; ++i) { + if (key[i] < '0' || key[i] > '9') return false; + number = number * 10 + static_cast(key[i] - '0'); + if (number > INT_MAX) return false; + } + return true; +} + +bool verifyHelpers(const char *base) { + for (const char *name : {"WPEWebProcess", "WPENetworkProcess", "WPEGPUProcess"}) { + const std::string path = std::string(base) + "/wpe-webkit-2.0/" + name; + Fd helper(openOwned(path.c_str(), false)); + if (helper.value < 0 || faccessat(helper.value, "", X_OK, AT_EMPTY_PATH | AT_EACCESS) != 0) + return false; + } + return true; +} +} + +int main(int argc, char **argv) { + const bool check = argc == 2 && std::strcmp(argv[1], "--check") == 0; + if (argc != 2 && argc != 4) return fail("invalid authentication arguments"); + if (argc == 4) { + if (std::strcmp(argv[2], "--device-code") != 0) + return fail("invalid device authorization arguments"); + const size_t size = strnlen(argv[3], 65); + if (!size || size > 64) return fail("invalid device authorization code"); + for (size_t i = 0; i < size; ++i) { + const char value = argv[3][i]; + if (!(value >= 'A' && value <= 'Z') && !(value >= '0' && value <= '9') && value != '-') + return fail("invalid device authorization code"); + } + } + if (!check && (!validUrl(argv[1]) || !validKey())) + return fail("one HTTPS URL and a valid QTFB_KEY are required; --check tests setup only"); + if (geteuid() != 0) return fail("root ownership is required for the private mount namespace"); + if (unsetenv("LD_PRELOAD") != 0) return fail("could not clear inherited preload settings"); + + std::string browserPath, helperPath, libraryPath; + if (!installationPaths(browserPath, helperPath, libraryPath)) return fail("invalid installation layout"); + // The launcher sources the staged rmweb-env.sh, which exports this exact + // library path before exec. Pin the same installation-derived value here so + // an inherited caller-chosen search path never reaches the browser. Plain + // unsetenv is not an option: the staged runtime has no baked rpath pass and + // resolves its transitive libraries through LD_LIBRARY_PATH. + if (setenv("LD_LIBRARY_PATH", libraryPath.c_str(), 1) != 0) + return fail("could not pin the trusted library search path"); + const char *Browser = browserPath.c_str(); + const char *Helpers = helperPath.c_str(); + Fd browser(openOwned(Browser, false)); + Fd helpers(openOwned(Helpers, true)); + Fd target(openOwned(MountPoint, true)); + if (browser.value < 0 || helpers.value < 0 || target.value < 0 || !verifyHelpers(Helpers)) + return fail("browser or helper paths failed ownership and executable checks"); + if (faccessat(browser.value, "", X_OK, AT_EMPTY_PATH | AT_EACCESS) != 0) + return fail("browser is not executable"); + + // Only this process and its descendants see the overlay. Neither this + // process's PID nor the stock interface's mounts or lifecycle are changed. + if (unshare(CLONE_NEWNS) != 0) return fail("could not create a private mount namespace"); + if (mount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE, nullptr) != 0) + return fail("could not make namespace mounts private"); + // Resolve paths in the new namespace. File descriptors opened before + // unshare still refer to the old namespace's mount tree. + Fd namespaceHelpers(openOwned(Helpers, true)); + Fd namespaceTarget(openOwned(MountPoint, true)); + struct stat initialSource{}, currentSource{}, initialTarget{}, currentTarget{}; + if (namespaceHelpers.value < 0 || namespaceTarget.value < 0 + || fstat(helpers.value, &initialSource) != 0 || fstat(namespaceHelpers.value, ¤tSource) != 0 + || fstat(target.value, &initialTarget) != 0 || fstat(namespaceTarget.value, ¤tTarget) != 0 + || initialSource.st_dev != currentSource.st_dev || initialSource.st_ino != currentSource.st_ino + || initialTarget.st_dev != currentTarget.st_dev || initialTarget.st_ino != currentTarget.st_ino) + return fail("helper paths changed during namespace setup"); + const std::string sourceFd = "/proc/self/fd/" + std::to_string(namespaceHelpers.value); + const std::string targetFd = "/proc/self/fd/" + std::to_string(namespaceTarget.value); + if (mount(sourceFd.c_str(), targetFd.c_str(), nullptr, MS_BIND, nullptr) != 0) + return fail("could not bind the bundled helper directory"); + if (mount(nullptr, MountPoint, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY | MS_NOSUID | MS_NODEV, nullptr) != 0) + return fail("could not make the helper overlay read-only"); + struct stat before{}, after{}; + struct statvfs filesystem{}; + Fd mounted(openOwned(MountPoint, true)); + if (mounted.value < 0 || fstat(helpers.value, &before) != 0 || fstat(mounted.value, &after) != 0 + || before.st_dev != after.st_dev || before.st_ino != after.st_ino + || fstatvfs(mounted.value, &filesystem) != 0 || !(filesystem.f_flag & ST_RDONLY) + || !verifyHelpers(MountPoint)) + return fail("private helper overlay verification failed"); + if (check) { + std::puts("rmweb-auth-entry: private read-only helper overlay verified"); + return 0; + } + char *arguments[] = {const_cast(Browser), argv[1], + argc == 4 ? argv[2] : nullptr, argc == 4 ? argv[3] : nullptr, nullptr}; + fexecve(browser.value, arguments, environ); + return fail("could not execute the browser"); +} diff --git a/engine/wpeqt/auth-main.cpp b/engine/wpeqt/auth-main.cpp new file mode 100644 index 0000000..259af01 --- /dev/null +++ b/engine/wpeqt/auth-main.cpp @@ -0,0 +1,515 @@ +// An ephemeral authentication window; the calling application owns the OAuth callback. +#include "auth-policy.h" +#include "auth-surface.h" +#include "auth-passkey.h" +#include "qtfbclient.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr double Scale = 2.0; +// Only this snapshot crosses into the UI: never a complete URI, form value, +// title, script result, error description, cookie, or authentication result. +struct Snapshot { + QImage frame; + QString host; + bool loading = true; + bool failed = false; + bool callback = false; + quint64 generation = 0; +}; +class AuthEngine { + friend struct AuthEngineTestAccess; // Actual-WPE regression fixture; no production instance. +public: + AuthEngine(rmweb::AuthLaunch launch, QSize size, rmweb::AuthPasskey *passkeys = nullptr) + : m_launch(std::move(launch)), m_size(size), m_context(g_main_context_new()), m_passkeys(passkeys) {} + void start() { std::thread([this] { run(); }).detach(); } + Snapshot takeSnapshot() { + std::lock_guard guard(m_mutex); + Snapshot result = m_snapshot; + m_snapshot.frame = {}; + return result; + } + bool touch(WPEEventType type, int id, int x, int y, quint64 generation) { + if (id < -1) return false; + return post([=] { + if (!m_inputAllowed || m_passkeyRequest || generation != m_generation) return; + if (type == WPE_EVENT_TOUCH_DOWN) { + if (m_contacts.contains(id) || m_contacts.size() >= 16) return; + m_contacts.insert(id, QPoint(x, y)); + wpe_view_focus_in(view()); + } else if (!m_contacts.contains(id)) return; + m_contacts[id] = QPoint(x, y); + deliver(wpe_event_touch_new(type, view(), WPE_INPUT_SOURCE_TOUCHSCREEN, + time(), WPEModifiers(0), nativeContactId(id), x / Scale, y / Scale)); + if (type == WPE_EVENT_TOUCH_UP) m_contacts.remove(id); + }); + } + bool key(int raw, bool pressed, quint64 generation) { + if (!pressed && !m_ready.load()) return true; + return post([=] { + if (pressed && (!m_inputAllowed || m_passkeyRequest || generation != m_generation)) return; + wpe_view_focus_in(view()); + deliverKeys(m_keyboard.event(raw, pressed)); + }); + } + bool cancel() { return !m_ready.load() || post([this] { cancelInput(); }); } + bool cancelPasskey() { + return !m_ready.load() || post([this] { + if (m_passkeyRequest) webkit_web_authentication_request_cancel(m_passkeyRequest); + }); + } + bool completePasskey(quint64 requestId, bool success, rmweb::AuthPasskeyAssertion assertion) { + return post([this, requestId, success, assertion = std::move(assertion)] { + if (!m_passkeyRequest || requestId != m_activePasskeyId) return; + auto *request = m_passkeyRequest; + g_object_ref(request); + clearPasskey(); + if (success) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: assertion_submitted"); + GBytes *id = g_bytes_new(assertion.credentialId.constData(), assertion.credentialId.size()); + GBytes *data = g_bytes_new(assertion.authenticatorData.constData(), assertion.authenticatorData.size()); + GBytes *signature = g_bytes_new(assertion.signature.constData(), assertion.signature.size()); + GBytes *user = assertion.userHandle.isEmpty() ? nullptr + : g_bytes_new(assertion.userHandle.constData(), assertion.userHandle.size()); + webkit_web_authentication_request_complete_assertion(request, id, data, signature, user); + g_bytes_unref(id); g_bytes_unref(data); g_bytes_unref(signature); + if (user) g_bytes_unref(user); + } else { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: assertion_cancelled"); + webkit_web_authentication_request_cancel(request); + } + g_object_unref(request); + }); + } +private: + void clearPasskey() { + if (!m_passkeyRequest) return; + if (m_passkeyCancelledSignal) + g_signal_handler_disconnect(webkit_web_authentication_request_get_cancellable(m_passkeyRequest), m_passkeyCancelledSignal); + m_passkeyCancelledSignal = 0; + const quint64 id = m_activePasskeyId; + m_activePasskeyId = 0; + g_object_unref(m_passkeyRequest); m_passkeyRequest = nullptr; + if (m_passkeys) QMetaObject::invokeMethod(m_passkeys, [this, id] { m_passkeys->cancel(id); }, Qt::QueuedConnection); + } + static gboolean webAuthentication(WebKitWebView *, WebKitWebAuthenticationRequest *request, gpointer opaque) { + auto *self = static_cast(opaque); + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: native_request_received"); + if (!self->m_passkeys || self->m_passkeyRequest) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: native_request_unavailable"); + return FALSE; + } + auto *cancellable = webkit_web_authentication_request_get_cancellable(request); + if (g_cancellable_is_cancelled(cancellable)) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: native_request_cancelled"); + return FALSE; + } + gsize hashLength = 0; + const auto *hashData = static_cast(g_bytes_get_data( + webkit_web_authentication_request_get_client_data_hash(request), &hashLength)); + const auto optionsBytes = QByteArray(webkit_web_authentication_request_get_options_json(request)); + QJsonParseError parseError; + const auto options = QJsonDocument::fromJson(optionsBytes, &parseError); + if (hashLength != 32 || optionsBytes.size() > 128 * 1024 || parseError.error != QJsonParseError::NoError || !options.isObject()) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: native_request_malformed"); + webkit_web_authentication_request_cancel(request); + return TRUE; + } + const QString origin = QString::fromUtf8(webkit_web_authentication_request_get_origin(request)); + const QString relyingParty = QString::fromUtf8(webkit_web_authentication_request_get_rp_id(request)); + const QJsonObject input {{"version", 1}, {"origin", origin}, {"options", options.object()}, + {"clientDataHash", QString::fromLatin1(QByteArray(hashData, 32).toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))}}; + self->cancelInput(); + self->m_passkeyRequest = WEBKIT_WEB_AUTHENTICATION_REQUEST(g_object_ref(request)); + self->m_activePasskeyId = ++self->m_passkeySerial; + self->m_passkeyCancelledSignal = g_signal_connect(cancellable, "cancelled", G_CALLBACK(+[](GCancellable *, gpointer data) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: native_request_cancelled"); + static_cast(data)->clearPasskey(); + }), self); + const auto id = self->m_activePasskeyId; + QMetaObject::invokeMethod(self->m_passkeys, [self, id, relyingParty, input] { + self->m_passkeys->start(id, relyingParty, input); + }, Qt::QueuedConnection); + return TRUE; + } + void reportHttpFailure(guint status) { + // Enough to distinguish a server rejection from missing native + // dispatch. Never retain or log a resource URL, header or body, and + // bound diagnostics even if a page keeps retrying a failed resource. + if (status >= 400 && status <= 599 && m_httpFailureReports < 16) { + ++m_httpFailureReports; + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth http: failure_status=%u", status); + } + } + static void resourceResponse(WebKitWebResource *resource, GParamSpec *, gpointer data) { + if (auto *response = webkit_web_resource_get_response(resource)) + static_cast(data)->reportHttpFailure(webkit_uri_response_get_status_code(response)); + } + // Pen -1 maps to zero; fingers map to 1..0x80000000. WPE reserves + // UINT32_MAX and UINT32_MAX-1 internally, so never cast a negative ID alone. + static guint32 nativeContactId(int id) { return guint32(id) + 1U; } + WPEView *view() const { return webkit_web_view_get_wpe_view(m_webView); } + static guint32 time() { return guint32(g_get_monotonic_time() / 1000); } + void deliver(WPEEvent *event) { + if (!event) return; + wpe_view_event(view(), event); + wpe_event_unref(event); + } + void deliverKeys(const QVector &events) { + for (const auto &event : events) + deliver(wpe_event_keyboard_new(event.pressed ? WPE_EVENT_KEYBOARD_KEY_DOWN : WPE_EVENT_KEYBOARD_KEY_UP, + view(), WPE_INPUT_SOURCE_KEYBOARD, time(), WPEModifiers(event.key.modifiers), + event.key.code, event.key.value)); + } + void cancelInput() { + deliverKeys(m_keyboard.cancel()); + const auto contacts = m_contacts; + m_contacts.clear(); + for (auto it = contacts.cbegin(); it != contacts.cend(); ++it) + deliver(wpe_event_touch_new(WPE_EVENT_TOUCH_CANCEL, view(), WPE_INPUT_SOURCE_TOUCHSCREEN, + time(), WPEModifiers(0), nativeContactId(it.key()), it.value().x() / Scale, it.value().y() / Scale)); + } + void fail() { + m_inputAllowed = false; + if (m_passkeyRequest) webkit_web_authentication_request_cancel(m_passkeyRequest); + if (m_webView) cancelInput(); + std::lock_guard guard(m_mutex); + m_snapshot.failed = true; + m_snapshot.loading = false; + m_snapshot.frame = {}; + } + bool post(std::function fn) { + if (!m_ready.load()) return false; + if (m_pending.fetch_add(1) >= 64) { + --m_pending; + return false; // Caller closes the app; never silently loses a held key. + } + auto *work = new std::function([this, fn = std::move(fn)] { + fn(); + --m_pending; + }); + GSource *source = g_idle_source_new(); + g_source_set_callback(source, [](gpointer data) -> gboolean { + (*static_cast *>(data))(); + return G_SOURCE_REMOVE; + }, work, [](gpointer data) { delete static_cast *>(data); }); + // Attaching a source never executes a callback on the submitting Qt + // thread, even during startup (unlike g_main_context_invoke). + g_source_attach(source, m_context); + g_source_unref(source); + return true; + } + static gboolean policy(WebKitWebView *, WebKitPolicyDecision *decision, + WebKitPolicyDecisionType type, gpointer data) { + auto *self = static_cast(data); + if (type == WEBKIT_POLICY_DECISION_TYPE_RESPONSE) { + auto *response = WEBKIT_RESPONSE_POLICY_DECISION(decision); + if (webkit_response_policy_decision_is_mime_type_supported(response)) return FALSE; + } else if (type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { + auto *action = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); + const char *uri = webkit_uri_request_get_uri(webkit_navigation_action_get_request(action)); + if (uri && rmweb::authNavigation(self->m_launch, uri) != rmweb::AuthNavigation::Blocked) return FALSE; + } + // No downloads, external schemes, pop-up windows, or TLS exceptions. + webkit_policy_decision_ignore(decision); + self->fail(); + return TRUE; + } + static void loadChanged(WebKitWebView *webView, WebKitLoadEvent event, gpointer data) { + auto *self = static_cast(data); + if (event == WEBKIT_LOAD_STARTED) { + self->cancelInput(); + self->m_inputAllowed = false; + ++self->m_generation; + } + const char *uri = webkit_web_view_get_uri(webView); + const auto navigation = uri ? rmweb::authNavigation(self->m_launch, uri) : rmweb::AuthNavigation::Blocked; + // WPE load-changed is for the main frame. Navigation actions also + // include child frames but expose no main-frame flag, so allow exact + // internal blanks there and reject a top-level blank here. Stop outside + // the snapshot mutex: WebKit may synchronously emit another callback. + if (event == WEBKIT_LOAD_COMMITTED && navigation == rmweb::AuthNavigation::LocalBlank) { + webkit_web_view_stop_loading(webView); + self->fail(); + return; + } + std::lock_guard guard(self->m_mutex); + if (event == WEBKIT_LOAD_STARTED) { + self->m_snapshot.generation = self->m_generation; + self->m_snapshot.frame = {}; + self->m_snapshot.loading = true; + self->m_snapshot.failed = false; + self->m_snapshot.callback = false; + } + if (uri && (navigation == rmweb::AuthNavigation::Web || navigation == rmweb::AuthNavigation::Callback)) + self->m_snapshot.host = QUrl::fromEncoded(uri).host(); + if (event == WEBKIT_LOAD_COMMITTED && navigation == rmweb::AuthNavigation::Callback) + self->m_snapshot.callback = true; + if (event == WEBKIT_LOAD_FINISHED) { + self->m_snapshot.loading = false; + self->m_inputAllowed = !self->m_snapshot.failed && !self->m_snapshot.callback; + } + } + static void buffer(WPEView *, WPEBuffer *buffer, gpointer data) { + auto *self = static_cast(data); + if (!WPE_IS_BUFFER_SHM(buffer) || wpe_buffer_get_width(buffer) != self->m_size.width() + || wpe_buffer_get_height(buffer) != self->m_size.height()) return; + auto *shm = WPE_BUFFER_SHM(buffer); + const guint stride = wpe_buffer_shm_get_stride(shm); + GBytes *bytes = wpe_buffer_shm_get_data(shm); // Borrowed; never unref or release the WPE buffer. + gsize length = 0; + const auto *pixels = bytes ? static_cast(g_bytes_get_data(bytes, &length)) : nullptr; + if (!pixels || stride < guint(self->m_size.width() * 4) || stride > 32768 + || length < gsize(stride) * gsize(self->m_size.height())) return; + QImage frame = QImage(pixels, self->m_size.width(), self->m_size.height(), stride, QImage::Format_ARGB32).copy(); + std::lock_guard guard(self->m_mutex); + if (!self->m_snapshot.failed) self->m_snapshot.frame = std::move(frame); + } + void run() { + g_main_context_push_thread_default(m_context); + GError *error = nullptr; + WPEDisplay *display = wpe_display_headless_new(); + if (!display || !wpe_display_connect(display, &error)) { + g_clear_error(&error); + fail(); + return; + } + WebKitNetworkSession *session = webkit_network_session_new_ephemeral(); + if (!session || !webkit_network_session_is_ephemeral(session)) { fail(); return; } + webkit_network_session_set_persistent_credential_storage_enabled(session, FALSE); + webkit_network_session_set_tls_errors_policy(session, WEBKIT_TLS_ERRORS_POLICY_FAIL); + g_signal_connect(session, "download-started", G_CALLBACK(+[](WebKitNetworkSession *, WebKitDownload *download, gpointer) { + webkit_download_cancel(download); + }), nullptr); + m_webView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", display, + "network-session", session, nullptr)); + g_object_unref(display); + g_object_unref(session); + WebKitSettings *settings = webkit_web_view_get_settings(m_webView); + webkit_settings_set_enable_developer_extras(settings, FALSE); + webkit_settings_set_enable_write_console_messages_to_stdout(settings, FALSE); + webkit_settings_set_javascript_can_access_clipboard(settings, FALSE); + webkit_settings_set_javascript_can_open_windows_automatically(settings, FALSE); + webkit_settings_set_enable_media_stream(settings, FALSE); + webkit_settings_set_enable_webrtc(settings, FALSE); + webkit_settings_set_enable_page_cache(settings, FALSE); + g_signal_connect(m_webView, "decide-policy", G_CALLBACK(policy), this); + g_signal_connect(m_webView, "webauthn-request", G_CALLBACK(webAuthentication), this); + g_signal_connect(m_webView, "resource-load-started", G_CALLBACK(+[](WebKitWebView *, WebKitWebResource *resource, + WebKitURIRequest *, gpointer data) { + g_signal_connect(resource, "notify::response", G_CALLBACK(resourceResponse), data); + }), this); + g_signal_connect(m_webView, "load-changed", G_CALLBACK(loadChanged), this); + g_signal_connect(m_webView, "load-failed", G_CALLBACK(+[](WebKitWebView *, WebKitLoadEvent, const char *, GError *error, gpointer data) -> gboolean { + // A superseded/stopped navigation is ordinary browser behavior. + // Do not clear a failure already set by a rejected navigation. + if (g_error_matches(error, WEBKIT_NETWORK_ERROR, WEBKIT_NETWORK_ERROR_CANCELLED)) return TRUE; + static_cast(data)->fail(); + return TRUE; // Never render an error page containing a callback URL. + }), this); + g_signal_connect(m_webView, "load-failed-with-tls-errors", G_CALLBACK(+[](WebKitWebView *, const char *, GTlsCertificate *, GTlsCertificateFlags, gpointer data) -> gboolean { + static_cast(data)->fail(); + return TRUE; + }), this); + g_signal_connect(m_webView, "web-process-terminated", G_CALLBACK(+[](WebKitWebView *, WebKitWebProcessTerminationReason, gpointer data) { + static_cast(data)->fail(); + }), this); + g_signal_connect(m_webView, "permission-request", G_CALLBACK(+[](WebKitWebView *, WebKitPermissionRequest *request, gpointer) -> gboolean { + webkit_permission_request_deny(request); + return TRUE; + }), nullptr); + g_signal_connect(m_webView, "run-file-chooser", G_CALLBACK(+[](WebKitWebView *, WebKitFileChooserRequest *request, gpointer) -> gboolean { + webkit_file_chooser_request_cancel(request); + return TRUE; + }), nullptr); + WPEView *wpeView = view(); + if (WPEToplevel *top = wpe_view_get_toplevel(wpeView)) { + wpe_toplevel_scale_changed(top, Scale); + wpe_toplevel_resize(top, m_size.width() / Scale, m_size.height() / Scale); + } + wpe_view_resized(wpeView, m_size.width() / Scale, m_size.height() / Scale); + g_signal_connect(wpeView, "buffer-rendered", G_CALLBACK(buffer), this); + wpe_view_set_visible(wpeView, FALSE); + wpe_view_set_visible(wpeView, TRUE); + wpe_view_focus_in(wpeView); + m_ready = true; + webkit_web_view_load_uri(m_webView, m_launch.initialUrl.toEncoded().constData()); + // No persistent profile/history/HTML or form-field JavaScript. The + // process owns this temporary session until AppLoad/Return closes it. + g_main_loop_run(g_main_loop_new(m_context, FALSE)); + } + rmweb::AuthLaunch m_launch; + const QSize m_size; + GMainContext *m_context; + WebKitWebView *m_webView = nullptr; // GLib worker only. + std::atomic m_ready{false}; + std::atomic m_pending{0}; + std::mutex m_mutex; + Snapshot m_snapshot; + QHash m_contacts; + rmweb::AuthKeyboard m_keyboard; + quint64 m_generation = 0; // GLib worker only. + bool m_inputAllowed = false; + rmweb::AuthPasskey *m_passkeys = nullptr; // Qt main thread owns the helper session. + WebKitWebAuthenticationRequest *m_passkeyRequest = nullptr; // GLib thread only. + gulong m_passkeyCancelledSignal = 0; + quint64 m_passkeySerial = 0; + quint64 m_activePasskeyId = 0; + unsigned m_httpFailureReports = 0; +}; + +class SnapshotPump : public QObject { +public: + SnapshotPump(QObject *parent, std::function consume) : QObject(parent) { + m_timer.setTimerType(Qt::PreciseTimer); + m_timer.setInterval(750); + QObject::connect(&m_timer, &QTimer::timeout, this, [this, consume = std::move(consume)] { + consume(); + if (m_interaction.isValid() && m_interaction.elapsed() >= 1000) + m_timer.setInterval(750); + }); + m_timer.start(); + } + void noteInteraction() { + m_interaction.start(); + // Restart only when entering the burst. Continuous input must not + // postpone the next pickup, and passive pages retain e-ink batching. + if (m_timer.interval() != 125) m_timer.start(125); + } +private: + QTimer m_timer; + QElapsedTimer m_interaction; +}; + +SnapshotPump *startSnapshotPump(QObject *parent, std::function consume) { + return new SnapshotPump(parent, std::move(consume)); +} + +bool privateProcess() { + const rlimit core = {0, 0}; + if (setrlimit(RLIMIT_CORE, &core) != 0) return false; + const int nullFd = open("/dev/null", O_RDWR | O_CLOEXEC); + if (nullFd < 0) return false; + bool ok = true; + for (int fd : {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}) if (dup2(nullFd, fd) < 0) ok = false; + if (nullFd > STDERR_FILENO) close(nullFd); + return ok; // WebKit/GLib child diagnostics cannot disclose URLs or page text. +} +} + +int main(int argc, char **argv) { + if (!privateProcess()) return 2; + if (argc != 2 && argc != 4) return 2; + if (argc == 4 && QByteArray(argv[2]) != "--device-code") return 2; + const auto launch = rmweb::parseAuthLaunch(argv[1], argc == 4 ? QString::fromLatin1(argv[3]) : QString{}); + if (!launch || (argc == 4 && launch->deviceCode.isEmpty()) || qEnvironmentVariableIsEmpty("QTFB_KEY")) return 2; + std::signal(SIGTERM, [](int) { _exit(0); }); + std::signal(SIGINT, [](int) { _exit(0); }); + QCoreApplication app(argc, argv); + rmweb::QtfbClient client; + rmweb::AuthSurface surface; + rmweb::AuthPasskey passkeys; + AuthEngine engine(*launch, surface.contentSize(), &passkeys); + surface.setOrigin(launch->initialUrl.host()); + surface.setDeviceCode(launch->deviceCode); + bool closing = false; + auto close = [&client, &passkeys, &closing](int code) { + if (closing) return; + closing = true; + passkeys.shutdown(); + client.close(); + // Avoid running WebKit teardown on Qt's thread. The private network + // session and its helpers terminate with this process/IPC connection. + std::_Exit(code); + }; + quint64 displayedGeneration = 0; + auto *paintPump = startSnapshotPump(&app, [&] { + const auto snapshot = engine.takeSnapshot(); + displayedGeneration = snapshot.generation; + surface.beginNavigation(displayedGeneration); + surface.setOrigin(snapshot.host.isEmpty() ? launch->initialUrl.host() : snapshot.host); + surface.setLoading(snapshot.loading); + surface.setFailed(snapshot.failed); + surface.setCallbackReached(snapshot.callback); + if (!snapshot.frame.isNull()) surface.setFrame(snapshot.frame); + }); + QObject::connect(&client, &rmweb::QtfbClient::initialized, &app, [&](QSize) { + client.submitImage(surface.image()); + engine.start(); + paintPump->noteInteraction(); + }); + bool paintPending = false; + QObject::connect(&surface, &rmweb::AuthSurface::repaintRequested, &app, [&] { + if (paintPending) return; + paintPending = true; + QTimer::singleShot(0, &app, [&] { + paintPending = false; + if (client.isReady()) client.submitImage(surface.image()); + }); + }); + QObject::connect(&surface, &rmweb::AuthSurface::closeRequested, &app, [&] { close(0); }); + QObject::connect(&passkeys, &rmweb::AuthPasskey::promptChanged, &surface, &rmweb::AuthSurface::setPasskeyPrompt); + QObject::connect(&passkeys, &rmweb::AuthPasskey::completed, &app, + [&](quint64 id, bool success, rmweb::AuthPasskeyAssertion assertion) { + if (!closing && !engine.completePasskey(id, success, std::move(assertion))) close(2); + }); + QObject::connect(&surface, &rmweb::AuthSurface::passkeyCancelRequested, &app, [&] { + if (!engine.cancelPasskey()) close(2); + }); + QObject::connect(&client, &rmweb::QtfbClient::connectionClosed, &app, [&] { close(0); }); + QObject::connect(&client, &rmweb::QtfbClient::error, &app, [&](const QString &) { close(2); }); + QObject::connect(&client, &rmweb::QtfbClient::rotationChanged, &surface, [&] { + surface.cancelTouches(); + if (!engine.cancel()) close(2); + }); + QObject::connect(&client, &rmweb::QtfbClient::touchPressed, &surface, &rmweb::AuthSurface::press); + QObject::connect(&client, &rmweb::QtfbClient::touchesCancelled, &surface, &rmweb::AuthSurface::cancelTouches); + QObject::connect(&client, &rmweb::QtfbClient::touchMoved, &surface, &rmweb::AuthSurface::move); + QObject::connect(&client, &rmweb::QtfbClient::touchReleased, &surface, &rmweb::AuthSurface::release); + QObject::connect(&client, &rmweb::QtfbClient::penPressed, &surface, &rmweb::AuthSurface::penPress); + QObject::connect(&client, &rmweb::QtfbClient::penMoved, &surface, &rmweb::AuthSurface::penMove); + QObject::connect(&client, &rmweb::QtfbClient::penReleased, &surface, &rmweb::AuthSurface::penRelease); + QObject::connect(&surface, &rmweb::AuthSurface::touchPressed, &app, [&](int id, int x, int y) { + paintPump->noteInteraction(); + if (!engine.touch(WPE_EVENT_TOUCH_DOWN, id, x, y, displayedGeneration)) close(2); + }); + QObject::connect(&surface, &rmweb::AuthSurface::touchMoved, &app, [&](int id, int x, int y) { + paintPump->noteInteraction(); + if (!engine.touch(WPE_EVENT_TOUCH_MOVE, id, x, y, displayedGeneration)) close(2); + }); + QObject::connect(&surface, &rmweb::AuthSurface::touchReleased, &app, [&](int id, int x, int y) { + paintPump->noteInteraction(); + if (!engine.touch(WPE_EVENT_TOUCH_UP, id, x, y, displayedGeneration)) close(2); + }); + QObject::connect(&surface, &rmweb::AuthSurface::touchesCancelled, &app, [&] { if (!engine.cancel()) close(2); }); + QObject::connect(&client, &rmweb::QtfbClient::keyEvent, &app, [&](int raw, bool pressed) { + if (!pressed || surface.acceptsKeys()) { + paintPump->noteInteraction(); + if (!engine.key(raw, pressed, displayedGeneration)) close(2); + } + }); + if (!client.startFromEnvironment()) return 2; + const int result = app.exec(); + close(result); + return result; +} diff --git a/engine/wpeqt/auth-passkey.cpp b/engine/wpeqt/auth-passkey.cpp new file mode 100644 index 0000000..f5f28d2 --- /dev/null +++ b/engine/wpeqt/auth-passkey.cpp @@ -0,0 +1,217 @@ +#include "auth-passkey.h" +#include +#include +#include +#include +#include + +namespace rmweb { +namespace { +constexpr qsizetype MaxLine = 64 * 1024; +constexpr qsizetype MaxOutput = 512 * 1024; +bool decode(const QJsonValue &value, QByteArray &out, qsizetype minimum, qsizetype maximum) { + if (!value.isString()) return false; + const auto text = value.toString(); + static const QRegularExpression alphabet(QStringLiteral("\\A[A-Za-z0-9_-]*\\z")); + if (text.size() > maximum * 2 || !alphabet.match(text).hasMatch()) return false; + const auto encoded = text.toLatin1(); + const auto decoded = QByteArray::fromBase64Encoding(encoded, + QByteArray::Base64UrlEncoding | QByteArray::AbortOnBase64DecodingErrors); + if (!decoded || decoded.decoded.size() < minimum || decoded.decoded.size() > maximum + || decoded.decoded.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) != encoded) + return false; + out = decoded.decoded; + return true; +} +} +AuthPasskey::AuthPasskey(QObject *parent) + : AuthPasskey(QCoreApplication::applicationDirPath() + QStringLiteral("/rmweb-auth-passkey"), {}, parent) {} +AuthPasskey::AuthPasskey(QString executable, QStringList arguments, QObject *parent) + : QObject(parent), m_executable(std::move(executable)), m_arguments(std::move(arguments)) { + qRegisterMetaType(); + qRegisterMetaType(); + m_process.setStandardErrorFile(QProcess::nullDevice()); + m_deadline.setSingleShot(true); + m_deadline.setInterval(125000); + m_killTimer.setSingleShot(true); + m_killTimer.setInterval(5000); + connect(&m_deadline, &QTimer::timeout, this, [this] { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_deadline"); + fail(); + }); + connect(&m_killTimer, &QTimer::timeout, this, [this] { m_process.kill(); }); + connect(&m_process, &QProcess::readyReadStandardOutput, this, &AuthPasskey::readOutput); + connect(&m_process, &QProcess::started, this, [] { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_started"); + }); + connect(&m_process, &QProcess::finished, this, &AuthPasskey::finish); + connect(&m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_start_failed"); + else if (!m_failed) + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_process_error"); + fail(); + if (error == QProcess::FailedToStart) finish(-1, QProcess::CrashExit); + }); +} +AuthPasskey::~AuthPasskey() { shutdown(); } +void AuthPasskey::publishPrompt() { Q_EMIT promptChanged(m_prompt); } +void AuthPasskey::start(quint64 requestId, const QString &relyingParty, const QJsonObject &request) { + const auto bytes = QJsonDocument(request).toJson(QJsonDocument::Compact); + static const QRegularExpression domain(QStringLiteral("\\A[a-zA-Z0-9][a-zA-Z0-9.-]{0,251}[a-zA-Z0-9]\\z")); + if (!requestId || m_requestId || m_process.state() != QProcess::NotRunning + || bytes.size() > 128 * 1024 || !domain.match(relyingParty).hasMatch()) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: request_rejected"); + Q_EMIT completed(requestId, false, {}); + return; + } + m_requestId = requestId; + m_buffer.clear(); m_outputBytes = 0; + m_failed = false; m_terminal = false; m_haveAssertion = false; m_assertion = {}; + m_reportedStatuses = 0; + m_prompt = {true, relyingParty, QStringLiteral("Preparing phone sign-in"), {}}; + publishPrompt(); + m_deadline.start(); + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_start_requested"); + m_process.start(m_executable, m_arguments, QIODevice::ReadWrite); + if (m_process.write(bytes) != bytes.size()) { + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_input_failed"); + fail(); return; + } + m_process.closeWriteChannel(); +} +void AuthPasskey::cancel(quint64 requestId) { + if (m_requestId && requestId == m_requestId) { + if (!m_failed) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_cancelled"); + fail(); + } +} +void AuthPasskey::fail() { + if (!m_requestId) return; + m_failed = true; m_assertion = {}; m_haveAssertion = false; + m_prompt = {}; publishPrompt(); + m_deadline.stop(); + if (m_process.state() != QProcess::NotRunning) { + m_process.terminate(); + if (!m_killTimer.isActive()) m_killTimer.start(); + } +} +void AuthPasskey::rejectOutput() { + if (!m_failed) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_output_rejected"); + fail(); +} +void AuthPasskey::readOutput() { + if (!m_requestId) { m_process.readAllStandardOutput(); return; } + while (m_process.bytesAvailable() > 0) { + const auto bytes = m_process.read(qMin(m_process.bytesAvailable(), MaxLine + 1)); + m_outputBytes += bytes.size(); + if (m_failed) return; + m_buffer.append(bytes); + if (m_outputBytes > MaxOutput) { rejectOutput(); continue; } + qsizetype newline; + while ((newline = m_buffer.indexOf('\n')) >= 0) { + if (newline > MaxLine || m_terminal) { rejectOutput(); break; } + const auto line = m_buffer.left(newline); + m_buffer.remove(0, newline + 1); + QJsonParseError error; + const auto document = QJsonDocument::fromJson(line, &error); + if (error.error != QJsonParseError::NoError || !document.isObject() || !message(document.object())) { + rejectOutput(); break; + } + } + if (m_buffer.size() > MaxLine) rejectOutput(); + } +} +bool AuthPasskey::message(const QJsonObject &object) { + const auto type = object.value("type").toString(); + if (type == "qr") { + const auto sizeValue = object.value("size"); + const int size = sizeValue.toInt(); + const auto modules = object.value("modules").toString(); + if (!sizeValue.isDouble() || sizeValue.toDouble() != size || size < 21 || size > 177 + || (size - 21) % 4 || modules.size() != size * size || !m_prompt.qr.isNull()) return false; + QImage qr(size + 8, size + 8, QImage::Format_RGB32); + qr.fill(Qt::white); + for (int y = 0; y < size; ++y) for (int x = 0; x < size; ++x) { + const auto c = modules.at(y * size + x); + if (c != u'0' && c != u'1') return false; + if (c == u'1') qr.setPixelColor(x + 4, y + 4, Qt::black); + } + m_prompt.qr = qr; + m_prompt.status = QStringLiteral("Scan with your phone"); + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: qr_ready"); + publishPrompt(); + return true; + } + if (type == "status") { + const auto state = object.value("state").toString(); + QString label; + // Report each known state once per ceremony. The helper can repeat + // progress messages, but no helper-provided string enters syslog. + unsigned status = 0; + if (state == "waiting_for_phone") { + label = QStringLiteral("Scan with your phone"); status = 1; + if (!(m_reportedStatuses & status)) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: waiting_for_phone"); + } else if (state == "connecting") { + label = QStringLiteral("Connecting to your phone"); status = 2; + if (!(m_reportedStatuses & status)) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: connecting"); + } else if (state == "verifying") { + label = QStringLiteral("Verifying phone connection"); status = 4; + if (!(m_reportedStatuses & status)) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: verifying"); + } else if (state == "connected") { + label = QStringLiteral("Continue on your phone"); status = 8; + if (!(m_reportedStatuses & status)) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: connected"); + } else if (state == "confirm_on_phone") { + label = QStringLiteral("Continue on your phone"); status = 16; + if (!(m_reportedStatuses & status)) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: confirm_on_phone"); + } + else return false; + m_reportedStatuses |= status; + m_prompt.status = label; publishPrompt(); + return true; + } + if (type == "result") { + AuthPasskeyAssertion result; + if (!decode(object.value("credentialId"), result.credentialId, 1, 1024) + || !decode(object.value("authenticatorData"), result.authenticatorData, 37, 16384) + || !decode(object.value("signature"), result.signature, 1, 4096) + || (object.contains("userHandle") && !object.value("userHandle").isNull() + && !decode(object.value("userHandle"), result.userHandle, 1, 64))) return false; + m_assertion = std::move(result); m_haveAssertion = true; m_terminal = true; + syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: assertion_received"); + return true; + } + if (type == "error") { + const auto name = object.value("name").toString(); + if (name != "NotAllowedError" && name != "NotSupportedError" && name != "OperationError") return false; + if (name == "NotAllowedError") syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: not_allowed"); + else if (name == "NotSupportedError") syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: not_supported"); + else syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: operation_error"); + m_terminal = true; + return true; + } + return false; +} +void AuthPasskey::finish(int exitCode, QProcess::ExitStatus exitStatus) { + if (!m_requestId) return; + readOutput(); + const auto id = m_requestId; + const bool success = !m_failed && m_terminal && m_buffer.isEmpty() && m_haveAssertion + && exitStatus == QProcess::NormalExit && exitCode == 0; + if (success) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_exit_success"); + else syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_exit_failure"); + const auto assertion = success ? m_assertion : AuthPasskeyAssertion{}; + m_requestId = 0; m_buffer.clear(); m_assertion = {}; + m_deadline.stop(); m_killTimer.stop(); + m_prompt = {}; publishPrompt(); + Q_EMIT completed(id, success, assertion); +} +void AuthPasskey::shutdown() { + if (!m_requestId && m_process.state() == QProcess::NotRunning) return; + if (!m_failed) syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-auth passkey: helper_shutdown"); + fail(); + if (m_process.state() != QProcess::NotRunning && !m_process.waitForFinished(5000)) { + m_process.kill(); m_process.waitForFinished(1000); + } +} +} // namespace rmweb diff --git a/engine/wpeqt/auth-passkey.h b/engine/wpeqt/auth-passkey.h new file mode 100644 index 0000000..17b1388 --- /dev/null +++ b/engine/wpeqt/auth-passkey.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class AuthPasskeyTest; +namespace rmweb { +struct AuthPasskeyPrompt { + bool active = false; + QString relyingParty; + QString status; + QImage qr; +}; +struct AuthPasskeyAssertion { + QByteArray credentialId; + QByteArray authenticatorData; + QByteArray signature; + QByteArray userHandle; +}; + +// One private helper process per browser ceremony. Diagnostics contain only +// fixed lifecycle labels, never request/response data or page JavaScript. +class AuthPasskey final : public QObject { + Q_OBJECT +public: + explicit AuthPasskey(QObject *parent = nullptr); + ~AuthPasskey() override; + void start(quint64 requestId, const QString &relyingParty, const QJsonObject &request); + void cancel(quint64 requestId); + void shutdown(); +Q_SIGNALS: + void promptChanged(rmweb::AuthPasskeyPrompt prompt); + void completed(quint64 requestId, bool success, rmweb::AuthPasskeyAssertion assertion); +private: + friend class ::AuthPasskeyTest; + AuthPasskey(QString executable, QStringList arguments, QObject *parent = nullptr); + void readOutput(); + bool message(const QJsonObject &object); + void fail(); + void rejectOutput(); + void finish(int exitCode, QProcess::ExitStatus exitStatus); + void publishPrompt(); + QProcess m_process; + QTimer m_deadline; + QTimer m_killTimer; + QString m_executable; + QStringList m_arguments; + quint64 m_requestId = 0; + QByteArray m_buffer; + qsizetype m_outputBytes = 0; + bool m_failed = false; + bool m_terminal = false; + bool m_haveAssertion = false; + unsigned m_reportedStatuses = 0; + AuthPasskeyPrompt m_prompt; + AuthPasskeyAssertion m_assertion; +}; +} // namespace rmweb +Q_DECLARE_METATYPE(rmweb::AuthPasskeyPrompt) +Q_DECLARE_METATYPE(rmweb::AuthPasskeyAssertion) diff --git a/engine/wpeqt/auth-policy.cpp b/engine/wpeqt/auth-policy.cpp new file mode 100644 index 0000000..7252b18 --- /dev/null +++ b/engine/wpeqt/auth-policy.cpp @@ -0,0 +1,169 @@ +#include "auth-policy.h" +#include + +namespace rmweb { +namespace { +std::optional webUrl(const QByteArray &encoded, int limit) { + if (encoded.isEmpty() || encoded.size() > limit) return {}; + for (const unsigned char c : encoded) if (c <= 0x20 || c == 0x7f) return {}; + const QUrl url = QUrl::fromEncoded(encoded, QUrl::StrictMode); + if (!url.isValid() || url.host().isEmpty() || url.authority().contains('@')) return {}; + return url; +} +std::optional loopbackCallback(const QString &address) { + const auto encoded = address.toUtf8(); + const auto url = webUrl(encoded, 8192); + if (!url || url->scheme() != "http" || url->port() < 1024 || url->port() > 65535 + || (url->host() != "localhost" && url->host() != "127.0.0.1" && url->host() != "::1") + || !url->path().startsWith('/') || url->hasQuery() || url->hasFragment()) return {}; + // The query value has already been decoded once. Require a literal endpoint + // so a second decoder, encoded separator, or normalized path cannot change + // the caller's callback target. The outer redirect_uri may be URL-encoded. + if (encoded.contains('%') || url->toEncoded(QUrl::FullyEncoded) != encoded + || url->adjusted(QUrl::NormalizePathSegments).toEncoded(QUrl::FullyEncoded) != encoded) return {}; + return url; +} +} +std::optional parseAuthLaunch(const QByteArray &encoded, const QString &deviceCode) { + const auto url = webUrl(encoded, 8192); + if (!url || url->scheme() != "https" || url->hasFragment()) return {}; + if (deviceCode.size() > 64) return {}; + for (const QChar c : deviceCode) { + if (!(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') && c != '-') return {}; + } + const QUrlQuery query(*url); + const auto states = query.allQueryItemValues("state", QUrl::FullyDecoded); + const auto redirects = query.allQueryItemValues("redirect_uri", QUrl::FullyDecoded); + if (states.isEmpty() && redirects.isEmpty()) return AuthLaunch{*url, {}, {}, deviceCode}; + if (!deviceCode.isEmpty() || states.size() != 1 || states.front().size() < 16 || states.front().size() > 256 + || redirects.size() != 1) return {}; + for (const QChar c : states.front()) { + if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') + && !(c >= '0' && c <= '9') && c != '-' && c != '_') return {}; + } + const auto callback = loopbackCallback(redirects.front()); + if (!callback) return {}; + return AuthLaunch{*url, *callback, states.front(), {}}; +} +AuthNavigation authNavigation(const AuthLaunch &launch, const QByteArray &encoded) { + if (encoded == "about:blank" || encoded == "about:srcdoc") return AuthNavigation::LocalBlank; + const auto url = webUrl(encoded, 16 * 1024); + if (!url) return AuthNavigation::Blocked; + if (url->scheme() == "https") return AuthNavigation::Web; + if (launch.callbackUrl.isEmpty() || url->hasFragment() + || url->adjusted(QUrl::RemoveQuery) != launch.callbackUrl + || encoded.left(encoded.indexOf('?')) != launch.callbackUrl.toEncoded(QUrl::FullyEncoded)) return AuthNavigation::Blocked; + const QUrlQuery query(*url); + if (query.allQueryItemValues("state", QUrl::FullyDecoded) != QStringList{launch.state}) + return AuthNavigation::Blocked; + const auto codes = query.allQueryItemValues("code", QUrl::FullyDecoded); + const auto errors = query.allQueryItemValues("error", QUrl::FullyDecoded); + // A denied authorization still belongs to the caller's callback handler. Neither + // callback variant is treated here as proof of successful authentication. + const auto values = errors.isEmpty() ? codes : errors; + if ((!codes.isEmpty() && !errors.isEmpty()) || values.size() != 1 + || values.front().isEmpty() || values.front().size() > 8192) return AuthNavigation::Blocked; + for (const QChar c : values.front()) if (c.isNull() || c.isLowSurrogate() || c.isHighSurrogate() || c.category() == QChar::Other_Control) + return AuthNavigation::Blocked; + return AuthNavigation::Callback; +} +std::optional authKey(int raw) { + if (raw < 0 || (raw & ~0x7000ff)) return {}; + const int base = raw & 0xfffff; + quint32 value = 0; + if ((base >= 32 && base <= 96) || (base >= 123 && base <= 126)) { + value = quint32(base); + if (base >= 'A' && base <= 'Z' && !(raw & 0x100000)) value += 'a' - 'A'; + } else { + switch (base) { + case 9: value = 0xff09; break; // Tab + case 13: value = 0xff0d; break; // Return + case 27: value = 0xff1b; break; // Escape + case 127: value = 0xffff; break; + case 128: value = 0xff08; break; // Layout Backspace, not common.h PGUP. + case 129: value = 0xff55; break; + case 130: value = 0xff56; break; + case 131: value = 0xff54; break; + case 132: value = 0xff52; break; + case 133: value = 0xff51; break; + case 134: value = 0xff53; break; + case 135: value = 0xff50; break; + case 136: value = 0xff57; break; + default: return {}; + } + } + const quint32 modifiers = ((raw & 0x200000) ? 1U : 0U) + | ((raw & 0x100000) ? 2U : 0U) | ((raw & 0x400000) ? 4U : 0U); + // WPE 2.48 uses XKB physical keycodes (evdev + 8) for DOM event.code. + // AppLoad sends its own character/layout IDs, so forwarding base directly + // would label Tab as Escape and Return as Digit4. + quint32 code = 0; + if (base >= 'A' && base <= 'Z') { + static constexpr quint32 letters[] = { + 0x26, 0x38, 0x36, 0x28, 0x1a, 0x29, 0x2a, 0x2b, 0x1f, + 0x2c, 0x2d, 0x2e, 0x3a, 0x39, 0x20, 0x21, 0x18, 0x1b, + 0x27, 0x1c, 0x1e, 0x37, 0x19, 0x35, 0x1d, 0x34 + }; + code = letters[base - 'A']; + } else if (base >= '1' && base <= '9') code = 0x0a + base - '1'; + else switch (base) { + case '0': case ')': code = 0x13; break; + case '!': code = 0x0a; break; + case '@': code = 0x0b; break; + case '#': code = 0x0c; break; + case '$': code = 0x0d; break; + case '%': code = 0x0e; break; + case '^': code = 0x0f; break; + case '&': code = 0x10; break; + case '*': code = 0x11; break; + case '(': code = 0x12; break; + case '-': case '_': code = 0x14; break; + case '=': case '+': code = 0x15; break; + case '[': case '{': code = 0x22; break; + case ']': case '}': code = 0x23; break; + case ';': case ':': code = 0x2f; break; + case '\'': case '"': code = 0x30; break; + case '`': case '~': code = 0x31; break; + case '\\': case '|': code = 0x33; break; + case ',': case '<': code = 0x3b; break; + case '.': case '>': code = 0x3c; break; + case '/': case '?': code = 0x3d; break; + case ' ': code = 0x41; break; + case 9: code = 0x17; break; + case 13: code = 0x24; break; + case 27: code = 0x09; break; + case 127: code = 0x77; break; + case 128: code = 0x16; break; + case 129: code = 0x70; break; + case 130: code = 0x75; break; + case 131: code = 0x74; break; + case 132: code = 0x6f; break; + case 133: code = 0x71; break; + case 134: code = 0x72; break; + case 135: code = 0x6e; break; + case 136: code = 0x73; break; + } + return AuthKey{code, value, modifiers}; +} +QVector AuthKeyboard::event(int raw, bool pressed) { + const auto key = authKey(raw); + const auto identity = quint32(raw & 0xfffff); + if (!pressed) { + // AppLoad recomputes modifiers/alternate symbols at release. Keep the + // original key identity; a lost/changed release cancels owned keys. + if (!key || !m_pressed.contains(identity)) return cancel(); + const auto original = m_pressed.take(identity); + return {{original, false}}; + } + if (!key || m_pressed.contains(identity)) return {}; + if (m_pressed.size() >= 16) return cancel(); + m_pressed.insert(identity, *key); + return {{*key, true}}; +} +QVector AuthKeyboard::cancel() { + QVector result; + for (const auto &key : m_pressed) result.append({key, false}); + m_pressed.clear(); + return result; +} +} // namespace rmweb diff --git a/engine/wpeqt/auth-policy.h b/engine/wpeqt/auth-policy.h new file mode 100644 index 0000000..ddb636f --- /dev/null +++ b/engine/wpeqt/auth-policy.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rmweb { +struct AuthLaunch { + QUrl initialUrl; + QUrl callbackUrl; + QString state; + QString deviceCode; +}; +enum class AuthNavigation { Blocked, Web, Callback, LocalBlank }; +// The caller supplies an HTTPS URL and owns authorization state and completion. +// A redirect_uri/state pair opts into a canonical HTTP loopback callback; +// without that pair, HTTP navigation is never allowed. Device codes are display +// hints for ordinary HTTPS launches, separate from callback authorization. +std::optional parseAuthLaunch(const QByteArray &url, const QString &deviceCode = {}); +// LocalBlank covers only exact about:blank/about:srcdoc child documents; the +// driver rejects either top-level commit. Neither proves OAuth/enrollment success. +AuthNavigation authNavigation(const AuthLaunch &launch, const QByteArray &url); + +struct AuthKey { + quint32 code; + quint32 value; // XKB/WPE keysym, not a Qt key enum. + quint32 modifiers; // WPE: control=1, shift=2, alt=4. +}; +// Exact AppLoad v0.5.3 default.layout.json codes; modifier-only packets have +// no character. Never reads or stores a form field or synthesizes DOM mutations. +std::optional authKey(int apploadCode); +struct AuthKeyEvent { AuthKey key; bool pressed; }; +class AuthKeyboard { +public: + QVector event(int apploadCode, bool pressed); + QVector cancel(); +private: + QMap m_pressed; +}; +} // namespace rmweb diff --git a/engine/wpeqt/auth-surface.cpp b/engine/wpeqt/auth-surface.cpp new file mode 100644 index 0000000..0d7f310 --- /dev/null +++ b/engine/wpeqt/auth-surface.cpp @@ -0,0 +1,385 @@ +#include "auth-surface.h" +#include +#include +#include + +namespace rmweb { +namespace { +constexpr int HeaderHeight = 160; +// Hand-drawn 5x7 glyph rows: no runtime font service or text input state. +const char *glyph(char c) { + switch (c) { +#define G(c, rows) \ + case c: \ + return rows + G('A', "0e11111f111111"); + G('B', "1e11111e11111e"); + G('C', "0e11101010110e"); + G('D', "1e11111111111e"); + G('E', "1f10101e10101f"); + G('F', "1f10101e101010"); + G('G', "0e11101711110f"); + G('H', "1111111f111111"); + G('I', "0e04040404040e"); + G('J', "0702020202120c"); + G('K', "11121418141211"); + G('L', "1010101010101f"); + G('M', "111b1515111111"); + G('N', "11191513111111"); + G('O', "0e11111111110e"); + G('P', "1e11111e101010"); + G('Q', "0e11111115120d"); + G('R', "1e11111e141211"); + G('S', "0f10100e01011e"); + G('T', "1f040404040404"); + G('U', "1111111111110e"); + G('V', "11111111110a04"); + G('W', "11111115151b11"); + G('X', "11110a040a1111"); + G('Y', "11110a04040404"); + G('Z', "1f01020408101f"); + G('a', "00000e010f110f"); + G('b', "1010161911111e"); + G('c', "00000e1110110e"); + G('d', "01010d1311110f"); + G('e', "00000e111f100e"); + G('f', "0609081c080808"); + G('g', "00000f110f010e"); + G('h', "10101619111111"); + G('i', "04000c0404040e"); + G('j', "0200060202120c"); + G('k', "101011121c1211"); + G('l', "0c04040404040e"); + G('m', "00001a15151515"); + G('n', "00001619111111"); + G('o', "00000e1111110e"); + G('p', "00001e111e1010"); + G('q', "00000f110f0101"); + G('r', "00001619101010"); + G('s', "00000f100e011e"); + G('t', "08081c08080906"); + G('u', "0000111111130d"); + G('v', "00001111110a04"); + G('w', "0000111115150a"); + G('x', "0000110a040a11"); + G('y', "000011110f010e"); + G('z', "00001f0204081f"); + G('0', "0e11131519110e"); + G('1', "040c040404040e"); + G('2', "0e11010204081f"); + G('3', "1e01010e01011e"); + G('4', "02060a121f0202"); + G('5', "1f10101e01011e"); + G('6', "0e10101e11110e"); + G('7', "1f010204080808"); + G('8', "0e11110e11110e"); + G('9', "0e11110f01010e"); + G('.', "00000000000c0c"); + G(',', "000000000c0c08"); + G('-', "0000001f000000"); + G('_', "0000000000001f"); + G('!', "04040404040004"); + G('?', "0e110102040004"); + G(':', "000c0c000c0c00"); + G(';', "000c0c000c0c08"); + G('/', "01010204081010"); + G('\\', "10100804020101"); + G('|', "04040404040404"); + G('(', "02040808080402"); + G(')', "08040202020408"); + G('[', "0e08080808080e"); + G(']', "0e02020202020e"); + G('{', "02040408040402"); + G('}', "08040402040408"); + G('<', "01020408040201"); + G('>', "10080402040810"); + G('=', "00001f001f0000"); + G('+', "0004041f040400"); + G('*', "00150e1f0e1500"); + G('#', "0a0a1f0a1f0a0a"); + G('@', "0e11171516100f"); + G('$', "040f140e051e04"); + G('%', "18190204081303"); + G('&', "0c12140c15120d"); + G('^', "040a1100000000"); + G('~', "00000916000000"); + G('\'', "04040800000000"); + G('"', "0a0a1400000000"); + G('`', "08040200000000"); +#undef G + default: + return "00000000000000"; + } +} +int hex(char c) { + return c <= '9' ? c - '0' : c - 'a' + 10; +} +void label(QPainter &p, const QString &text, QRect bounds, int scale = 5) { + const QByteArray ascii = text.toLatin1(); + const int width = int(ascii.size()) * 6 * scale - scale; + const QPoint origin(bounds.center().x() - width / 2, bounds.center().y() - 7 * scale / 2); + p.save(); + p.setClipRect(bounds); + for (int i = 0; i < ascii.size(); ++i) { + const char *rows = glyph(ascii.at(i)); + for (int y = 0; y < 7; ++y) { + const int bits = hex(rows[y * 2]) * 16 + hex(rows[y * 2 + 1]); + for (int x = 0; x < 5; ++x) + if (bits & (1 << (4 - x))) + p.fillRect(origin.x() + (i * 6 + x) * scale, origin.y() + y * scale, scale, + scale, Qt::black); + } + } + p.restore(); +} +void button(QPainter &p, QRect rect, const QString &text, int scale = 5) { + p.setBrush(Qt::white); + p.setPen(QPen(Qt::black, 2)); + p.drawRoundedRect(rect.adjusted(5, 5, -5, -5), 12, 12); + label(p, text, rect.adjusted(8, 8, -8, -8), scale); +} +} // namespace +AuthSurface::AuthSurface(QSize size, QObject *parent) : QObject(parent), m_size(size) { + if (size != QSize(1620, 2160)) + m_size = {1620, 2160}; +} +QRect AuthSurface::contentRect() const { + return {0, HeaderHeight, m_size.width(), m_size.height() - HeaderHeight}; +} +QSize AuthSurface::contentSize() const { + return contentRect().size(); +} +QRect AuthSurface::returnRect() const { + return {18, 24, 290, 100}; +} +QRect AuthSurface::passkeyCancelRect() const { + return {m_size.width() / 2 - 260, 1680, 520, 110}; +} +void AuthSurface::setPasskeyPrompt(const AuthPasskeyPrompt &prompt) { + if (m_closing) return; + if (prompt.active != m_passkey.active) cancelTouches(); + m_passkey = prompt; + if (!prompt.active) m_passkeyCancelling = false; + Q_EMIT repaintRequested(); +} +bool AuthSurface::pageReady() const { + return !m_frame.isNull() && !m_waitingForFrame && !m_loading && !m_failed && !m_callback && + !m_closing && !m_passkey.active; +} +bool AuthSurface::acceptsKeys() const { + return pageReady(); +} +QImage AuthSurface::image() const { + QImage output(m_size, QImage::Format_RGBA8888); + output.fill(Qt::white); + QPainter p(&output); + if (!m_frame.isNull()) + p.drawImage(contentRect().topLeft(), m_frame); + button(p, returnRect(), m_callback ? QStringLiteral("Return") : QStringLiteral("Cancel")); + QString host = m_origin; + if (host.size() > 48) + host = host.left(22) + QStringLiteral("...") + host.right(23); + label(p, host, {330, 18, 900, 36}, 3); + if (!m_deviceCode.isEmpty()) + label(p, QStringLiteral("Code: ") + m_deviceCode, {330, 56, 900, 30}, 2); + const QString status = m_passkey.active ? QStringLiteral("Phone passkey") + : m_callback ? QStringLiteral("Return") + : m_failed ? QStringLiteral("Unable to load") + : m_loading ? QStringLiteral("Loading...") + : QStringLiteral("Sign in securely"); + label(p, status, {330, 98, 900, 38}, 3); + p.setPen(QPen(Qt::black, 2)); + p.drawLine(0, HeaderHeight - 1, m_size.width(), HeaderHeight - 1); + if (m_callback || m_failed || m_frame.isNull()) { + QRect card(m_size.width() / 2 - 580, contentRect().center().y() - 110, 1160, 220); + p.setBrush(Qt::white); + p.drawRoundedRect(card, 20, 20); + const QString text = m_callback ? QStringLiteral("Callback received") + : m_failed ? QStringLiteral("Unable to load sign-in page") + : QStringLiteral("Loading sign-in page..."); + label(p, text, card.adjusted(10, 16, -10, -100), 5); + label(p, + m_callback ? QStringLiteral("Return to the app to continue") + : m_failed ? QStringLiteral("Close and try again") + : QStringLiteral("Cancel is always available"), + card.adjusted(10, 110, -10, -10), 4); + } + if (m_passkey.active) { + p.fillRect(contentRect(), Qt::white); + label(p, QStringLiteral("Use a passkey from your phone"), {90, 260, 1440, 80}, 5); + // Show the whole verified RP ID; never hide its middle with an ellipsis. + const QString &site = m_passkey.relyingParty; + for (int start = 0; start < site.size(); start += 72) + label(p, site.mid(start, 72), {90, 352 + (start / 72) * 32, 1440, 32}, 3); + if (!m_passkey.qr.isNull() && !m_passkeyCancelling) { + const int scale = 960 / m_passkey.qr.width(); + const int side = m_passkey.qr.width() * scale; + p.setRenderHint(QPainter::SmoothPixmapTransform, false); + p.drawImage(QRect((m_size.width() - side) / 2, 500 + (960 - side) / 2, side, side), m_passkey.qr); + } + label(p, m_passkeyCancelling ? QStringLiteral("Cancelling...") : m_passkey.status, + {90, 1490, 1440, 70}, 5); + label(p, QStringLiteral("Keep your phone nearby with Bluetooth on"), {90, 1570, 1440, 55}, 3); + button(p, passkeyCancelRect(), QStringLiteral("Cancel passkey"), 4); + } + return output; +} +void AuthSurface::beginNavigation(quint64 generation) { + if (generation <= m_navigationGeneration || m_closing) + return; + cancelTouches(); + m_navigationGeneration = generation; + m_frame = {}; + m_waitingForFrame = true; + Q_EMIT repaintRequested(); +} +void AuthSurface::setFrame(const QImage &frame) { + if (frame.isNull() || frame.size() != contentSize() || m_closing) + return; + m_frame = frame; + m_waitingForFrame = false; + Q_EMIT repaintRequested(); +} +void AuthSurface::setLoading(bool loading) { + if (m_loading == loading) + return; + if (loading) { + cancelTouches(); + m_waitingForFrame = true; + } + m_loading = loading; + Q_EMIT repaintRequested(); +} +void AuthSurface::setFailed(bool failed) { + if (m_failed == failed) + return; + if (failed) + cancelTouches(); + m_failed = failed; + Q_EMIT repaintRequested(); +} +void AuthSurface::setCallbackReached(bool reached) { + if (m_callback == reached) + return; + cancelTouches(); + m_callback = reached; + Q_EMIT repaintRequested(); +} +void AuthSurface::setOrigin(const QString &host) { + static const QRegularExpression domain( + QStringLiteral("\\A[a-zA-Z0-9](?:[a-zA-Z0-9.-]{0,251}[a-zA-Z0-9])?\\z")); + const QString value = host.size() <= 253 && domain.match(host).hasMatch() ? host : QString(); + if (value == m_origin) + return; + m_origin = value; + Q_EMIT repaintRequested(); +} +void AuthSurface::setDeviceCode(const QString &code) { + static const QRegularExpression pattern(QStringLiteral("\\A[A-Z0-9-]{1,64}\\z")); + const QString value = pattern.match(code).hasMatch() ? code : QString(); + if (value == m_deviceCode) + return; + m_deviceCode = value; + Q_EMIT repaintRequested(); +} +AuthSurface::Contact AuthSurface::targetAt(QPoint point) const { + Contact c; + c.start = point; + if (!QRect(QPoint(), m_size).contains(point)) + return c; + if (returnRect().contains(point)) { + c.target = Return; + return c; + } + if (m_passkey.active && !m_passkeyCancelling && passkeyCancelRect().contains(point)) { + c.target = PasskeyCancel; + return c; + } + if (!pageReady()) + return c; + if (contentRect().contains(point)) { + c.target = Page; + return c; + } + return c; +} +void AuthSurface::cancelTouches() { + bool page = false; + for (const auto &c : m_contacts) + page |= c.target == Page && !c.cancelled; + m_contacts.clear(); + if (page) + Q_EMIT touchesCancelled(); +} +void AuthSurface::press(int id, int x, int y) { + if (id >= 0) + pressContact(id, x, y); +} +// Raw finger IDs are nonnegative. The pen's internal ID cannot collide with +// them, and simultaneous pen/finger input uses the same cancellation policy. +void AuthSurface::penPress(int x, int y) { pressContact(-1, x, y); } +void AuthSurface::penMove(int x, int y) { move(-1, x, y); } +void AuthSurface::penRelease(int x, int y) { release(-1, x, y); } +void AuthSurface::pressContact(int id, int x, int y) { + if (m_closing || m_contacts.contains(id)) + return; + if (!m_contacts.isEmpty()) { + const auto ids = m_contacts.keys(); + cancelTouches(); + Contact blocked; + blocked.cancelled = true; + for (int oldId : ids) + m_contacts.insert(oldId, blocked); + if (m_contacts.size() < 16) + m_contacts.insert(id, blocked); + return; + } + const Contact c = targetAt({x, y}); + m_contacts.insert(id, c); + if (c.target == Page) + Q_EMIT touchPressed(id, x, y - HeaderHeight); +} +void AuthSurface::move(int id, int x, int y) { + auto it = m_contacts.find(id); + if (it == m_contacts.end() || it->cancelled) + return; + if (it->target == Page) { + if (!contentRect().contains({x, y}) || !pageReady()) { + cancelTouches(); + return; + } + Q_EMIT touchMoved(id, x, y - HeaderHeight); + } else if ((it->start - QPoint(x, y)).manhattanLength() > 40) + it->cancelled = true; +} +void AuthSurface::release(int id, int x, int y) { + auto it = m_contacts.find(id); + if (it == m_contacts.end()) + return; + const Contact c = *it; + m_contacts.erase(it); + if (c.cancelled || m_closing) + return; + const Contact end = targetAt({x, y}); + if (c.target == Page) { + if (end.target == Page) + Q_EMIT touchReleased(id, x, y - HeaderHeight); + else + Q_EMIT touchesCancelled(); + return; + } + if (c.target != end.target || (c.start - QPoint(x, y)).manhattanLength() > 40) + return; + if (c.target == Return) { + cancelTouches(); + m_closing = true; + Q_EMIT closeRequested(); + } else if (c.target == PasskeyCancel) { + cancelTouches(); + m_passkeyCancelling = true; + m_passkey.qr = {}; + Q_EMIT repaintRequested(); + Q_EMIT passkeyCancelRequested(); + } +} +} // namespace rmweb diff --git a/engine/wpeqt/auth-surface.h b/engine/wpeqt/auth-surface.h new file mode 100644 index 0000000..662377d --- /dev/null +++ b/engine/wpeqt/auth-surface.h @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include +#include +#include +#include "auth-passkey.h" + +namespace rmweb { +// Private authentication chrome. No text-field model, history, fonts or QPA. +class AuthSurface : public QObject { + Q_OBJECT +public: + explicit AuthSurface(QSize size = {1620, 2160}, QObject *parent = nullptr); + QImage image() const; + QSize contentSize() const; + bool acceptsKeys() const; + QRect contentRect() const; + QRect returnRect() const; + QRect passkeyCancelRect() const; + void setPasskeyPrompt(const AuthPasskeyPrompt &prompt); + void beginNavigation(quint64 generation); + void setFrame(const QImage &frame); + void setLoading(bool loading); + void setFailed(bool failed); + void setCallbackReached(bool reached); + void setOrigin(const QString &host); + void setDeviceCode(const QString &code); + void press(int id, int x, int y); + void move(int id, int x, int y); + void release(int id, int x, int y); + void penPress(int x, int y); + void penMove(int x, int y); + void penRelease(int x, int y); + void cancelTouches(); +Q_SIGNALS: + void repaintRequested(); + void closeRequested(); + void passkeyCancelRequested(); + void touchPressed(int id, int x, int y); + void touchMoved(int id, int x, int y); + void touchReleased(int id, int x, int y); + void touchesCancelled(); + +private: + enum Target { None, Page, Return, PasskeyCancel }; + struct Contact { + Target target = None; + QPoint start; + bool cancelled = false; + }; + Contact targetAt(QPoint point) const; + void pressContact(int id, int x, int y); + bool pageReady() const; + QSize m_size; + QImage m_frame; + quint64 m_navigationGeneration = 0; + QHash m_contacts; + bool m_loading = true; + bool m_waitingForFrame = true; + bool m_failed = false; + bool m_callback = false; + bool m_closing = false; + QString m_origin; + QString m_deviceCode; + AuthPasskeyPrompt m_passkey; + bool m_passkeyCancelling = false; +}; +} // namespace rmweb diff --git a/engine/wpeqt/qtfbclient.cpp b/engine/wpeqt/qtfbclient.cpp new file mode 100644 index 0000000..bf24488 --- /dev/null +++ b/engine/wpeqt/qtfbclient.cpp @@ -0,0 +1,328 @@ +#include "qtfbclient.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_LINUX +#error "QTFB client requires Linux SOCK_SEQPACKET and POSIX shared memory" +#endif + +namespace { +// Wire layout verified against asivery/rm-appload v0.5.3, commit +// 5bb34a362f09f753f18bd6261558f8e2737aacdb, src/qtfb/common.h/fbmanagement.cpp. +// Its 24/32-byte little-endian ABI is unchanged in d58c3476, which adds optional +// rotation messages. Explicit offsets avoid C++ padding and upstream client bugs. +constexpr size_t ClientPacketBytes = 24; +constexpr size_t ServerPacketBytes = 32; +constexpr int DeadlineMs = 1500; +constexpr int MaxPacketsPerWake = 32; +constexpr int Initialize = 0, Update = 1, CustomInitialize = 2, Terminate = 3; +constexpr int UserInput = 4, StateChanged = 7, StateInitial = 8; +constexpr int Rgba8888 = 2; + +using Packet = std::array; +Packet packet(int type) { + Packet bytes{}; + bytes[0] = char(type); + return bytes; +} +void writeInt(Packet &bytes, int offset, int value) { + qToLittleEndian(value, bytes.data() + offset); +} +int readInt(const char *bytes, int offset) { + return qFromLittleEndian(bytes + offset); +} +} + +namespace rmweb { + +QtfbClient::QtfbClient(QObject *parent) : QObject(parent) { + m_handshakeDeadline.setSingleShot(true); + m_writeDeadline.setSingleShot(true); + connect(&m_handshakeDeadline, &QTimer::timeout, this, [this] { + fail(QStringLiteral("AppLoad framebuffer initialization timed out")); + }); + connect(&m_writeDeadline, &QTimer::timeout, this, [this] { + fail(QStringLiteral("AppLoad framebuffer stopped accepting updates")); + }); +} + +QtfbClient::~QtfbClient() { cleanup(); } + +bool QtfbClient::startFromEnvironment() { + const QByteArray value = qgetenv("QTFB_KEY"); + bool valid = !value.isEmpty() && value.size() <= 10; + for (const char c : value) valid = valid && c >= '0' && c <= '9'; + bool number = false; + const int key = value.toInt(&number); + if (!valid || !number || key < 0) { + fail(QStringLiteral("A valid AppLoad QTFB_KEY is required")); + return false; + } + return start(key); +} + +bool QtfbClient::start(int key, const QString &socketPath, QSize requestedSize) { + Q_ASSERT(thread() == QThread::currentThread()); + if (m_state != State::Idle) return false; + const QByteArray path = QFile::encodeName(socketPath); + sockaddr_un address{}; + if (key < 0 || requestedSize.width() < 1 || requestedSize.height() < 1 + || requestedSize.width() > 1620 || requestedSize.height() > 2160 + || path.isEmpty() || path[0] != '/' || path.contains('\0') + || size_t(path.size()) >= sizeof address.sun_path) { + fail(QStringLiteral("Invalid AppLoad framebuffer connection parameters")); + return false; + } + m_key = key; + m_size = requestedSize; + m_shmSize = size_t(m_size.width()) * size_t(m_size.height()) * 4; + m_state = State::Connecting; + m_socket = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (m_socket < 0) { + fail(QStringLiteral("Could not create AppLoad framebuffer socket")); + return false; + } + address.sun_family = AF_UNIX; + memcpy(address.sun_path, path.constData(), size_t(path.size()) + 1); + const int result = ::connect(m_socket, reinterpret_cast(&address), sizeof address); + // For local sockets EAGAIN means the accept backlog is full, not an in-flight + // connect. Fail clearly instead of treating it as a connected descriptor. + if (result < 0 && errno != EINPROGRESS) { + fail(QStringLiteral("Could not connect to AppLoad framebuffer")); + return false; + } + if (result == 0) m_state = State::Initializing; + m_readNotifier = new QSocketNotifier(m_socket, QSocketNotifier::Read, this); + m_writeNotifier = new QSocketNotifier(m_socket, QSocketNotifier::Write, this); + connect(m_readNotifier, &QSocketNotifier::activated, this, [this] { readable(); }); + connect(m_writeNotifier, &QSocketNotifier::activated, this, [this] { writable(); }); + m_handshakeDeadline.start(DeadlineMs); + flush(); + return m_state != State::Closed; +} + +bool QtfbClient::isReady() const { return m_state == State::Ready; } + +bool QtfbClient::submitImage(const QImage &image) { + Q_ASSERT(thread() == QThread::currentThread()); + if (!isReady() || image.isNull() || image.size() != m_size) return false; + // Bound conversion and queued storage to one <=14 MiB frame. All transport + // I/O is nonblocking; image conversion and copy do bounded CPU work only. + m_pendingImage = image.convertToFormat(QImage::Format_RGBA8888); + if (m_pendingImage.isNull()) return false; + flush(); + return isReady(); +} + +void QtfbClient::writable() { + if (m_state == State::Connecting) { + int socketError = 0; + socklen_t size = sizeof socketError; + if (getsockopt(m_socket, SOL_SOCKET, SO_ERROR, &socketError, &size) != 0 || socketError != 0) { + fail(QStringLiteral("AppLoad framebuffer connection failed")); + return; + } + m_state = State::Initializing; + } + flush(); +} + +bool QtfbClient::sendPacket(const char *data, size_t length) { + const ssize_t written = ::send(m_socket, data, length, MSG_DONTWAIT | MSG_NOSIGNAL); + if (written == ssize_t(length)) { + m_writeDeadline.stop(); + return true; + } + if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) { + if (!m_writeDeadline.isActive()) m_writeDeadline.start(DeadlineMs); + m_writeNotifier->setEnabled(true); + return false; + } + fail(QStringLiteral("AppLoad framebuffer update could not be sent")); + return false; +} + +void QtfbClient::flush() { + if (m_state == State::Connecting || m_state == State::Closed || m_state == State::Idle) return; + if (!m_initSent) { + Packet request = packet(CustomInitialize); + writeInt(request, 4, m_key); + request[8] = char(Rgba8888); + qToLittleEndian(quint16(m_size.width()), request.data() + 10); + qToLittleEndian(quint16(m_size.height()), request.data() + 12); + if (!sendPacket(request.data(), request.size())) return; + m_initSent = true; + } + if (isReady() && !m_pendingImage.isNull()) { + const size_t rowBytes = size_t(m_size.width()) * 4; + for (int row = 0; row < m_size.height(); ++row) { + memcpy(static_cast(m_shm) + size_t(row) * rowBytes, + m_pendingImage.constScanLine(row), rowBytes); + } + Packet update = packet(Update); // UPDATE_ALL=0; the packet is zero-filled. + if (!sendPacket(update.data(), update.size())) return; + m_pendingImage = {}; + // Disable before the signal so a new frame submitted from the signal + // can arm it again if that send encounters backpressure. + m_writeNotifier->setEnabled(false); + Q_EMIT frameSubmitted(); + return; + } + m_writeNotifier->setEnabled(false); +} + +bool QtfbClient::receiveInitialization(const char *bytes) { + if (m_state != State::Initializing || !m_initSent) { + fail(QStringLiteral("Unexpected AppLoad framebuffer initialization reply")); + return false; + } + const int key = readInt(bytes, 8); + const quint64 size = qFromLittleEndian(bytes + 16); + if (key < 0 || size != m_shmSize) { + fail(QStringLiteral("AppLoad framebuffer size does not match the request")); + return false; + } + const QByteArray name = QByteArray("/qtfb_") + QByteArray::number(key); + m_shmFd = shm_open(name.constData(), O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0); + struct stat info{}; + if (m_shmFd < 0 || fstat(m_shmFd, &info) != 0 || !S_ISREG(info.st_mode) + || info.st_uid != geteuid() || info.st_size != off_t(m_shmSize)) { + fail(QStringLiteral("AppLoad framebuffer shared memory is invalid")); + return false; + } + void *mapped = mmap(nullptr, m_shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_shmFd, 0); + if (mapped == MAP_FAILED) { + fail(QStringLiteral("Could not map AppLoad framebuffer shared memory")); + return false; + } + m_shm = mapped; + m_handshakeDeadline.stop(); + m_state = State::Ready; + Q_EMIT initialized(m_size); + return isReady(); +} + +void QtfbClient::receiveInput(const char *bytes) { + const int type = readInt(bytes, 8), id = readInt(bytes, 12); + const int x = readInt(bytes, 16), y = readInt(bytes, 20); + if (type == 0x13) { + if (id == 0 && x == 0 && y == 0 && readInt(bytes, 24) == 0) + Q_EMIT touchesCancelled(); + return; + } + if (type == 0x40 || type == 0x41) { + // v0.5.3 default.layout.json is the key-code source of truth. The + // common.h special-key constants omit Backspace and disagree with it. + constexpr int modifiers = 0x700000; + const int key = x & ~modifiers; + const bool known = key == 9 || key == 13 || key == 27 + || (key >= 32 && key <= 96) || (key >= 123 && key <= 136) + || (key == 0 && (x & modifiers) != 0); + if (id == 0 && y == 0 && readInt(bytes, 24) == 0 && x >= 0 && known) + Q_EMIT keyEvent(x, type == 0x40); + return; // Key codes and modifier bits are not framebuffer coordinates. + } + if (id < 0 || x < 0 || y < 0 || x >= m_size.width() || y >= m_size.height()) return; + if (type >= 0x20 && type <= 0x22) { + const int pressure = readInt(bytes, 24); + // AppLoad reports one pen (device 0), pressure 0..100, and 0 on release. + if (id != 0 || pressure < 0 || pressure > 100 || (type == 0x21 && pressure != 0)) return; + } + switch (type) { + case 0x10: Q_EMIT touchPressed(id, x, y); break; + case 0x11: Q_EMIT touchReleased(id, x, y); break; + case 0x12: Q_EMIT touchMoved(id, x, y); break; + case 0x20: Q_EMIT penPressed(x, y); break; + case 0x21: Q_EMIT penReleased(x, y); break; + case 0x22: Q_EMIT penMoved(x, y); break; + default: break; // Physical buttons are not pointer events. + } +} + +void QtfbClient::readable() { + if (m_state == State::Connecting) writable(); + for (int count = 0; count < MaxPacketsPerWake && m_socket >= 0; ++count) { + std::array bytes{}; + iovec buffer{bytes.data(), bytes.size()}; + msghdr message{}; + message.msg_iov = &buffer; + message.msg_iovlen = 1; + const ssize_t received = recvmsg(m_socket, &message, MSG_DONTWAIT); + if (received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return; + if (received < 0 && errno == EINTR) continue; + if (received == 0) { close(); return; } + if (received != ssize_t(ServerPacketBytes) || (message.msg_flags & MSG_TRUNC)) { + fail(QStringLiteral("Invalid AppLoad framebuffer packet length")); + return; + } + const int type = static_cast(bytes[0]); + if (type == Initialize) { + if (!receiveInitialization(bytes.data())) return; + } else if (!isReady()) { + fail(QStringLiteral("AppLoad framebuffer data arrived before initialization")); + return; + } else if (type == UserInput) { + receiveInput(bytes.data()); + } else if (type == StateChanged || type == StateInitial) { + const int reason = readInt(bytes.data(), 8), rotation = readInt(bytes.data(), 12); + if (reason != 0 || rotation < 0 || rotation > 3) { + fail(QStringLiteral("Invalid AppLoad framebuffer rotation")); + return; + } + Q_EMIT rotationChanged(rotation); + } else if (type == Terminate) { + close(); + return; + } else { + fail(QStringLiteral("Unknown AppLoad framebuffer packet")); + return; + } + } +} + +void QtfbClient::cleanup() { + m_handshakeDeadline.stop(); + m_writeDeadline.stop(); + for (QSocketNotifier **notifier : {&m_readNotifier, &m_writeNotifier}) { + if (*notifier) { + (*notifier)->setEnabled(false); + (*notifier)->deleteLater(); + *notifier = nullptr; + } + } + if (m_socket >= 0) { + const Packet terminate = packet(Terminate); + (void) ::send(m_socket, terminate.data(), terminate.size(), MSG_DONTWAIT | MSG_NOSIGNAL); + ::close(m_socket); + m_socket = -1; + } + if (m_shm) { munmap(m_shm, m_shmSize); m_shm = nullptr; } + if (m_shmFd >= 0) { ::close(m_shmFd); m_shmFd = -1; } + m_pendingImage = {}; + m_state = State::Closed; +} + +void QtfbClient::close() { + if (m_state == State::Closed) return; + cleanup(); + Q_EMIT connectionClosed(); +} + +void QtfbClient::fail(const QString &message) { + if (m_state == State::Closed) return; + cleanup(); + Q_EMIT error(message); + Q_EMIT connectionClosed(); +} +} diff --git a/engine/wpeqt/qtfbclient.h b/engine/wpeqt/qtfbclient.h new file mode 100644 index 0000000..07da308 --- /dev/null +++ b/engine/wpeqt/qtfbclient.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include + +class QSocketNotifier; + +namespace rmweb { + +// One connection to AppLoad's 64-bit Linux QTFB ABI. No display/input devices or +// QPA are opened here. Call methods on this object's thread; no automatic retry. +class QtfbClient final : public QObject { + Q_OBJECT +public: + explicit QtfbClient(QObject *parent = nullptr); + ~QtfbClient() override; + + bool startFromEnvironment(); + bool start(int key, const QString &socketPath = QStringLiteral("/tmp/qtfb.sock"), + QSize size = QSize(1620, 2160)); + bool isReady() const; + QSize size() const { return m_size; } + + // Accept the latest exact-size frame, replacing any unsent frame. False + // means invalid/not ready. frameSubmitted is a socket send, NOT panel ACK. + bool submitImage(const QImage &image); + void close(); + +Q_SIGNALS: + void initialized(QSize size); + void frameSubmitted(); + void touchPressed(int id, int x, int y); + void touchMoved(int id, int x, int y); + void touchReleased(int id, int x, int y); + // AppLoad touch lifecycle extension: a fresh sequence or cancellation. + void touchesCancelled(); + // Separate from finger contacts; callers explicitly opt into pen input. + void penPressed(int x, int y); + void penMoved(int x, int y); + void penReleased(int x, int y); + // Default AppLoad layout code plus its 0x700000 modifier bits. + void keyEvent(int code, bool pressed); + void rotationChanged(int rotation); + void connectionClosed(); + void error(const QString &message); + +private: + enum class State { Idle, Connecting, Initializing, Ready, Closed }; + void readable(); + void writable(); + void flush(); + void fail(const QString &message); + void cleanup(); + bool receiveInitialization(const char *packet); + void receiveInput(const char *packet); + bool sendPacket(const char *data, size_t length); + + State m_state = State::Idle; + int m_socket = -1; + int m_shmFd = -1; + int m_key = -1; + void *m_shm = nullptr; + size_t m_shmSize = 0; + QSize m_size; + bool m_initSent = false; + QImage m_pendingImage; + QSocketNotifier *m_readNotifier = nullptr; + QSocketNotifier *m_writeNotifier = nullptr; + QTimer m_handshakeDeadline; + QTimer m_writeDeadline; +}; + +} // namespace rmweb diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..2d8f3e0 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,103 @@ +# Native assertion and AppLoad patches + +`wpe-2.48.5-native-assertion-provider.patch` applies to the official WPE WebKit +2.48.5 source archive, SHA-256 +`01f36010705adb14404c56baf033147f7927cc7c6badec81bb141266fcdd8d0b`. +`wpe-2.48.5-native-assertion-provider-files.json` records the patched source bytes. +Original license notices remain intact. New GLib API files use LGPL-2.0-or-later; +the WPE coordinator uses BSD-2-Clause. Deliver the upstream source and patch with +the built library. + +The native `WebKitWebView::webauthn-request` signal carries immutable trusted +origin, RP ID, request-options JSON, and the exact 32-byte client-data hash. +The application retains the request while its helper runs, observes the borrowed +`GCancellable`, then completes once with copied assertion buffers or cancels. +All calls occur on the view's GLib thread. A missing handler fails explicitly. + +The UI process checks its own frame URL/origin, same-origin ancestors, HTTPS, +and the RP suffix against WebKit's public-suffix policy. Original WebCore secure +context, document focus, permissions policy, and abort handling remain active. +Client-data JSON is constructed once in WebKit; the helper signs its exact hash. +Returned RP hash, presence/verification flags, allow-list membership, and bounds +are checked before producing the DOM credential. + +This profile supports ordinary phone/hybrid assertions only. Registration, +platform authenticators, conditional/silent mediation, cross-origin/inherited +blank-frame requests, related-origin requests, and unsupported extensions fail +explicitly. Navigation, frame destruction, page closure, timeout, and abort +cancel pending work; stale completion is rejected. + +The official SDK build and offline actual-engine tests passed, including the +fixed diagnostic classification cases. The contributor also reported a +disposable relying-party **PASS** on Paper Pro 3.28.0.172 / Qt 6.10.3 on +2026-09-17; maintainer verification of that on-device result is pending. This +is separate from arbitrary account sign-in. See +`tests/auth-webauthn-provider/README.md` for the synthetic fixture's precise +boundary. + +## Fixed diagnostic events + +The WPE UI-process provider emits literal `rmweb-webauthn:` labels through +`syslog(LOG_AUTHPRIV | LOG_NOTICE, ...)`: `assertion-received`, +`reject-secure-origin`, `reject-mediation`, `reject-rp`, `unhandled-ui`, +`completed`, and `cancelled`. Unsupported option labels are +`reject-options-appid`, `reject-options-credProps`, `reject-options-largeBlob`, +`reject-options-prf`, `reject-options-platform`, and +`reject-options-credential-type`. Bounds labels distinguish +`reject-challenge-empty`, `reject-challenge-too-large`, `reject-allowlist-too-large`, +`reject-credential-id-empty`, and `reject-credential-id-too-large`; they never +include a numeric length or count. Only the first failed gate is reported. +No request values or formatted arguments are passed to syslog. Diagnostic calls +do not change rejection results, timeouts, or cancellation behavior. + +The challenge limit is 16,384 decoded bytes. Empty challenges still fail. +Credential IDs remain limited to 1,024 bytes, allowlists to 64 entries, and the helper request envelope to 128 KiB. +The trusted-origin, RP, verification, response-binding, and lifetime checks are +unchanged. This is a bounded compatibility budget, not a WebAuthn maximum or +a guarantee that every relying party is supported. + +`assertion-received` means the request reached the UI-process provider; WebCore +can reject a request before that boundary. `completed` means the provider +accepted assertion buffers, not that the relying party accepted sign-in. +`cancelled` covers every non-success completion, including timeout, an invalid +assertion, and an unhandled UI request. Late calls produce no additional finish +event. System syslog routing and retention apply to these fixed labels. + +## Bundled headless frame pacing + +Beyond the assertion provider, this patch intentionally bundles a second, +separable component in `Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp`: +it reworks frame pacing on the headless view. Upstream schedules frames at a +fixed 60 fps from buffer arrival; the bundled change + +- caps passive compositing at 8 frames per second, +- raises pacing to 30 Hz for about one second after observed native input + (pointer, scroll, keyboard, touch) so queued animated frames drain promptly + and a tap or keystroke is not displayed late, and +- stamps the pacing reference at completed presentation instead of buffer + arrival, so fast producers no longer alternate delayed with immediate frames. + +First and overdue frames stay immediate and buffer backpressure is unchanged. + +`WPEViewHeadless` is rmweb's production render path: both the regular browser +and the authentication browser create their display with +`wpe_display_headless_new`, so every presented frame flows through this view. +On e-ink the panel cannot usefully show 60 fps, while each composited frame +costs CPU time and power before its snapshot reaches the display controller. +Bounding passive compositing to 8 fps saves both; the short 30 Hz window after +input preserves interaction latency where it is perceptible. + +The component rides in this patch rather than a second patch because both touch +the same staged engine build, pin and qualification cycle; it is documented +here so the bundle carries no undocumented behavior. The offline actual-engine +timing fixture `tests/auth_frame_pacing_smoke.cpp` covers static, animated and +continuous frame production, including the post-input burst and the return to +passive pacing. + +## AppLoad prerequisites + +[AppLoad prerequisites](appload.md) documents the pinned AppLoad source, the QTFB lifetime, +keyboard-log removal and touch-cancellation patches, their apply order and +regression checks. These patches change the separate AppLoad dependency; they +are not applied by rmweb's browser build. Firmware-specific stock-UI hooks must +already be qualified for the target device. diff --git a/patches/appload-tests/CMakeLists.txt b/patches/appload-tests/CMakeLists.txt new file mode 100644 index 0000000..ebb0e81 --- /dev/null +++ b/patches/appload-tests/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.16) +project(appload_exit_regression LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick) +if(NOT EXISTS "${APPLOAD_SOURCE}/src/qtfb/fbmanagement.cpp") + message(FATAL_ERROR "Pass -DAPPLOAD_SOURCE=/path/to/reviewed/appload") +endif() +add_executable(appload_exit_test appload_exit_test.cpp + "${APPLOAD_SOURCE}/src/qtfb/FBController.cpp" + "${APPLOAD_SOURCE}/src/qtfb/FBController.h") +target_include_directories(appload_exit_test PRIVATE "${APPLOAD_SOURCE}/src/qtfb") +target_link_libraries(appload_exit_test PRIVATE Qt6::Core Qt6::Gui Qt6::Quick) +target_compile_options(appload_exit_test PRIVATE -Wall -Wextra) +enable_testing() +foreach(scenario initial-window pending-close retained-paint input-empty repeated-open queued-reopen repeated-initialize negative-key sigpipe touch-lifecycle) + add_test(NAME ${scenario} COMMAND appload_exit_test ${scenario}) + set_tests_properties(${scenario} PROPERTIES TIMEOUT 8 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +endforeach() diff --git a/patches/appload-tests/appload_exit_test.cpp b/patches/appload-tests/appload_exit_test.cpp new file mode 100644 index 0000000..8240857 --- /dev/null +++ b/patches/appload-tests/appload_exit_test.cpp @@ -0,0 +1,262 @@ +// Exercise the actual upstream socket loop and actual QtQuick framebuffer item. +// Each scenario is a separate bounded process, so a baseline deadlock cannot +// strand a test runner. No global listener, device nodes or stock UI are used. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fbmanagement.cpp" + +#define REQUIRE(condition) do { if (!(condition)) { \ + std::cerr << "FAIL line " << __LINE__ << ": " #condition << std::endl; std::_Exit(2); \ +} } while (false) + +static bool waitFor(const std::function &predicate, bool events = true) { + QElapsedTimer clock; clock.start(); + while (!predicate() && clock.elapsed() < 1200) { + if (events) QCoreApplication::processEvents(); + QThread::msleep(1); + } + return predicate(); +} + +struct Session { + int peer = -1; + int shmKey = -1; + std::atomic done{false}; + std::thread worker; + explicit Session(int key) { + int fds[2]; REQUIRE(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds) == 0); + peer = fds[0]; + timeval timeout{1, 0}; + REQUIRE(setsockopt(peer, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout) == 0); + worker = std::thread([this, fd = fds[1]] { managementClientThread(fd); done = true; }); + qtfb::ClientMessage message{}; + message.type = MESSAGE_CUSTOM_INITIALIZE; + message.customInit.framebufferKey = key; + message.customInit.framebufferType = FBFMT_RMPP_RGBA8888; + message.customInit.width = 32; message.customInit.height = 48; + sendMessage(message); + qtfb::ServerMessage response{}; + REQUIRE(recv(peer, &response, sizeof response, 0) == sizeof response); + REQUIRE(response.type == MESSAGE_INITIALIZE && response.init.shmSize == 32 * 48 * 4); + shmKey = response.init.shmKeyDefined; + } + void sendMessage(const qtfb::ClientMessage &message) { + REQUIRE(send(peer, &message, sizeof message, MSG_NOSIGNAL) == sizeof message); + } + void closePeer() { if (peer >= 0) { close(peer); peer = -1; } } + void join(bool events = true) { + REQUIRE(waitFor([&] { return done.load(); }, events)); + worker.join(); + } + ~Session() { closePeer(); if (worker.joinable()) join(); } +}; + +static bool sharedMemoryExists(int key) { + FORMAT_SHM(name, key); + int fd = shm_open(name, O_RDONLY, 0); + if (fd >= 0) { close(fd); return true; } + REQUIRE(errno == ENOENT); + return false; +} +static void associate(FBController &controller, int key) { + controller.setWidth(32); controller.setHeight(48); + controller.setFramebufferID(key); +} + +// QPainter passes its actual source image into this engine. Hold it while the +// GUI detaches and the worker deletes its backend; then read the retained bytes. +class BlockingDevice : public QPaintDevice { +public: + class Engine : public QPaintEngine { + public: + Engine() : QPaintEngine(QPaintEngine::AllFeatures) {} + std::atomic entered{false}, resume{false}; + QImage retained; + bool begin(QPaintDevice *) override { return true; } + bool end() override { return true; } + Type type() const override { return QPaintEngine::User; } + void updateState(const QPaintEngineState &) override {} + void drawPixmap(const QRectF &, const QPixmap &, const QRectF &) override { REQUIRE(false); } + void drawImage(const QRectF &, const QImage &image, const QRectF &, Qt::ImageConversionFlags) override { + retained = image; + entered = true; + while (!resume.load()) QThread::msleep(1); + REQUIRE(retained.constBits()[0] == 255); + } + }; + mutable Engine engine; + QPaintEngine *paintEngine() const override { return &engine; } + int metric(PaintDeviceMetric m) const override { + switch (m) { + case PdmWidth: return 32; + case PdmHeight: return 48; + case PdmDepth: return 32; + case PdmDpiX: case PdmDpiY: case PdmPhysicalDpiX: case PdmPhysicalDpiY: return 96; + case PdmDevicePixelRatio: return 1; + case PdmDevicePixelRatioScaled: return 65536; + default: return 0; + } + } +}; + +class InputController : public FBController { +public: + using FBController::touchEvent; +}; + +int main(int argc, char **argv) { + QGuiApplication app(argc, argv); + REQUIRE(argc == 2); + const std::string scenario(argv[1]); + const int key = 1024 + int(getpid()); + InputController controller; + associate(controller, key); + + if (scenario == "touch-lifecycle") { + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + for (auto type : {QEvent::TouchBegin, QEvent::TouchCancel}) { + const QList points = type == QEvent::TouchBegin + ? QList{QEventPoint(7, QEventPoint::State::Pressed, {}, {})} + : QList{}; + QTouchEvent event(type, nullptr, Qt::NoModifier, points); + controller.touchEvent(&event); + qtfb::ServerMessage response{}; + REQUIRE(recv(session.peer, &response, sizeof response, 0) == sizeof response); + REQUIRE(response.type == MESSAGE_USERINPUT); + REQUIRE(response.userInput.inputType == 0x13); + REQUIRE(response.userInput.devId == 0 && response.userInput.x == 0 + && response.userInput.y == 0 && response.userInput.d == 0); + if (type == QEvent::TouchBegin) { + REQUIRE(recv(session.peer, &response, sizeof response, 0) == sizeof response); + REQUIRE(response.type == MESSAGE_USERINPUT); + REQUIRE(response.userInput.inputType == INPUT_TOUCH_PRESS); + REQUIRE(response.userInput.devId == 7); + } + } + QTouchEvent stationary(QEvent::TouchUpdate, nullptr, Qt::NoModifier, + {QEventPoint(1, QEventPoint::State::Stationary, {}, {})}); + controller.touchEvent(&stationary); + qtfb::ServerMessage unexpected{}; + REQUIRE(recv(session.peer, &unexpected, sizeof unexpected, MSG_DONTWAIT) == -1); + REQUIRE(errno == EAGAIN || errno == EWOULDBLOCK); + } else if (scenario == "initial-window") { + int activated = 0, deactivated = 0; + QObject::connect(&controller, &FBController::activeChanged, &app, [&] { + if (controller.active()) ++activated; else ++deactivated; + }); + QCoreApplication::processEvents(); + // AppLoad's real window closes on an inactive notification. Registering + // its initially empty framebuffer must not send that notification. + REQUIRE(activated == 0 && deactivated == 0); + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + REQUIRE(activated == 1 && deactivated == 0); + session.closePeer(); session.join(); + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(activated == 1 && deactivated == 1); + } else if (scenario == "pending-close") { + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + controller.markedUpdate(); // Schedule a paint without ever painting. + session.closePeer(); + session.join(false); // Worker must not wait for the GUI's pending paint. + qtfb::UserInputContents input{INPUT_TOUCH_RELEASE, 1, 12, 16, 0}; + qtfb::management::forwardUserInput(key, &input); // Must not block on a leaked mutex. + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(!sharedMemoryExists(session.shmKey)); + } else if (scenario == "retained-paint") { + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + BlockingDevice device; + std::thread painting([&] { QPainter painter(&device); controller.paint(&painter); }); + REQUIRE(waitFor([&] { return device.engine.entered.load(); })); + session.closePeer(); session.join(); + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(sharedMemoryExists(session.shmKey)); + device.engine.resume = true; painting.join(); + REQUIRE(device.engine.retained.constBits()[0] == 255); + device.engine.retained = {}; + REQUIRE(!sharedMemoryExists(session.shmKey)); + } else if (scenario == "input-empty") { + controller.setAllowScaling(true); + REQUIRE(controller.convertPointToQTFBPixels({7, 9}) == QPoint(7, 9)); + controller.virtualKeyboardKeyDown(65); + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + session.closePeer(); session.join(); + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(controller.convertPointToQTFBPixels({7, 9}) == QPoint(7, 9)); + controller.virtualKeyboardKeyUp(65); + } else if (scenario == "repeated-open") { + for (int i = 0; i < 100; ++i) { + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + controller.markedUpdate(); + session.closePeer(); session.join(); + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(!sharedMemoryExists(session.shmKey)); + } + } else if (scenario == "queued-reopen") { + Session first(key); + REQUIRE(waitFor([&] { return controller.active(); })); + first.closePeer(); first.join(false); // Old detach is still queued. + Session second(key); // Same ID must bind to the current backend. + REQUIRE(waitFor([&] { return !sharedMemoryExists(first.shmKey); })); + REQUIRE(controller.active()); + REQUIRE(sharedMemoryExists(second.shmKey)); + second.closePeer(); second.join(); + REQUIRE(waitFor([&] { return !controller.active(); })); + REQUIRE(!sharedMemoryExists(second.shmKey)); + } else if (scenario == "repeated-initialize") { + Session session(key); + REQUIRE(waitFor([&] { return controller.active(); })); + qtfb::ClientMessage message{}; + message.type = MESSAGE_CUSTOM_INITIALIZE; + message.customInit.framebufferKey = key; + message.customInit.framebufferType = FBFMT_RMPP_RGBA8888; + message.customInit.width = 32; message.customInit.height = 48; + session.sendMessage(message); + session.join(); // Invalid second initialization closes and removes the connection. + REQUIRE(waitFor([&] { return !controller.active(); })); + qtfb::UserInputContents input{INPUT_TOUCH_RELEASE, 1, 12, 16, 0}; + qtfb::management::forwardUserInput(key, &input); + REQUIRE(!sharedMemoryExists(session.shmKey)); + } else if (scenario == "negative-key") { + int fds[2]; REQUIRE(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds) == 0); + std::atomic done{false}; + std::thread worker([&] { managementClientThread(fds[1]); done = true; }); + qtfb::ClientMessage message{}; + message.type = MESSAGE_CUSTOM_INITIALIZE; + message.customInit.framebufferKey = -1; + message.customInit.framebufferType = FBFMT_RMPP_RGBA8888; + message.customInit.width = 32; message.customInit.height = 48; + REQUIRE(send(fds[0], &message, sizeof message, MSG_NOSIGNAL) == sizeof message); + REQUIRE(waitFor([&] { return done.load(); })); + worker.join(); + qtfb::ServerMessage response{}; + REQUIRE(recv(fds[0], &response, sizeof response, 0) == 0); + close(fds[0]); + } else if (scenario == "sigpipe") { + signal(SIGPIPE, SIG_DFL); + int fds[2]; REQUIRE(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds) == 0); + close(fds[0]); + qtfb::management::ClientConnection item; item.clientFD = fds[1]; + qtfb::management::ClientBackend backend; backend.connections.push_back(&item); + { SYNCHRONIZE; qtfb::management::connections[key] = &backend; } + qtfb::UserInputContents input{INPUT_TOUCH_RELEASE, 1, 12, 16, 0}; + qtfb::management::forwardUserInput(key, &input); + { SYNCHRONIZE; qtfb::management::connections.erase(key); } + close(fds[1]); + } else { REQUIRE(false); } + std::cout << "PASS " << scenario << std::endl; + return 0; +} diff --git a/patches/appload-v0.5.3-no-key-logging.patch b/patches/appload-v0.5.3-no-key-logging.patch new file mode 100644 index 0000000..169106c --- /dev/null +++ b/patches/appload-v0.5.3-no-key-logging.patch @@ -0,0 +1,9 @@ +diff --git a/resources/qml/virtualKeyboard/Key.qml b/resources/qml/virtualKeyboard/Key.qml +--- a/resources/qml/virtualKeyboard/Key.qml ++++ b/resources/qml/virtualKeyboard/Key.qml +@@ -115,5 +115,4 @@ + function handlePress(touchArea, x, y) { +- console.log("Press " + label) + isClick = true; + pressMouseX = x; + pressMouseY = y; diff --git a/patches/appload-v0.5.3-qtfb-lifetime.patch b/patches/appload-v0.5.3-qtfb-lifetime.patch new file mode 100644 index 0000000..aec400b --- /dev/null +++ b/patches/appload-v0.5.3-qtfb-lifetime.patch @@ -0,0 +1,490 @@ +diff --git a/src/qtfb/FBController.cpp b/src/qtfb/FBController.cpp +index f022c33..525ad6a 100644 +--- a/src/qtfb/FBController.cpp ++++ b/src/qtfb/FBController.cpp +@@ -20,8 +20,12 @@ bool FBController::active() const { + } + + void FBController::setActive(bool active){ +- _active = active; +- emit activeChanged(); ++ // Registration can queue an empty association before a client connects. ++ // window.qml treats an inactive transition as the application closing. ++ if(_active != active) { ++ _active = active; ++ emit activeChanged(); ++ } + markedUpdate(); + } + +@@ -30,15 +34,14 @@ FBController::~FBController(){ + } + + void FBController::paint(QPainter *painter) { +- isMidPaint = true; +- QDEBUG << "FB Repaint triggered for " << _framebufferID << ". Status: " << _active; +- // Do we have an SHM associated? +- if(this->image && this->_active) { ++ // A shared-data snapshot keeps SHM alive while a render thread paints. ++ const QImage frame = imageSnapshot(); ++ if(!frame.isNull()) { + // Cool. Paint it. + if(_allowScaling) { +- painter->drawImage(QRect(0, 0, width(), height()), *image, image->rect()); ++ painter->drawImage(QRect(0, 0, width(), height()), frame, frame.rect()); + } else { +- painter->drawImage(0, 0, *image); ++ painter->drawImage(0, 0, frame); + } + } else { + /* +@@ -52,32 +55,29 @@ void FBController::paint(QPainter *painter) { + painter->drawText(rect, "Unbound Framebuffer " + QString::number(_framebufferID), Qt::AlignCenter | Qt::AlignTop); + */ + } +- isMidPaint = false; +-} +- +-void FBController::associateSHM(QImage *image) { +- this->image = image; +- int key = _framebufferID; +- QMetaObject::invokeMethod(this, [this, key]() { +- if(!qtfb::management::isControllerAssociated(key)) { +- // The framebuffer connection was terminated as we were +- // waiting for the event loop to process this request. +- this->image = nullptr; +- this->setActive(false); +- return; +- } +- this->setActive(this->image != nullptr); +- }, Qt::QueuedConnection); ++} ++ ++QImage FBController::imageSnapshot() const { ++ QMutexLocker lock(&imageMutex); ++ return image; ++} ++ ++void FBController::associateSHM(QImage frame) { ++ { ++ QMutexLocker lock(&imageMutex); ++ image = std::move(frame); ++ } ++ setActive(!imageSnapshot().isNull()); + } + + void FBController::markedUpdate(const QRect &rect) { +- isMidPaint = true; +- if(_allowScaling && image) { ++ const QImage frame = imageSnapshot(); ++ if(_allowScaling && !frame.isNull()) { + update(QRect( +- (rect.x() / image->width()) * this->width(), +- (rect.y() / image->height()) * this->height(), +- (rect.width() / image->width()) * this->width(), +- (rect.height() / image->height()) * this->height() ++ (rect.x() / frame.width()) * this->width(), ++ (rect.y() / frame.height()) * this->height(), ++ (rect.width() / frame.width()) * this->width(), ++ (rect.height() / frame.height()) * this->height() + )); + } else { + update(rect); +@@ -94,10 +94,11 @@ bool FBController::allowScaling() const { + + + QPoint FBController::convertPointToQTFBPixels(const QPointF &input) { +- if(_allowScaling && image) { ++ const QImage frame = imageSnapshot(); ++ if(_allowScaling && !frame.isNull() && width() > 0 && height() > 0) { + return QPoint( +- (input.x() * image->width()) / this->width(), +- (input.y() * image->height()) / this->height() ++ (input.x() * frame.width()) / this->width(), ++ (input.y() * frame.height()) / this->height() + ); + } else { + return QPoint(input.x(), input.y()); +diff --git a/src/qtfb/FBController.h b/src/qtfb/FBController.h +index aa46b73..5f3dae6 100644 +--- a/src/qtfb/FBController.h ++++ b/src/qtfb/FBController.h +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + + #include "common.h" + +@@ -38,9 +39,8 @@ public: + + void markedUpdate(const QRect &rect = QRect()); + void setActive(bool active); // NOT QML ACCESSIBLE! +- bool isMidPaint; + virtual void paint(QPainter *painter); +- void associateSHM(QImage *image); ++ void associateSHM(QImage image); + + QPoint convertPointToQTFBPixels(const QPointF &input); + +@@ -73,5 +73,7 @@ private: + bool checkingGestureDragDown = false; + bool refreshedScreenAlready = false; + +- QImage *image = nullptr; ++ QImage imageSnapshot() const; ++ mutable QMutex imageMutex; ++ QImage image; + }; +diff --git a/src/qtfb/fbmanagement.cpp b/src/qtfb/fbmanagement.cpp +index 038bec3..98cc852 100644 +--- a/src/qtfb/fbmanagement.cpp ++++ b/src/qtfb/fbmanagement.cpp +@@ -3,6 +3,7 @@ + #include "common.h" + #include + #include ++#include + /* + My implementation of the shared QT-framebuffer idea works by: + - Having all the clients communicate with the server via a UNIX socket, which governs +@@ -23,30 +24,31 @@ My implementation of the shared QT-framebuffer idea works by: + static std::mutex globalBackendsListMutex; + #define SYNCHRONIZE const std::lock_guard __lock(globalBackendsListMutex) + +-void tryToMatchUp(qtfb::FBKey key){ +- // Try to find the client in the clients' list +- if(qtfb::management::connections.find(key) == qtfb::management::connections.end()) { +- return; // Client not found +- } +- if(qtfb::management::framebuffers.find(key) == qtfb::management::framebuffers.end()) { +- return; // Framebuffer not found +- } +- // Both of them found! Assign one to another. +- qtfb::management::ClientBackend *connection = qtfb::management::connections[key]; +- QPointer controller = qtfb::management::framebuffers[key]; +- if(connection->shm == NULL || controller.isNull()){ +- CERR << "Invalid state: Cannot have an partially-associated connection in the connections map!" << std::endl; +- return; +- } +- controller->associateSHM(connection->image); +- CERR << "Associated connection <==> framebuffer " << key << std::endl; ++// Resolve controllers only on their GUI thread. The queue owns no worker-stack ++// connection pointers, and the copied QImage owns its SHM until the last paint. ++void tryToMatchUp(qtfb::FBKey key) { ++ QMetaObject::invokeMethod(QCoreApplication::instance(), [key]() { ++ QPointer controller; ++ QImage image; ++ { ++ SYNCHRONIZE; ++ const auto frame = qtfb::management::framebuffers.find(key); ++ if (frame == qtfb::management::framebuffers.end()) return; ++ controller = frame->second; ++ const auto backend = qtfb::management::connections.find(key); ++ if (backend != qtfb::management::connections.end() && backend->second->image) ++ image = *backend->second->image; ++ } ++ if (controller) controller->associateSHM(std::move(image)); ++ }, Qt::QueuedConnection); + } + + // Never wait for messages to come through. Not all clients have to be actively waiting for new input. + // If the socket's buffer was to overflow, simply drop the message. +-#define SEND(message) send(connection->clientFD, &message, sizeof(message), MSG_DONTWAIT) ++#define SEND(message) send(connection->clientFD, &message, sizeof(message), MSG_DONTWAIT | MSG_NOSIGNAL) + + void qtfb::management::registerController(FBKey key, QPointer controller) { ++ SYNCHRONIZE; + if(key == -1) return; + if(qtfb::management::framebuffers.find(key) != qtfb::management::framebuffers.end()) { + CERR << "Violation: Tried to attach to an already defined framebuffer (" << key << "). Please change the framebufferID!" << std::endl; +@@ -59,11 +61,13 @@ void qtfb::management::registerController(FBKey key, QPointer cont + } + + bool qtfb::management::isControllerAssociated(FBKey key) { ++ SYNCHRONIZE; + auto position = qtfb::management::connections.find(key); + return position != qtfb::management::connections.end(); + } + + void qtfb::management::unregisterController(FBKey key) { ++ SYNCHRONIZE; + auto position = qtfb::management::framebuffers.find(key); + if(position != qtfb::management::framebuffers.end()) { + qtfb::management::framebuffers.erase(position); +@@ -71,6 +75,22 @@ void qtfb::management::unregisterController(FBKey key) { + CERR << "Unregistered framebuffer controller ID: " << key << std::endl; + } + ++struct SharedFrameStorage { ++ unsigned char *address; ++ size_t size; ++ int fd; ++ int key; ++}; ++ ++static void releaseSharedFrame(void *opaque) { ++ auto *storage = static_cast(opaque); ++ munmap(storage->address, storage->size); ++ close(storage->fd); ++ FORMAT_SHM(name, storage->key); ++ shm_unlink(name); ++ delete storage; ++} ++ + static bool createSHM(qtfb::management::ClientBackend *connection, int shmType, int width, int height) { + size_t shmSize; + QImage::Format format; +@@ -125,6 +145,7 @@ static bool createSHM(qtfb::management::ClientBackend *connection, int shmType, + if(connection->shm == MAP_FAILED) { + CERR << "Failed to mmap() the SHM for framebuffer!" << std::endl; + shm_unlink(shmText); ++ close(connection->shmFD); + connection->shmFD = -1; + connection->shm = NULL; + connection->shmSize = 0; +@@ -136,7 +157,8 @@ static bool createSHM(qtfb::management::ClientBackend *connection, int shmType, + memset(connection->shm, 0xFF, shmSize); + CERR << "Defined SHM (" << shmSize << " bytes) at " << (void *) connection->shm << std::endl; + // We have the SHM defined. +- connection->image = new QImage(connection->shm, width, height, bpl, format, nullptr, nullptr); ++ auto *storage = new SharedFrameStorage{connection->shm, shmSize, connection->shmFD, connection->shmKey}; ++ connection->image = new QImage(connection->shm, width, height, bpl, format, releaseSharedFrame, storage); + + return true; + } +@@ -160,6 +182,8 @@ static bool createDefaultSHM(qtfb::management::ClientBackend *connection, int sh + + static int handleInitialize(qtfb::management::ClientConnection *connection, qtfb::ClientMessage *inbound, int messageType) { + SYNCHRONIZE; ++ // Register each stack connection once; duplicates would survive close. ++ if (connection->fbKey != -1 || inbound->init.framebufferKey < 0) return RESP_ERR; + connection->fbKey = inbound->init.framebufferKey; + if(qtfb::management::connections.find(inbound->init.framebufferKey) != qtfb::management::connections.end()) { + // If there already exists a backend like that, check if the parameters are the same +@@ -219,56 +243,45 @@ static int handleInitialize(qtfb::management::ClientConnection *connection, qtfb + return RESP_OK; + } + +-static int handleUpdateRegion(QPointer controller, qtfb::management::ClientConnection *connection, qtfb::ClientMessage *inbound) { +- int x, y, w, h; +- switch(inbound->update.type) { +- case UPDATE_ALL: +- CERR << "Updated all of framebuffer " << connection->fbKey << std::endl; +- QMetaObject::invokeMethod(controller, [controller]() { +- controller->markedUpdate(); +- }, Qt::QueuedConnection); +- break; +- case UPDATE_PARTIAL: +- CERR << "Updated region " << inbound->update.x << " " << inbound->update.y << " " << inbound->update.w << " " << inbound->update.h << " of framebuffer " << connection->fbKey << std::endl; +- x = inbound->update.x, y = inbound->update.y, w = inbound->update.w, h = inbound->update.h; +- QMetaObject::invokeMethod(controller, [controller, x, y, w, h]() { +- controller->markedUpdate(QRect( +- x, +- y, +- w, +- h +- )); +- }, Qt::QueuedConnection); +- break; +- default: +- CERR << "Unknown update method! " << inbound->update.type << " <-- " << inbound->update.x << " " << inbound->update.y << " " << inbound->update.w << " " << inbound->update.h << " of framebuffer " << connection->fbKey << std::endl; +- } +- +- return RESP_OK; +-} +- +-static int invokeOnConnectedFramebuffer(qtfb::management::ClientConnection *connection, std::function)> consumer) { +- if(connection->fbKey == -1){ +- CERR << "Cannot update region of an uninitialized connection!" << std::endl; ++static int queueControllerMessage(qtfb::management::ClientConnection *connection, ++ const qtfb::ClientMessage &message) { ++ if (connection->fbKey == -1) return RESP_ERR; ++ if (message.type == MESSAGE_SET_REFRESH_MODE && (message.refreshMode < 0 || message.refreshMode > 4)) + return RESP_ERR; ++ const auto key = connection->fbKey; ++ qint64 imageKey = 0; ++ { ++ SYNCHRONIZE; ++ const auto backend = qtfb::management::connections.find(key); ++ if (backend == qtfb::management::connections.end() || !backend->second->image) return RESP_ERR; ++ imageKey = backend->second->image->cacheKey(); + } +- if(qtfb::management::framebuffers.find(connection->fbKey) != qtfb::management::framebuffers.end()) { +- QPointer controller = qtfb::management::framebuffers[connection->fbKey]; +- if(controller.isNull()) { +- return RESP_OK; ++ QMetaObject::invokeMethod(QCoreApplication::instance(), [key, imageKey, message]() { ++ QPointer controller; ++ { ++ SYNCHRONIZE; ++ const auto backend = qtfb::management::connections.find(key); ++ const auto frame = qtfb::management::framebuffers.find(key); ++ if (backend == qtfb::management::connections.end() || !backend->second->image ++ || backend->second->image->cacheKey() != imageKey ++ || frame == qtfb::management::framebuffers.end()) return; ++ controller = frame->second; + } +- return consumer(controller); +- } else { +- CERR << "Could not find the framebuffer to act upon." << std::endl; +- } ++ if (!controller) return; ++ switch (message.type) { ++ case MESSAGE_UPDATE: ++ if (message.update.type == UPDATE_ALL) controller->markedUpdate(); ++ else if (message.update.type == UPDATE_PARTIAL) ++ controller->markedUpdate(QRect(message.update.x, message.update.y, ++ message.update.w, message.update.h)); ++ break; ++ case MESSAGE_SET_REFRESH_MODE: controller->setRefreshMode(message.refreshMode); break; ++ case MESSAGE_REQUEST_FULL_REFRESH: emit controller->requestFullRefresh(); break; ++ } ++ }, Qt::QueuedConnection); + return RESP_OK; + } + +-static inline void safetyWaitForEventLoopToCatchUp() { +- using namespace std::chrono_literals; +- std::this_thread::sleep_for(1s); +-} +- + static void managementClientThread(int incomingFD) { + qtfb::management::ClientConnection connection; + connection.clientFD = incomingFD; +@@ -284,36 +297,12 @@ static void managementClientThread(int incomingFD) { + status = handleInitialize(&connection, &inboundMessage, inboundMessage.type); + break; + case MESSAGE_UPDATE: +- status = invokeOnConnectedFramebuffer(&connection, [&](auto controller) { +- return handleUpdateRegion(controller, &connection, &inboundMessage); +- }); +- break; +- case MESSAGE_TERMINATE: +- CERR << "The client requested closing the connection." << std::endl; +- goto close; + case MESSAGE_SET_REFRESH_MODE: +- status = invokeOnConnectedFramebuffer(&connection, [=](auto controller) { +- int refresh = inboundMessage.refreshMode; +- if(refresh > 4 || refresh < 0) { +- CERR << "The client tried to set refresh mode to an undefined value!" << std::endl; +- return RESP_ERR; +- } +- QMetaObject::invokeMethod(controller, [controller, refresh]() { +- controller->setRefreshMode(refresh); +- }); +- safetyWaitForEventLoopToCatchUp(); +- return RESP_OK; +- }); +- break; + case MESSAGE_REQUEST_FULL_REFRESH: +- status = invokeOnConnectedFramebuffer(&connection, [=](auto controller) { +- QMetaObject::invokeMethod(controller, [controller]() { +- emit controller->requestFullRefresh(); +- }); +- safetyWaitForEventLoopToCatchUp(); +- return RESP_OK; +- }); ++ status = queueControllerMessage(&connection, inboundMessage); + break; ++ case MESSAGE_TERMINATE: ++ goto close; + default: + CERR << "Client has tried to send a message with an invalid type: " << inboundMessage.type << std::endl; + goto close; +@@ -323,65 +312,44 @@ static void managementClientThread(int incomingFD) { + goto close; + } + } +- close: SYNCHRONIZE; +- qtfb::management::ClientBackend *backendToKill = NULL; +- if(connection.fbKey != -1) { +- auto position = qtfb::management::connections.find(connection.fbKey); +- if(position != qtfb::management::connections.end()){ +- // There can be more than one backend. Was this the last? +- qtfb::management::ClientBackend *backend = qtfb::management::connections[connection.fbKey]; +- auto position2 = std::find(backend->connections.begin(), backend->connections.end(), &connection); +- if(position2 != backend->connections.end()) { +- backend->connections.erase(position2); +- } else { +- CERR << "Cannot erase connection in list!" << std::endl; +- } +- if(backend->connections.empty()) { +- backendToKill = backend; // Delay destruction of the backend to after the (potential) object had dissotiated. ++ close: ++ qtfb::management::ClientBackend *backendToKill = nullptr; ++ { ++ SYNCHRONIZE; ++ const auto position = qtfb::management::connections.find(connection.fbKey); ++ if (position != qtfb::management::connections.end()) { ++ auto *backend = position->second; ++ const auto member = std::find(backend->connections.begin(), backend->connections.end(), &connection); ++ if (member != backend->connections.end()) backend->connections.erase(member); ++ if (backend->connections.empty()) { ++ backendToKill = backend; + qtfb::management::connections.erase(position); + } + } + } +- +- if(connection.fbKey != -1 && backendToKill){ +- if(qtfb::management::framebuffers.find(connection.fbKey) != qtfb::management::framebuffers.end()) { +- // The client that died was associated with a framebuffer! +- QPointer controller = qtfb::management::framebuffers[connection.fbKey]; +- if(!controller.isNull()) { +- // Disassociate +- while(controller->isMidPaint) { +- usleep(1000); +- if(controller.isNull()) { +- // The controller had been removed as we were waiting for the repaint to stop +- goto continueDeletion; +- } +- } +- +- controller->associateSHM(NULL); +- CERR << "Disassociating framebuffer " << connection.fbKey << std::endl; +- } +- } +- } +- +- continueDeletion: +- +- CERR << "Closing client socket " << incomingFD << std::endl; ++ // No sender can still obtain the worker's stack connection. Never wait for ++ // GUI painting while holding the global mutex (or from the worker at all). + close(incomingFD); +- if(backendToKill != NULL) delete backendToKill; ++ if (backendToKill) { ++ tryToMatchUp(connection.fbKey); ++ delete backendToKill; ++ } + } + + qtfb::management::ClientBackend::~ClientBackend() { +- delete image; +- if(shm != NULL) { +- munmap(shm, shmSize); +- } +- if(translationShm != NULL){ +- delete[] translationShm; +- } +- if(shmKey != -1) { +- FORMAT_SHM(shmName, shmKey); +- shm_unlink(shmName); ++ if (image) { ++ // The QImage cleanup callback releases mapping/fd/name only after the ++ // controller and every render snapshot have also dropped their copies. ++ delete image; ++ } else { ++ if (shm) munmap(shm, shmSize); ++ if (shmFD >= 0) close(shmFD); ++ if (shmKey != -1) { ++ FORMAT_SHM(shmName, shmKey); ++ shm_unlink(shmName); ++ } + } ++ delete[] translationShm; + } + + static void managementMainThread(){ diff --git a/patches/appload-v0.5.3-touch-lifecycle.patch b/patches/appload-v0.5.3-touch-lifecycle.patch new file mode 100644 index 0000000..2161a41 --- /dev/null +++ b/patches/appload-v0.5.3-touch-lifecycle.patch @@ -0,0 +1,34 @@ +--- a/src/qtfb/FBController.cpp ++++ b/src/qtfb/FBController.cpp +@@ -184,6 +184,22 @@ + + void FBController::touchEvent(QTouchEvent *me) { + if(_framebufferID != -1) { ++ // Qt may cancel a sequence without delivering per-contact releases. ++ // Also reset at TouchBegin so a previously dropped cancellation cannot ++ // leave the client treating every later tap as simultaneous input. ++ if (me->type() == QEvent::TouchBegin || me->type() == QEvent::TouchCancel) { ++ qtfb::UserInputContents reset { ++ .inputType = 0x13, // Optional touch-lifecycle extension. ++ .devId = 0, .x = 0, .y = 0, .d = 0, ++ }; ++ qtfb::management::forwardUserInput(_framebufferID, &reset); ++ checkingGestureDragDown = false; ++ refreshedScreenAlready = false; ++ } ++ if (me->type() == QEvent::TouchCancel) { ++ me->accept(); ++ return; ++ } + int lenPoints = me->points().length(); + if(lenPoints == 5 && !refreshedScreenAlready) { + emit requestFullRefresh(); +@@ -218,7 +234,7 @@ + case QEventPoint::State::Updated: + packet.inputType = INPUT_TOUCH_UPDATE; + break; +- default: break; ++ default: continue; + } + qtfb::management::forwardUserInput(_framebufferID, &packet); + } diff --git a/patches/appload.md b/patches/appload.md new file mode 100644 index 0000000..6bb78a6 --- /dev/null +++ b/patches/appload.md @@ -0,0 +1,75 @@ +# AppLoad prerequisites for authentication + +The qualified loader starts from [rm-appload v0.5.3](https://github.com/asivery/rm-appload/tree/5bb34a362f09f753f18bd6261558f8e2737aacdb), +commit `5bb34a362f09f753f18bd6261558f8e2737aacdb`. The patches here change only +AppLoad's QTFB implementation and one keyboard diagnostic. They contain no +application catalog, document integration, boot hook or firmware resources. + +## Apply and rebuild + +Apply in this order to a clean checkout of that exact revision: + +```sh +patch --batch -d /absolute/path/to/rm-appload -p1 \ + < patches/appload-v0.5.3-qtfb-lifetime.patch +patch --batch -d /absolute/path/to/rm-appload -p1 \ + < patches/appload-v0.5.3-no-key-logging.patch +patch --batch -d /absolute/path/to/rm-appload -p1 \ + < patches/appload-v0.5.3-touch-lifecycle.patch +``` + +- **QTFB lifetime:** removes the disconnect worker's wait for GUI painting while + holding the backend mutex. Reference-counted image storage retains shared + memory until the last paint finishes; queued work checks the current image + before modifying its controller. This prevents a closing client from blocking + the stock UI or releasing a frame still being painted. +- **Keyboard privacy:** removes the key-label `console.log` from `Key.qml`. + Layout, gestures, modifiers and wire packets remain unchanged. A rebuilt file + on disk is insufficient: the running AppLoad must load that replacement + before any credentials are typed. +- **Touch lifetime:** forwards optional QTFB input type `0x13`, with zeroed + payload fields, before `TouchBegin` and on `TouchCancel`. The matching rmweb + client cancels abandoned contacts without activating a control. Stationary + points are no longer forwarded as duplicate presses. Existing packet sizes + and pointer/key encodings remain unchanged; older clients ignore the new type. + +Build the patched source with the official Ferrari SDK matching the device and +reviewed XOVI dependencies. Keep AppLoad's complete corresponding GPL-3.0 source, +patches, license and build receipt with any distributed AppLoad binary. rmweb's +build scripts do not build or install this dependency. + +## Firmware qualification remains separate + +These patches do **not** make stock AppLoad v0.5.3's QML hooks compatible with +all firmware. The physical passkey test used a separately qualified AppLoad +build for Paper Pro **3.28.0.172 / Qt 6.10.3**, including the 3.28 hook changes +from [upstream PR 59](https://github.com/asivery/rm-appload/pull/59), merged as +`3b42440e369a82535fb93df4a9155de9199cf487`. Those firmware hooks are not bundled +here. Use an AppLoad installation qualified for the exact firmware and preserve +its existing hook changes when applying these fixes. Do not substitute upstream +master without requalifying its scaling, rotation and stock-UI integration. + +Back up the installed loader and its receipts, verify the rebuilt library and +resources, then activate through the device's existing reversible AppLoad +installation procedure. Test opening, keyboard input, touch cancellation, +Cancel/Return, reopening and stock-UI health. Replacing a loaded shared library +alone does not activate it. No installer or boot change is supplied here. + +## Actual-source regression checks + +Run on Linux with Qt 6.8 or newer Core, Gui and Quick, against the fully patched +source tree: + +```sh +cmake -S patches/appload-tests -B build/appload-tests -G Ninja \ + -DAPPLOAD_SOURCE=/absolute/path/to/rm-appload +cmake --build build/appload-tests +ctest --test-dir build/appload-tests --output-on-failure +``` + +The fixture compiles the real socket loop and controller. Ten bounded cases +exercise initial association, close before painting, retained frames after +close, empty input, repeated opens, queued replacement, duplicate initialization, +negative framebuffer keys, SIGPIPE and touch-sequence reset. It uses private +socket pairs and an offscreen Qt platform, without the global server or tablet +files. Passing these checks does not qualify the firmware hooks or physical UI. diff --git a/patches/wpe-2.48.5-native-assertion-provider-files.json b/patches/wpe-2.48.5-native-assertion-provider-files.json new file mode 100644 index 0000000..82c7b2b --- /dev/null +++ b/patches/wpe-2.48.5-native-assertion-provider-files.json @@ -0,0 +1,37 @@ +{ + "Source/WebCore/CMakeLists.txt": "e6770aa80c60033d9c735a4bdee47628d3433342e1e2e09f157f44f448eada21", + "Source/WebCore/Headers.cmake": "33c0b2e78f53cb4f688bf551b95d6581f54ae118a3421b6505096188fc5ec846", + "Source/WebCore/Modules/identity/CredentialRequestCoordinator.cpp": "52c74c0e5d8e7e73af31713f2e53432dadbc373248c2aa92236c37246e2d3e5a", + "Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp": "7f44cf3c229b3388dcbdced8a3a1cece65829ce5158aeefcc052433e0b30dbf9", + "Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h": "0aa3acde3927f25da89b7a652617a38368285a32755da7fa230260b347498cdc", + "Source/WebCore/Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h": "85009b13e41757c5d3cdc119738d3fd9b771fd290520aeadac222a38b18881d1", + "Source/WebCore/PlatformWPE.cmake": "6ca54c5990449a4e5aa6f367fae6250ff02691523bc37a8b3cbd73a33def1bb8", + "Source/WebCore/Sources.txt": "12eddc07c62c0fcd6cc951bc48e2a749164ee08fa3175db52798b9a54fd27f82", + "Source/WebCore/SourcesCocoa.txt": "fd43398fc6b50f180f6afae38a9094f0e1a761ba9e54913ba8754dc362263cc8", + "Source/WebCore/platform/LocalizedStrings.cpp": "8c4ea30931553967744f72cc6d5f20be385dbea0ee7bbdf9ba57d50d7b72d34d", + "Source/WebCore/platform/LocalizedStrings.h": "48afe621f3cb04ddb70039a8bcd8faa581fa75e0f4f4fb54fdbc39c4883c21be", + "Source/WebKit/PlatformWPE.cmake": "97161d10c7c3e5b1512e1816cc9c5c990bcbb313a0ba34d89ad51bde8213aabc", + "Source/WebKit/Sources.txt": "2a8c6ed55a7fbbd854c59905f1a5ed6013f1f0e8d31215a4175afcea95a4db97", + "Source/WebKit/SourcesCocoa.txt": "78507ee2bc225100c34943ce846fcdf16207bf7fcda8dea26a3ea6d460a350a3", + "Source/WebKit/SourcesWPE.txt": "8b275aad7dbb559fbe58a608f98f0e5bac34c6287caeef6d21b978750c2e210a", + "Source/WebKit/UIProcess/API/APIUIClient.h": "d2ccfdac83e983a810e5379507276e8815cd3b547ddb8579d8c683b7eddf299a", + "Source/WebKit/UIProcess/API/C/WKPage.cpp": "6b36f94629b1efc9c542513822ea4c23c7270af270f95ec46e909983d9928875", + "Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp": "b9ab3e775aec9812ea87143726f157eeb730d66bca8c9541fc960a8eb94238d9", + "Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp": "c4470d3bfdc036c895af68a6af279a7016cbc372fe25702d60757ff8d01c44be", + "Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.h.in": "04dbc9546c9a4e91bb30bdbd033914af0215562ab34993888a5532103aebfffb", + "Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequestPrivate.h": "2f21e3bf257810a6233e14a4478c47be4de3b37601560b76003effcac1214178", + "Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp": "0d1579fe9636044bd19fef523129e634ce430625364d513265da80170c4a17bf", + "Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h": "2197a4e3b332e976daca3a884d0947b9049aabf795df0a92cbaa79419579f011", + "Source/WebKit/UIProcess/API/glib/webkit.h.in": "85924ada0199f05832083988247b7004235cb6d8b415e9047e56596055c63e1c", + "Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp": "150b5f65d0b91edd29f8f4e94bdb266a8c646f135ec1f7b8fb18c8e3262250f2", + "Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h": "0a2883b26c65bc941358729b78da1e6950554373dfd241ba95a4d07564612a00", + "Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.h": "764fcc1b7213c3c6a09407463f02c50e3376f1374cf466561b53e72e9887eb94", + "Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h": "214658bb313ae8cdebf9c44d3e4df5f98c19be0255066856677f031d3fb61583", + "Source/WebKit/UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp": "655a6ca6ccd9a69b04d7678fcf8b643c6616e54ed56a7922f5279e0eceeb1804", + "Source/WebKit/UIProcess/WebPageProxy.cpp": "bd99d0084e65854e9478ac2de66326dfa324c4c92f36eb5da9de06e09a3195a8", + "Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp": "98d9c51f351fd4fb6f1df98968647f3a6a3ee8d8ce45d5ef125367097cb37891", + "Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h": "a75ca0a118666079847b1d5df65fc23074b2e97143750139c004ddf55dc510e8", + "Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp": "542d36e5fe715392ea669488019701d3ad59cd049e14d3d3ec7918dde330f657", + "Source/WebKit/WebProcess/WebAuthentication/WebAuthenticatorCoordinator.cpp": "615bce3e368c915e54bd05daf66f1c54a3ac87b19280b3cd49e3d1f4200d68ba", + "Source/WebKit/WebProcess/WebPage/WebPage.cpp": "68f3ace10379da16ebc51233d8e36a17aec0f3a541077c391018562ac0dffb2a" +} diff --git a/patches/wpe-2.48.5-native-assertion-provider.patch b/patches/wpe-2.48.5-native-assertion-provider.patch new file mode 100644 index 0000000..56dc101 --- /dev/null +++ b/patches/wpe-2.48.5-native-assertion-provider.patch @@ -0,0 +1,1273 @@ +diff --git a/Source/WebCore/CMakeLists.txt b/Source/WebCore/CMakeLists.txt +--- a/Source/WebCore/CMakeLists.txt ++++ b/Source/WebCore/CMakeLists.txt +@@ -69,6 +69,7 @@ + "${WEBCORE_DIR}/Modules/web-locks" + "${WEBCORE_DIR}/Modules/webaudio" + "${WEBCORE_DIR}/Modules/webauthn" ++ "${WEBCORE_DIR}/Modules/webauthn/apdu" + "${WEBCORE_DIR}/Modules/webauthn/cbor" + "${WEBCORE_DIR}/Modules/webauthn/fido" + "${WEBCORE_DIR}/Modules/webcodecs" +diff --git a/Source/WebCore/Headers.cmake b/Source/WebCore/Headers.cmake +--- a/Source/WebCore/Headers.cmake ++++ b/Source/WebCore/Headers.cmake +@@ -355,7 +355,9 @@ + Modules/cookie-store/CookieChangeSubscription.h + Modules/cookie-store/CookieStoreGetOptions.h + ++ Modules/credentialmanagement/BasicCredential.h + Modules/credentialmanagement/CredentialRequestOptions.h ++ Modules/credentialmanagement/MediationRequirement.h + + Modules/encryptedmedia/CDMClient.h + Modules/encryptedmedia/MediaKeySystemClient.h +@@ -629,18 +631,25 @@ + Modules/webauthn/AuthenticationExtensionsClientInputsJSON.h + Modules/webauthn/AuthenticationExtensionsClientOutputs.h + Modules/webauthn/AuthenticationExtensionsClientOutputsJSON.h ++ Modules/webauthn/AuthenticatorAttachment.h + Modules/webauthn/AuthenticatorCoordinator.h + Modules/webauthn/AuthenticatorCoordinatorClient.h + Modules/webauthn/AuthenticatorResponseData.h ++ Modules/webauthn/AuthenticatorSelectionCriteria.h + Modules/webauthn/AuthenticatorTransport.h ++ Modules/webauthn/CredentialPropertiesOutput.h + Modules/webauthn/PublicKeyCredentialCreationOptions.h + Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h + Modules/webauthn/PublicKeyCredentialDescriptor.h + Modules/webauthn/PublicKeyCredentialDescriptorJSON.h ++ Modules/webauthn/PublicKeyCredentialEntity.h + Modules/webauthn/PublicKeyCredentialParameters.h + Modules/webauthn/PublicKeyCredentialRequestOptions.h + Modules/webauthn/PublicKeyCredentialRequestOptionsJSON.h ++ Modules/webauthn/PublicKeyCredentialRpEntity.h + Modules/webauthn/PublicKeyCredentialType.h ++ Modules/webauthn/PublicKeyCredentialUserEntity.h ++ Modules/webauthn/ResidentKeyRequirement.h + Modules/webauthn/UserVerificationRequirement.h + Modules/webauthn/WebAuthenticationConstants.h + Modules/webauthn/WebAuthenticationUtils.h +@@ -2756,6 +2765,8 @@ + testing/MockGamepad.h + testing/MockGamepadProvider.h + ++ testing/MockWebAuthenticationConfiguration.h ++ + workers/ScriptBuffer.h + workers/WorkerAnimationController.h + workers/WorkerDebuggerProxy.h +diff --git a/Source/WebCore/Modules/identity/CredentialRequestCoordinator.cpp b/Source/WebCore/Modules/identity/CredentialRequestCoordinator.cpp +--- a/Source/WebCore/Modules/identity/CredentialRequestCoordinator.cpp ++++ b/Source/WebCore/Modules/identity/CredentialRequestCoordinator.cpp +@@ -265,7 +265,7 @@ + Ref credential = DigitalCredential::create( + { parsedObject->vm(), parsedObject }, + responseData.protocol); +- m_currentPromise->resolve(WTFMove(credential.ptr())); ++ m_currentPromise->resolve(credential.ptr()); + m_currentPromise.reset(); + } + +diff --git a/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp b/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp +--- a/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp ++++ b/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp +@@ -49,10 +49,12 @@ + return create(ArrayBuffer::create(rawId), ArrayBuffer::create(authenticatorData), ArrayBuffer::create(signature), WTFMove(userhandleBuffer), std::nullopt, attachment); + } + ++#if PLATFORM(COCOA) + Ref AuthenticatorAssertionResponse::create(Ref&& rawId, RefPtr&& userHandle, String&& name, SecAccessControlRef accessControl, AuthenticatorAttachment attachment) + { + return adoptRef(*new AuthenticatorAssertionResponse(WTFMove(rawId), WTFMove(userHandle), WTFMove(name), accessControl, attachment)); + } ++#endif + + void AuthenticatorAssertionResponse::setAuthenticatorData(Vector&& authenticatorData) + { +@@ -67,6 +69,7 @@ + { + } + ++#if PLATFORM(COCOA) + AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref&& rawId, RefPtr&& userHandle, String&& name, SecAccessControlRef accessControl, AuthenticatorAttachment attachment) + : AuthenticatorResponse(WTFMove(rawId), attachment) + , m_userHandle(WTFMove(userHandle)) +@@ -74,6 +77,7 @@ + , m_accessControl(accessControl) + { + } ++#endif + + AuthenticatorResponseData AuthenticatorAssertionResponse::data() const + { +diff --git a/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h b/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h +--- a/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h ++++ b/Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h +@@ -29,10 +29,11 @@ + + #include "AuthenticationResponseJSON.h" + #include "AuthenticatorResponse.h" ++#if PLATFORM(COCOA) + #include + #include +- + OBJC_CLASS LAContext; ++#endif + + namespace WebCore { + +@@ -40,7 +41,9 @@ + public: + static Ref create(Ref&& rawId, Ref&& authenticatorData, Ref&& signature, RefPtr&& userHandle, std::optional&&, AuthenticatorAttachment); + WEBCORE_EXPORT static Ref create(const Vector& rawId, const Vector& authenticatorData, const Vector& signature, const Vector& userHandle, AuthenticatorAttachment); ++#if PLATFORM(COCOA) + WEBCORE_EXPORT static Ref create(Ref&& rawId, RefPtr&& userHandle, String&& name, SecAccessControlRef, AuthenticatorAttachment); ++#endif + virtual ~AuthenticatorAssertionResponse() = default; + + ArrayBuffer* authenticatorData() const { return m_authenticatorData.get(); } +@@ -49,10 +52,14 @@ + const String& name() const { return m_name; } + const String& displayName() const { return m_displayName; } + size_t numberOfCredentials() const { return m_numberOfCredentials; } ++#if PLATFORM(COCOA) + SecAccessControlRef accessControl() const { return m_accessControl.get(); } ++#endif + const String& group() const { return m_group; } + bool synchronizable() const { return m_synchronizable; } ++#if PLATFORM(COCOA) + LAContext * laContext() const { return m_laContext.get(); } ++#endif + RefPtr largeBlob() const { return m_largeBlob; } + const String& accessGroup() const { return m_accessGroup; } + +@@ -63,7 +70,9 @@ + void setNumberOfCredentials(size_t numberOfCredentials) { m_numberOfCredentials = numberOfCredentials; } + void setGroup(const String& group) { m_group = group; } + void setSynchronizable(bool synchronizable) { m_synchronizable = synchronizable; } ++#if PLATFORM(COCOA) + void setLAContext(LAContext *context) { m_laContext = context; } ++#endif + void setLargeBlob(Ref&& largeBlob) { m_largeBlob = WTFMove(largeBlob); } + void setAccessGroup(const String& accessGroup) { m_accessGroup = accessGroup; } + +@@ -71,7 +80,9 @@ + + private: + AuthenticatorAssertionResponse(Ref&&, Ref&&, Ref&&, RefPtr&&, AuthenticatorAttachment); ++#if PLATFORM(COCOA) + AuthenticatorAssertionResponse(Ref&&, RefPtr&&, String&&, SecAccessControlRef, AuthenticatorAttachment); ++#endif + + Type type() const final { return Type::Assertion; } + AuthenticatorResponseData data() const final; +@@ -85,8 +96,10 @@ + String m_group; + bool m_synchronizable; + size_t m_numberOfCredentials { 0 }; ++#if PLATFORM(COCOA) + RetainPtr m_accessControl; + RetainPtr m_laContext; ++#endif + RefPtr m_largeBlob; + String m_accessGroup; + }; +diff --git a/Source/WebCore/Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h b/Source/WebCore/Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h +--- a/Source/WebCore/Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h ++++ b/Source/WebCore/Modules/webauthn/PublicKeyCredentialCreationOptionsJSON.h +@@ -27,6 +27,9 @@ + + #if ENABLE(WEB_AUTHN) + #include "AuthenticationExtensionsClientInputsJSON.h" ++#include "AuthenticatorSelectionCriteria.h" ++#include "PublicKeyCredentialDescriptorJSON.h" ++#include "PublicKeyCredentialParameters.h" + #include "PublicKeyCredentialRpEntity.h" + #include "PublicKeyCredentialUserEntityJSON.h" + #include +@@ -35,9 +38,6 @@ + + enum class AuthenticatorAttachment : uint8_t; + enum class AttestationConveyancePreference : uint8_t; +-struct AuthenticatorSelectionCriteria; +-struct PublicKeyCredentialDescriptorJSON; +-struct PublicKeyCredentialParameters; + + struct PublicKeyCredentialCreationOptionsJSON { + PublicKeyCredentialRpEntity rp; +diff --git a/Source/WebCore/PlatformWPE.cmake b/Source/WebCore/PlatformWPE.cmake +--- a/Source/WebCore/PlatformWPE.cmake ++++ b/Source/WebCore/PlatformWPE.cmake +@@ -147,3 +147,9 @@ + list(APPEND WebCore_PRIVATE_DEFINITIONS + BITMAP_TEXTURE_POOL_MAX_SIZE_IN_MB=80 + ) ++ ++if (ENABLE_WEB_AUTHN) ++ list(APPEND WebCore_SOURCES ++ Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp ++ ) ++endif () +diff --git a/Source/WebCore/Sources.txt b/Source/WebCore/Sources.txt +--- a/Source/WebCore/Sources.txt ++++ b/Source/WebCore/Sources.txt +@@ -457,7 +457,6 @@ + Modules/webauthn/fido/FidoParsingUtils.cpp + Modules/webauthn/fido/Pin.cpp + Modules/webauthn/fido/U2fCommandConstructor.cpp +-Modules/webauthn/fido/U2fResponseConverter.cpp + Modules/webcodecs/VideoColorSpace.cpp + Modules/webcodecs/WebCodecsAudioDecoder.cpp + Modules/webcodecs/WebCodecsAudioData.cpp +diff --git a/Source/WebCore/SourcesCocoa.txt b/Source/WebCore/SourcesCocoa.txt +--- a/Source/WebCore/SourcesCocoa.txt ++++ b/Source/WebCore/SourcesCocoa.txt +@@ -135,6 +135,7 @@ + Modules/speech/cocoa/WebSpeechRecognizerTask.mm + Modules/speech/cocoa/WebSpeechRecognizerTaskMock.mm + Modules/system-preview/ARKitBadgeSystemImage.mm ++Modules/webauthn/fido/U2fResponseConverter.cpp + Modules/webdatabase/cocoa/DatabaseManagerCocoa.mm + Modules/webxr/WebXROpaqueFramebufferCocoa.cpp + accessibility/cocoa/AXCoreObjectCocoa.mm +diff --git a/Source/WebCore/platform/LocalizedStrings.cpp b/Source/WebCore/platform/LocalizedStrings.cpp +--- a/Source/WebCore/platform/LocalizedStrings.cpp ++++ b/Source/WebCore/platform/LocalizedStrings.cpp +@@ -1534,7 +1534,7 @@ + } + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && PLATFORM(COCOA) + // On macOS, Touch ID prompt is not guaranteed to show on top of the UI client, and therefore additional + // information is provided to help users to make decisions. + String makeCredentialTouchIDPromptTitle(const String& bundleName, const String& domain) +@@ -1551,7 +1551,7 @@ + { + return WEB_UI_STRING("Continue with Touch ID.", "Continue with Touch ID."); + } +-#endif // ENABLE(WEB_AUTHN) ++#endif // ENABLE(WEB_AUTHN) && PLATFORM(COCOA) + + String pdfPasswordFormTitle() + { +diff --git a/Source/WebCore/platform/LocalizedStrings.h b/Source/WebCore/platform/LocalizedStrings.h +--- a/Source/WebCore/platform/LocalizedStrings.h ++++ b/Source/WebCore/platform/LocalizedStrings.h +@@ -402,7 +402,7 @@ + WEBCORE_EXPORT String inputWeekLabel(const DateComponents&); + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && PLATFORM(COCOA) + WEBCORE_EXPORT String makeCredentialTouchIDPromptTitle(const String& bundleName, const String& domain); + WEBCORE_EXPORT String getAssertionTouchIDPromptTitle(const String& bundleName, const String& domain); + WEBCORE_EXPORT String genericTouchIDPromptTitle(); +diff --git a/Source/WebKit/PlatformWPE.cmake b/Source/WebKit/PlatformWPE.cmake +--- a/Source/WebKit/PlatformWPE.cmake ++++ b/Source/WebKit/PlatformWPE.cmake +@@ -211,6 +211,7 @@ + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitUserMessage.h.in + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebContext.h.in + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebResource.h.in ++ ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebAuthenticationRequest.h.in + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebView.h.in + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebViewSessionState.h.in + ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebsiteData.h.in +@@ -300,6 +301,7 @@ + "-DUSE_GTK4=0" + "-DENABLE_2022_GLIB_API=$" + "-DENABLE_WPE_PLATFORM=$" ++ "-DENABLE_WEB_AUTHN=$" + "-DUSE_GI_FINISH_FUNC_ANNOTATION=${USE_GI_FINISH_FUNC_ANNOTATION}" + ) + unset(USE_GI_FINISH_FUNC_ANNOTATION) +@@ -487,6 +489,12 @@ + WebProcess/glib/SystemSettingsManager + ) + ++if (ENABLE_WEB_AUTHN) ++ list(APPEND WebKit_MESSAGES_IN_FILES ++ UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy ++ ) ++endif () ++ + if (ENABLE_WPE_PLATFORM) + list(APPEND WebKit_PRIVATE_INCLUDE_DIRECTORIES + "${WPEPlatform_DERIVED_SOURCES_DIR}" +diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt +--- a/Source/WebKit/Sources.txt ++++ b/Source/WebKit/Sources.txt +@@ -492,8 +492,6 @@ + UIProcess/API/APIUIClient.cpp + UIProcess/API/APIUserScript.cpp + UIProcess/API/APIUserStyleSheet.cpp +-UIProcess/API/APIWebAuthenticationAssertionResponse.cpp +-UIProcess/API/APIWebAuthenticationPanel.cpp + UIProcess/API/APIWebPushDaemonConnection.cpp + UIProcess/API/APIWebsitePolicies.cpp + UIProcess/API/APIWindowFeatures.cpp +@@ -612,19 +610,8 @@ + UIProcess/UserContent/WebScriptMessageHandler.cpp + UIProcess/UserContent/WebUserContentControllerProxy.cpp + +-UIProcess/WebAuthentication/fido/CtapNfcDriver.cpp +-UIProcess/WebAuthentication/fido/FidoAuthenticator.cpp +-UIProcess/WebAuthentication/fido/FidoService.cpp +-UIProcess/WebAuthentication/fido/U2fAuthenticator.cpp +- +-UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp +-UIProcess/WebAuthentication/Mock/MockHidConnection.cpp +-UIProcess/WebAuthentication/Mock/MockHidService.cpp +- +-UIProcess/WebAuthentication/AuthenticatorTransportService.cpp +-UIProcess/WebAuthentication/Authenticator.cpp +-UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.cpp +-UIProcess/WebAuthentication/WebAuthenticationRequestData.cpp ++ ++ + + UIProcess/WebsiteData/WebDeviceOrientationAndMotionAccessController.cpp + UIProcess/WebsiteData/WebsiteDataRecord.cpp +diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt +--- a/Source/WebKit/SourcesCocoa.txt ++++ b/Source/WebKit/SourcesCocoa.txt +@@ -929,3 +929,18 @@ + WebSWServerToContextConnectionMessageReceiver.cpp + WebUserContentControllerMessageReceiver.cpp + WebUserContentControllerProxyMessageReceiver.cpp ++ ++# Native Cocoa authenticator implementation. ++UIProcess/API/APIWebAuthenticationAssertionResponse.cpp ++UIProcess/API/APIWebAuthenticationPanel.cpp ++UIProcess/WebAuthentication/fido/CtapNfcDriver.cpp ++UIProcess/WebAuthentication/fido/FidoAuthenticator.cpp ++UIProcess/WebAuthentication/fido/FidoService.cpp ++UIProcess/WebAuthentication/fido/U2fAuthenticator.cpp ++UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp ++UIProcess/WebAuthentication/Mock/MockHidConnection.cpp ++UIProcess/WebAuthentication/Mock/MockHidService.cpp ++UIProcess/WebAuthentication/AuthenticatorTransportService.cpp ++UIProcess/WebAuthentication/Authenticator.cpp ++UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.cpp ++UIProcess/WebAuthentication/WebAuthenticationRequestData.cpp +diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt +--- a/Source/WebKit/SourcesWPE.txt ++++ b/Source/WebKit/SourcesWPE.txt +@@ -306,3 +306,6 @@ + WebProcess/glib/WebProcessGLib.cpp + + WebProcess/wpe/WebProcessMainWPE.cpp ++ ++UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp @no-unify ++UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp @no-unify +diff --git a/Source/WebKit/UIProcess/API/APIUIClient.h b/Source/WebKit/UIProcess/API/APIUIClient.h +--- a/Source/WebKit/UIProcess/API/APIUIClient.h ++++ b/Source/WebKit/UIProcess/API/APIUIClient.h +@@ -56,6 +56,11 @@ + #include + #endif + ++ ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++struct _WebKitWebAuthenticationRequest; ++#endif ++ + namespace WebCore { + class RegistrableDomain; + class ResourceRequest; +@@ -210,6 +215,9 @@ + virtual void confirmPDFOpening(WebKit::WebPageProxy&, const WTF::URL&, WebKit::FrameInfoData&&, CompletionHandler&& completionHandler) { completionHandler(true); } + + #if ENABLE(WEB_AUTHN) ++#if PLATFORM(WPE) ++ virtual bool runWebAuthenticationRequest(WebKit::WebPageProxy&, _WebKitWebAuthenticationRequest*) { return false; } ++#endif + virtual void runWebAuthenticationPanel(WebKit::WebPageProxy&, WebAuthenticationPanel&, WebKit::WebFrameProxy&, WebKit::FrameInfoData&&, CompletionHandler&& completionHandler) { completionHandler(WebKit::WebAuthenticationPanelResult::Unavailable); } + + virtual void requestWebAuthenticationConditonalMediationRegistration(const WTF::String&, CompletionHandler)>&& completionHandler) +diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp +--- a/Source/WebKit/UIProcess/API/C/WKPage.cpp ++++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp +@@ -48,8 +48,10 @@ + #include "APISerializedScriptValue.h" + #include "APISessionState.h" + #include "APIUIClient.h" ++#if !PLATFORM(WPE) + #include "APIWebAuthenticationPanel.h" + #include "APIWebAuthenticationPanelClient.h" ++#endif + #include "APIWebsitePolicies.h" + #include "APIWindowFeatures.h" + #include "AuthenticationChallengeDisposition.h" +@@ -2260,7 +2262,7 @@ + m_client.handleAutoplayEvent(toAPI(&page), toWKAutoplayEvent(event), toWKAutoplayEventFlags(flags), m_client.base.clientInfo); + } + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + // The current method is specialized for WebKitTestRunner. + void runWebAuthenticationPanel(WebPageProxy&, API::WebAuthenticationPanel& panel, WebFrameProxy&, FrameInfoData&&, CompletionHandler&& completionHandler) final + { +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp +--- a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp ++++ b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp +@@ -68,6 +68,12 @@ + } + + private: ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++ bool runWebAuthenticationRequest(WebPageProxy&, _WebKitWebAuthenticationRequest* request) final ++ { ++ return webkitWebViewRunWebAuthenticationRequest(m_webView, request); ++ } ++#endif + void createNewPage(WebPageProxy& page, Ref&& configuration, Ref&& apiNavigationAction, CompletionHandler&&)>&& completionHandler) final + { + WebKitNavigationAction navigationAction(WTFMove(apiNavigationAction)); +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp +new file mode 100644 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp +@@ -0,0 +1,189 @@ ++/* Native WPE assertion provider. SPDX-License-Identifier: LGPL-2.0-or-later */ ++#include "config.h" ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++#include "WebKitWebAuthenticationRequestPrivate.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++using namespace WebCore; ++using namespace WebKit; ++ ++struct _WebKitWebAuthenticationRequestPrivate { ++ CString origin; ++ CString rpId; ++ CString optionsJSON; ++ GRefPtr clientHash; ++ GRefPtr cancellable; ++ RefPtr clientJSON; ++ Vector rpHash; ++ Vector> allowedCredentials; ++ bool requireUV { false }; ++ RequestCompletionHandler completion; ++ GSource* timeout { nullptr }; // The attached source holds a reference to this request. ++}; ++ ++WEBKIT_DEFINE_FINAL_TYPE(WebKitWebAuthenticationRequest, webkit_web_authentication_request, G_TYPE_OBJECT, GObject) ++ ++static void webkit_web_authentication_request_class_init(WebKitWebAuthenticationRequestClass*) ++{ ++} ++ ++static std::span bytes(GBytes* value) ++{ ++ if (!value) ++ return { }; ++ gsize size = 0; ++ const auto* data = static_cast(g_bytes_get_data(value, &size)); ++ return { data, size }; ++} ++ ++static bool finish(WebKitWebAuthenticationRequest* request, const AuthenticatorResponseData& response, const ExceptionData& exception) ++{ ++ ASSERT(isMainThread()); ++ auto& state = *request->priv; ++ if (!state.completion) ++ return false; ++ GRefPtr protectedRequest = request; ++ auto completion = WTFMove(state.completion); // Invalidate before any reentrant callbacks. ++ if (auto* timeout = std::exchange(state.timeout, nullptr)) ++ g_source_destroy(timeout); ++ if (!exception.message.isNull()) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: cancelled"); ++ g_cancellable_cancel(state.cancellable.get()); ++ } else ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: completed"); ++ completion(response, AuthenticatorAttachment::CrossPlatform, exception); ++ return true; ++} ++ ++void webkitWebAuthenticationRequestReject(WebKitWebAuthenticationRequest* request, ExceptionCode code) ++{ ++ finish(request, { }, { code, "The WebAuthn request did not complete."_s }); ++} ++ ++WebKitWebAuthenticationRequest* webkitWebAuthenticationRequestCreate(const SecurityOriginData& origin, ++ const PublicKeyCredentialRequestOptions& options, RequestCompletionHandler&& completion) ++{ ++ ASSERT(isMainThread()); ++ auto* request = WEBKIT_WEB_AUTHENTICATION_REQUEST(g_object_new(WEBKIT_TYPE_WEB_AUTHENTICATION_REQUEST, nullptr)); ++ auto& state = *request->priv; ++ state.origin = origin.toString().utf8(); ++ state.rpId = options.rpId.utf8(); ++ state.requireUV = options.userVerification == UserVerificationRequirement::Required; ++ state.cancellable = adoptGRef(g_cancellable_new()); ++ state.completion = WTFMove(completion); ++ state.clientJSON = buildClientDataJson(ClientDataType::Get, options.challenge, ++ origin.securityOrigin(), WebAuthn::Scope::SameOrigin); ++ auto hash = buildClientDataJsonHash(*state.clientJSON); ++ state.clientHash = adoptGRef(g_bytes_new(hash.data(), hash.size())); ++ auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256); ++ digest->addBytes(byteCast(state.rpId.span())); ++ state.rpHash = digest->computeHash(); ++ ++ unsigned timeout = std::min(options.timeout.value_or(120000), 120000U); ++ Ref json = JSON::Object::create(); ++ json->setString("rpId"_s, options.rpId); ++ json->setString("challenge"_s, base64URLEncodeToString(options.challenge.span())); ++ json->setInteger("timeout"_s, timeout); ++ json->setString("userVerification"_s, state.requireUV ? "required"_s ++ : options.userVerification == UserVerificationRequirement::Discouraged ? "discouraged"_s : "preferred"_s); ++ Ref credentials = JSON::Array::create(); ++ for (const auto& credential : options.allowCredentials) { ++ state.allowedCredentials.append(Vector(credential.id.span())); ++ Ref descriptor = JSON::Object::create(); ++ descriptor->setString("type"_s, "public-key"_s); ++ descriptor->setString("id"_s, base64URLEncodeToString(credential.id.span())); ++ credentials->pushObject(WTFMove(descriptor)); ++ } ++ json->setArray("allowCredentials"_s, WTFMove(credentials)); ++ state.optionsJSON = json->toJSONString().utf8(); ++ ++ state.timeout = g_timeout_source_new(timeout); ++ g_source_set_callback(state.timeout, [](gpointer data) -> gboolean { ++ webkitWebAuthenticationRequestReject(WEBKIT_WEB_AUTHENTICATION_REQUEST(data), ExceptionCode::NotAllowedError); ++ return G_SOURCE_REMOVE; ++ }, g_object_ref(request), g_object_unref); ++ g_source_attach(state.timeout, g_main_context_get_thread_default()); ++ g_source_unref(state.timeout); ++ return request; ++} ++ ++const gchar* webkit_web_authentication_request_get_origin(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), nullptr); ++ return request->priv->origin.data(); ++} ++const gchar* webkit_web_authentication_request_get_rp_id(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), nullptr); ++ return request->priv->rpId.data(); ++} ++const gchar* webkit_web_authentication_request_get_options_json(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), nullptr); ++ return request->priv->optionsJSON.data(); ++} ++GBytes* webkit_web_authentication_request_get_client_data_hash(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), nullptr); ++ return request->priv->clientHash.get(); ++} ++GCancellable* webkit_web_authentication_request_get_cancellable(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), nullptr); ++ return request->priv->cancellable.get(); ++} ++void webkit_web_authentication_request_cancel(WebKitWebAuthenticationRequest* request) ++{ ++ g_return_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request)); ++ webkitWebAuthenticationRequestReject(request, ExceptionCode::NotAllowedError); ++} ++ ++gboolean webkit_web_authentication_request_complete_assertion(WebKitWebAuthenticationRequest* request, ++ GBytes* credentialID, GBytes* authenticatorData, GBytes* signature, GBytes* userHandle) ++{ ++ g_return_val_if_fail(WEBKIT_IS_WEB_AUTHENTICATION_REQUEST(request), FALSE); ++ ASSERT(isMainThread()); ++ const auto& state = *request->priv; ++ if (!state.completion) ++ return FALSE; ++ auto id = bytes(credentialID); ++ auto data = bytes(authenticatorData); ++ auto sig = bytes(signature); ++ auto user = bytes(userHandle); ++ bool allowed = state.allowedCredentials.isEmpty() || std::ranges::any_of(state.allowedCredentials, [id](const auto& candidate) { ++ return std::ranges::equal(candidate.span(), id); ++ }); ++ bool valid = !id.empty() && id.size() <= 1024 && data.size() >= 37 && data.size() <= 16384 ++ && !sig.empty() && sig.size() <= 4096 && user.size() <= 64 && allowed ++ && (state.allowedCredentials.size() || !user.empty()) && (!userHandle || !user.empty()); ++ if (valid) { ++ uint8_t flags = data[32]; ++ valid = std::ranges::equal(data.first(32), state.rpHash.span()) ++ && (flags & 0x01) && (!state.requireUV || (flags & 0x04)) ++ && !(flags & 0x40) && (!(flags & 0x10) || (flags & 0x08)); ++ } ++ if (!valid) { ++ webkitWebAuthenticationRequestReject(request, ExceptionCode::NotAllowedError); ++ return FALSE; ++ } ++ AuthenticatorResponseData response; ++ response.rawId = ArrayBuffer::create(id); ++ response.clientDataJSON = state.clientJSON; ++ response.authenticatorData = ArrayBuffer::create(data); ++ response.signature = ArrayBuffer::create(sig); ++ if (userHandle) ++ response.userHandle = ArrayBuffer::create(user); ++ return finish(request, response, { }); ++} ++#endif +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.h.in b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.h.in +new file mode 100644 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequest.h.in +@@ -0,0 +1,27 @@ ++/* Native WPE assertion provider API. SPDX-License-Identifier: LGPL-2.0-or-later */ ++@API_SINGLE_HEADER_CHECK@ ++#ifndef WebKitWebAuthenticationRequest_h ++#define WebKitWebAuthenticationRequest_h ++#include ++#include <@API_INCLUDE_PREFIX@/WebKitDefines.h> ++G_BEGIN_DECLS ++#define WEBKIT_TYPE_WEB_AUTHENTICATION_REQUEST (webkit_web_authentication_request_get_type()) ++WEBKIT_DECLARE_FINAL_TYPE(WebKitWebAuthenticationRequest, webkit_web_authentication_request, WEBKIT, WEB_AUTHENTICATION_REQUEST, GObject) ++ ++/* Immutable values, owned by the request. All calls run on the view's GLib thread. ++ * options_json is PublicKeyCredentialRequestOptionsJSON; its origin is separate. ++ * The caller must use exactly client_data_hash, not regenerate clientDataJSON. */ ++WEBKIT_API const gchar * webkit_web_authentication_request_get_origin(WebKitWebAuthenticationRequest *request); ++WEBKIT_API const gchar * webkit_web_authentication_request_get_rp_id(WebKitWebAuthenticationRequest *request); ++WEBKIT_API const gchar * webkit_web_authentication_request_get_options_json(WebKitWebAuthenticationRequest *request); ++WEBKIT_API GBytes * webkit_web_authentication_request_get_client_data_hash(WebKitWebAuthenticationRequest *request); ++WEBKIT_API GCancellable * webkit_web_authentication_request_get_cancellable(WebKitWebAuthenticationRequest *request); ++ ++/* Copies the buffers before returning. user_handle may be NULL for a permitted ++ * non-discoverable credential. Returns FALSE for invalid or stale completion. ++ * Invalid results reject the pending request; no second completion is allowed. */ ++WEBKIT_API gboolean webkit_web_authentication_request_complete_assertion(WebKitWebAuthenticationRequest *request, ++ GBytes *credential_id, GBytes *authenticator_data, GBytes *signature, GBytes *user_handle); ++WEBKIT_API void webkit_web_authentication_request_cancel(WebKitWebAuthenticationRequest *request); ++G_END_DECLS ++#endif +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequestPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequestPrivate.h +new file mode 100644 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebAuthenticationRequestPrivate.h +@@ -0,0 +1,12 @@ ++/* SPDX-License-Identifier: LGPL-2.0-or-later */ ++#pragma once ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++#include "WebKitWebAuthenticationRequest.h" ++#include "WebAuthenticatorCoordinatorProxy.h" ++#include ++#include ++#include ++WebKitWebAuthenticationRequest* webkitWebAuthenticationRequestCreate(const WebCore::SecurityOriginData&, ++ const WebCore::PublicKeyCredentialRequestOptions&, WebKit::RequestCompletionHandler&&); ++void webkitWebAuthenticationRequestReject(WebKitWebAuthenticationRequest*, WebCore::ExceptionCode); ++#endif +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp +--- a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp +@@ -21,6 +21,9 @@ + + #include "config.h" + #include "WebKitWebView.h" ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++#include "WebKitWebAuthenticationRequest.h" ++#endif + + #include "APIContentWorld.h" + #include "APIData.h" +@@ -140,6 +143,9 @@ + */ + + enum { ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++ WEB_AUTHENTICATION_REQUEST, ++#endif + LOAD_CHANGED, + LOAD_FAILED, + LOAD_FAILED_WITH_TLS_ERRORS, +@@ -2517,6 +2523,21 @@ + * + * Since: 2.40 + */ ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++ /** ++ * WebKitWebView::webauthn-request: ++ * @web_view: the view owning the requesting document ++ * @request: an immutable native assertion request ++ * ++ * Return TRUE and retain @request for asynchronous completion. Observe its ++ * GCancellable and stop native transport when cancelled. All API calls must ++ * run on this view's GLib thread. An unhandled request fails as unsupported. ++ */ ++ signals[WEB_AUTHENTICATION_REQUEST] = g_signal_new("webauthn-request", ++ G_TYPE_FROM_CLASS(webViewClass), G_SIGNAL_RUN_LAST, 0, ++ g_signal_accumulator_true_handled, nullptr, g_cclosure_marshal_generic, ++ G_TYPE_BOOLEAN, 1, WEBKIT_TYPE_WEB_AUTHENTICATION_REQUEST); ++#endif + signals[QUERY_PERMISSION_STATE] = g_signal_new( + "query-permission-state", + G_TYPE_FROM_CLASS(webViewClass), +@@ -5763,3 +5784,12 @@ + + return webView->priv->defaultContentSecurityPolicy.data(); + } ++ ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++bool webkitWebViewRunWebAuthenticationRequest(WebKitWebView* view, WebKitWebAuthenticationRequest* request) ++{ ++ gboolean handled = FALSE; ++ g_signal_emit(view, signals[WEB_AUTHENTICATION_REQUEST], 0, request, &handled); ++ return handled; ++} ++#endif +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h +--- a/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h +@@ -130,3 +130,8 @@ + #if PLATFORM(GTK) || (PLATFORM(WPE) && ENABLE(WPE_PLATFORM)) + WebKit::RendererBufferFormat webkitWebViewGetRendererBufferFormat(WebKitWebView*); + #endif ++ ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++struct _WebKitWebAuthenticationRequest; ++bool webkitWebViewRunWebAuthenticationRequest(WebKitWebView*, _WebKitWebAuthenticationRequest*); ++#endif +diff --git a/Source/WebKit/UIProcess/API/glib/webkit.h.in b/Source/WebKit/UIProcess/API/glib/webkit.h.in +--- a/Source/WebKit/UIProcess/API/glib/webkit.h.in ++++ b/Source/WebKit/UIProcess/API/glib/webkit.h.in +@@ -129,6 +129,9 @@ + #include <@API_INCLUDE_PREFIX@/WebKitWebInspector.h> + #endif + #include <@API_INCLUDE_PREFIX@/WebKitWebResource.h> ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++#include <@API_INCLUDE_PREFIX@/WebKitWebAuthenticationRequest.h> ++#endif + #include <@API_INCLUDE_PREFIX@/WebKitWebView.h> + #if PLATFORM(WPE) + #include +diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp b/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp +--- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp ++++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp +@@ -61,7 +61,7 @@ + #include + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + #include "VirtualAuthenticatorManager.h" + #include + #endif +@@ -1674,7 +1674,7 @@ + return { }; + } + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + static WebCore::AuthenticatorTransport toAuthenticatorTransport(Inspector::Protocol::Automation::AuthenticatorTransport transport) + { + switch (transport) { +@@ -1695,7 +1695,7 @@ + + Inspector::Protocol::ErrorStringOr WebAutomationSession::addVirtualAuthenticator(const String& browsingContextHandle, Ref&& authenticator) + { +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + auto protocol = authenticator->getString("protocol"_s); + if (!protocol) + SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'protocol' is missing or invalid."_s); +@@ -1736,7 +1736,7 @@ + + Inspector::Protocol::ErrorStringOr WebAutomationSession::removeVirtualAuthenticator(const String& browsingContextHandle, const String& authenticatorId) + { +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + auto page = webPageProxyForHandle(browsingContextHandle); + if (!page) + SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); +diff --git a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h +--- a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h ++++ b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h +@@ -25,7 +25,7 @@ + + #pragma once + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + + #include "Authenticator.h" + #include "AuthenticatorPresenterCoordinator.h" +diff --git a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.h b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.h +--- a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.h ++++ b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.h +@@ -25,7 +25,7 @@ + + #pragma once + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + + #include "AuthenticatorManager.h" + #include +diff --git a/Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h b/Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h +--- a/Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h ++++ b/Source/WebKit/UIProcess/WebAuthentication/WebAuthenticatorCoordinatorProxy.h +@@ -47,6 +47,11 @@ + typedef NSString *ASAuthorizationPublicKeyCredentialAttestationKind; + #endif + ++#if PLATFORM(WPE) ++#include ++struct _WebKitWebAuthenticationRequest; ++#endif ++ + namespace WebCore { + enum class AuthenticatorAttachment : uint8_t; + struct ExceptionData; +@@ -90,6 +95,9 @@ + void deref() const final { RefCounted::deref(); } + + std::optional sharedPreferencesForWebProcess() const; ++#if PLATFORM(WPE) ++ void cancelRequest(std::optional); ++#endif + + #if HAVE(WEB_AUTHN_AS_MODERN) + static WeakPtr& activeConditionalMediationProxy(); +@@ -118,6 +126,10 @@ + void handleRequest(WebAuthenticationRequestData&&, RequestCompletionHandler&&); + + WeakPtr m_webPageProxy; ++#if PLATFORM(WPE) ++ GRefPtr<_WebKitWebAuthenticationRequest> m_request; ++ std::optional m_requestFrame; ++#endif + + #if HAVE(UNIFIED_ASC_AUTH_UI) || HAVE(WEB_AUTHN_AS_MODERN) + bool isASCAvailable(); +diff --git a/Source/WebKit/UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp b/Source/WebKit/UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp +new file mode 100644 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp +@@ -0,0 +1,195 @@ ++/* WPE native assertion bridge. SPDX-License-Identifier: BSD-2-Clause */ ++#include "config.h" ++#include "WebAuthenticatorCoordinatorProxy.h" ++#if PLATFORM(WPE) && ENABLE(WEB_AUTHN) ++#include "APIUIClient.h" ++#include "FrameInfoData.h" ++#include "WebAuthenticatorCoordinatorProxyMessages.h" ++#include "WebFrameProxy.h" ++#include "WebKitWebAuthenticationRequestPrivate.h" ++#include "WebPageProxy.h" ++#include "WebProcessProxy.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++using namespace WebCore; ++WTF_MAKE_TZONE_ALLOCATED_IMPL(WebAuthenticatorCoordinatorProxy); ++ ++Ref WebAuthenticatorCoordinatorProxy::create(WebPageProxy& page) ++{ ++ return adoptRef(*new WebAuthenticatorCoordinatorProxy(page)); ++} ++WebAuthenticatorCoordinatorProxy::WebAuthenticatorCoordinatorProxy(WebPageProxy& page) ++ : m_webPageProxy(page) ++{ ++ page.protectedLegacyMainFrameProcess()->addMessageReceiver(Messages::WebAuthenticatorCoordinatorProxy::messageReceiverName(), page.webPageIDInMainFrameProcess(), *this); ++} ++WebAuthenticatorCoordinatorProxy::~WebAuthenticatorCoordinatorProxy() ++{ ++ cancelRequest(std::nullopt); ++ if (RefPtr page = m_webPageProxy.get()) ++ page->protectedLegacyMainFrameProcess()->removeMessageReceiver(Messages::WebAuthenticatorCoordinatorProxy::messageReceiverName(), page->webPageIDInMainFrameProcess()); ++} ++std::optional WebAuthenticatorCoordinatorProxy::sharedPreferencesForWebProcess() const ++{ ++ RefPtr page = m_webPageProxy.get(); ++ return page ? page->protectedLegacyMainFrameProcess()->sharedPreferencesForWebProcess() : std::nullopt; ++} ++void WebAuthenticatorCoordinatorProxy::makeCredential(FrameIdentifier, FrameInfoData&&, ++ PublicKeyCredentialCreationOptions&&, MediationRequirement, RequestCompletionHandler&& handler) ++{ ++ handler({ }, AuthenticatorAttachment::CrossPlatform, { ExceptionCode::NotSupportedError, "This browser supports phone passkey sign-in only."_s }); ++} ++ ++static std::optional trustedOrigin(WebPageProxy& page, FrameIdentifier frameID, const FrameInfoData& info) ++{ ++ RefPtr frame = WebFrameProxy::webFrame(frameID); ++ if (!frame || frame->page() != &page || &frame->process() != &page.legacyMainFrameProcess() ++ || !frame->url().protocolIs("https"_s) || frame->provisionalURL().isValid()) ++ return std::nullopt; ++ auto origin = SecurityOriginData::fromURLWithoutStrictOpaqueness(frame->url()); ++ if (info.securityOrigin != origin || info.isMainFrame != frame->isMainFrame() ++ || !origin.host().containsOnlyASCII() || URL::hostIsIPAddress(origin.host())) ++ return std::nullopt; ++ // This first provider supports same-origin HTTPS documents only. Never ++ // infer an about:blank origin or accept an IPC-supplied ancestor origin. ++ for (RefPtr ancestor = frame->parentFrame(); ancestor; ancestor = ancestor->parentFrame()) { ++ if (ancestor->page() != &page || !ancestor->url().protocolIs("https"_s) ++ || SecurityOriginData::fromURLWithoutStrictOpaqueness(ancestor->url()) != origin) ++ return std::nullopt; ++ } ++ return origin; ++} ++ ++static bool supportedOptions(const PublicKeyCredentialRequestOptions& options) ++{ ++ if (!options.challenge.length()) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-challenge-empty"); ++ return false; ++ } ++ if (options.challenge.length() > 16384) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-challenge-too-large"); ++ return false; ++ } ++ if (options.allowCredentials.size() > 64) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-allowlist-too-large"); ++ return false; ++ } ++ if (options.authenticatorAttachment == AuthenticatorAttachment::Platform) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-platform"); ++ return false; ++ } ++ if (options.extensions) { ++ const auto& extensions = *options.extensions; ++ if (!extensions.appid.isNull()) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-appid"); ++ return false; ++ } ++ if (extensions.credProps.value_or(false)) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-credProps"); ++ return false; ++ } ++ if (extensions.largeBlob) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-largeBlob"); ++ return false; ++ } ++ if (extensions.prf) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-prf"); ++ return false; ++ } ++ } ++ for (const auto& credential : options.allowCredentials) { ++ if (credential.type != PublicKeyCredentialType::PublicKey) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-options-credential-type"); ++ return false; ++ } ++ if (!credential.id.length()) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-credential-id-empty"); ++ return false; ++ } ++ if (credential.id.length() > 1024) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-credential-id-too-large"); ++ return false; ++ } ++ } ++ return true; ++} ++ ++void WebAuthenticatorCoordinatorProxy::getAssertion(FrameIdentifier frameID, FrameInfoData&& info, ++ PublicKeyCredentialRequestOptions&& options, MediationRequirement mediation, ++ std::optional parentOrigin, RequestCompletionHandler&& handler) ++{ ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: assertion-received"); ++ RefPtr page = m_webPageProxy.get(); ++ auto origin = page ? trustedOrigin(*page, frameID, info) : std::nullopt; ++ if (!origin || parentOrigin) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-secure-origin"); ++ handler({ }, AuthenticatorAttachment::CrossPlatform, { ExceptionCode::SecurityError, "The requesting document is not an eligible secure context."_s }); ++ return; ++ } ++ const bool unsupportedMediation = mediation == MediationRequirement::Conditional || mediation == MediationRequirement::Silent; ++ if (unsupportedMediation) ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-mediation"); ++ if (unsupportedMediation || !supportedOptions(options)) { ++ handler({ }, AuthenticatorAttachment::CrossPlatform, { ExceptionCode::NotSupportedError, "The requested WebAuthn mode or options are not supported."_s }); ++ return; ++ } ++ if (options.rpId.isEmpty()) ++ options.rpId = origin->host(); ++ URL rpURL { makeString("https://"_s, options.rpId, '/' ) }; ++ if (!options.rpId.containsOnlyASCII() || options.rpId.length() > 253 ++ || !rpURL.isValid() || rpURL.host() != options.rpId ++ || !origin->securityOrigin()->isMatchingRegistrableDomainSuffix(options.rpId)) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: reject-rp"); ++ handler({ }, AuthenticatorAttachment::CrossPlatform, { ExceptionCode::SecurityError, "The relying party ID is not authorized for this origin."_s }); ++ return; ++ } ++ cancelRequest(std::nullopt); ++ auto request = adoptGRef(webkitWebAuthenticationRequestCreate(*origin, options, WTFMove(handler))); ++ m_request = request; ++ m_requestFrame = frameID; ++ if (!page->uiClient().runWebAuthenticationRequest(*page, request.get())) { ++ syslog(LOG_AUTHPRIV | LOG_NOTICE, "rmweb-webauthn: unhandled-ui"); ++ webkitWebAuthenticationRequestReject(request.get(), ExceptionCode::NotSupportedError); ++ } ++} ++void WebAuthenticatorCoordinatorProxy::cancelRequest(std::optional frameID) ++{ ++ if (frameID && m_requestFrame != frameID) { ++ RefPtr frame = WebFrameProxy::webFrame(*frameID); ++ if (!frame || !frame->isMainFrame()) ++ return; ++ } ++ auto request = WTFMove(m_request); ++ m_requestFrame = std::nullopt; ++ if (request) ++ webkit_web_authentication_request_cancel(request.get()); ++} ++void WebAuthenticatorCoordinatorProxy::cancel(CompletionHandler&& handler) ++{ ++ cancelRequest(std::nullopt); ++ handler(); ++} ++void WebAuthenticatorCoordinatorProxy::isUserVerifyingPlatformAuthenticatorAvailable(const SecurityOriginData&, QueryCompletionHandler&& handler) ++{ ++ handler(false); ++} ++void WebAuthenticatorCoordinatorProxy::isConditionalMediationAvailable(const SecurityOriginData&, QueryCompletionHandler&& handler) ++{ ++ handler(false); ++} ++void WebAuthenticatorCoordinatorProxy::getClientCapabilities(const SecurityOriginData&, CapabilitiesCompletionHandler&& handler) ++{ ++ handler({ { "conditionalGet"_s, false }, { "conditionalCreate"_s, false }, ++ { "hybridTransport"_s, true }, { "relatedOrigins"_s, false }, ++ { "userVerifyingPlatformAuthenticator"_s, false } }); ++} ++} // namespace WebKit ++#endif +diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp +--- a/Source/WebKit/UIProcess/WebPageProxy.cpp ++++ b/Source/WebKit/UIProcess/WebPageProxy.cpp +@@ -6464,7 +6464,12 @@ + void WebPageProxy::didDestroyFrame(IPC::Connection& connection, FrameIdentifier frameID) + { + #if ENABLE(WEB_AUTHN) ++#if PLATFORM(WPE) ++ if (RefPtr messenger = m_webAuthnCredentialsMessenger) ++ messenger->cancelRequest(frameID); ++#else + protectedWebsiteDataStore()->protectedAuthenticatorManager()->cancelRequest(webPageIDInMainFrameProcess(), frameID); ++#endif + #endif + if (RefPtr automationSession = m_configuration->processPool().automationSession()) + automationSession->didDestroyFrame(frameID); +@@ -6756,7 +6761,12 @@ + } + + #if ENABLE(WEB_AUTHN) ++#if PLATFORM(WPE) ++ if (RefPtr messenger = m_webAuthnCredentialsMessenger) ++ messenger->cancelRequest(frameID); ++#else + protectedWebsiteDataStore()->protectedAuthenticatorManager()->cancelRequest(m_webPageID, frameID); ++#endif + #endif + } + +@@ -11317,7 +11327,12 @@ + #endif + + #if ENABLE(WEB_AUTHN) ++#if PLATFORM(WPE) ++ if (RefPtr messenger = m_webAuthnCredentialsMessenger) ++ messenger->cancelRequest(std::nullopt); ++#else + protectedWebsiteDataStore()->protectedAuthenticatorManager()->cancelRequest(m_webPageID, std::nullopt); ++#endif + #endif + + m_speechRecognitionPermissionManager = nullptr; +@@ -14923,7 +14938,11 @@ + #if ENABLE(WEB_AUTHN) + void WebPageProxy::setMockWebAuthenticationConfiguration(MockWebAuthenticationConfiguration&& configuration) + { ++#if !PLATFORM(WPE) + protectedWebsiteDataStore()->setMockWebAuthenticationConfiguration(WTFMove(configuration)); ++#else ++ UNUSED_PARAM(configuration); ++#endif + } + #endif + +diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp +--- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp ++++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp +@@ -95,7 +95,7 @@ + #include "WebPrivacyHelpers.h" + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + #include "VirtualAuthenticatorManager.h" + #endif // ENABLE(WEB_AUTHN) + +@@ -160,7 +160,7 @@ + , m_configuration(WTFMove(configuration)) + , m_trackingPreventionDebugMode(m_configuration->resourceLoadStatisticsDebugModeEnabled()) + , m_queue(WorkQueue::create("com.apple.WebKit.WebsiteDataStore"_s)) +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + , m_authenticatorManager(AuthenticatorManager::create()) + #endif + #if ENABLE(DEVICE_ORIENTATION) +@@ -2246,7 +2246,7 @@ + } + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + void WebsiteDataStore::setMockWebAuthenticationConfiguration(WebCore::MockWebAuthenticationConfiguration&& configuration) + { + if (!m_authenticatorManager->isMock()) { +diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h +--- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h ++++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h +@@ -336,7 +336,7 @@ + void addSecKeyProxyStore(Ref&&); + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + AuthenticatorManager& authenticatorManager() { return m_authenticatorManager.get(); } + Ref protectedAuthenticatorManager(); + void setMockWebAuthenticationConfiguration(WebCore::MockWebAuthenticationConfiguration&&); +@@ -630,7 +630,7 @@ + Vector> m_secKeyProxyStores; + #endif + +-#if ENABLE(WEB_AUTHN) ++#if ENABLE(WEB_AUTHN) && !PLATFORM(WPE) + Ref m_authenticatorManager; + #endif + +diff --git a/Source/WebKit/WebProcess/WebAuthentication/WebAuthenticatorCoordinator.cpp b/Source/WebKit/WebProcess/WebAuthentication/WebAuthenticatorCoordinator.cpp +--- a/Source/WebKit/WebProcess/WebAuthentication/WebAuthenticatorCoordinator.cpp ++++ b/Source/WebKit/WebProcess/WebAuthentication/WebAuthenticatorCoordinator.cpp +@@ -28,7 +28,6 @@ + + #if ENABLE(WEB_AUTHN) + +-#include "DefaultWebBrowserChecks.h" + #include "FrameInfoData.h" + #include "WebAuthenticatorCoordinatorProxyMessages.h" + #include "WebFrame.h" +@@ -57,13 +56,6 @@ + using namespace WebCore; + + WTF_MAKE_TZONE_ALLOCATED_IMPL(WebAuthenticatorCoordinator); +- +-namespace { +-inline bool isWebBrowser() +-{ +- return isParentProcessAFullWebBrowser(WebProcess::singleton()); +-} +-} + + WebAuthenticatorCoordinator::WebAuthenticatorCoordinator(WebPage& webPage) + : m_webPage(webPage) +diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp +--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp ++++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp +@@ -399,7 +399,7 @@ + #endif + + #if ENABLE(WEB_AUTHN) +-#include "WebAuthenticatorCoordinator.h" ++#include "WebProcess/WebAuthentication/WebAuthenticatorCoordinator.h" + #include + #endif // ENABLE(WEB_AUTHN) + +diff --git a/Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp b/Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp +--- a/Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp ++++ b/Source/WebKit/WPEPlatform/wpe/headless/WPEViewHeadless.cpp +@@ -27,6 +27,7 @@ + #include "WPEViewHeadless.h" + + #include "WPEToplevelHeadless.h" ++#include "WPEEvent.h" + #include + #include + #include +@@ -40,6 +41,7 @@ + GRefPtr committedBuffer; + GRefPtr frameSource; + gint64 lastFrameTime; ++ gint64 lastInputTime; + }; + WEBKIT_DEFINE_FINAL_TYPE(WPEViewHeadless, wpe_view_headless, WPE_TYPE_VIEW, WPEView) + +@@ -90,6 +92,9 @@ + if (priv->committedBuffer) + wpe_view_buffer_released(view, priv->committedBuffer.get()); + priv->committedBuffer = WTFMove(priv->pendingBuffer); ++ // Pace from completed presentation, not buffer arrival: otherwise ++ // fast producers alternate delayed frames with immediate frames. ++ priv->lastFrameTime = g_get_monotonic_time(); + wpe_view_buffer_rendered(view, priv->committedBuffer.get()); + + if (g_source_is_destroyed(priv->frameSource.get())) +@@ -98,6 +103,32 @@ + }, object, nullptr); + g_source_attach(priv->frameSource.get(), g_main_context_get_thread_default()); + g_source_set_ready_time(priv->frameSource.get(), -1); ++ ++ // Observe native input without consuming it. Briefly drain the compositor ++ // pipeline faster so old animated frames do not delay a tap or keystroke. ++ g_signal_connect(view, "event", G_CALLBACK(+[](WPEView* view, WPEEvent* event, gpointer) -> gboolean { ++ switch (wpe_event_get_event_type(event)) { ++ case WPE_EVENT_POINTER_DOWN: ++ case WPE_EVENT_POINTER_UP: ++ case WPE_EVENT_SCROLL: ++ case WPE_EVENT_KEYBOARD_KEY_DOWN: ++ case WPE_EVENT_KEYBOARD_KEY_UP: ++ case WPE_EVENT_TOUCH_DOWN: ++ case WPE_EVENT_TOUCH_UP: ++ case WPE_EVENT_TOUCH_MOVE: { ++ auto* priv = WPE_VIEW_HEADLESS(view)->priv; ++ priv->lastInputTime = g_get_monotonic_time(); ++ if (priv->pendingBuffer && priv->frameSource && !g_source_is_destroyed(priv->frameSource.get())) { ++ auto next = priv->lastFrameTime + (G_USEC_PER_SEC / 30); ++ g_source_set_ready_time(priv->frameSource.get(), next <= priv->lastInputTime ? 0 : next); ++ } ++ break; ++ } ++ default: ++ break; ++ } ++ return FALSE; ++ }), nullptr); + } + + static void wpeViewHeadlessDispose(GObject* object) +@@ -117,10 +148,11 @@ + auto* priv = WPE_VIEW_HEADLESS(view)->priv; + priv->pendingBuffer = buffer; + auto now = g_get_monotonic_time(); +- if (!priv->lastFrameTime) +- priv->lastFrameTime = now; +- auto next = priv->lastFrameTime + (G_USEC_PER_SEC / 60); +- priv->lastFrameTime = now; ++ // Bound passive e-ink compositing to eight frames per second. Native ++ // input gets one second at 30 Hz to drain already queued frames promptly. ++ // First and overdue frames remain immediate; backpressure is unchanged. ++ auto rate = priv->lastInputTime && now - priv->lastInputTime < G_USEC_PER_SEC ? 30 : 8; ++ auto next = priv->lastFrameTime ? priv->lastFrameTime + (G_USEC_PER_SEC / rate) : now; + if (next <= now) + g_source_set_ready_time(priv->frameSource.get(), 0); + else diff --git a/scripts/app_only_cache.py b/scripts/app_only_cache.py new file mode 100644 index 0000000..613c9f6 --- /dev/null +++ b/scripts/app_only_cache.py @@ -0,0 +1,39 @@ +"""Ownership check for this build lane's disposable cache directories.""" +from pathlib import Path + +MARKER = '.rmweb-app-only-cache' +CONTENTS = b'rmweb-app-only-cache-v1\n' + + +def reject_redirected_outputs(cache: Path): + # The verified runtime archive intentionally contains library symlinks. + # Other cache-owned write trees must not redirect writes outside the cache. + if any(path.is_symlink() for path in cache.iterdir()): + raise SystemExit('Refusing a symlink in app-only cache output paths') + for name in ('sources', 'stage', 'build-app', 'artifacts'): + tree = cache / name + if tree.exists() and not tree.is_dir(): + raise SystemExit('An app-only cache directory is not a directory') + if tree.exists() and any(path.is_symlink() for path in tree.rglob('*')): + raise SystemExit('Refusing a symlink in app-only cache output paths') + + +def require_owned_cache(cache: Path): + cache.mkdir(parents=True, exist_ok=True) + reject_redirected_outputs(cache) + marker = cache / MARKER + if marker.is_symlink(): + raise SystemExit('Refusing a symlinked app-only cache marker') + if marker.exists(): + if not marker.is_file() or marker.stat().st_size != len(CONTENTS) or marker.read_bytes() != CONTENTS: + raise SystemExit('Invalid app-only cache ownership marker') + return + reserved = ('stage', 'runtime', 'build-app', 'artifacts', 'header-manifest.json') + if any((cache / name).exists() or (cache / name).is_symlink() for name in reserved): + raise SystemExit('Refusing occupied, unmarked app-only cache; select a fresh dedicated directory') + try: + with marker.open('xb') as output: + output.write(CONTENTS) + except FileExistsError: + # Another creator may race us; never infer ownership from existence alone. + require_owned_cache(cache) diff --git a/scripts/build-auth-browser.py b/scripts/build-auth-browser.py new file mode 100755 index 0000000..2b175d3 --- /dev/null +++ b/scripts/build-auth-browser.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""Build only the authentication browser against verified isolated artifacts.""" +import argparse +import fcntl +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import shutil +import stat +import subprocess +import tempfile + +REPOSITORY = Path(__file__).resolve().parents[1] +_engine_spec = importlib.util.spec_from_file_location('rmweb_auth_engine', REPOSITORY / 'scripts/build-auth-engine.py') +_engine_recipe = importlib.util.module_from_spec(_engine_spec) +_engine_spec.loader.exec_module(_engine_recipe) +RUNTIME_ENVIRONMENT = _engine_recipe.ENVIRONMENT +MARKER = '.rmweb-auth-build-cache' +MARKER_BYTES = b'rmweb-auth-build-cache-v1\n' +SDK_SHA = '65e5b98f9f7c83d857c5c720f7f6cb2d61fe20f4513dec023ad00aac27c85ae4' +RUNTIME_SHA = 'e7d6d169ec73743b9f6a60601eba607b741d2144c5b944bf828915b830aafb6b' +WPE_SHA = '01f36010705adb14404c56baf033147f7927cc7c6badec81bb141266fcdd8d0b' +HELPER_REVISION = 'a6bdb700918f4c8c06c52d41f4d2d9e79888c5ad' +TARGETS = ('rmweb-auth-browser', 'rmweb-auth-entry') +REQUIRED_RUNTIME = {'rmweb-env.sh', 'lib/libWPEWebKit-2.0.so.1', + 'libexec/wpe-webkit-2.0/WPEWebProcess', 'libexec/wpe-webkit-2.0/WPENetworkProcess', + 'libexec/wpe-webkit-2.0/WPEGPUProcess'} + + +def sha(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def external(path): + path = Path(os.path.abspath(path.expanduser())) + if path == REPOSITORY or REPOSITORY in path.parents: + raise ValueError('authentication build inputs/cache must remain outside Git') + for part in [path, *path.parents]: + if part.is_symlink(): + raise ValueError('symlink build/input path') + return path + + +def digest(value): + return isinstance(value, str) and re.fullmatch(r'[0-9a-f]{64}', value) is not None + + +def relative(name): + return (isinstance(name, str) and re.fullmatch(r'[A-Za-z0-9_+./-]{1,240}', name) + and not name.startswith('/') and all(part not in ('', '.', '..') for part in name.split('/'))) + + +def read_manifest(path): + if path.is_symlink() or not path.is_file() or path.stat().st_size > 2 * 1024 * 1024: + raise ValueError('missing or unsafe artifact manifest') + value = json.loads(path.read_text()) + if not isinstance(value, dict): + raise ValueError('artifact manifest must be an object') + return value + + +def regular_files(directory): + if directory.is_symlink() or not directory.is_dir(): + raise ValueError('expected a regular artifact directory') + result = set() + for path in directory.rglob('*'): + if path.is_symlink() or not (path.is_file() or path.is_dir()): + raise ValueError('artifact tree contains a redirected or special file') + if path.is_file(): + result.add(path.relative_to(directory).as_posix()) + return result + + +def verify_files(directory, records): + if not isinstance(records, dict) or not 1 <= len(records) <= 4096 or regular_files(directory) != set(records): + raise ValueError('artifact file inventory mismatch') + result = {} + total = 0 + for name, record in records.items(): + if not relative(name) or not isinstance(record, dict): + raise ValueError('unsafe artifact file record') + mode = record.get('mode') + if mode in ('0644', '0755'): + mode = int(mode, 8) + size = record.get('bytes') + if (type(mode) is not int or mode not in (0o644, 0o755) or type(size) is not int + or not 0 <= size <= 768 * 1024 * 1024 or not digest(record.get('sha256'))): + raise ValueError('invalid artifact size/mode/hash') + path = directory / name + if path.stat().st_size != size or stat.S_IMODE(path.stat().st_mode) != mode or sha(path) != record['sha256']: + raise ValueError('artifact differs from manifest') + result[name] = {'sha256': record['sha256'], 'bytes': size, 'mode': mode} + total += size + if total > 2 * 1024 * 1024 * 1024: + raise ValueError('artifact tree exceeds bound') + return result + + +def executable(path): + if path.is_symlink() or not path.is_file() or not path.stat().st_mode & 0o111 or path.stat().st_size > 64 * 1024 * 1024: + raise ValueError('invalid authentication executable') + with path.open('rb') as stream: + header = stream.read(64) + if len(header) != 64 or header[:6] != b'\x7fELF\x02\x01' or int.from_bytes(header[18:20], 'little') != 183: + raise ValueError('authentication executable is not AArch64 ELF') + return {'sha256': sha(path), 'bytes': path.stat().st_size} + + +def helper_source_hash(): + source = REPOSITORY / 'engine/auth-passkey-helper' + files = {str(path.relative_to(source)): sha(path) for path in sorted(source.rglob('*')) + if path.is_file() and '__pycache__' not in path.parts + and 'target' not in path.relative_to(source).parts and 'libwebauthn' not in path.relative_to(source).parts} + return hashlib.sha256(json.dumps(files, sort_keys=True, separators=(',', ':')).encode()).hexdigest() + + +def validate_inputs(engine, helper): + e = read_manifest(engine / 'manifest.json') + if (e.get('schemaVersion'), e.get('purpose'), e.get('firmware')) != (1, 'auth-engine', '3.28.0.172'): + raise ValueError('unsupported isolated engine profile') + if (e.get('sdk', {}).get('sha256'), e.get('sdk', {}).get('qtVersion')) != (SDK_SHA, '6.10.3'): + raise ValueError('engine SDK mismatch') + if (e.get('upstream', {}).get('version'), e.get('upstream', {}).get('sourceSha256'), e.get('upstream', {}).get('runtimeSha256')) != ('2.48.5', WPE_SHA, RUNTIME_SHA): + raise ValueError('engine upstream provenance mismatch') + runtime = verify_files(engine / 'runtime', e.get('runtimeFiles')) + devel = verify_files(engine / 'devel', e.get('develFiles')) + if not REQUIRED_RUNTIME.issubset(runtime): + raise ValueError('engine runtime incomplete') + if (engine / 'runtime/rmweb-env.sh').read_text() != RUNTIME_ENVIRONMENT: + raise ValueError('engine runtime launch contract differs from current checkout; rebuild engine artifacts') + for name in runtime: + if name not in ('rmweb-env.sh', 'VERSION') and name.split('/')[0] not in ('lib', 'libexec', 'share', 'licenses'): + raise ValueError('unexpected runtime payload') + if name.startswith('libexec/') and runtime[name]['mode'] != 0o755: + raise ValueError('runtime process is not executable') + if any(not (name.startswith('include/') or name.startswith('lib/pkgconfig/')) for name in devel): + raise ValueError('unexpected development payload') + patch = e.get('patch', {}) + if not relative(patch.get('path')) or not digest(patch.get('sha256')) or sha(engine / patch['path']) != patch['sha256']: + raise ValueError('engine provider patch mismatch') + if patch['sha256'] != sha(REPOSITORY / 'patches/wpe-2.48.5-native-assertion-provider.patch'): + raise ValueError('engine provider patch differs from current checkout') + if patch['path'] not in {'runtime/' + name for name in runtime}: + raise ValueError('engine provider patch must ship in the runtime inventory') + h = read_manifest(helper / 'manifest.json') + if h.get('schemaVersion') != 1 or h.get('SDKsha256') != SDK_SHA or h.get('sourceRevision', {}).get('libwebauthn') != HELPER_REVISION: + raise ValueError('phone helper provenance mismatch') + if h.get('sourceRevision', {}).get('helperSourceSha256') != helper_source_hash(): + raise ValueError('phone helper source differs from current checkout') + if h.get('patchSHA256') != sha(REPOSITORY / 'engine/auth-passkey-helper/patches/libwebauthn-buffered-response.patch'): + raise ValueError('phone helper patch mismatch') + binary = h.get('binary', {}) + if binary.get('path') != 'rmweb-auth-passkey' or executable(helper / 'rmweb-auth-passkey') != {key: binary.get(key) for key in ('sha256', 'bytes')}: + raise ValueError('phone helper executable mismatch') + records = h.get('licenses') + if not isinstance(records, dict) or any(not name.startswith('licenses/') for name in records): + raise ValueError('phone helper license inventory mismatch') + licenses = verify_files(helper / 'licenses', {name.removeprefix('licenses/'): value for name, value in records.items()}) + if regular_files(engine) != {'manifest.json'} | {'runtime/' + n for n in runtime} | {'devel/' + n for n in devel}: + raise ValueError('unexpected engine artifact files') + if regular_files(helper) != {'manifest.json', 'rmweb-auth-passkey'} | {'licenses/' + n for n in licenses}: + raise ValueError('unexpected helper artifact files') + return e, h, runtime, devel, licenses + + + +def validate_published(directory): + manifest = read_manifest(directory / 'manifest.json') + if manifest.get('schemaVersion') != 2 or manifest.get('purpose') != 'auth-browser': + raise ValueError('refusing an unrecognized existing artifact tree') + artifacts = manifest.get('artifacts', {}) + if set(artifacts) != {*TARGETS, 'rmweb-auth-passkey'}: + raise ValueError('existing executable inventory mismatch') + runtime = verify_files(directory / 'runtime', manifest.get('runtime', {}).get('files')) + licenses = verify_files(directory / 'licenses', manifest.get('helper', {}).get('files')) + for name, expected in artifacts.items(): + if executable(directory / name) != expected: + raise ValueError('existing executable changed') + if regular_files(directory) != {'manifest.json', *artifacts} | {'runtime/' + n for n in runtime} | {'licenses/' + n for n in licenses}: + raise ValueError('existing artifacts contain unowned files') + + +def claim_cache(cache): + cache.mkdir(mode=0o700, parents=True, exist_ok=True) + if cache.stat().st_uid != os.getuid() or stat.S_IMODE(cache.stat().st_mode) & 0o077: + raise ValueError('build cache must be owned and private (0700)') + if any(path.is_symlink() or not (path.is_file() or path.is_dir()) for path in cache.rglob('*')): + raise ValueError('build cache contains a redirected or special file') + marker = cache / MARKER + if not marker.exists(): + if any(cache.iterdir()): + raise ValueError('refusing a nonempty unmarked build cache') + with marker.open('xb') as stream:stream.write(MARKER_BYTES) + elif not marker.is_file() or marker.read_bytes() != MARKER_BYTES: + raise ValueError('invalid build-cache ownership marker') + # The supported smoke runner reuses this cache without changing artifacts. + allowed = {MARKER, '.build.lock', 'stage', 'build-auth', 'artifacts', '.artifacts.previous', + 'build-wpe-smoke', 'build-wpe-provider', 'build-passkey-browser'} + if any(path.name not in allowed and not path.name.startswith('.artifacts-stage-') for path in cache.iterdir()): + raise ValueError('unexpected file in authentication cache') + + +def source_manifest(): + directory = REPOSITORY / 'engine/wpeqt' + names = {'CMakeLists.txt', 'auth-entry.cpp', 'qtfbclient.cpp', 'qtfbclient.h'} + names |= {path.name for path in directory.glob('auth-*') if path.suffix in ('.h', '.cpp')} + result = {f'engine/wpeqt/{name}': sha(directory / name) for name in sorted(names)} + for name in ('scripts/build-auth-browser.py', 'scripts/build-auth-engine.py', 'device/auth/entry'): + result[name] = sha(REPOSITORY / name) + return result + + +def build_cpp(cache, engine, image): + command = r'''set -euo pipefail +source /opt/remarkable-sdk/environment-setup-cortexa53-crypto-remarkable-linux +test "$(qmake -query QT_VERSION)" = 6.10.3 +cp -a /work/stage/usr/. "$SDKTARGETSYSROOT/usr/" +cp -a /engine/runtime/lib/. "$SDKTARGETSYSROOT/usr/lib/" +export PKG_CONFIG_SYSROOT_DIR="$SDKTARGETSYSROOT" +export PKG_CONFIG_PATH="$SDKTARGETSYSROOT/usr/lib/pkgconfig:$SDKTARGETSYSROOT/usr/share/pkgconfig" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_PATH" +cmake -S /src/engine/wpeqt -B /work/build-auth -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS -Wl,-rpath-link,$SDKTARGETSYSROOT/usr/lib" +cmake --build /work/build-auth --target rmweb-auth-browser rmweb-auth-entry -j4 +for artifact in rmweb-auth-browser rmweb-auth-entry; do + aarch64-remarkable-linux-readelf -h "/work/build-auth/$artifact" + aarch64-remarkable-linux-readelf -d "/work/build-auth/$artifact" +done +''' + subprocess.run(['docker', 'run', '--rm', '--network', 'none', '--platform', 'linux/arm64', + '--volume', f'{REPOSITORY}:/src:ro', '--volume', f'{cache}:/work', + '--volume', f'{engine}:/engine:ro', image['Id'], 'bash', '-c', command], check=True) + + +def build(cache, engine, helper, sdk_image): + e, h, runtime, devel, licenses = validate_inputs(engine, helper) + claim_cache(cache) + descriptor = os.open(cache / '.build.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(descriptor, 'a') as lock: + try:fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError:raise ValueError('another authentication build owns this cache') + for previous_name in ('artifacts', '.artifacts.previous'): + if (cache / previous_name).exists():validate_published(cache / previous_name) + image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', sdk_image]))[0] + if image.get('Architecture') != 'arm64' or image.get('Os') != 'linux' or not re.fullmatch(r'sha256:[0-9a-f]{64}', image.get('Id', '')): + raise ValueError('SDK image must target Linux ARM64') + before = source_manifest() + inputs_before = (sha(engine / 'manifest.json'), sha(helper / 'manifest.json')) + for name in ('stage', 'build-auth'): + if (cache / name).exists():shutil.rmtree(cache / name) + (cache / 'stage').mkdir() + shutil.copytree(engine / 'devel', cache / 'stage/usr') + build_cpp(cache, engine, image) + if before != source_manifest(): + raise ValueError('authentication sources changed during the build') + validate_inputs(engine, helper) + if inputs_before != (sha(engine / 'manifest.json'), sha(helper / 'manifest.json')): + raise ValueError('authentication inputs changed during the build') + pending = Path(tempfile.mkdtemp(prefix='.artifacts-stage-', dir=cache)) + try: + artifacts = {} + for name in TARGETS: + artifacts[name] = executable(cache / 'build-auth' / name) + shutil.copy2(cache / 'build-auth' / name, pending / name) + shutil.copy2(helper / 'rmweb-auth-passkey', pending / 'rmweb-auth-passkey') + artifacts['rmweb-auth-passkey'] = executable(pending / 'rmweb-auth-passkey') + shutil.copytree(engine / 'runtime', pending / 'runtime') + shutil.copytree(helper / 'licenses', pending / 'licenses') + verify_files(pending / 'runtime', runtime) + verify_files(pending / 'licenses', licenses) + manifest = {'schemaVersion':2, 'purpose':'auth-browser', + 'sdk':{'imageId':image['Id'], 'firmware':'3.28.0.172', 'qtVersion':'6.10.3', 'requiredInstallerSHA256':SDK_SHA}, + 'runtime':{'baseArchiveSHA256':RUNTIME_SHA, 'wpeVersion':'2.48.5', 'wpeSourceSHA256':WPE_SHA, + 'providerPatchSHA256':e['patch']['sha256'], 'files':runtime}, + 'helper':{'sourceRevision':HELPER_REVISION, 'patchSHA256':h['patchSHA256'], 'files':licenses}, + 'sources':before, 'engineManifestSHA256':inputs_before[0], 'helperManifestSHA256':inputs_before[1], + 'develFiles':devel, 'artifacts':artifacts} + (pending / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') + previous = cache / '.artifacts.previous' + destination = cache / 'artifacts' + if destination.exists(): + if previous.exists():shutil.rmtree(previous) + destination.rename(previous) + try:pending.rename(destination) + except Exception: + if previous.exists() and not destination.exists():previous.rename(destination) + raise + finally: + if pending.exists():shutil.rmtree(pending) + return cache / 'artifacts' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cache', type=Path, required=True) + parser.add_argument('--engine-artifacts', type=Path, required=True) + parser.add_argument('--helper-artifacts', type=Path, required=True) + parser.add_argument('--sdk-image', default='rmweb-app-sdk:3.28.0.172') + args = parser.parse_args() + cache, engine, helper = map(external, (args.cache, args.engine_artifacts, args.helper_artifacts)) + if any(a == b or a in b.parents or b in a.parents for a, b in [(cache,engine),(cache,helper),(engine,helper)]): + raise ValueError('cache and artifact inputs must be separate trees') + print(build(cache, engine, helper, args.sdk_image)) + + +if __name__ == '__main__': + try:main() + except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error: + raise SystemExit(f'Authentication build failed: {error}') diff --git a/scripts/build-auth-engine-container.sh b/scripts/build-auth-engine-container.sh new file mode 100644 index 0000000..c021889 --- /dev/null +++ b/scripts/build-auth-engine-container.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Runs only in the dedicated offline SDK container. +set -euo pipefail +export LC_ALL=C.UTF-8 +jobs=${1:?worker count required} +mkdir -p /work/logs +# Earlier downstream markers require a fresh volume; never relabel cached objects. +if [[ -f /build/.purpose ]]; then + [[ $(cat /build/.purpose) == rmweb-auth-webauthn-engine-v1 ]] || { + echo 'Unrecognized build volume; use a fresh dedicated rmweb build volume' >&2 + exit 1 + } +else + [[ -z $(ls -A /build) ]] + printf '%s\n' rmweb-auth-webauthn-engine-v1 >/build/.purpose +fi +[[ ! -L /build/.engine-build.lock ]] +[[ ! -e /build/.engine-build.lock || -f /build/.engine-build.lock ]] +exec 9>>/build/.engine-build.lock +flock -n 9 +for directory in dev configure-webauthn-on; do + [[ ! -L /build/$directory ]] + [[ ! -e /build/$directory || -d /build/$directory ]] +done +python3 /src/scripts/build-auth-engine.py --container-prepare +dpkg -i /work/host-packages/*.deb >/work/logs/host-install.log 2>&1 +# shellcheck source=/dev/null +source /opt/remarkable-sdk/environment-setup-cortexa53-crypto-remarkable-linux +export LC_ALL=C.UTF-8 +test "$(qmake -query QT_VERSION)" = 6.10.3 +SR=$SDKTARGETSYSROOT + +# Only development declarations are taken from these byte-pinned Ubuntu24 +# packages. No Ubuntu target libraries or executables enter the SDK sysroot. +for package in /work/packages/*.deb; do + dpkg-deb -x "$package" /build/dev +done +cp -a /work/headers/stage/usr/. "$SR/usr/" +cp -a /build/dev/usr/include/. "$SR/usr/include/" +# Debian keeps this generated ARM64 header in its multiarch include directory; +# the SDK uses the ordinary /usr/include/libxslt location. +cp -p /build/dev/usr/include/aarch64-linux-gnu/libxslt/xsltconfig.h "$SR/usr/include/libxslt/" +cp -a /build/runtime/lib/. "$SR/usr/lib/" +cp -p /work/inputs/gbm-24.0.9.h "$SR/usr/include/gbm.h" +# Keep target pkg-config paths under /usr/lib, not Debian multiarch paths. +for name in epoxy harfbuzz-icu libtasn1 libxslt libexslt egl glesv2; do + file="/build/dev/usr/lib/aarch64-linux-gnu/pkgconfig/$name.pc" + test -f "$file" + sed 's@/lib/aarch64-linux-gnu@/lib@g' "$file" >"$SR/usr/lib/pkgconfig/$name.pc" +done +cat >"$SR/usr/lib/pkgconfig/xkbcommon.pc" <<'PC' +prefix=/usr +libdir=${prefix}/lib +includedir=${prefix}/include +Name: xkbcommon +Description: Pinned rmweb1.7.0 runtime development overlay +Version: 1.7.0 +Libs: -L${libdir} -lxkbcommon +Cflags: -I${includedir} +PC + +export PKG_CONFIG_PATH="$SR/usr/lib/pkgconfig:$SR/usr/share/pkgconfig" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_PATH" +export PKG_CONFIG_SYSROOT_DIR="$SR" +for package in glib-2.0 gio-2.0 harfbuzz-icu epoxy libtasn1 xkbcommon wpe-1.0 libsoup-3.0 libxslt egl glesv2; do + printf '%s %s\n' "$package" "$(pkg-config --modversion "$package")" +done | tee /work/logs/dependency-versions.txt + +unset CMAKE_TOOLCHAIN_FILE CC CXX CPP LD AR NM STRIP RANLIB OBJCOPY OBJDUMP READELF +unset CFLAGS CXXFLAGS CPPFLAGS LDFLAGS ASFLAGS OECORE_TUNE_CCARGS CONFIG_SITE +CPUFLAGS="-mcpu=cortex-a53+crc+crypto -mbranch-protection=standard" +cmake -S /build/wpewebkit-2.48.5 -B /build/configure-webauthn-on -G Ninja \ + -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCMAKE_C_COMPILER=aarch64-remarkable-linux-gcc \ + -DCMAKE_CXX_COMPILER=aarch64-remarkable-linux-g++ \ + -DCMAKE_C_FLAGS="$CPUFLAGS --sysroot=$SR" \ + -DCMAKE_CXX_FLAGS="$CPUFLAGS --sysroot=$SR" \ + -DCMAKE_EXE_LINKER_FLAGS="-latomic -Wl,-rpath-link,$SR/usr/lib" \ + -DCMAKE_SHARED_LINKER_FLAGS="-latomic -Wl,-rpath-link,$SR/usr/lib" \ + -DCMAKE_MODULE_LINKER_FLAGS="-latomic -Wl,-rpath-link,$SR/usr/lib" \ + -DCMAKE_SYSROOT="$SR" -DCMAKE_FIND_ROOT_PATH="$SR" \ + -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_JOB_POOLS=link_pool=1 -DCMAKE_JOB_POOL_LINK=link_pool \ + -DPORT=WPE -DUSE_SKIA=ON -DENABLE_WEB_AUTHN=ON \ + -DUSE_GSTREAMER=OFF -DENABLE_VIDEO=OFF -DENABLE_WEB_AUDIO=OFF -DENABLE_MEDIA_SOURCE=OFF \ + -DENABLE_MEDIA_STREAM=OFF -DENABLE_WEB_CODECS=OFF -DENABLE_SPEECH_SYNTHESIS=OFF \ + -DENABLE_WEBGL=OFF -DENABLE_WEBXR=OFF -DENABLE_WEB_RTC=OFF \ + -DENABLE_SPELLCHECK=OFF -DENABLE_GAMEPAD=OFF -DENABLE_BUBBLEWRAP_SANDBOX=OFF \ + -DENABLE_INTROSPECTION=OFF -DENABLE_DOCUMENTATION=OFF \ + -DUSE_AVIF=OFF -DUSE_JPEGXL=OFF -DUSE_LIBHYPHEN=OFF -DENABLE_JOURNALD_LOG=OFF \ + -DENABLE_WPE_PLATFORM=ON -DENABLE_WPE_PLATFORM_HEADLESS=ON \ + -DENABLE_WPE_PLATFORM_DRM=OFF -DENABLE_WPE_PLATFORM_WAYLAND=OFF \ + -DENABLE_WPE_QT_API=OFF -DUSE_ATK=OFF -DUSE_WOFF2=OFF \ + -DUSE_LIBBACKTRACE=OFF -DUSE_SYSPROF_CAPTURE=OFF \ + -DENABLE_MINIBROWSER=OFF -DENABLE_COG=OFF -DENABLE_API_TESTS=OFF -DENABLE_WEBDRIVER=OFF + +# Require the real provider/API objects first, including their generated support. +mapfile -t focused < <(python3 - <<'PYFOCUS' +from pathlib import Path +base = Path('/build/configure-webauthn-on') +for name in ('UIProcess/WebAuthentication/wpe/WebAuthenticatorCoordinatorProxyWPE.cpp', + 'UIProcess/API/glib/WebKitWebAuthenticationRequest.cpp', + 'UIProcess/API/glib/WebKitUIClient.cpp', 'UIProcess/API/glib/WebKitWebView.cpp'): + print('Source/WebKit/CMakeFiles/WebKit.dir/' + name + '.o') +for source in sorted((base / 'DerivedSources/WebKit/unified-sources').glob('*.cpp')): + if any(name in source.read_text() for name in ('UIProcess/WebPageProxy.cpp', + 'UIProcess/WebsiteData/WebsiteDataStore.cpp', 'UIProcess/Automation/WebAutomationSession.cpp')): + print('Source/WebKit/CMakeFiles/WebKit.dir/__/__/DerivedSources/WebKit/unified-sources/' + source.name + '.o') +PYFOCUS +) +ninja -C /build/configure-webauthn-on -j "$jobs" "${focused[@]}" +ninja -C /build/configure-webauthn-on -j "$jobs" WebKit WPEWebProcess WPENetworkProcess WPEGPUProcess WPEInjectedBundle InspectorResources +# This path is owned by the checked dedicated build volume, never a live runtime. +python3 - <<'PYCLEAN' +from pathlib import Path +import shutil +path = Path('/build/auth-runtime-stage') +if path.is_symlink(): + raise SystemExit('Refusing redirected install stage') +if path.exists(): + shutil.rmtree(path) +PYCLEAN +DESTDIR=/build/auth-runtime-stage cmake --install /build/configure-webauthn-on +python3 /src/scripts/build-auth-engine.py --container-stage diff --git a/scripts/build-auth-engine.py b/scripts/build-auth-engine.py new file mode 100644 index 0000000..14081c7 --- /dev/null +++ b/scripts/build-auth-engine.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 +"""Build the isolated WPE passkey engine; never modify the shared rmweb runtime.""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import urllib.request + +REPOSITORY = Path(__file__).resolve().parents[1] +LOCK = REPOSITORY / 'toolchain/auth-engine-inputs.json' +PATCH = REPOSITORY / 'patches/wpe-2.48.5-native-assertion-provider.patch' +SOURCE_FILES = REPOSITORY / 'patches/wpe-2.48.5-native-assertion-provider-files.json' +MARKER = b'rmweb-auth-engine-cache-v1\n' +VOLUME_MARKER = 'rmweb-auth-webauthn-engine-v1\n' +ENGINE = 'wpewebkit-2.48.5' +RUNTIME_VERSION = 'rmweb-auth WPEWebKit 2.48.5 native assertion provider\n' +ENVIRONMENT = '''# Isolated rmweb authentication runtime; no profile or diagnostic overrides. +case "${RMWEB_AUTH_RUNTIME:-}" in + /*) ;; + *) echo "RMWEB_AUTH_RUNTIME must name an absolute runtime directory" >&2; return 1 ;; +esac +export LD_LIBRARY_PATH="$RMWEB_AUTH_RUNTIME/lib" +export LIBGL_DRIVERS_PATH="$RMWEB_AUTH_RUNTIME/lib/dri" +export GALLIUM_DRIVER=llvmpipe LIBGL_ALWAYS_SOFTWARE=1 EGL_PLATFORM=surfaceless +export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 +export WEBKIT_INJECTED_BUNDLE_PATH="$RMWEB_AUTH_RUNTIME/lib/wpe-webkit-2.0/injected-bundle" +export WEBKIT_SKIA_ENABLE_CPU_RENDERING=1 WEBKIT_SKIA_CPU_PAINTING_THREADS=0 +export WEBKIT_DISABLE_ASYNC_SCROLLING=1 WEBKIT_FORCE_VBLANK_TIMER=1 +export GIO_EXTRA_MODULES="$RMWEB_AUTH_RUNTIME/lib/gio/modules" +export FONTCONFIG_PATH=/etc/fonts HOME=/home/root +export JSC_useJIT=0 JSC_useBaselineJIT=0 JSC_useDFGJIT=0 JSC_useFTLJIT=0 +''' + + +def sha(path): + with path.open('rb') as source: + return hashlib.file_digest(source, 'sha256').hexdigest() + + +def verify_file(path, entry): + if path.is_symlink() or not path.is_file() or path.stat().st_size != entry['bytes'] or sha(path) != entry['sha256']: + raise ValueError(f'Pinned input mismatch: {path.name}') + + +def external(path): + path = path.expanduser().absolute() + if path.is_symlink(): + raise ValueError('Cache roots must not be symlinks') + path = path.resolve() + if path == REPOSITORY or REPOSITORY in path.parents: + raise ValueError('Engine caches must remain outside the repository') + return path + + +def own_cache(path): + path.mkdir(parents=True, exist_ok=True) + marker = path / '.rmweb-auth-engine-cache' + if not marker.exists(): + if any(path.iterdir()): + raise ValueError('Select an empty dedicated engine cache') + marker.write_bytes(MARKER) + if marker.is_symlink() or not marker.is_file() or marker.read_bytes() != MARKER: + raise ValueError('Invalid engine cache marker') + # Verified archive/source extractions live only inside the dedicated volume. + # Host output trees contain plain files; reject redirected writes throughout. + if any(p.is_symlink() for p in path.rglob('*')): + raise ValueError('Engine cache write paths must not contain symlinks') + + +def download_inputs(cache, input_cache, lock): + for relative, entry in lock['files'].items(): + target = cache / relative + target.parent.mkdir(parents=True, exist_ok=True) + if not target.exists(): + existing = input_cache / relative if input_cache else None + if existing is not None and existing.exists(): + if input_cache not in existing.resolve().parents: + raise ValueError('Input cache file escapes its root') + verify_file(existing, entry) + with target.open('xb') as output, existing.open('rb') as source: + shutil.copyfileobj(source, output) + else: + with tempfile.NamedTemporaryFile(dir=target.parent, delete=False) as output: + temporary = Path(output.name) + try: + with urllib.request.urlopen(entry['url'], timeout=60) as source: + remaining = entry['bytes'] + 1 + while remaining: + block = source.read(min(1024 * 1024, remaining)) + if not block: + break + output.write(block) + remaining -= len(block) + output.flush() + verify_file(temporary, entry) + temporary.replace(target) + finally: + temporary.unlink(missing_ok=True) + verify_file(target, entry) + + +def prepare_headers(cache): + headers = cache / 'headers' + (headers / 'sources').mkdir(parents=True, exist_ok=True) + for name in ('wpewebkit-2.48.5.tar.xz', 'libsoup-3.6.0.tar.xz', 'libwpe-1.16.2.tar.xz', 'libxkbcommon-1.7.0.tar.xz'): + destination = headers / 'sources' / name + if not destination.exists(): + shutil.copyfile(cache / 'inputs' / name, destination) + subprocess.run([sys.executable, str(REPOSITORY / 'scripts/prepare-app-headers.py'), '--cache', str(headers)], check=True) + # The dependency profile below rejects changed SDK/header inputs for a reused + # volume. Stable generated-header times avoid rebuilding correct objects when + # the identical headers are copied into a fresh disposable SDK container. + for path in (headers / 'stage/usr').rglob('*'): + if path.is_file(): + os.utime(path, (0, 0)) + + +def contained_files(root, source_links=False): + """Dereference only regular files whose resolved target stays in this tree.""" + root = root.resolve() + for path in sorted(root.rglob('*')): + resolved = path.resolve() + if root not in resolved.parents: + raise ValueError('Runtime symlink escapes the verified input') + if path.is_symlink() and resolved.is_dir(): + raise ValueError('Runtime directory symlinks are unsupported') + if source_links and path.is_symlink(): + # The pinned source archive contains an unused, dangling Skia Cargo + # link. Preserve contained source links exactly; runtime staging + # never enables this branch and still requires complete plain files. + yield path.relative_to(root), path + continue + if resolved.is_dir(): + continue + if not resolved.is_file() or not stat.S_ISREG(resolved.stat().st_mode): + raise ValueError('Runtime contains a non-regular file') + yield path.relative_to(root), resolved + + +def copy_plain(root, destination, accept=lambda path: True): + for relative, source in contained_files(root): + if not accept(relative): + continue + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + target.chmod(0o755 if source.stat().st_mode & 0o111 else 0o644) + + +def file_map(root): + result = {} + for relative, source in contained_files(root): + path = root / relative + if path.is_symlink(): + raise ValueError('Published artifacts must be plain files') + result[str(relative)] = {'sha256': sha(source), 'bytes': source.stat().st_size, + 'mode': source.stat().st_mode & 0o777} + return result + + +def sync_source(canonical, destination): + if destination.is_symlink() or (destination.exists() and not destination.is_dir()): + raise ValueError('Source destination must be an owned directory') + expected = dict(contained_files(canonical, source_links=True)) + destination.mkdir(exist_ok=True) + for relative, source in list(contained_files(destination, source_links=True)): + if relative in expected: + continue + # Python generators create bytecode next to their sources. Remove only + # an identifiable cache for a canonical .py input before any next build. + python_source = relative.parent.parent / (relative.name.split('.')[0] + '.py') + if relative.parent.name == '__pycache__' and relative.suffix == '.pyc' and python_source in expected: + (destination / relative).unlink() + else: + raise ValueError('Unexpected source file in private build tree: ' + str(relative)) + for relative, source in expected.items(): + target = destination / relative + if source.is_symlink(): + link = os.readlink(source) + if not target.is_symlink() or os.readlink(target) != link: + target.unlink(missing_ok=True) + target.parent.mkdir(parents=True, exist_ok=True) + target.symlink_to(link) + continue + if target.is_symlink(): + target.unlink() + target.parent.mkdir(parents=True, exist_ok=True) + if not target.is_file() or sha(target) != sha(source): + shutil.copy2(source, target) + # A reverted patch can restore old archive timestamps. Mark changed + # content fresh so Ninja cannot retain an object from different code. + os.utime(target, None) + + +def publish_artifacts(output, destination): + previous = destination.with_name('.artifacts.previous') + for path in (destination, previous): + if path.is_symlink() or (path.exists() and any(p.is_symlink() for p in path.rglob('*'))): + raise ValueError('Refusing redirected artifact output') + if not destination.exists() and previous.exists(): + previous.replace(destination) + if previous.exists(): + shutil.rmtree(previous) + if destination.exists(): + destination.replace(previous) + try: + output.replace(destination) + except OSError: + if previous.exists(): + previous.replace(destination) + raise + # Retain the previous complete result until the next successful invocation. + + +def verify_dependency_profile(root, expected): + profile = root / '.dependency-profile.json' + if profile.is_symlink(): + raise ValueError('Refusing redirected dependency profile') + if profile.exists(): + if not profile.is_file() or json.loads(profile.read_text()) != expected: + raise ValueError('Changed SDK/dependency inputs require a fresh build volume') + else: + if (root / 'configure-webauthn-on').exists(): + raise ValueError('Existing build objects have no verified dependency profile') + profile.write_text(json.dumps(expected, sort_keys=True) + '\n') + + +def own_build_volume(root): + marker = root / '.purpose' + if marker.exists(): + if marker.is_symlink() or not marker.is_file() or marker.read_text() != VOLUME_MARKER: + # Earlier markers are not migrated: use a fresh volume so unrelated + # or downstream-owned build objects are never silently adopted. + raise ValueError('Unrecognized build volume; use a fresh dedicated rmweb build volume') + else: + if any(root.iterdir()): + raise ValueError('Build volume must be empty or owned') + marker.write_text(VOLUME_MARKER) + + +def prepare_container(): + root = Path('/build') + own_build_volume(root) + work = Path('/work') + lock = json.loads(LOCK.read_text()) + for relative, entry in lock['files'].items(): + verify_file(work / relative, entry) + plan = json.loads((work / 'build-plan.json').read_text()) + dependency_profile = {'sdk': plan['sdk'], 'inputs': sha(LOCK), + 'headers': sha(REPOSITORY / 'scripts/prepare-app-headers.py')} + verify_dependency_profile(root, dependency_profile) + runtime = root / 'runtime' + with tempfile.TemporaryDirectory(prefix='base-runtime-', dir=root) as temporary: + canonical_runtime = Path(temporary) / 'runtime' + with tarfile.open(work / 'inputs/rmweb-0.9.1.tar.gz') as archive: + archive.extractall(canonical_runtime, filter='data') + if runtime.is_symlink() or (canonical_runtime / 'VERSION').read_text().strip() != '0.9.1': + raise ValueError('Unexpected private base runtime') + if runtime.exists(): + shutil.rmtree(runtime) + canonical_runtime.replace(runtime) + # Extract canonical source each time, apply the reviewed patch, then update + # changed files only. Unchanged object inputs retain their build timestamps. + with tempfile.TemporaryDirectory(prefix='engine-source-', dir=root) as temporary: + with tarfile.open(work / 'inputs/wpewebkit-2.48.5.tar.xz') as archive: + archive.extractall(temporary, filter='data') + canonical = Path(temporary) / ENGINE + subprocess.run(['git', 'apply', '--check', str(PATCH)], cwd=canonical, check=True) + subprocess.run(['git', 'apply', str(PATCH)], cwd=canonical, check=True) + receipt = json.loads(SOURCE_FILES.read_text()) + files = receipt.get('files', receipt) + for relative, entry in files.items(): + expected = entry.get('sha256') if isinstance(entry, dict) else entry + if sha(canonical / relative) != expected: + raise ValueError('Provider source receipt mismatch') + sync_source(canonical, root / ENGINE) + applied = root / 'provider-applied.patch' + if applied.is_symlink(): + raise ValueError('Refusing redirected provider receipt') + shutil.copyfile(PATCH, applied) + + +def stage_container(): + work, build = Path('/work'), Path('/build') + plan = json.loads((work / 'build-plan.json').read_text()) + for name, expected in plan['buildInputs'].items(): + if sha(REPOSITORY / name) != expected: + raise ValueError('Engine recipe or provider changed during the build') + with tempfile.TemporaryDirectory(prefix='artifacts-', dir=work) as temporary: + output = Path(temporary) + runtime, devel = output / 'runtime', output / 'devel' + runtime.mkdir(); devel.mkdir() + allowed = {'lib', 'libexec', 'share', 'licenses', 'VERSION'} + copy_plain(build / 'runtime', runtime, lambda p: p.parts[0] in allowed) + copy_plain(work / 'headers/stage/usr', devel) + installed = build / 'auth-runtime-stage/usr' + # Installation provides all new WPE libraries, injected bundle and helpers. + copy_plain(installed, runtime, lambda p: p.parts[0] in {'lib', 'libexec', 'share'} + and not (p.parts[:2] == ('lib', 'pkgconfig')) and p.suffix != '.a') + copy_plain(installed, devel, lambda p: p.parts[0] == 'include' or p.parts[:2] == ('lib', 'pkgconfig')) + (runtime / 'rmweb-env.sh').write_text(ENVIRONMENT) + (runtime / 'rmweb-env.sh').chmod(0o644) + (runtime / 'VERSION').write_text(RUNTIME_VERSION) + licenses = runtime / 'licenses' + licenses.mkdir(exist_ok=True) + shutil.copyfile(PATCH, licenses / PATCH.name) + shutil.copyfile(SOURCE_FILES, licenses / 'provider-source-files.json') + shutil.copyfile(work / 'inputs/wpewebkit-2.48.5.tar.xz', licenses / 'wpewebkit-2.48.5.tar.xz') + recipe_names = list(plan['buildInputs']) + for name in recipe_names: + target = licenses / 'build-recipe' / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(REPOSITORY / name, target) + for path in (build / ENGINE / 'Source').rglob('*'): + if path.is_file() and path.name.upper().startswith(('LICENSE', 'COPYING')): + relative = path.relative_to(build / ENGINE) + target = licenses / 'wpe-source' / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, target) + provenance = {**plan, 'sourceURL': 'https://wpewebkit.org/releases/wpewebkit-2.48.5.tar.xz', + 'supportRuntimeURL': 'https://github.com/exp78/rmweb/releases/download/v0.9.1/rmweb-0.9.1.tar.gz'} + (licenses / 'engine-provenance.json').write_text(json.dumps(provenance, indent=2) + '\n') + for name in ('lib/libWPEWebKit-2.0.so.1', 'libexec/wpe-webkit-2.0/WPEWebProcess', + 'libexec/wpe-webkit-2.0/WPENetworkProcess', 'libexec/wpe-webkit-2.0/WPEGPUProcess'): + if not (runtime / name).is_file(): + raise ValueError('Missing matching engine component: ' + name) + for root in (runtime, devel): + for path in root.rglob('*'): + if path.is_file(): + path.chmod(0o755 if path.stat().st_mode & 0o111 else 0o644) + manifest = {'schemaVersion': 1, 'purpose': 'auth-engine', 'firmware': '3.28.0.172', + 'sdk': plan['sdk'], 'upstream': plan['upstream'], + 'patch': {'path': 'runtime/licenses/' + PATCH.name, 'sha256': plan['patchSha256']}, + 'runtimeFiles': file_map(runtime), 'develFiles': file_map(devel)} + (output / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') + publish_artifacts(output, work / 'artifacts') + + +def main(): + if sys.argv[1:] == ['--container-prepare']: + prepare_container(); return + if sys.argv[1:] == ['--container-stage']: + stage_container(); return + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cache', type=Path, required=True) + parser.add_argument('--input-cache', type=Path) + parser.add_argument('--sdk-image', default='rmweb-app-sdk:3.28.0.172') + parser.add_argument('--jobs', type=int, default=6) + parser.add_argument('--build-volume', help='Reuse only a volume with the exact owned-purpose marker') + args = parser.parse_args() + if not 1 <= args.jobs <= 12: + parser.error('jobs must be between1 and12') + cache = external(args.cache); own_cache(cache) + with (cache / '.build.lock').open('a') as lease: + fcntl.flock(lease, fcntl.LOCK_EX | fcntl.LOCK_NB) + lock = json.loads(LOCK.read_text()) + download_inputs(cache, external(args.input_cache) if args.input_cache else None, lock) + prepare_headers(cache) + image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', args.sdk_image]))[0] + if image['Os'] != 'linux' or image['Architecture'] != 'arm64': + raise ValueError('Expected an ARM64 Linux SDK image') + plan = {'sdk': {'imageId': image['Id'], 'sha256': lock['sdkSha256'], 'qtVersion': lock['qtVersion']}, + 'upstream': {'version': '2.48.5', 'sourceSha256': lock['files']['inputs/wpewebkit-2.48.5.tar.xz']['sha256'], + 'runtimeSha256': lock['files']['inputs/rmweb-0.9.1.tar.gz']['sha256']}, + 'patchSha256': sha(PATCH), 'recipeSha256': sha(Path(__file__)), 'inputLockSha256': sha(LOCK)} + names = ['scripts/build-auth-engine.py', 'scripts/build-auth-engine-container.sh', + 'scripts/prepare-app-headers.py', 'scripts/app_only_cache.py', + 'toolchain/Dockerfile.app-only-sdk-3.28', + str(LOCK.relative_to(REPOSITORY)), str(PATCH.relative_to(REPOSITORY)), + str(SOURCE_FILES.relative_to(REPOSITORY))] + plan['buildInputs'] = {name: sha(REPOSITORY / name) for name in names} + (cache / 'build-plan.json').write_text(json.dumps(plan, indent=2) + '\n') + volume = args.build_volume or 'rmweb-auth-engine-' + hashlib.sha256(str(cache).encode()).hexdigest()[:16] + if not volume or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-' for c in volume): + raise ValueError('Invalid dedicated Docker volume name') + subprocess.run(['docker', 'run', '--rm', '--network', 'none', '--platform', 'linux/arm64', + '--cpus', str(min(args.jobs + 2, 14)), '--memory', '40g', + '--mount', f'type=bind,src={REPOSITORY},dst=/src,readonly', + '--mount', f'type=bind,src={cache},dst=/work', + '--mount', f'type=volume,src={volume},dst=/build', + image['Id'], 'bash', '/src/scripts/build-auth-engine-container.sh', str(args.jobs)], check=True) + print('Engine artifacts:', cache / 'artifacts') + + +if __name__ == '__main__': + try: + main() + except (ValueError, OSError, subprocess.CalledProcessError) as error: + raise SystemExit(str(error)) diff --git a/scripts/prepare-app-headers.py b/scripts/prepare-app-headers.py new file mode 100644 index 0000000..06f7623 --- /dev/null +++ b/scripts/prepare-app-headers.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Prepare development headers only; never build/replace the release runtime.""" +from pathlib import Path +import argparse +import hashlib +import json +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +from app_only_cache import require_owned_cache + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--cache', type=Path, required=True, help='Dedicated external app-only cache') +args = parser.parse_args() +BASE = args.cache.expanduser().resolve() +REPOSITORY = Path(__file__).resolve().parents[1] +if BASE == REPOSITORY or REPOSITORY in BASE.parents: + parser.error('cache must remain outside the repository') +require_owned_cache(BASE) +UNIFDEF = shutil.which('unifdef') +MKENUMS = shutil.which('glib-mkenums') +if not UNIFDEF or not MKENUMS: + parser.error('install unifdef and glib-mkenums (macOS: brew install unifdef glib)') +SOURCES = BASE / 'sources' +FLAGS = ['-DWTF_PLATFORM_GTK=0', '-DWTF_PLATFORM_WPE=1', '-DUSE_GTK4=0', + '-DENABLE_2022_GLIB_API=1', '-DENABLE_WPE_PLATFORM=1', '-DUSE_GI_FINISH_FUNC_ANNOTATION=0'] +INPUTS = { + 'wpewebkit-2.48.5.tar.xz': ('https://wpewebkit.org/releases/wpewebkit-2.48.5.tar.xz', + '01f36010705adb14404c56baf033147f7927cc7c6badec81bb141266fcdd8d0b'), + 'libwpe-1.16.2.tar.xz': ('https://wpewebkit.org/releases/libwpe-1.16.2.tar.xz', + '960bdd11c3f2cf5bd91569603ed6d2aa42fd4000ed7cac930a804eac367888d7'), + 'libsoup-3.6.0.tar.xz': ('https://download.gnome.org/sources/libsoup/3.6/libsoup-3.6.0.tar.xz', + '62959f791e8e8442f8c13cedac8c4919d78f9120d5bb5301be67a5e53318b4a3'), + 'libxkbcommon-1.7.0.tar.xz': ('https://xkbcommon.org/download/libxkbcommon-1.7.0.tar.xz', + '65782f0a10a4b455af9c6baab7040e2f537520caa2ec2092805cdfd36863b247'), +} + +def run(*args): + return subprocess.run([str(arg) for arg in args], check=True, capture_output=True).stdout + +def write(path, text): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + +def copy(source, destination): + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, destination) + +def configure_version(source, destination): + text = source.read_text() + for key, value in [('MAJOR', 2), ('MINOR', 48), ('MICRO', 5)]: + text = text.replace(f'@PROJECT_VERSION_{key}@', str(value)) + write(destination, text) + +SOURCES.mkdir(parents=True, exist_ok=True) +for filename, (url, expected) in INPUTS.items(): + archive_path = SOURCES / filename + if not archive_path.exists(): + temporary = archive_path.with_suffix(archive_path.suffix + '.partial') + try: + with urllib.request.urlopen(url, timeout=60) as response, temporary.open('wb') as output: + shutil.copyfileobj(response, output) + if hashlib.sha256(temporary.read_bytes()).hexdigest() != expected: + raise SystemExit(f'Checksum mismatch: {filename}') + temporary.replace(archive_path) + finally: + temporary.unlink(missing_ok=True) + if hashlib.sha256(archive_path.read_bytes()).hexdigest() != expected: + raise SystemExit(f'Checksum mismatch: {filename}') + +# Always regenerate from verified archives; never execute scripts from an old, +# editable source extraction. Keep output separate from downloads and SDK files. +work = tempfile.TemporaryDirectory(prefix='headers-', dir=BASE) +EXTRACTED = Path(work.name) / 'sources' +STAGE = Path(work.name) / 'stage/usr' +for filename in INPUTS: + with tarfile.open(SOURCES / filename) as archive: + selected = [] + for item in archive: + path = item.name + if not item.isfile(): + continue + if filename.startswith('wpewebkit'): + wanted = ('/Source/WebKit/UIProcess/API/' in path + or '/Source/WebKit/WPEPlatform/' in path + or '/Source/JavaScriptCore/API/glib/' in path + or path.endswith('/Source/WebKit/PlatformWPE.cmake') + or path.endswith('/Source/WebKit/Scripts/glib/generate-api-header.py')) + wanted = wanted and (path.endswith(('.h', '.h.in', 'CMakeLists.txt', 'PlatformWPE.cmake', 'generate-api-header.py'))) + elif filename.startswith('libwpe'): + wanted = '/include/wpe/' in path and path.endswith('.h') + elif filename.startswith('libsoup'): + wanted = '/libsoup/' in path + else: + wanted = '/include/xkbcommon/' in path and path.endswith('.h') + if wanted: + selected.append(item) + archive.extractall(EXTRACTED, members=selected, filter='data') +WPE = EXTRACTED / 'wpewebkit-2.48.5' +WEBKIT = WPE / 'Source/WebKit' +JSC = WPE / 'Source/JavaScriptCore/API/glib' +PUBLIC = STAGE / 'include/wpe-webkit-2.0' +GENERATOR = WEBKIT / 'Scripts/glib/generate-api-header.py' + +# Main API templates are selected from the exact upstream install manifest. +platform = (WEBKIT / 'PlatformWPE.cmake').read_text() +block = re.search(r'set\(WPE_API_HEADER_TEMPLATES\s+(.*?)\n\)', platform, re.S).group(1) +templates = re.findall(r'\$\{WEBKIT_DIR\}/([^\s]+)', block) +templates.append('UIProcess/API/glib/WebKitNetworkSession.h.in') +for relative in templates: + source = WEBKIT / relative + destination = PUBLIC / 'wpe' / source.name.removesuffix('.in') + destination.parent.mkdir(parents=True, exist_ok=True) + run(sys.executable, GENERATOR, 'WPE', source, destination, UNIFDEF, *FLAGS) +for name in ['WebKitColor.h', 'WebKitRectangle.h', 'WebKitWebViewBackend.h']: + copy(WEBKIT / 'UIProcess/API/wpe' / name, PUBLIC / 'wpe' / name) +configure_version(WEBKIT / 'UIProcess/API/wpe/WebKitVersion.h.in', PUBLIC / 'wpe/WebKitVersion.h') +headers = sorted((PUBLIC / 'wpe').glob('*.h')) +headers = [path for path in headers if path.name != 'WebKitEnumTypes.h'] +enum = run(MKENUMS, '--template', WEBKIT / 'UIProcess/API/wpe/WebKitEnumTypes.h.in', *headers).decode() +write(PUBLIC / 'wpe/WebKitEnumTypes.h', enum.replace('web_kit', 'webkit').replace('WEBKIT_TYPE_KIT', 'WEBKIT_TYPE')) + +# JSC GLib declarations are exported by libWPEWebKit in this release. +for source in JSC.glob('*.h.in'): + destination = PUBLIC / 'jsc' / source.name.removesuffix('.in') + destination.parent.mkdir(parents=True, exist_ok=True) + if source.name == 'JSCVersion.h.in': + configure_version(source, destination) + else: + run(sys.executable, GENERATOR, 'WPE', source, destination, UNIFDEF, *FLAGS) +for name in ['JSCOptions.h', 'JSCAutocleanups.h']: + copy(JSC / name, PUBLIC / 'jsc' / name) + +# WPEPlatform uses opaque GL handles, so no Mesa implementation build is needed. +wpe_platform = WEBKIT / 'WPEPlatform' +platform_cmake = (wpe_platform / 'CMakeLists.txt').read_text() +block = re.search(r'set\(WPEPlatform_INSTALLED_HEADERS\s+(.*?)\n\)', platform_cmake, re.S).group(1) +relative_headers = re.findall(r'\$\{WEBKIT_DIR\}/WPEPlatform/(\S+\.h)', block) +platform_dest = PUBLIC / 'wpe-platform' +for relative in relative_headers: + copy(wpe_platform / relative, platform_dest / relative) +for source in (wpe_platform / 'wpe/headless').glob('*.h'): + copy(source, platform_dest / 'wpe/headless' / source.name) +configure_version(wpe_platform / 'wpe/WPEVersion.h.in', platform_dest / 'wpe/WPEVersion.h') +config = (wpe_platform / 'wpe/WPEConfig.h.in').read_text() +config = config.replace('#cmakedefine WPE_PLATFORM_HEADLESS', '#define WPE_PLATFORM_HEADLESS') +config = re.sub(r'#cmakedefine (\S+)', r'/* #undef \1 */', config) +write(platform_dest / 'wpe/WPEConfig.h', config) +headers = [path for path in sorted((platform_dest / 'wpe').glob('*.h')) if path.name != 'WPEEnumTypes.h'] +enum = run(MKENUMS, '--template', wpe_platform / 'wpe/WPEEnumTypes.h.in', *headers).decode() +for before, after in [('w_pe','wpe'), ('WPE_TYPE_PE','WPE_TYPE'), ('WPE_TYPEEGL','WPE_TYPE_EGL'), ('wpeegl','wpe_egl')]: + enum = enum.replace(before, after) +write(platform_dest / 'wpe/WPEEnumTypes.h', enum) + +for source in (EXTRACTED / 'libwpe-1.16.2/include/wpe').glob('*.h'): + copy(source, STAGE / 'include/wpe-1.0/wpe' / source.name) + +soup = EXTRACTED / 'libsoup-3.6.0/libsoup' +block = re.search(r'soup_introspection_headers = \[(.*?)\n\]', (soup/'meson.build').read_text(), re.S).group(1) +soup_headers = re.findall(r"'([^']+\.h)'", block) +soup_dest = STAGE / 'include/libsoup-3.0/libsoup' +for relative in soup_headers: + copy(soup / relative, soup_dest / Path(relative).name) +copy(soup / 'include/soup-installed.h', soup_dest / 'soup.h') +run(sys.executable, soup / 'generate-version-header.py', soup / 'soup-version.h.in', soup_dest / 'soup-version.h', '3.6.0') +enum = run(MKENUMS, '--template', soup/'soup-enum-types.h.template', *[soup / item for item in soup_headers]).decode() +write(soup_dest / 'soup-enum-types.h', enum) + +for source in (EXTRACTED / 'libxkbcommon-1.7.0/include/xkbcommon').glob('*.h'): + copy(source, STAGE / 'include/xkbcommon' / source.name) + +def pc(name, version, requires, library, include): + text = f'''prefix=/usr +exec_prefix=${{prefix}} +libdir=${{exec_prefix}}/lib +includedir=${{prefix}}/include +Name: {name} +Description: App-only development overlay for the pinned rmweb runtime +Version: {version} +Requires: {requires} +Libs: -L${{libdir}} -l{library} +Cflags: -I${{includedir}}/{include} +''' + write(STAGE / 'lib/pkgconfig' / f'{name}.pc', text) + +pc('wpe-webkit-2.0', '2.48.5', 'glib-2.0 libsoup-3.0 wpe-1.0 wpe-platform-2.0', 'WPEWebKit-2.0', 'wpe-webkit-2.0') +pc('wpe-platform-2.0', '2.48.5', 'glib-2.0 gobject-2.0 gio-2.0', 'WPEPlatform-2.0', 'wpe-webkit-2.0/wpe-platform') +pc('wpe-1.0', '1.16.2', '', 'wpe-1.0', 'wpe-1.0') +pc('libsoup-3.0', '3.6.0', 'glib-2.0 gobject-2.0 gio-2.0', 'soup-3.0', 'libsoup-3.0') +manifest = {'schemaVersion': 1, 'inputs': {name: {'url': url, 'sha256': sha, 'bytes': (SOURCES/name).stat().st_size} + for name, (url, sha) in INPUTS.items()}, 'generatedHeaders': {str(path.relative_to(STAGE)): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(STAGE.rglob('*')) if path.is_file()}} +manifest['generatorTools'] = {'python': sys.version.split()[0], 'glibMkenums': run(MKENUMS, '--version').decode().strip(), 'unifdef': UNIFDEF} +# The stage directory is owned by this build lane. Replace it only after all +# pinned header generators succeed. +if (BASE / 'stage').exists(): + shutil.rmtree(BASE / 'stage') +(STAGE.parent).replace(BASE / 'stage') +write(BASE / 'header-manifest.json', json.dumps(manifest, indent=2) + '\n') +work.cleanup() +staged = BASE / 'stage/usr' +print(f'Prepared {len(manifest["generatedHeaders"])} development files under {staged}') diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 9bd1b2d..0021df8 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -1,10 +1,13 @@ #!/usr/bin/env bash set -euo pipefail -# Build + run ALL pure-logic host unit tests (no device, no SDK). clang++ C++17. +# Build + run host tests (no device or SDK): clang++ C++17, Qt 6, Python and Node 22+. +# First install the locked demo dependencies: npm ci --prefix tools/passkey-acceptance cd "$(dirname "$0")/.." mkdir -p build fail=0 for t in tests/*_test.cpp; do + # Qt suites have their own CMake targets below. + case "$t" in tests/auth_*_test.cpp|tests/qtfb_client_test.cpp) continue;; esac name="$(basename "$t" .cpp)" if clang++ -std=c++17 -Wall -Wextra -o "build/$name" "$t"; then "./build/$name" || { echo "FAIL (runtime): $name"; fail=1; } @@ -12,6 +15,37 @@ for t in tests/*_test.cpp; do echo "FAIL (compile): $name"; fail=1 fi done +for suite in auth-policy auth-surface auth-passkey; do + if cmake -S "tests/$suite" -B "build/$suite" -G Ninja \ + && cmake --build "build/$suite" \ + && ctest --test-dir "build/$suite" --output-on-failure; then + : + else + echo "FAIL: $suite tests"; fail=1 + fi +done +# AppLoad's local socket and shared-memory transport uses the Linux ABI. +if [ "$(uname -s)" = Linux ]; then + if cmake -S tests/qtfb -B build/qtfb -G Ninja \ + && cmake --build build/qtfb \ + && ctest --test-dir build/qtfb --output-on-failure; then + : + else + echo "FAIL: AppLoad QTFB tests"; fail=1 + fi +fi +for t in tests/test_auth_engine_build.py tests/auth_build_recipe_test.py tests/auth_launcher_test.py \ + engine/auth-passkey-helper/tests/package_test.py \ + tests/auth-webauthn-provider/https_fixture_test.py \ + tools/passkey-acceptance/native/test-preparation.py; do + if python3 "$t"; then :; else echo "FAIL (Python): $t"; fail=1; fi +done +if npm --prefix tools/passkey-acceptance run check \ + && npm --prefix tools/passkey-acceptance test; then + : +else + echo "FAIL: passkey acceptance tests (run npm ci --prefix tools/passkey-acceptance first)"; fail=1 +fi # Shell unit tests (launcher / installer no-brick logic) — pure bash + stubbed systemctl/mount. for t in tests/*_test.sh; do [ -e "$t" ] || continue @@ -20,7 +54,8 @@ for t in tests/*_test.sh; do done # Optional lint of the shipped shell (skip cleanly if shellcheck isn't installed). if command -v shellcheck >/dev/null 2>&1; then - for f in device/rmweb device/rmweb-env.sh device/install.sh; do + for f in device/rmweb device/rmweb-env.sh device/install.sh device/auth/entry \ + tools/passkey-acceptance/native/launch; do [ -e "$f" ] && { shellcheck -s sh "$f" || { echo "FAIL (shellcheck): $f"; fail=1; }; } done else diff --git a/tests/auth-passkey-browser/CMakeLists.txt b/tests/auth-passkey-browser/CMakeLists.txt new file mode 100644 index 0000000..4ea1f26 --- /dev/null +++ b/tests/auth-passkey-browser/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) +project(auth_passkey_browser_smoke LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Gui) +find_package(PkgConfig REQUIRED) +pkg_check_modules(WPE REQUIRED IMPORTED_TARGET wpe-webkit-2.0 wpe-platform-2.0 glib-2.0 gobject-2.0) +add_executable(auth-passkey-browser-smoke browser-test.cpp + ../../engine/wpeqt/auth-policy.cpp ../../engine/wpeqt/auth-surface.cpp + ../../engine/wpeqt/auth-passkey.cpp ../../engine/wpeqt/qtfbclient.cpp) +target_link_libraries(auth-passkey-browser-smoke PRIVATE Qt6::Core Qt6::Gui PkgConfig::WPE) +target_compile_definitions(auth-passkey-browser-smoke PRIVATE QT_NO_KEYWORDS) +target_compile_options(auth-passkey-browser-smoke PRIVATE -Wall -Wextra -Werror) diff --git a/tests/auth-passkey-browser/README.md b/tests/auth-passkey-browser/README.md new file mode 100644 index 0000000..4933d90 --- /dev/null +++ b/tests/auth-passkey-browser/README.md @@ -0,0 +1,43 @@ +# Browser–helper integration fixture + +This standalone SDK/WPE executable includes the production `AuthEngine`, links +the real Qt `AuthPasskey`, and invokes the real native WebKit provider from an +offline synthetic HTTPS document. Its child fixture is selected only by this +test executable's `--synthetic-helper` argument; the production helper path and +browser launch protocol are unchanged. + +The child checks version, origin, RP, challenge, verification requirement, +allowlist, timeout and canonical 32-byte browser hash. It returns fixed synthetic +credential/authenticator bytes and echoes that hash as a synthetic signature. +The DOM independently hashes the returned original `clientDataJSON` and compares +all returned bytes. This tests the cross-thread and process glue, not signature +verification or phone authentication. + +Abort and navigation cases wait until the helper's QR message, cancel the native +request, and require a normal child exit after actual SIGTERM. The child writes a +deliberately late result before exiting; it must be discarded. A further stale +native completion must leave the aborted/replacement document unchanged. Prompt +clearing, single completion, and native request release are also checked. +The fixture waits for the initial blank load to finish before inserting its +synthetic page. Navigation checks wait for the replacement's actual finished +load and keep promise outcomes in document-local objects, so a retiring page's +rejection handler cannot overwrite the replacement sentinel through `window`. + +Build this directory with the same verified SDK, staged headers and isolated +runtime as `tests/auth-wpe-smoke`, then run: + +```sh +sh tests/auth-passkey-browser/run-cases.sh /path/to/auth-passkey-browser-smoke +``` + +Run in a disposable ARM64 container with networking disabled, runtime WebKit +helpers available at their compiled path, and the SDK loader used by child +processes. No Bluetooth, device, credentials, user account, or external server is +used. Each scenario has a 15-second deadline; the fixture helper has its own +12-second alarm. WebKit diagnostics are suppressed; output contains fixed case +names and PASS/FAIL only. + +The official 3.28 SDK build and all three cases passed against the linked +patched WPE runtime. Three additional navigation runs also passed after fixing +the fixture's document-lifetime races. These are synthetic integration results; +they do not establish a real phone assertion or account sign-in. diff --git a/tests/auth-passkey-browser/browser-test.cpp b/tests/auth-passkey-browser/browser-test.cpp new file mode 100644 index 0000000..6f1eb52 --- /dev/null +++ b/tests/auth-passkey-browser/browser-test.cpp @@ -0,0 +1,329 @@ +// The real driver, Qt process session and WebKit provider joined together. +// Only this executable's explicit fixture mode produces synthetic assertions. +#define main unusedAuthBrowserMain +#include "../../engine/wpeqt/auth-main.cpp" +#undef main +#include +#include +#include +#include +#include +#include +#include + +namespace { +std::mutex diagnosticMutex; +QList> diagnostics; +bool diagnosticOverflow = false; +} +// Exercise the production syslog calls without writing fixture activity into +// the host log. This symbol exists only in the regression executable. +extern "C" void syslog(int priority, const char *format, ...) { + char text[512]; + va_list arguments; + va_start(arguments, format); + const int length = vsnprintf(text, sizeof(text), format, arguments); + va_end(arguments); + std::lock_guard guard(diagnosticMutex); + if (length < 0 || size_t(length) >= sizeof(text) || diagnostics.size() >= 128) { + diagnosticOverflow = true; + return; + } + diagnostics.append({priority, QByteArray(text, length)}); +} + +class AuthPasskeyTest { +public: + static std::unique_ptr create(const QString &mode) { + return std::unique_ptr(new rmweb::AuthPasskey( + QCoreApplication::applicationFilePath(), {"--synthetic-helper", mode})); + } + static QProcess &process(rmweb::AuthPasskey &session) { return session.m_process; } +}; + +namespace { +struct AuthEngineTestAccess { + static bool ready(AuthEngine &engine) { return engine.m_ready.load(); } + static bool post(AuthEngine &engine, std::function fn) { return engine.post(std::move(fn)); } + static WebKitWebView *page(AuthEngine &engine) { return engine.m_webView; } + static void httpFailure(AuthEngine &engine, unsigned status) { engine.reportHttpFailure(status); } + static bool requestCleared(AuthEngine &engine) { + return !engine.m_passkeyRequest && !engine.m_activePasskeyId; + } + static void fixturePolicy(AuthEngine &engine, bool enable) { + auto callback = reinterpret_cast(AuthEngine::policy); + if (enable) g_signal_handlers_unblock_matched(engine.m_webView, G_SIGNAL_MATCH_FUNC, + 0, 0, nullptr, callback, nullptr); + else g_signal_handlers_block_matched(engine.m_webView, G_SIGNAL_MATCH_FUNC, + 0, 0, nullptr, callback, nullptr); + } +}; + +QByteArray encoded(const QByteArray &value) { + return value.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); +} +QByteArray authenticatorData() { + auto data = QCryptographicHash::hash("example.com", QCryptographicHash::Sha256); + data.append(char(0x05)); // User presence and verification; no extensions. + data.append(QByteArray(4, '\0')); + return data; +} +volatile sig_atomic_t terminated = 0; +int helper(const QByteArray &mode) { + if (mode != "success" && mode != "late") return 2; + alarm(12); + std::signal(SIGTERM, [](int) { terminated = 1; }); + QFile input; + if (!input.open(stdin, QIODevice::ReadOnly)) return 2; + const auto bytes = input.read(128 * 1024 + 1); + if (bytes.size() > 128 * 1024 || !input.atEnd()) return 2; + QJsonParseError error; + const auto document = QJsonDocument::fromJson(bytes, &error); + const auto request = document.object(); + const auto options = request.value("options").toObject(); + const QJsonArray allowed {QJsonObject {{"type", "public-key"}, {"id", "AQID"}}}; + const auto suppliedHash = request.value("clientDataHash").toString().toLatin1(); + const auto hash = QByteArray::fromBase64(suppliedHash, QByteArray::Base64UrlEncoding); + if (error.error != QJsonParseError::NoError || request.size() != 4 + || request.value("version").toInt() != 1 + || request.value("origin").toString() != "https://login.example.com" + || options.value("rpId").toString() != "example.com" + || options.value("challenge").toString() != "AQIDBA" + || options.value("userVerification").toString() != "required" + || options.value("timeout").toInt() != 10000 + || options.value("allowCredentials").toArray() != allowed + || hash.size() != 32 || encoded(hash) != suppliedHash) return 3; + QFile output; + if (!output.open(stdout, QIODevice::WriteOnly)) return 2; + const auto write = [&output](const QJsonObject &object) { + const auto line = QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n'; + return output.write(line) == line.size() && output.flush(); + }; + if (!write({{"type", "qr"}, {"size", 21}, {"modules", QString(441, '1')}})) return 4; + if (mode == "late") { + // Exit zero only after actual TERM and a successful late result write. + // This distinguishes graceful cancellation from a later SIGKILL. + while (!terminated) usleep(1000); + } + return write({{"type", "result"}, {"credentialId", "AQID"}, + {"authenticatorData", QString::fromLatin1(encoded(authenticatorData()))}, + {"signature", QString::fromLatin1(encoded(hash))}, {"userHandle", "BA"}}) ? 0 : 4; +} + +AuthEngine *engine = nullptr; +rmweb::AuthPasskey *session = nullptr; +const char *scenario = nullptr; +int reportFd = -1; +std::atomic loaded{false}, replacementLoaded{false}, scriptPending{false}, cleared{false}; +std::atomic dom{-1}; // -1 pending, 0 mismatch, 1 success, 2 abort, 3 replacement. +bool sawQr = false, promptActive = false, childClean = false, cancellationSent = false; +bool observedCompletion = false, completionSucceeded = false; +unsigned completions = 0; +quint64 completedId = 0; + +bool safeDiagnostics() { + const QList labels { + "native_request_received", "native_request_cancelled", "helper_start_requested", "helper_started", + "helper_cancelled", "qr_ready", "assertion_received", "helper_exit_success", "helper_exit_failure", + "assertion_submitted" + }; + const QByteArray prefix("rmweb-auth passkey: "); + std::lock_guard guard(diagnosticMutex); + if (diagnosticOverflow) return false; + QList messages; + QList providerMessages; + unsigned libraryWarnings = 0; + for (const auto &event : diagnostics) { + // The pinned engine emits this exact library warning on first use. + // Keep the exception narrow: one message, one facility and severity. + if (event.second == "Libgcrypt warning: missing initialization - please fix the application") { + if (event.first != (LOG_USER | LOG_WARNING) || ++libraryWarnings > 1) return false; + continue; + } + if (event.second == "rmweb-webauthn: assertion-received" + || event.second == "rmweb-webauthn: completed" || event.second == "rmweb-webauthn: cancelled") { + if (event.first != (LOG_AUTHPRIV | LOG_NOTICE)) return false; + providerMessages.append(event.second); + continue; + } + if (event.first != (LOG_AUTHPRIV | LOG_NOTICE) || !event.second.startsWith(prefix) + || !labels.contains(event.second.mid(prefix.size()))) return false; + messages.append(event.second); + } + const QList expectedProvider {"rmweb-webauthn: assertion-received", + std::strcmp(scenario, "success") == 0 ? "rmweb-webauthn: completed" : "rmweb-webauthn: cancelled"}; + return providerMessages == expectedProvider && messages.contains("rmweb-auth passkey: native_request_received") + && messages.contains("rmweb-auth passkey: helper_started") + && messages.contains("rmweb-auth passkey: qr_ready"); +} +[[noreturn]] void finish(bool okay) { + if (session) session->shutdown(); + okay = okay && safeDiagnostics(); + dprintf(reportFd, "%s: %s\n", scenario, okay ? "PASS" : "FAIL"); + // Same process-lifetime boundary as the production detached GLib worker. + std::_Exit(okay ? 0 : 1); +} +bool named(const char *name) { return !std::strcmp(scenario, name); } + +constexpr auto requestScript = + // Capture this document's result object: an old rejection handler must not + // write through the WindowProxy into the replacement document's sentinel. + "window.fixtureState={outcome:'pending'};const state=window.fixtureState;window.abort=new AbortController();" + "navigator.credentials.get({publicKey:{challenge:new Uint8Array([1,2,3,4])," + "rpId:'example.com',timeout:10000,userVerification:'required'," + "allowCredentials:[{type:'public-key',id:new Uint8Array([1,2,3])}]},signal:window.abort.signal})" + ".then(async c=>{let bytes=x=>Array.from(new Uint8Array(x)).join(',');" + "let d=JSON.parse(new TextDecoder().decode(c.response.clientDataJSON));" + "let h=await crypto.subtle.digest('SHA-256',c.response.clientDataJSON);" + "let a=Array.from(new Uint8Array(c.response.authenticatorData));" + "let r=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode('example.com'))));" + "state.outcome=d.type==='webauthn.get'&&d.challenge==='AQIDBA'&&d.origin==='https://login.example.com'" + "&&bytes(c.rawId)==='1,2,3'&&bytes(c.response.userHandle)==='4'" + "&&bytes(h)===bytes(c.response.signature)&&a.length===37&&a.slice(0,32).join(',')===r.join(',')" + "&&a[32]===5&&a.slice(33).every(v=>v===0)?'success':'mismatch';" + "},e=>state.outcome=e.name)"; + +void evaluate(const char *script) { + // Setup expressions may return a Promise, which is not a serializable + // evaluate_javascript result even when the side effect succeeded. + const QByteArray setup = QByteArray(script) + "; undefined"; + webkit_web_view_evaluate_javascript(AuthEngineTestAccess::page(*engine), setup.constData(), -1, + nullptr, nullptr, nullptr, [](GObject *source, GAsyncResult *result, gpointer) { + GError *error = nullptr; + JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error); + if (error || !value) dom = 0; + g_clear_error(&error); + if (value) g_object_unref(value); + }, nullptr); +} +void fixtureLoaded(WebKitWebView *page, WebKitLoadEvent event, gpointer) { + if (event != WEBKIT_LOAD_FINISHED) return; + const auto *uri = webkit_web_view_get_uri(page); + if (named("navigation") && uri && !std::strcmp(uri, "https://login.example.com/replacement")) { + replacementLoaded = true; + return; + } + if (loaded.load()) return; + if (!uri || std::strcmp(uri, "https://login.example.com/fixture")) return; + loaded = true; + AuthEngineTestAccess::fixturePolicy(*engine, true); + evaluate(requestScript); +} +void queryOutcome() { + if (scriptPending.exchange(true)) return; + if (!AuthEngineTestAccess::post(*engine, [] { + webkit_web_view_evaluate_javascript(AuthEngineTestAccess::page(*engine), + "String(window.fixtureState?.outcome || 'pending')", -1, nullptr, nullptr, nullptr, + [](GObject *source, GAsyncResult *result, gpointer) { + GError *error = nullptr; + JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error); + if (error || !value) dom = 0; + else { + char *text = jsc_value_to_string(value); + const QByteArray outcome(text ? text : ""); + g_free(text); + if (outcome == "success") dom = 1; + else if (outcome == "AbortError") dom = 2; + else if (outcome == "replacement") dom = 3; + else if (outcome != "pending") dom = 0; + } + g_clear_error(&error); + if (value) g_object_unref(value); + scriptPending = false; + }, nullptr); + })) finish(false); +} +} + +int main(int argc, char **argv) { + if (argc == 3 && QByteArray(argv[1]) == "--synthetic-helper") return helper(argv[2]); + if (argc != 2) return 2; + scenario = argv[1]; + if (!named("success") && !named("abort") && !named("navigation") && !named("http-diagnostics")) return 2; + reportFd = dup(STDOUT_FILENO); + if (reportFd < 0 || fcntl(reportFd, F_SETFD, FD_CLOEXEC) < 0 || !privateProcess()) return 2; + QCoreApplication app(argc, argv); + auto ownedSession = AuthPasskeyTest::create(named("success") ? "success" : "late"); + session = ownedSession.get(); + // No external request: the worker initially loads a local blank, replaced + // with fixed synthetic HTTPS HTML through the existing friend seam. + rmweb::AuthLaunch launch; + launch.initialUrl = QUrl("about:blank"); + AuthEngine instance(launch, QSize(1620, 2000), session); + engine = &instance; + if (named("http-diagnostics")) { + for (unsigned status : {0U, 200U, 399U, 600U, 0xffffffffU}) + AuthEngineTestAccess::httpFailure(instance, status); + for (unsigned i = 0; i < 30; ++i) AuthEngineTestAccess::httpFailure(instance, 503); + bool okay = !diagnosticOverflow && diagnostics.size() == 16; + for (const auto &event : diagnostics) + okay = okay && event.first == (LOG_AUTHPRIV | LOG_NOTICE) + && event.second == "rmweb-auth http: failure_status=503"; + dprintf(reportFd, "%s: %s\n", scenario, okay ? "PASS" : "FAIL"); + return okay ? 0 : 1; + } + QObject::connect(session, &rmweb::AuthPasskey::promptChanged, &app, + [&](rmweb::AuthPasskeyPrompt prompt) { + promptActive = prompt.active; + sawQr = sawQr || !prompt.qr.isNull(); + }); + QObject::connect(&AuthPasskeyTest::process(*session), &QProcess::finished, &app, + [](int code, QProcess::ExitStatus status) { childClean = code == 0 && status == QProcess::NormalExit; }); + QObject::connect(session, &rmweb::AuthPasskey::completed, &app, + [&](quint64 id, bool success, rmweb::AuthPasskeyAssertion assertion) { + ++completions; observedCompletion = true; completionSucceeded = success; completedId = id; + if (!engine->completePasskey(id, success, std::move(assertion))) finish(false); + if (!named("success")) { + // A late positive completion for the cancelled native request + // must not settle or change the current document. + rmweb::AuthPasskeyAssertion stale {QByteArray::fromHex("010203"), authenticatorData(), QByteArray(32, 'x'), QByteArray(1, 4)}; + if (!engine->completePasskey(id, true, std::move(stale))) finish(false); + } + if (!AuthEngineTestAccess::post(*engine, [] { cleared = AuthEngineTestAccess::requestCleared(*engine); })) finish(false); + }); + engine->start(); + QElapsedTimer elapsed; + elapsed.start(); + QTimer timer; + std::atomic bootstrapped{false}; + qint64 settledAt = -1; + QObject::connect(&timer, &QTimer::timeout, &app, [&] { + if (elapsed.elapsed() > 15000 || dom.load() == 0 || completions > 1) finish(false); + if (!bootstrapped && AuthEngineTestAccess::ready(*engine)) { + if (!AuthEngineTestAccess::post(*engine, [&bootstrapped] { + auto *page = AuthEngineTestAccess::page(*engine); + // Finish the worker's initial blank load before replacing it; + // otherwise its queued commit can cancel our synthetic page. + if (webkit_web_view_is_loading(page) || bootstrapped.exchange(true)) return; + webkit_web_view_stop_loading(page); + AuthEngineTestAccess::fixturePolicy(*engine, false); + g_signal_connect(page, "load-changed", G_CALLBACK(fixtureLoaded), nullptr); + webkit_web_view_load_alternate_html(page, "Synthetic assertion", + "https://login.example.com/fixture", "https://login.example.com/fixture"); + })) finish(false); + } + if (sawQr && !named("success") && !cancellationSent) { + cancellationSent = true; + if (!AuthEngineTestAccess::post(*engine, [] { + if (named("abort")) evaluate("window.abort.abort()"); + else webkit_web_view_load_alternate_html(AuthEngineTestAccess::page(*engine), + "", + "https://login.example.com/replacement", "https://login.example.com/replacement"); + })) finish(false); + } + // Cancellation may finish before the replacement document loads. Its + // retiring predecessor can legitimately report NotAllowedError then. + if (loaded && observedCompletion && (!named("navigation") || replacementLoaded)) queryOutcome(); + const int expected = named("success") ? 1 : named("abort") ? 2 : 3; + if (observedCompletion && dom.load() == expected && cleared.load()) { + if (settledAt < 0) settledAt = elapsed.elapsed(); + if (elapsed.elapsed() - settledAt >= 250) { + finish(completions == 1 && completedId == 1 && sawQr && childClean && !promptActive + && completionSucceeded == named("success")); + } + } + }); + timer.start(20); + app.exec(); + finish(false); +} diff --git a/tests/auth-passkey-browser/run-cases.sh b/tests/auth-passkey-browser/run-cases.sh new file mode 100644 index 0000000..476aa75 --- /dev/null +++ b/tests/auth-passkey-browser/run-cases.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu +if [ "$#" -ne 1 ] || [ ! -x "$1" ]; then + echo "Usage: $0 /path/to/auth-passkey-browser-smoke" >&2 + exit 2 +fi +for scenario in success abort navigation http-diagnostics; do + "$1" "$scenario" +done diff --git a/tests/auth-passkey/CMakeLists.txt b/tests/auth-passkey/CMakeLists.txt new file mode 100644 index 0000000..8a7c12c --- /dev/null +++ b/tests/auth-passkey/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) +project(rmweb_auth_passkey_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Test) +enable_testing() +add_executable(auth_passkey_test ../auth_passkey_test.cpp + ../../engine/wpeqt/auth-passkey.cpp ../../engine/wpeqt/auth-passkey.h) +target_include_directories(auth_passkey_test PRIVATE ../../engine/wpeqt) +target_compile_definitions(auth_passkey_test PRIVATE QT_NO_KEYWORDS) +target_link_libraries(auth_passkey_test PRIVATE Qt6::Core Qt6::Gui Qt6::Test) +target_compile_options(auth_passkey_test PRIVATE -Wall -Wextra -Werror) +add_test(NAME auth_passkey_test COMMAND auth_passkey_test) diff --git a/tests/auth-policy/CMakeLists.txt b/tests/auth-policy/CMakeLists.txt new file mode 100644 index 0000000..86bdb3f --- /dev/null +++ b/tests/auth-policy/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) +project(rmweb_auth_policy_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Test) +add_executable(auth_policy_test ../auth_policy_test.cpp ../../engine/wpeqt/auth-policy.cpp) +target_include_directories(auth_policy_test PRIVATE ../../engine/wpeqt) +target_link_libraries(auth_policy_test PRIVATE Qt6::Core Qt6::Test) +target_compile_definitions(auth_policy_test PRIVATE QT_NO_KEYWORDS) +target_compile_options(auth_policy_test PRIVATE -Wall -Wextra -Werror) +enable_testing() +add_test(NAME auth_policy_test COMMAND auth_policy_test) diff --git a/tests/auth-surface/CMakeLists.txt b/tests/auth-surface/CMakeLists.txt new file mode 100644 index 0000000..13325b0 --- /dev/null +++ b/tests/auth-surface/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) +project(rmweb_auth_surface_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Test) +enable_testing() +add_executable(auth_surface_test ../auth_surface_test.cpp + ../../engine/wpeqt/auth-surface.cpp ../../engine/wpeqt/auth-surface.h) +target_include_directories(auth_surface_test PRIVATE ../../engine/wpeqt) +target_compile_definitions(auth_surface_test PRIVATE QT_NO_KEYWORDS) +target_link_libraries(auth_surface_test PRIVATE Qt6::Core Qt6::Gui Qt6::Test) +target_compile_options(auth_surface_test PRIVATE -Wall -Wextra -Werror) +add_test(NAME auth_surface_test COMMAND auth_surface_test) diff --git a/tests/auth-webauthn-provider/CMakeLists.txt b/tests/auth-webauthn-provider/CMakeLists.txt new file mode 100644 index 0000000..47a4d64 --- /dev/null +++ b/tests/auth-webauthn-provider/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) +project(native_webauthn_provider_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +find_package(PkgConfig REQUIRED) +pkg_check_modules(WPE REQUIRED IMPORTED_TARGET wpe-webkit-2.0 wpe-platform-2.0 gio-2.0) +add_executable(native-webauthn-provider-test provider-test.cpp) +target_link_libraries(native-webauthn-provider-test PRIVATE PkgConfig::WPE ${CMAKE_DL_LIBS}) +target_compile_options(native-webauthn-provider-test PRIVATE -Wall -Wextra -Werror) + +# Export the test-only syslog sink for real shared-engine diagnostic checks. +set_target_properties(native-webauthn-provider-test PROPERTIES ENABLE_EXPORTS TRUE) diff --git a/tests/auth-webauthn-provider/README.md b/tests/auth-webauthn-provider/README.md new file mode 100644 index 0000000..62a5f5a --- /dev/null +++ b/tests/auth-webauthn-provider/README.md @@ -0,0 +1,124 @@ +# Native WebAuthn provider tests + +This fixture uses the actual WPE WebKit DOM, IPC, public GLib request API, and +headless view. It loads synthetic HTML with `https://login.example.com` as its +base URL. Run in an isolated container with networking disabled and an ephemeral +WebKit session; no authorization pages, accounts, Bluetooth, or phone are used. + +Configure this directory with the official 3.28 SDK and the WPE development +headers. Run `run-cases.sh /path/to/native-webauthn-provider-test` against the +patched runtime. Each process has a ten-second deadline. The runner stops on the +first failure. + +The core cases cover immutable origin/RP/options, the exact client-data hash, buffer +validation, single completion, reentrant and late completion after cancellation, +abort, navigation, timeout, RP/public-suffix rejection, unsupported modes, and +truthful capabilities, unfocused-document rejection, and real inherited +about:blank/srcdoc child rejection. The success fixture supplies **synthetic bytes, not a +cryptographically valid assertion**. It verifies DOM/native byte identity and +hash ownership; it cannot prove a phone signature or relying-party acceptance. + +The added API is resolved dynamically, allowing the same test executable to +report an explicit missing-provider failure against the unmodified engine. +The official SDK compile with `-Wall -Wextra -Werror`, that baseline feature +failure, and all 29 cases against the linked native-provider engine passed. + +## Real HTTPS child-frame cases + +`run-https-cases.py` separately runs `child-https-success` and +`child-https-detach`. It generates an ephemeral CA and a leaf certificate for +`login.example.com`, then serves fixed `/parent` and `/child` pages on container +loopback. The provider loads real HTTPS documents, focuses the child, and calls +the native WebAuthn API. Removal happens after native dispatch and must cancel +once while rejecting both reentrant and late completion. + +The launcher requires a root Linux `--network none` container with only the +loopback interface initially, `--cap-add NET_ADMIN`, and +`--add-host login.example.com:127.0.0.1`. It temporarily creates a dummy +`wpe-test0` interface with the TEST-NET address `192.0.2.1/32`: glibc rejects +family-specific `AI_ADDRCONFIG` lookups in a loopback-only namespace, which +otherwise prevents GIO from reaching the TLS server. The launcher verifies +the exact interface set, absence of IPv4 external routes, and resolution to +loopback, then removes the owned interface during cleanup. No external network +is attached. It passes the CA through +`SSL_CERT_FILE` to test processes only, removes proxy overrides, and makes no +certificate-policy exceptions or system trust changes. Server and fixture +process groups have bounded cleanup; temporary certificate material is removed +on success, failure, or SIGTERM. Container teardown is the final cleanup boundary +for SIGKILL or host failure. + +After compiling the fixture, use the runtime setup in +`tests/auth-wpe-smoke/run.sh`; its isolated container runs both this launcher and +the 31-case core runner. To run only these two cases in that configured +container: + +```sh +python3 /provider-fixtures/run-https-cases.py /provider/native-webauthn-provider-test +``` + +The harness itself can be checked without a WebKit build: + +```sh +python3 tests/auth-webauthn-provider/https_fixture_test.py -v +docker run --rm --network none --cap-add NET_ADMIN --add-host login.example.com:127.0.0.1 \ + --platform linux/arm64 \ + -v "$PWD/tests/auth-webauthn-provider:/fixture:ro" \ + rmweb-app-sdk:3.28.0.172 python3 /fixture/https_fixture_test.py -v +``` + +All eleven harness cases passed in the official SDK container. They check the +resolver fix, interrupted interface creation and cleanup, trusted TLS, rejection +of an unknown CA or wrong hostname, fixed routes, exact case dispatch, failure +cleanup, and cancellation cleanup. On macOS five pass and the six isolated +process cases are skipped. Separately, both actual WebKit HTTPS cases passed +against the initial linked engine (29 core plus 2 HTTPS cases). These +results do not establish a valid phone signature or relying-party acceptance. + +## Diagnostic classification and privacy + +The same executable exports a test-only `syslog` sink. It captures at most 128 +records of 511 bytes in memory and never forwards them to the system logger. +`run-diagnostic-cases.sh /path/to/native-webauthn-provider-test` runs 23 real +DOM/native cases, checking exact ordered labels and `LOG_AUTHPRIV | LOG_NOTICE`. +Any additional values, messages, incorrect provider priority, repeated terminal +event, or overflow fails the check. The exact pinned-library warning +`Libgcrypt warning: missing initialization - please fix the application` is +allowed at most once with `LOG_USER | LOG_WARNING`; no other unrelated record is ignored. Extension requests contain synthetic sentinel +values in their challenge, AppID, or PRF input; none may enter the captured +records. No real account, credential, phone, or Bluetooth adapter is involved. + +The cases cover success, cancellation/abort/navigation/timeout, an unhandled UI, +secure-origin and RP rejection, unsupported mediation, AppID/credProps/largeBlob/ +PRF classification, and distinct empty/oversized challenge, excessive allowlist, +and empty/oversized credential-ID labels. The platform and +credential-type defensive labels cannot be exercised through this WebKit +version's ordinary JavaScript request dictionary: platform attachment is not +serialized in request IPC, and the descriptor IDL accepts only `public-key`. +Their guards remain in the provider without adding a test policy exception. + +The diagnostic test compiled with the official 3.28 SDK and failed against the +previous engine specifically because the fixed events were absent. The original +29-case runner and seven new option/bounds behavior cases passed against that +engine. After rebuilding, all 21 diagnostic cases, the original 29 provider +cases, both unchanged real-HTTPS cases, and the joined Qt/helper cases passed +through `tests/auth-wpe-smoke/run.sh` with networking disabled. The combined +runner also passed its synthetic keyboard and navigation checks. These results +qualify offline diagnostics and preserve the authentication behavior checks; +they do not establish successful phone authentication or device deployment. + +The later five-way bounds-label refinement failed against the preceding engine +with the expected diagnostic label mismatch. The subsequent challenge-cap +candidate adds `challenge-1025` and `challenge-16384` acceptance cases and moves +`challenge-large` to 16,385 bytes. The test independently encodes known challenge +bytes with GLib and compares them with native options and returned +`clientDataJSON`; WebCrypto SHA-256 of the returned JSON must match the native +32-byte hash. The 1,025-byte acceptance case failed against the preceding +1 KiB-limit engine before the cap was changed. Empty challenge, allowlist count, +and credential-ID rejection cases are retained. Against the rebuilt 16 KiB-limit +engine, both acceptance cases and all five bounds-classification cases passed +with exact diagnostic labels (seven focused cases in an isolated SDK container). +The final combined engine/helper/browser suite passed through +`tests/auth-wpe-smoke/run.sh`: 31 core provider cases, 23 diagnostic cases, +2 real HTTPS child-frame cases, and 4 Qt bridge cases, plus synthetic keyboard +and navigation checks. Installation and physical retry remain separate gates; +these results do not establish the cause of the reported live failure. diff --git a/tests/auth-webauthn-provider/https-server.py b/tests/auth-webauthn-provider/https-server.py new file mode 100644 index 0000000..d136f05 --- /dev/null +++ b/tests/auth-webauthn-provider/https-server.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Fixed synthetic pages, served only on container loopback with real TLS.""" + +import argparse +import http.server +import ssl +from pathlib import Path + + +PAGES = { + "/parent": b'Parent fixture' + b'', + "/child": b'Child fixture' + b'Same-origin WebAuthn fixture', +} + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + payload = PAGES.get(self.path) + self.send_response(200 if payload is not None else 404) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(payload or b""))) + self.end_headers() + if payload is not None: + self.wfile.write(payload) + + def log_message(self, *_args): + pass + + +class Server(http.server.ThreadingHTTPServer): + daemon_threads = True + + def __init__(self, certificate, key, port=443): + self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + self.context.minimum_version = ssl.TLSVersion.TLSv1_2 + self.context.load_cert_chain(certificate, key) + super().__init__(("127.0.0.1", port), Handler) + + def get_request(self): + connection, address = super().get_request() + connection.settimeout(2) + try: + return self.context.wrap_socket(connection, server_side=True), address + except Exception: + connection.close() + raise + + def handle_error(self, *_args): + # The fixture has no diagnostic need for HTTP headers or request data. + pass + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("certificate", type=Path) + parser.add_argument("key", type=Path) + parser.add_argument("ready", type=Path) + args = parser.parse_args() + with Server(args.certificate, args.key) as server: + args.ready.write_text("ready\n", encoding="ascii") + server.serve_forever(poll_interval=0.1) + + +if __name__ == "__main__": + main() diff --git a/tests/auth-webauthn-provider/https_fixture_test.py b/tests/auth-webauthn-provider/https_fixture_test.py new file mode 100644 index 0000000..48e65e1 --- /dev/null +++ b/tests/auth-webauthn-provider/https_fixture_test.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Tests for the TLS harness, separate from WebKit/provider qualification.""" + +import importlib.util +import os +import signal +import socket +import ssl +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from pathlib import Path + + +HERE = Path(__file__).resolve().parent + + +def load(name, filename): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +runner = load("tls_runner", "run-https-cases.py") +server_module = load("tls_server", "https-server.py") + + +class TlsServerTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.temp = tempfile.TemporaryDirectory() + cls.ca, leaf, key = runner.certificates(Path(cls.temp.name)) + cls.server = server_module.Server(leaf, key, port=0) + cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) + cls.thread.start() + + @classmethod + def tearDownClass(cls): + cls.server.shutdown() + cls.server.server_close() + cls.thread.join(timeout=2) + cls.temp.cleanup() + + def connect(self, context, hostname="login.example.com"): + raw = socket.create_connection(self.server.server_address, timeout=3) + try: + return context.wrap_socket(raw, server_hostname=hostname) + except Exception: + raw.close() + raise + + def request(self, path): + context = ssl.create_default_context(cafile=str(self.ca)) + with self.connect(context) as connection: + connection.sendall(f"GET {path} HTTP/1.0\r\nHost: login.example.com\r\n\r\n".encode("ascii")) + response = bytearray() + while data := connection.recv(4096): + response.extend(data) + return bytes(response) + + def test_trusted_parent_and_child_are_actual_fixed_html(self): + self.assertIn(b'