Page not found
The page you requested could not be found.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c405c18..31628403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,6 @@ on: permissions: contents: read - deployments: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,6 +15,9 @@ concurrency: jobs: build: + permissions: + contents: read + deployments: write # Match the OS supported by the pinned Playwright version. runs-on: ubuntu-24.04 steps: @@ -70,3 +72,23 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy ./out --project-name=connectbot-website gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + webauthn: + permissions: + contents: read + deployments: write + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Validate WebAuthn association + run: python3 scripts/check-webauthn.py + - name: Package WebAuthn site + run: python3 scripts/prepare-webauthn.py + - name: Deploy WebAuthn association + if: github.ref == 'refs/heads/develop' && github.repository == 'connectbot/connectbot.github.io' && github.event_name == 'push' + uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3 + with: + apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy ./local/webauthn --project-name=connectbot-webauthn --branch=main + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/development.md b/docs/development.md index 9eb830e1..4a54718f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -52,12 +52,14 @@ pnpm run analyze ## Deployment -The site is automatically deployed to GitHub Pages when changes are pushed to the `develop` branch. The CI workflow: +The site is automatically deployed to Cloudflare Pages when changes are pushed to the `develop` branch. The CI workflow: 1. Runs linting and type checking 2. Builds the static site -3. Deploys to GitHub Pages +3. Deploys to Cloudflare Pages You can preview the production build locally by serving the `./out` directory: ```bash npx serve ./out ``` + +See [WebAuthn hosting](webauthn.md) for the independent passkey association deployment. diff --git a/public/brand.css b/public/brand.css new file mode 100644 index 00000000..45b6125c --- /dev/null +++ b/public/brand.css @@ -0,0 +1,13 @@ +:root { + --site-background: #fafbfc; + --site-text: #1e293b; + --site-link-color: #1d4ed8; + --site-link-hover-color: #1e40af; +} + +.dark { + --site-background: #0f172a; + --site-text: #e2e8f0; + --site-link-color: #93c5fd; + --site-link-hover-color: #bfdbfe; +} diff --git a/scripts/check-webauthn.py b/scripts/check-webauthn.py new file mode 100644 index 00000000..5be64cc1 --- /dev/null +++ b/scripts/check-webauthn.py @@ -0,0 +1,29 @@ +"""Validate the release identities published by the WebAuthn RP domain.""" +import hashlib +import json +from pathlib import Path +import ssl + +ROOT = Path(__file__).resolve().parents[1] +CERTIFICATES = ( + 'connectbot-app-signing-original.pem', + 'connectbot-play-app-signing-intermediate.pem', + 'connectbot-app-signing-2026.pem', +) +fingerprints = [] +for certificate in CERTIFICATES: + pem = (ROOT / 'public/certificates' / certificate).read_text() + digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest().upper() + fingerprints.append(':'.join(digest[i:i + 2] for i in range(0, 64, 2))) +expected = [{ + 'relation': ['delegate_permission/common.get_login_creds'], + 'target': { + 'namespace': 'android_app', + 'package_name': 'org.connectbot', + 'sha256_cert_fingerprints': fingerprints, + }, +}] +actual = json.loads((ROOT / 'webauthn/.well-known/assetlinks.json').read_text()) +if actual != expected: + raise SystemExit('WebAuthn association must match the three official release certificates') +print('WebAuthn association matches all three official release certificates') diff --git a/scripts/prepare-webauthn.py b/scripts/prepare-webauthn.py new file mode 100644 index 00000000..25e02211 --- /dev/null +++ b/scripts/prepare-webauthn.py @@ -0,0 +1,14 @@ +"""Package the static RP site with canonical shared brand assets.""" +from pathlib import Path +import shutil + +root = Path(__file__).resolve().parents[1] +out = root / 'local/webauthn' +if out.exists(): + shutil.rmtree(out) +shutil.copytree(root / 'webauthn', out) +for asset in ('brand.css', 'logo.png', 'fonts/logo-font.woff2'): + target = out / asset + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(root / 'public' / asset, target) +print(f'Prepared {out}') diff --git a/src/app/styles.css b/src/app/styles.css index 7ed0e795..ebcada3f 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -1,16 +1,13 @@ @import 'nextra-theme-docs/style.css'; +@import '../../public/brand.css'; :root { --site-header-height: var(--nextra-navbar-height); - --site-link-color: #1d4ed8; - --site-link-hover-color: #1e40af; --x-color-primary-400: #93c5fd; --x-color-primary-600: var(--site-link-color); } .dark { - --site-link-color: #93c5fd; - --site-link-hover-color: #bfdbfe; --x-color-primary-600: var(--site-link-color); } diff --git a/webauthn/.well-known/assetlinks.json b/webauthn/.well-known/assetlinks.json new file mode 100644 index 00000000..9a85e1f7 --- /dev/null +++ b/webauthn/.well-known/assetlinks.json @@ -0,0 +1,16 @@ +[ + { + "relation": [ + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "org.connectbot", + "sha256_cert_fingerprints": [ + "AA:43:74:6A:46:12:78:42:16:9F:81:76:41:2A:1F:4B:1B:1E:2F:8A:D9:E1:5A:BA:21:7C:38:22:DB:EB:2D:5F", + "2F:EF:9C:FE:29:BE:E1:93:20:61:A8:1B:FB:B8:B5:7C:5C:65:88:54:B8:2C:C9:7B:BF:26:52:65:DD:B2:27:7C", + "D2:49:13:B6:92:DB:31:A9:29:CD:30:AE:86:18:DF:97:71:88:82:B6:EA:A1:ED:6E:12:4B:C4:3D:02:8E:7F:BE" + ] + } + } +] diff --git a/webauthn/404.html b/webauthn/404.html new file mode 100644 index 00000000..231fcb45 --- /dev/null +++ b/webauthn/404.html @@ -0,0 +1,16 @@ + + +
+ + +The page you requested could not be found.
This domain publishes the Android app association for ConnectBot’s passkey support.
This site does not receive or store your private keys. Passkeys are managed by your selected credential provider.