From 319cbcd94ffc07b5e5afb65a2cea069d87061c1f Mon Sep 17 00:00:00 2001 From: wenkai fan Date: Mon, 27 Jul 2026 13:45:59 -0400 Subject: [PATCH 1/2] docs(windows): update README/CHANGELOG + scaffold prebuilt cef_host distribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs: - README status line: macOS-only -> "macOS 12+ and Windows 10+", with an honest parity summary and what's still pending (per-tile CDP, sandbox, signing). - How-it-works diagram now shows both host paths (IOSurface/Unix-socket vs DXGI-shared-texture/named-pipe) and the one shared opcode protocol. - Building: added the Windows path (flutter run -d windows drives fetch+compile). - Roadmap: Windows marked landed with a precise hardening-not-features list; Linux noted as not-started. - CHANGELOG: an Unreleased entry (no version bump — a federation publish decision). Windows prebuilt distribution scaffolding (packages/flutter_cef_windows/tool/, mirrors the macOS GCS/fetch model; signing STUBBED pending an Authenticode cert): - cef_host_hash.ps1: deterministic 64-hex content hash of the 5 build inputs (CR-stripped so autocrlf can't perturb it) = the GCS object key. - fetch_cef_host.ps1: consumer fetch — fail-OPEN on missing (build from source), fail-CLOSED on sha mismatch and on Authenticode verification. The signature gate is real (Get-AuthenticodeSignature, Status=Valid + thumbprint/subject pin) but accepts UNSIGNED only with FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1 until the cert lands. - publish-cef-host.ps1: build -> signtool (only when FLUTTER_CEF_SIGN_THUMBPRINT set, else skip) -> zip + .sha256 -> idempotent gsutil upload keyed by hash. - windows/CMakeLists.txt: prefer a fetched prebuilt (only when its stamp matches this tree's hash — guards a stale prebuilt/), else the unchanged from-source build; a fail-closed fetch result FATALs rather than silently building. - Makefile: publish-cef-host-windows target; .gitignore: prebuilt/. Verified (my own runs): hash deterministic (57ba6732...); all 3 .ps1 ASCII/no-BOM/parse-clean; from-source build still succeeds via fetch fail-open (real 404 -> build from source -> Built); macOS zero-diff. Stubbed/untestable without infra: signtool signing and gsutil upload/download (no cert, no GCS creds) — logic verified by dry-runs + AST-extracted branch tests. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 27 +++ Makefile | 12 + README.md | 97 ++++++-- packages/flutter_cef_windows/.gitignore | 4 + .../tool/cef_host_hash.ps1 | 95 ++++++++ .../tool/fetch_cef_host.ps1 | 208 ++++++++++++++++++ .../tool/publish-cef-host.ps1 | 169 ++++++++++++++ .../windows/CMakeLists.txt | 123 ++++++++--- 8 files changed, 681 insertions(+), 54 deletions(-) create mode 100644 packages/flutter_cef_windows/tool/cef_host_hash.ps1 create mode 100644 packages/flutter_cef_windows/tool/fetch_cef_host.ps1 create mode 100644 packages/flutter_cef_windows/tool/publish-cef-host.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce5c53..0cb2aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## Unreleased + +* **Windows support** (`flutter_cef_windows`, experimental) — the port reaches + feature parity with macOS for browsing, pointer/keyboard/IME input, persistent + + shared profiles, cookies, the JS bridge (`addJavaScriptChannel` / + `runJavaScriptReturningResult`, per-session routed), JS dialogs, find-in-page, + content zoom, downloads, and single-tile agent-control (CDP over an inherited + pipe, token-gated loopback relay). The app-facing Dart API is unchanged and one + IPC opcode protocol drives both OSes. Windows specifics: + * **Rendering**: CEF `OnAcceleratedPaint`'s shared D3D11 texture is copied into + a DXGI-shared "bridge" texture that Flutter's ANGLE compositor samples (a + `GpuSurfaceTexture`); software `OnPaint` is the fallback. + * **Profiles at rest**: encrypted with **DPAPI** (OSCrypt) — always available + and signing-independent, so a named `profile:` persists in every build with + **no** macOS-style ad-hoc→ephemeral downgrade. DPAPI is same-user-readable + (weaker than the macOS Keychain); the profile dir also gets a current-user + SID DACL. + * **Transport/host**: one `cef_host.exe` per profile over a named pipe + (overlapped I/O); the CEF runtime is fetched + SHA-verified on first build + and `cef_host` is compiled from source (`build_cef_host.bat`, VS2022 via + `vswhere`). + * **Not yet on Windows** (hardening): per-tile CDP isolation (agent-control is + fail-closed single-tile), the Chromium sandbox (`no_sandbox` today), and a + signed prebuilt `cef_host` on GCS (tooling scaffolded, awaits an Authenticode + cert). Requires Windows 10+ with Developer Mode. + * macOS behaviour is **byte-for-byte unchanged**. + ## 0.2.0 * **Persistent, shared profiles**: `CefWebView(profile: 'name')` / diff --git a/Makefile b/Makefile index 0aedd4a..bcbeea9 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,15 @@ publish-cef-host: @test -n "$(CODESIGN_ID)" || { echo "error: no 'Developer ID Application' identity in the keychain"; exit 1; } @command -v gsutil >/dev/null 2>&1 || { echo "error: gsutil not found (install the Google Cloud SDK)"; exit 1; } CODESIGN_ID="$(CODESIGN_ID)" bash packages/flutter_cef_macos/tool/publish-cef-host.sh + +# publish-cef-host-windows: the Windows analogue (PowerShell). Builds cef_host via +# build_cef_host.bat, (STUBBED) signs it if FLUTTER_CEF_SIGN_THUMBPRINT is set, +# and publishes cef_host-windows-x64.zip to GCS keyed by the input hash. Guards +# (gsutil present, signtool when signing) live inside the .ps1; run from a shell +# where `powershell` resolves. Idempotent. +# +# make publish-cef-host-windows +# GCS_PREFIX=campus_prebuilt_cef_host-staging make publish-cef-host-windows # dry-run to staging +.PHONY: publish-cef-host-windows +publish-cef-host-windows: + powershell -NoProfile -ExecutionPolicy Bypass -File packages/flutter_cef_windows/tool/publish-cef-host.ps1 diff --git a/README.md b/README.md index 471b158..9ffd596 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,18 @@ Embed a **live Chromium browser** (via the [Chromium Embedded Framework](https://bitbucket.org/chromiumembedded/cef/)) as a Flutter widget — rendered into a `Texture`, so it composites, transforms, clips, and zooms like any other widget, and **keeps rendering even when off-screen / not focused**. Pointer, scroll, and trackpad two-finger pans are forwarded by coordinate (pans are caught even when an ancestor opts into Flutter's trackpad gesture API, as canvas hosts do), and keyboard input reaches the page as real `keydown → keypress → keyup` events (Enter activates a focused button / submits a form, Space toggles a checkbox) — including platform IME composition for CJK / emoji and the ⌃⌘Space emoji picker. Text input is bound to the hosting `FlutterView` (as `EditableText` does), so it **works in multi-view / multi-window apps**; the page cursor drives a `MouseRegion`. -> Status: **experimental, macOS 12+ only** (CEF 144 runtime floor). Real Chromium (any site — JS/CSS/WebGL/video). **Multi-process by default** (GPU-accelerated OSR — `OnAcceleratedPaint` GPU compositing into a shared IOSurface, Retina-crisp; renderer/utility crashes isolated, so heavy SPAs like Google sign-in render and survive); `CEF_MULTI_PROCESS=OFF packages/flutter_cef_macos/native/build_cef_host.sh` for the simpler single-process build. No mobile (iOS bans third-party engines); desktop by nature. +> Status: **experimental — macOS 12+ and Windows 10+** (CEF 144 runtime floor). +> Real Chromium (any site — JS/CSS/WebGL/video). **Multi-process by default** +> (GPU-accelerated OSR — `OnAcceleratedPaint` GPU compositing into a shared +> surface, HiDPI-crisp; renderer/utility crashes isolated, so heavy SPAs like +> Google sign-in render and survive). macOS composites into an `IOSurface`; +> Windows copies the shared D3D11 texture into a DXGI-shared bridge that Flutter's +> ANGLE compositor samples. **macOS is the reference platform**; the Windows port +> reaches feature parity for browsing, input/IME, persistent+shared profiles, +> cookies, the JS bridge, dialogs/find/zoom/downloads, and single-tile +> agent-control — with per-tile CDP isolation, the Chromium sandbox, and +> code-signing still pending (see [Roadmap](#roadmap)). No mobile (iOS bans +> third-party engines); desktop by nature. ```dart import 'package:flutter_cef/flutter_cef.dart'; @@ -79,22 +90,34 @@ bar, live title). ``` Dart CefWebView + CefWebController (MethodChannel "flutter_cef") - → macOS plugin (FlutterCefPlugin / CefWebSession): - allocates a global IOSurface + CVPixelBuffer, registers a FlutterTexture, - spawns one cef_host.app per view, relays input + cursor over a Unix socket - → cef_host.app: CEF windowless (OSR), multi-process — the GPU/Viz process - composites the page and hands OnAcceleratedPaint a shared-texture - IOSurface, which cef_host copies into the host-shared IOSurface → - "present" → the texture re-samples. (OnPaint software blit is the - single-process fallback.) + → host plugin (per OS): + macOS (FlutterCefPlugin / CefWebSession): allocates a global IOSurface + + CVPixelBuffer, registers a FlutterTexture, spawns one cef_host.app + per profile, relays input + cursor over a Unix socket + Windows (FlutterCefPlugin, C++): registers a GpuSurfaceTexture (DXGI shared + handle), spawns one cef_host.exe per profile, relays over a named + pipe (overlapped I/O) + → cef_host: CEF windowless (OSR), multi-process — the GPU/Viz process + composites the page and hands OnAcceleratedPaint a shared-texture handle: + macOS copies it into the host-shared IOSurface; + Windows opens the shared D3D11 texture and CopyResources it into a + DXGI-shared "bridge" texture (NT→legacy handle) that ANGLE samples. + → "present" → the texture re-samples. (OnPaint software blit is the fallback.) ``` -Same pattern JCEF (JetBrains) and CefSharp use to render Chromium into a non-native toolkit — adapted to Flutter's `Texture` + `IOSurface`. +Same pattern JCEF (JetBrains) and CefSharp use to render Chromium into a +non-native toolkit — adapted to Flutter's `Texture` (IOSurface on macOS, a +DXGI-shared D3D11 texture on Windows). One IPC opcode protocol +([`PROTOCOL.md`](packages/flutter_cef_windows/native/cef_host/PROTOCOL.md)) +drives both; only the surface, transport, app-loop, sandbox, and framework-path +seams differ per OS (see [`PORTING.md`](PORTING.md)). ## Building -CEF (~200 MB) is **fetched**, not vendored. Build the renderer once (needs -`cmake` + `ninja` — `brew install cmake ninja`): +CEF (~200 MB) is **fetched**, not vendored, on both platforms. + +**macOS** — build the renderer once (needs `cmake` + `ninja` — +`brew install cmake ninja`): ```sh # The macOS implementation lives in packages/flutter_cef_macos. @@ -104,6 +127,23 @@ export FLUTTER_CEF_HOST="$PWD/native/cef_host/build/cef_host.app/Contents/MacOS/ cd ../../example && flutter run -d macos ``` +**Windows** — nothing to run by hand: `flutter build windows` (or +`flutter run -d windows`) drives everything from the plugin's CMake. On the +first build it fetches the pinned CEF distribution +(`native/cef_host/fetch_cef.ps1`, SHA-verified, cached under `%LOCALAPPDATA%`) +and compiles `cef_host.exe`/`.dll` (`build_cef_host.bat` → the VS2022 toolchain, +located via `vswhere`; `cmake` + `ninja` ship with VS). Needs Windows 10+ with +Developer Mode on (Flutter uses symlinks for plugins). + +```sh +cd example && flutter run -d windows +``` + +> A prebuilt, content-hash-keyed `cef_host` on GCS (so consumers fetch it +> instead of compiling, as macOS does) is scaffolded in +> `packages/flutter_cef_windows/tool/` but not yet live — it awaits Authenticode +> signing (see [Roadmap](#roadmap)); until then Windows builds from source. + ### Bundling into a distributable app For a shipped `.app` (no dev env var), `cef_host.app` must live in your bundle's @@ -452,17 +492,28 @@ Next: scheme handlers, a typed DevTools/CDP client (the inspector window already ships via `openDevTools`; this is the programmatic CDP surface), and `CefPermissionHandler` (WebRTC camera/mic prompts). -- **Windows / Linux** — the package is **federated** (`flutter_cef` + - `flutter_cef_platform_interface` + `flutter_cef_macos`); a new platform is a - sibling `flutter_cef_` package. The CEF logic + IPC protocol are portable; - each OS supplies its own host plugin + shared-texture / transport / sandbox - glue. See [`PORTING.md`](PORTING.md) for the full contract and seam map. A - Windows port (`flutter_cef_windows`) is in progress: the Dart controller / - widget surface is already cross-platform (the JS bridge, JS dialogs, - find-in-page, content zoom, and downloads all speak the same - method-channel/wire protocol on both OSes — see - `packages/flutter_cef_windows/native/cef_host/PROTOCOL.md`), with the Windows - host's sandbox + code-signing story still pending. +- **Windows** (`flutter_cef_windows`) — **landed** and at feature parity for + browsing, pointer/keyboard/IME input, persistent + shared profiles, cookies, + the JS bridge, JS dialogs, find-in-page, content zoom, downloads, and + single-tile agent-control. The Dart controller / widget surface is shared and + one IPC opcode protocol drives both OSes + ([`PROTOCOL.md`](packages/flutter_cef_windows/native/cef_host/PROTOCOL.md)). + Still pending on Windows (hardening, not features): + - **Per-tile CDP isolation** — agent-control is enforced single-tile + (fail-closed: no grant on a multi-tile host); the deny-by-default per-tile + Target-domain filter that macOS ships (so N tiles on one profile can each be + agent-driven in isolation) is the next step. + - **Chromium sandbox** — the host currently runs `no_sandbox`; the + `bootstrapc.exe` + LPAC sandbox is designed (spike-proven) but not yet on. + - **Code-signing + prebuilt distribution** — an Authenticode-signed, + content-hash-keyed `cef_host` on GCS (fetched, not compiled, like macOS); + tooling is scaffolded in `packages/flutter_cef_windows/tool/`, awaiting a + signing certificate. + - Double-buffering the shared texture (the ~1.8% tearing measured on + fast-updating pages), and GPU device-lost / leak-soak hardening. +- **Linux** — not started; the same federated seam applies (a sibling + `flutter_cef_linux` supplies shared-memory/DMA-buf surface + Unix-socket + transport). See [`PORTING.md`](PORTING.md) for the contract and seam map. ## Credits diff --git a/packages/flutter_cef_windows/.gitignore b/packages/flutter_cef_windows/.gitignore index c822747..f49dc24 100644 --- a/packages/flutter_cef_windows/.gitignore +++ b/packages/flutter_cef_windows/.gitignore @@ -1,3 +1,7 @@ # Standalone cef_host build output (build_cef_host.bat's default BUILD_DIR; # the flutter-driven build uses the example's binary dir instead). native/cef_host/build/ + +# Fetched prebuilt cef_host (tool/fetch_cef_host.ps1 downloads cef_host.exe + +# cef_host.dll + provenance stamps here; never checked in). +native/cef_host/prebuilt/ diff --git a/packages/flutter_cef_windows/tool/cef_host_hash.ps1 b/packages/flutter_cef_windows/tool/cef_host_hash.ps1 new file mode 100644 index 0000000..5a1eeaa --- /dev/null +++ b/packages/flutter_cef_windows/tool/cef_host_hash.ps1 @@ -0,0 +1,95 @@ +# cef_host_hash.ps1 - deterministic content hash of the Windows cef_host build +# inputs. Prints a 64-hex digest to STDOUT (Write-Output). +# +# Dot-sourced by BOTH fetch_cef_host.ps1 (consumer, at CMake configure) and +# publish-cef-host.ps1 (CI) so they ALWAYS compute the same digest from the same +# source tree -- that digest IS the GCS object key, so any drift here is a silent +# cache miss. Mirrors packages/flutter_cef_macos/tool/cef_host_hash.sh. +# +# Inputs (the ONLY files that determine the shipped cef_host.dll/.exe bytes): +# build_cef_host.bat, CMakeLists.txt, cef_host_protocol.h, cef_host_win.cc, +# fetch_cef.ps1. fetch_cef.ps1 carries the CEF version pin ($CefVersion) -- the +# Windows analogue of the macOS build_cef_host.sh carrying CEF_VERSION -- so a +# CEF bump moves the digest without hashing the multi-hundred-MB CEF dist. +# Docs (PROTOCOL.md) and the standalone IPC test (test/) are NOT build inputs and +# are excluded; the build/ and prebuilt/ OUTPUT dirs are never inputs. +# +# Determinism across machines: the input list is byte-stable (ordinal) sorted, +# each file's CR (0x0D) is stripped before hashing so a git core.autocrlf checkout +# does NOT perturb the digest, and we emit "\n\n" per file +# (LF, lowercase hex) then SHA-256 the whole UTF-8 stream. Same tree -> same +# digest on every machine. +# +# ASCII-ONLY (no em-dashes / smart quotes): PowerShell 5.1 reads a UTF-8-no-BOM +# file as ANSI and mis-parses multibyte characters. Keep this file 7-bit. +# +# Usage: +# Dot-source: . .\cef_host_hash.ps1 ; Get-CefHostInputHash -CefHostDir +# Standalone: powershell -File .\cef_host_hash.ps1 (prints this pkg's hash) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +# SHA-256 of a byte array, returned as lowercase 64-hex. +function ConvertTo-Sha256Hex { + param([Parameter(Mandatory = $true)][byte[]]$Bytes) + $sha = [System.Security.Cryptography.SHA256]::Create() + try { $digest = $sha.ComputeHash($Bytes) } finally { $sha.Dispose() } + return ([System.BitConverter]::ToString($digest) -replace '-', '').ToLowerInvariant() +} + +# SHA-256 of a file with CR bytes stripped (CRLF and LF checkouts hash equal). +# All inputs are text, so a 0x0D only ever appears as part of a CRLF here. +function Get-NormalizedFileSha256 { + param([Parameter(Mandatory = $true)][string]$Path) + $bytes = [System.IO.File]::ReadAllBytes($Path) + $out = New-Object System.Collections.Generic.List[byte] + foreach ($b in $bytes) { if ($b -ne 13) { [void]$out.Add($b) } } + return (ConvertTo-Sha256Hex -Bytes $out.ToArray()) +} + +function Get-CefHostInputHash { + [CmdletBinding()] + param( + # .../packages/flutter_cef_windows/native/cef_host . Defaults to the copy + # beside this script so dot-source and standalone use both work arg-free. + [string]$CefHostDir = (Join-Path $PSScriptRoot '..\native\cef_host') + ) + + $root = (Resolve-Path -LiteralPath $CefHostDir).Path + + # Explicit, ordered build-input list (relative to $CefHostDir). Extend this if + # a new file starts to affect the shipped host bytes. + $inputs = [string[]]@( + 'build_cef_host.bat', + 'CMakeLists.txt', + 'cef_host_protocol.h', + 'cef_host_win.cc', + 'fetch_cef.ps1' + ) + # Byte-stable (ordinal) sort: identical order on every machine and culture. + [Array]::Sort($inputs, [System.StringComparer]::Ordinal) + + $sb = New-Object System.Text.StringBuilder + foreach ($rel in $inputs) { + $path = Join-Path $root $rel + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "cef_host_hash: missing build input '$rel' under '$root'" + } + $fileSha = Get-NormalizedFileSha256 -Path $path + # relpath (forward slashes; inputs are flat, this only future-proofs nested + # entries) then its sha, each LF-terminated. + $relKey = $rel -replace '\\', '/' + [void]$sb.Append($relKey); [void]$sb.Append("`n") + [void]$sb.Append($fileSha); [void]$sb.Append("`n") + } + + $streamBytes = [System.Text.Encoding]::UTF8.GetBytes($sb.ToString()) + return (ConvertTo-Sha256Hex -Bytes $streamBytes) +} + +# Standalone: print the digest for this package's native/cef_host. Dot-sourcing +# ('. .\cef_host_hash.ps1') sets InvocationName to '.', which skips this block. +if ($MyInvocation.InvocationName -ne '.') { + Write-Output (Get-CefHostInputHash) +} diff --git a/packages/flutter_cef_windows/tool/fetch_cef_host.ps1 b/packages/flutter_cef_windows/tool/fetch_cef_host.ps1 new file mode 100644 index 0000000..46fa034 --- /dev/null +++ b/packages/flutter_cef_windows/tool/fetch_cef_host.ps1 @@ -0,0 +1,208 @@ +# fetch_cef_host.ps1 - consumer fetch of a prebuilt cef_host.exe + cef_host.dll +# keyed by a CONTENT HASH of the build inputs, from public GCS. Runs at CMake +# configure time (windows/CMakeLists.txt), which then bundles +# native/cef_host/prebuilt/ beside the app instead of building the host from +# source. Self-locating (CWD-independent). Mirrors the macOS +# packages/flutter_cef_macos/tool/fetch_cef_host.sh. +# +# The hash (cef_host_hash.ps1) is derived from the checked-out cef_host sources + +# fetch_cef.ps1, so it is identical to what the publisher computed -- no committed +# manifest, release-model-agnostic. Fail-OPEN on network/missing (co-dev + offline +# builds fall back to build-from-source); fail-CLOSED on checksum mismatch and on +# a bad / foreign / absent code signature. +# +# --------------------------------------------------------------------------- +# SIGNING TRANSITION (STUBBED-BUT-REAL) -- READ BEFORE THE CERT SHIPS +# --------------------------------------------------------------------------- +# The Authenticode gate below is REAL: it requires Get-AuthenticodeSignature +# Status 'Valid' AND the signer certificate pinned to FLUTTER_CEF_CERT_THUMBPRINT +# (exact thumbprint) or FLUTTER_CEF_CERT_SUBJECT (subject substring). BECAUSE the +# signing certificate is not procured yet, published artifacts are UNSIGNED today. +# To accept an unsigned host you MUST opt in with FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1 +# (a LOUD warning is printed). Anything else -- unsigned WITHOUT the opt-in, a +# validly-signed host with NO pin configured, or any invalid/foreign signature -- +# fails CLOSED (exit 1). +# FLIP WHEN SIGNING SHIPS: set FLUTTER_CEF_CERT_THUMBPRINT in CI + consumers and +# DELETE the FLUTTER_CEF_ALLOW_UNSIGNED_HOST opt-in branch (and this paragraph). +# --------------------------------------------------------------------------- +# +# STDERR-only: this script has NO intended stdout value; every message goes to +# STDERR (Info) so a caller capturing stdout sees nothing from here. Exit codes: +# 0 = proceed (prebuilt placed, already-current, or fail-OPEN to source) +# 1 = fail-CLOSED (checksum / signature) -- the caller MUST refuse to build. +# +# ASCII-ONLY (PS 5.1 mis-parses multibyte in a UTF-8-no-BOM file). + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +# Progress -> STDERR. Write-Host / Write-Output would land on STDOUT under +# `powershell -File` and pollute anything a caller captures. +function Info($m) { [Console]::Error.WriteLine($m) } + +# fail-CLOSED authenticity gate for one binary. Returns $true if acceptable. +function Test-CefHostSignature { + param([Parameter(Mandatory = $true)][string]$Path) + $name = Split-Path -Leaf $Path + $sig = Get-AuthenticodeSignature -LiteralPath $Path + + if ($sig.Status -eq 'Valid') { + $cert = $sig.SignerCertificate + if ($env:FLUTTER_CEF_CERT_THUMBPRINT) { + $want = ($env:FLUTTER_CEF_CERT_THUMBPRINT -replace '\s', '').ToUpperInvariant() + $got = ($cert.Thumbprint -replace '\s', '').ToUpperInvariant() + if ($got -eq $want) { + Info "[flutter_cef] signature OK for $name (thumbprint $got)." + return $true + } + Info "[flutter_cef] $name signed by UNEXPECTED cert (thumbprint $got, wanted $want) - refusing." + return $false + } + elseif ($env:FLUTTER_CEF_CERT_SUBJECT) { + if ($cert.Subject -like "*$($env:FLUTTER_CEF_CERT_SUBJECT)*") { + Info "[flutter_cef] signature OK for $name (subject matches '$($env:FLUTTER_CEF_CERT_SUBJECT)')." + return $true + } + Info "[flutter_cef] $name signed by UNEXPECTED subject ('$($cert.Subject)') - refusing." + return $false + } + else { + Info "[flutter_cef] $name is validly signed but no FLUTTER_CEF_CERT_THUMBPRINT / FLUTTER_CEF_CERT_SUBJECT is set to pin it - refusing (cannot verify provenance)." + return $false + } + } + elseif ($sig.Status -eq 'NotSigned') { + if ($env:FLUTTER_CEF_ALLOW_UNSIGNED_HOST -eq '1') { + Info "===================================================================" + Info "[flutter_cef] WARNING: accepting UNSIGNED cef_host ($name) because" + Info "[flutter_cef] FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1 is set. This" + Info "[flutter_cef] is a TEMPORARY pre-certificate posture. Remove" + Info "[flutter_cef] the opt-in once code signing ships; do NOT rely" + Info "[flutter_cef] on it in a trusted / production pipeline." + Info "===================================================================" + return $true + } + Info "[flutter_cef] $name is UNSIGNED and FLUTTER_CEF_ALLOW_UNSIGNED_HOST is not set - refusing (fail-closed)." + return $false + } + else { + Info "[flutter_cef] $name has an INVALID signature (Status=$($sig.Status)) - refusing (fail-closed)." + return $false + } +} + +# --- Escape hatch: co-dev / build-from-source. Any non-empty value skips. --- +if ($env:FLUTTER_CEF_FROM_SOURCE) { + Info "[flutter_cef] FLUTTER_CEF_FROM_SOURCE set - skipping prebuilt fetch (build from source)." + exit 0 +} + +# --- Self-locate. --- +$here = $PSScriptRoot +$pkg = (Resolve-Path -LiteralPath (Join-Path $here '..')).Path +$cefHost = Join-Path $pkg 'native\cef_host' +$dest = Join-Path $cefHost 'prebuilt' +$stamp = Join-Path $dest 'cef_host_input_hash.txt' + +# --- Compute the input hash (shared with the publisher). --- +. (Join-Path $here 'cef_host_hash.ps1') +$hash = Get-CefHostInputHash -CefHostDir $cefHost + +$base = if ($env:FLUTTER_CEF_GCS_BASE) { $env:FLUTTER_CEF_GCS_BASE } + else { 'https://storage.googleapis.com/flutterflow-downloads/campus_prebuilt_cef_host' } +$file = 'cef_host-windows-x64.zip' +$url = "$base/$hash/$file" +$shaUrl = "$url.sha256" + +# --- Already current? the placed prebuilt carries the hash it was built from. --- +if ((Test-Path -LiteralPath (Join-Path $dest 'cef_host.exe')) -and + (Test-Path -LiteralPath (Join-Path $dest 'cef_host.dll')) -and + (Test-Path -LiteralPath $stamp) -and + ((Get-Content -LiteralPath $stamp -Raw).Trim() -eq $hash)) { + Info "[flutter_cef] prebuilt cef_host already current ($hash) - skipping fetch." + exit 0 +} + +# --- Transport-integrity sidecar. Fail-OPEN if unreachable (nothing published +# for this hash yet -- a fresh native change before CI publishes, or offline +# -> build from source). --- +Info "[flutter_cef] resolving prebuilt cef_host for input hash $hash" +$shaLine = & curl.exe -fsSL --retry 3 --retry-delay 1 $shaUrl +if ($LASTEXITCODE -ne 0 -or -not $shaLine) { + Info "[flutter_cef] no published cef_host for hash $hash ($shaUrl unreachable)." + Info "[flutter_cef] building from source (dev), or CI will publish it shortly." + exit 0 +} +$expected = (([string]$shaLine).Trim() -split '\s+')[0].ToLowerInvariant() + +# --- Download to a per-hash cache (re-use a good cached zip). --- +$cacheRoot = if ($env:FLUTTER_CEF_CACHE) { $env:FLUTTER_CEF_CACHE } + else { Join-Path $env:LOCALAPPDATA 'flutter_cef' } +$cacheDir = Join-Path $cacheRoot "prebuilt\$hash\x64" +New-Item -ItemType Directory -Force $cacheDir | Out-Null +$zip = Join-Path $cacheDir $file + +$needDownload = $true +if (Test-Path -LiteralPath $zip) { + if ((Get-FileHash -Algorithm SHA256 -LiteralPath $zip).Hash.ToLowerInvariant() -eq $expected) { + $needDownload = $false + } +} +if ($needDownload) { + Info "[flutter_cef] downloading prebuilt cef_host: $url" + $part = "$zip.part" + & curl.exe -fL --retry 3 --retry-delay 1 -o $part $url + if ($LASTEXITCODE -ne 0) { + Info "[flutter_cef] download failed - building from source." + Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue + exit 0 + } + $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $part).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + Info "[flutter_cef] SHA256 MISMATCH for $file (expected $expected, got $actual) - refusing." + Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue + exit 1 # fail-CLOSED: never place an unverified host + } + Move-Item -LiteralPath $part -Destination $zip -Force +} + +# --- Extract to a PRIVATE staging dir, verify there, then move into place. A +# consumer can never observe a half-extracted or unverified host. --- +$stage = Join-Path $cacheRoot (".fetch-$PID-" + [System.Guid]::NewGuid().ToString('N')) +try { + New-Item -ItemType Directory -Force $stage | Out-Null + Info "[flutter_cef] extracting prebuilt cef_host..." + Expand-Archive -LiteralPath $zip -DestinationPath $stage -Force + + $exe = Join-Path $stage 'cef_host.exe' + $dll = Join-Path $stage 'cef_host.dll' + if (-not (Test-Path -LiteralPath $exe) -or -not (Test-Path -LiteralPath $dll)) { + Info "[flutter_cef] archive did not contain cef_host.exe + cef_host.dll - refusing." + Remove-Item -LiteralPath $zip -Force -ErrorAction SilentlyContinue + exit 1 + } + + # fail-CLOSED authenticity gate on BOTH binaries (see the header). + if (-not (Test-CefHostSignature -Path $exe) -or -not (Test-CefHostSignature -Path $dll)) { + Info "[flutter_cef] SIGNATURE VERIFICATION FAILED for the fetched cef_host - refusing." + Remove-Item -LiteralPath $zip -Force -ErrorAction SilentlyContinue # poisoned; don't trust the cache + exit 1 + } + + # Verified: move into place. cef_host.exe + cef_host.dll + provenance stamps. + New-Item -ItemType Directory -Force $dest | Out-Null + foreach ($n in @('cef_host.exe', 'cef_host.dll', + 'cef_host_input_hash.txt', 'cef_version.txt', 'cef_host_source_sha.txt')) { + $src = Join-Path $stage $n + if (Test-Path -LiteralPath $src) { + Move-Item -LiteralPath $src -Destination (Join-Path $dest $n) -Force + } + } + # Stamp even if the archive predated the field. + Set-Content -LiteralPath $stamp -Value $hash -Encoding ascii + Info "[flutter_cef] prebuilt cef_host ready ($hash)." + exit 0 +} +finally { + Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/packages/flutter_cef_windows/tool/publish-cef-host.ps1 b/packages/flutter_cef_windows/tool/publish-cef-host.ps1 new file mode 100644 index 0000000..71bb30a --- /dev/null +++ b/packages/flutter_cef_windows/tool/publish-cef-host.ps1 @@ -0,0 +1,169 @@ +# publish-cef-host.ps1 - CI / maintainer publish of a prebuilt Windows cef_host +# to public GCS, keyed by a content hash of the build inputs. Mirrors the macOS +# packages/flutter_cef_macos/tool/publish-cef-host.sh (+ the Makefile +# publish-cef-host target). Run when native/cef_host or the CEF pin changes so +# consumers FETCH a matching host (fetch_cef_host.ps1) instead of compiling it. +# Idempotent: re-running with an unchanged tree is a no-op. +# +# --------------------------------------------------------------------------- +# SIGNING (STUBBED-BUT-REAL) +# --------------------------------------------------------------------------- +# If FLUTTER_CEF_SIGN_THUMBPRINT is set, signtool signs cef_host.exe + +# cef_host.dll (SHA-256 file digest + RFC3161 timestamp) before packaging, then +# verifies them. Otherwise it prints "signing skipped (no cert; unsigned +# artifact)" and publishes UNSIGNED -- expected today, since the certificate is +# not procured yet. The consumer (fetch_cef_host.ps1) only accepts an unsigned +# host when FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1. +# FLIP WHEN SIGNING SHIPS: make signing mandatory (fail if +# FLUTTER_CEF_SIGN_THUMBPRINT is unset) and drop the consumer's unsigned opt-in. +# --------------------------------------------------------------------------- +# +# Requires: gsutil (Google Cloud SDK) authed with object-create on the bucket. +# Env: GCS_BUCKET (default flutterflow-downloads), GCS_PREFIX (default +# campus_prebuilt_cef_host), FLUTTER_CEF_SIGN_THUMBPRINT, FLUTTER_CEF_SIGN_TIMESTAMP_URL. +# +# STDERR-only progress (Info); no intended stdout value. ASCII-ONLY (PS 5.1). + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +function Info($m) { [Console]::Error.WriteLine($m) } +function Fail($m) { Info $m; exit 1 } + +# Locate signtool.exe: PATH first, else the newest x64 copy under the Windows SDK. +function Resolve-Signtool { + $c = Get-Command signtool.exe -ErrorAction SilentlyContinue + if ($c) { return $c.Source } + $roots = @("${env:ProgramFiles(x86)}\Windows Kits\10\bin", "$env:ProgramFiles\Windows Kits\10\bin") + foreach ($r in $roots) { + if (Test-Path -LiteralPath $r) { + $hit = Get-ChildItem -LiteralPath $r -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\' } | + Sort-Object FullName -Descending | Select-Object -First 1 + if ($hit) { return $hit.FullName } + } + } + return $null +} + +# Read the CEF version pin ($CefVersion) out of fetch_cef.ps1 for provenance. +function Get-CefVersionPin { + param([Parameter(Mandatory = $true)][string]$CefHostDir) + foreach ($ln in (Get-Content -LiteralPath (Join-Path $CefHostDir 'fetch_cef.ps1'))) { + if ($ln -match "CefVersion\s*=\s*'([^']+)'") { return $Matches[1] } + } + return 'unknown' +} + +# --- Self-locate. --- +$here = $PSScriptRoot +$pkg = (Resolve-Path -LiteralPath (Join-Path $here '..')).Path +$cefHost = Join-Path $pkg 'native\cef_host' + +# --- Guard: gsutil present. --- +if (-not (Get-Command gsutil -ErrorAction SilentlyContinue)) { + Info "[publish] gsutil not found. Install the Google Cloud SDK, then authenticate:" + Info "[publish] https://cloud.google.com/sdk/docs/install" + Info "[publish] gcloud auth login (needs object-create on the target bucket)" + exit 2 +} + +# --- Content hash / object key (shared with the consumer). --- +. (Join-Path $here 'cef_host_hash.ps1') +$hash = Get-CefHostInputHash -CefHostDir $cefHost +Info "[publish] cef_host input hash: $hash" + +$bucket = if ($env:GCS_BUCKET) { $env:GCS_BUCKET } else { 'flutterflow-downloads' } +$prefix = if ($env:GCS_PREFIX) { $env:GCS_PREFIX } else { 'campus_prebuilt_cef_host' } +$file = 'cef_host-windows-x64.zip' +$dst = "gs://$bucket/$prefix/$hash/$file" + +# --- Idempotency: this exact tree is already published -> nothing to do. --- +# NOTE (flip when signing ships): mirror macOS and download + verify the remote +# artifact's Authenticode signature here before trusting the skip, to defeat a +# planted object pre-staged at a future content hash. Stubbed while unsigned. +& gsutil -q stat $dst +if ($LASTEXITCODE -eq 0) { + Info "[publish] $dst already exists - nothing to do (idempotent skip)." + exit 0 +} + +$work = Join-Path ([System.IO.Path]::GetTempPath()) ("cef_host_publish_" + [System.Guid]::NewGuid().ToString('N')) +$buildDir = Join-Path $work 'build' +$out = Join-Path $work 'out' +try { + New-Item -ItemType Directory -Force $out | Out-Null + + # --- Build cef_host.dll + stage cef_host.exe via the existing build script. --- + Info "[publish] building cef_host via build_cef_host.bat ..." + & (Join-Path $cefHost 'build_cef_host.bat') $buildDir $out + if ($LASTEXITCODE -ne 0) { Fail "[publish] build_cef_host.bat failed (exit $LASTEXITCODE)." } + $exe = Join-Path $out 'cef_host.exe' + $dll = Join-Path $out 'cef_host.dll' + if (-not (Test-Path -LiteralPath $exe) -or -not (Test-Path -LiteralPath $dll)) { + Fail "[publish] build did not produce cef_host.exe + cef_host.dll." + } + + # --- Sign (STUBBED-BUT-REAL). --- + if ($env:FLUTTER_CEF_SIGN_THUMBPRINT) { + $signtool = Resolve-Signtool + if (-not $signtool) { + Fail "[publish] FLUTTER_CEF_SIGN_THUMBPRINT set but signtool.exe not found (install the Windows SDK)." + } + $tsUrl = if ($env:FLUTTER_CEF_SIGN_TIMESTAMP_URL) { $env:FLUTTER_CEF_SIGN_TIMESTAMP_URL } + else { 'http://timestamp.digicert.com' } + Info "[publish] signing cef_host.exe + cef_host.dll (thumbprint $($env:FLUTTER_CEF_SIGN_THUMBPRINT)) ..." + & $signtool sign /sha1 $env:FLUTTER_CEF_SIGN_THUMBPRINT /fd SHA256 /tr $tsUrl /td SHA256 $exe $dll + if ($LASTEXITCODE -ne 0) { Fail "[publish] signtool sign failed (exit $LASTEXITCODE)." } + & $signtool verify /pa $exe $dll + if ($LASTEXITCODE -ne 0) { Fail "[publish] signtool verify failed after signing." } + } + else { + Info "[publish] signing skipped (no cert; unsigned artifact). Set FLUTTER_CEF_SIGN_THUMBPRINT to sign." + } + + # --- Provenance stamps beside the binaries (informational; the URL is the hash). --- + $srcSha = 'unknown' + if (Get-Command git -ErrorAction SilentlyContinue) { + $rev = & git -C $pkg rev-parse HEAD + if ($LASTEXITCODE -eq 0 -and $rev) { $srcSha = ([string]$rev).Trim() } + } + Set-Content -LiteralPath (Join-Path $out 'cef_host_input_hash.txt') -Value $hash -Encoding ascii + Set-Content -LiteralPath (Join-Path $out 'cef_version.txt') -Value (Get-CefVersionPin -CefHostDir $cefHost) -Encoding ascii + Set-Content -LiteralPath (Join-Path $out 'cef_host_source_sha.txt') -Value $srcSha -Encoding ascii + + # --- Package + sha256 sidecar. The zip's own bytes need not be deterministic: + # the URL is keyed by the INPUT hash, and the consumer verifies the download + # against this sidecar. --- + $stage = Join-Path $work 'stage' + New-Item -ItemType Directory -Force $stage | Out-Null + $zip = Join-Path $stage $file + Info "[publish] packaging $file ..." + Compress-Archive -Force -DestinationPath $zip -Path @( + (Join-Path $out 'cef_host.exe'), + (Join-Path $out 'cef_host.dll'), + (Join-Path $out 'cef_host_input_hash.txt'), + (Join-Path $out 'cef_version.txt'), + (Join-Path $out 'cef_host_source_sha.txt') + ) + $zipSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $zip).Hash.ToLowerInvariant() + $shaSidecar = "$zip.sha256" + Set-Content -LiteralPath $shaSidecar -Value ("{0} {1}" -f $zipSha, $file) -Encoding ascii + + # --- Upload (re-check to close a publish race; objects are immutable). --- + & gsutil -q stat $dst + if ($LASTEXITCODE -eq 0) { + Info "[publish] $dst appeared during build - skipping upload." + exit 0 + } + $cacheHdr = 'Cache-Control:public,max-age=31536000,immutable' + & gsutil -h $cacheHdr cp $zip $dst + if ($LASTEXITCODE -ne 0) { Fail "[publish] upload of $dst failed (exit $LASTEXITCODE)." } + & gsutil -h $cacheHdr cp $shaSidecar "$dst.sha256" + if ($LASTEXITCODE -ne 0) { Fail "[publish] upload of $dst.sha256 failed (exit $LASTEXITCODE)." } + Info "[publish] uploaded $dst (zip sha256 $zipSha)." + exit 0 +} +finally { + Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/packages/flutter_cef_windows/windows/CMakeLists.txt b/packages/flutter_cef_windows/windows/CMakeLists.txt index 9b14f6e..a39f83b 100644 --- a/packages/flutter_cef_windows/windows/CMakeLists.txt +++ b/packages/flutter_cef_windows/windows/CMakeLists.txt @@ -114,42 +114,103 @@ if(NOT EXISTS "${FLUTTER_CEF_CEF_ROOT}/cmake") endif() message(STATUS "flutter_cef_windows: resolved CEF_ROOT = ${FLUTTER_CEF_CEF_ROOT}") -# CEF is guaranteed present at this point (resolution FATALs otherwise). -# Invoke build_cef_host.bat DIRECTLY (no explicit `cmd /c`) so paths with -# spaces survive: Ninja already wraps the whole command line in `cmd /c "..."`, -# so a bare `"C:\Users\a b\repo\build_cef_host.bat" "arg" "arg"` becomes the -# safe `cmd /c ""...bat" "arg" "arg""` outer-quoted form (cmd strips only the -# outer pair). An extra explicit `cmd /c` here would nest a second cmd whose -# leading-quote stripping mangles a "C:\Users\a b\..." path. -file(TO_NATIVE_PATH "${CEF_HOST_SRC_DIR}/build_cef_host.bat" _cef_host_bat) -file(TO_NATIVE_PATH "${CEF_HOST_BUILD_DIR}" _cef_host_build_native) -file(TO_NATIVE_PATH "${CEF_HOST_DIST_DIR}" _cef_host_dist_native) -add_custom_command( - OUTPUT - "${CEF_HOST_DIST_DIR}/cef_host.dll" - "${CEF_HOST_DIST_DIR}/cef_host.exe" - COMMAND "${_cef_host_bat}" "${_cef_host_build_native}" "${_cef_host_dist_native}" - DEPENDS - "${CEF_HOST_SRC_DIR}/cef_host_win.cc" - "${CEF_HOST_SRC_DIR}/cef_host_protocol.h" - "${CEF_HOST_SRC_DIR}/CMakeLists.txt" - "${CEF_HOST_SRC_DIR}/build_cef_host.bat" - COMMENT "Building cef_host (CEF OSR host) via build_cef_host.bat" - VERBATIM -) -add_custom_target(flutter_cef_windows_host ALL - DEPENDS - "${CEF_HOST_DIST_DIR}/cef_host.dll" - "${CEF_HOST_DIST_DIR}/cef_host.exe" -) -add_dependencies(${PLUGIN_NAME} flutter_cef_windows_host) +# ---- PREFER a fetched prebuilt cef_host over building from source ---- +# +# At configure time run tool/fetch_cef_host.ps1 (the consumer half of the GCS +# prebuilt model, mirroring macOS). It fail-OPENS (exit 0, no prebuilt placed) +# when nothing is published for this input hash -- today's reality -- so the +# from-source path below still runs. It fails-CLOSED (nonzero) only on a real +# checksum / signature problem, which we propagate as a fatal error rather than +# silently building. The CEF-RUNTIME resolution above (fetch_cef.ps1 / +# FLUTTER_CEF_CEF_ROOT) is separate and untouched: the CEF DLLs are always +# needed beside the app whether the host is prebuilt or from-source. +set(CEF_HOST_PREBUILT_DIR "${CEF_HOST_SRC_DIR}/prebuilt") + +execute_process( + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/../tool/fetch_cef_host.ps1" + RESULT_VARIABLE _fetch_host_result) +if(NOT _fetch_host_result EQUAL 0) + message(FATAL_ERROR + "flutter_cef_windows: fetch_cef_host.ps1 FAILED (exit ${_fetch_host_result}). " + "A published prebuilt cef_host failed checksum / signature verification -- " + "refusing to build. Set FLUTTER_CEF_FROM_SOURCE=1 to bypass the fetch, or " + "investigate the poisoned object / signature.") +endif() + +# Trust the prebuilt only when its provenance stamp matches THIS tree's input +# hash. Guards a stale prebuilt/ dir from an older checkout: fetch fail-opens +# this run (nothing published for the NEW hash) but leaves the OLD binaries in +# place -- without this check we would bundle a host built from different sources. +set(_use_prebuilt_host OFF) +if(EXISTS "${CEF_HOST_PREBUILT_DIR}/cef_host.dll" + AND EXISTS "${CEF_HOST_PREBUILT_DIR}/cef_host.exe" + AND EXISTS "${CEF_HOST_PREBUILT_DIR}/cef_host_input_hash.txt") + execute_process( + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/../tool/cef_host_hash.ps1" + OUTPUT_VARIABLE _cef_host_expected_hash + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _cef_host_hash_result) + file(READ "${CEF_HOST_PREBUILT_DIR}/cef_host_input_hash.txt" _cef_host_stamp) + string(STRIP "${_cef_host_stamp}" _cef_host_stamp) + if(_cef_host_hash_result EQUAL 0 AND _cef_host_expected_hash STREQUAL _cef_host_stamp) + set(_use_prebuilt_host ON) + else() + message(STATUS + "flutter_cef_windows: prebuilt cef_host stamp mismatch " + "(have '${_cef_host_stamp}', want '${_cef_host_expected_hash}') -- " + "ignoring it; building from source.") + endif() +endif() + +if(_use_prebuilt_host) + # Use the fetched, verified ship pair as-is. No build_cef_host.bat, no host + # target -- cef_host.exe + cef_host.dll already exist under prebuilt/. + set(CEF_HOST_SHIP_DIR "${CEF_HOST_PREBUILT_DIR}") + message(STATUS + "flutter_cef_windows: using PREBUILT cef_host (${_cef_host_stamp}) from " + "${CEF_HOST_PREBUILT_DIR}") +else() + # From-source: build cef_host.dll + stage cef_host.exe via build_cef_host.bat. + set(CEF_HOST_SHIP_DIR "${CEF_HOST_DIST_DIR}") + message(STATUS "flutter_cef_windows: no matching prebuilt cef_host; building from source") + # Invoke build_cef_host.bat DIRECTLY (no explicit `cmd /c`) so paths with + # spaces survive: Ninja already wraps the whole command line in `cmd /c "..."`, + # so a bare `"C:\Users\a b\repo\build_cef_host.bat" "arg" "arg"` becomes the + # safe `cmd /c ""...bat" "arg" "arg""` outer-quoted form (cmd strips only the + # outer pair). An extra explicit `cmd /c` here would nest a second cmd whose + # leading-quote stripping mangles a "C:\Users\a b\..." path. + file(TO_NATIVE_PATH "${CEF_HOST_SRC_DIR}/build_cef_host.bat" _cef_host_bat) + file(TO_NATIVE_PATH "${CEF_HOST_BUILD_DIR}" _cef_host_build_native) + file(TO_NATIVE_PATH "${CEF_HOST_DIST_DIR}" _cef_host_dist_native) + add_custom_command( + OUTPUT + "${CEF_HOST_DIST_DIR}/cef_host.dll" + "${CEF_HOST_DIST_DIR}/cef_host.exe" + COMMAND "${_cef_host_bat}" "${_cef_host_build_native}" "${_cef_host_dist_native}" + DEPENDS + "${CEF_HOST_SRC_DIR}/cef_host_win.cc" + "${CEF_HOST_SRC_DIR}/cef_host_protocol.h" + "${CEF_HOST_SRC_DIR}/CMakeLists.txt" + "${CEF_HOST_SRC_DIR}/build_cef_host.bat" + COMMENT "Building cef_host (CEF OSR host) via build_cef_host.bat" + VERBATIM + ) + add_custom_target(flutter_cef_windows_host ALL + DEPENDS + "${CEF_HOST_DIST_DIR}/cef_host.dll" + "${CEF_HOST_DIST_DIR}/cef_host.exe" + ) + add_dependencies(${PLUGIN_NAME} flutter_cef_windows_host) +endif() # Files the app needs beside its exe: the host pair + the CEF runtime. # (Flat CEF layout on Windows; libcef.dll etc. resolve from the exe dir. # Set from what cef_leg (S1) needed to run windowless+GPU.) set(flutter_cef_windows_bundled_libraries - "${CEF_HOST_DIST_DIR}/cef_host.exe" - "${CEF_HOST_DIST_DIR}/cef_host.dll" + "${CEF_HOST_SHIP_DIR}/cef_host.exe" + "${CEF_HOST_SHIP_DIR}/cef_host.dll" "${FLUTTER_CEF_CEF_ROOT}/Release/libcef.dll" "${FLUTTER_CEF_CEF_ROOT}/Release/chrome_elf.dll" "${FLUTTER_CEF_CEF_ROOT}/Release/d3dcompiler_47.dll" From 882b0aa21afd8e9ccb6bd0e64830eaa92678cd80 Mon Sep 17 00:00:00 2001 From: wenkai fan Date: Mon, 27 Jul 2026 16:54:31 -0400 Subject: [PATCH 2/2] feat(windows): sign cef_host via jsign + Google Cloud KMS (reuse FlutterFlow EV cert) publish-cef-host.ps1 gains a GOOGLECLOUD signing mode that reuses the SAME EV code-signing cert + GCloud KMS key the FlutterFlow desktop app already signs with (flutterflow/windows/Makefile): jsign --storetype GOOGLECLOUD against projects/flutterflow-cicd/.../windows-code-sign, auth via 'gcloud auth print-access-token', verified with Get-AuthenticodeSignature. So cef_host is signed by 'FlutterFlow, Inc.' with no new cert to procure and no key material on disk. signtool/thumbprint stays as mode 2; unsigned as mode 3. Set FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE + FLUTTER_CEF_SIGN_CERTFILE to enable. Co-Authored-By: Claude Fable 5 --- .../tool/publish-cef-host.ps1 | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/flutter_cef_windows/tool/publish-cef-host.ps1 b/packages/flutter_cef_windows/tool/publish-cef-host.ps1 index 71bb30a..97d2fa5 100644 --- a/packages/flutter_cef_windows/tool/publish-cef-host.ps1 +++ b/packages/flutter_cef_windows/tool/publish-cef-host.ps1 @@ -6,16 +6,26 @@ # Idempotent: re-running with an unchanged tree is a no-op. # # --------------------------------------------------------------------------- -# SIGNING (STUBBED-BUT-REAL) +# SIGNING (three modes, priority order) # --------------------------------------------------------------------------- -# If FLUTTER_CEF_SIGN_THUMBPRINT is set, signtool signs cef_host.exe + -# cef_host.dll (SHA-256 file digest + RFC3161 timestamp) before packaging, then -# verifies them. Otherwise it prints "signing skipped (no cert; unsigned -# artifact)" and publishes UNSIGNED -- expected today, since the certificate is -# not procured yet. The consumer (fetch_cef_host.ps1) only accepts an unsigned -# host when FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1. -# FLIP WHEN SIGNING SHIPS: make signing mandatory (fail if -# FLUTTER_CEF_SIGN_THUMBPRINT is unset) and drop the consumer's unsigned opt-in. +# 1. GOOGLECLOUD (jsign) -- the FlutterFlow production identity. Set +# FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE (the KMS keyring path) + +# FLUTTER_CEF_SIGN_CERTFILE (the public cert chain .crt). Reuses the SAME +# EV cert + Google Cloud KMS key the FlutterFlow desktop app signs with +# (flutterflow/windows/Makefile) -- so cef_host is signed by "FlutterFlow, +# Inc." No key material on this machine: the private key stays in KMS, auth +# is `gcloud auth print-access-token`. Needs `jsign` + `gcloud` (authed with +# roles/cloudkms.signerVerifier on the key). +# FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE e.g. projects/flutterflow-cicd/locations/ +# us-central1/keyRings/windows-code-sign +# FLUTTER_CEF_SIGN_GCLOUD_ALIAS default windows-code-sign-key/cryptoKeyVersions/1 +# FLUTTER_CEF_SIGN_CERTFILE path to the public cert chain (.crt) +# 2. signtool -- set FLUTTER_CEF_SIGN_THUMBPRINT for a cert in the machine store. +# 3. unsigned -- neither set (pre-cert dev posture); the consumer then requires +# FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1. +# FLIP WHEN SIGNING IS WIRED IN CI: make signing mandatory (fail if no mode is +# set) and drop the consumer's unsigned opt-in. +# Common: FLUTTER_CEF_SIGN_TIMESTAMP_URL (default http://timestamp.digicert.com). # --------------------------------------------------------------------------- # # Requires: gsutil (Google Cloud SDK) authed with object-create on the bucket. @@ -104,22 +114,59 @@ try { Fail "[publish] build did not produce cef_host.exe + cef_host.dll." } - # --- Sign (STUBBED-BUT-REAL). --- - if ($env:FLUTTER_CEF_SIGN_THUMBPRINT) { + # --- Sign. Three modes, in priority order: + # 1. GOOGLECLOUD (jsign, the FlutterFlow production identity): set + # FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE. Reuses the SAME EV cert + Google + # Cloud KMS key the FlutterFlow desktop app signs with (see + # flutterflow/windows/Makefile), so cef_host is signed by "FlutterFlow, + # Inc." No key material touches this machine or repo -- the private key + # stays in KMS; auth is `gcloud auth print-access-token`. Needs `jsign` + # (choco install jsign), `gcloud` authed with roles/cloudkms.signerVerifier + # on the key, and the public cert chain (FLUTTER_CEF_SIGN_CERTFILE). + # 2. signtool + a local cert (FLUTTER_CEF_SIGN_THUMBPRINT) -- for a cert + # installed in the machine store. + # 3. unsigned (neither set) -- the pre-cert dev posture; the consumer then + # requires FLUTTER_CEF_ALLOW_UNSIGNED_HOST=1. + $tsUrl = if ($env:FLUTTER_CEF_SIGN_TIMESTAMP_URL) { $env:FLUTTER_CEF_SIGN_TIMESTAMP_URL } + else { 'http://timestamp.digicert.com' } + if ($env:FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE) { + if (-not (Get-Command jsign -ErrorAction SilentlyContinue)) { + Fail "[publish] FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE set but 'jsign' not found (choco install jsign)." + } + if (-not (Get-Command gcloud -ErrorAction SilentlyContinue)) { + Fail "[publish] jsign GOOGLECLOUD signing needs 'gcloud' on PATH (authed with KMS sign access)." + } + $certfile = $env:FLUTTER_CEF_SIGN_CERTFILE + if (-not $certfile -or -not (Test-Path -LiteralPath $certfile)) { + Fail "[publish] set FLUTTER_CEF_SIGN_CERTFILE to the public cert chain (.crt) for the KMS key." + } + $alias = if ($env:FLUTTER_CEF_SIGN_GCLOUD_ALIAS) { $env:FLUTTER_CEF_SIGN_GCLOUD_ALIAS } + else { 'windows-code-sign-key/cryptoKeyVersions/1' } + $token = (& gcloud auth print-access-token).Trim() + if (-not $token) { Fail "[publish] 'gcloud auth print-access-token' returned nothing (run gcloud auth login)." } + foreach ($f in @($exe, $dll)) { + Info "[publish] jsign GOOGLECLOUD signing $([IO.Path]::GetFileName($f)) ..." + & jsign --storetype GOOGLECLOUD --keystore $env:FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE ` + --alias $alias --certfile $certfile --tsaurl $tsUrl --storepass $token $f + if ($LASTEXITCODE -ne 0) { Fail "[publish] jsign failed on $f (exit $LASTEXITCODE)." } + $sig = Get-AuthenticodeSignature -LiteralPath $f + if ($sig.Status -ne 'Valid') { Fail "[publish] signature not Valid after jsign on $f ($($sig.Status))." } + } + Info "[publish] signed + verified (GOOGLECLOUD KMS, cert $([IO.Path]::GetFileName($certfile)))." + } + elseif ($env:FLUTTER_CEF_SIGN_THUMBPRINT) { $signtool = Resolve-Signtool if (-not $signtool) { Fail "[publish] FLUTTER_CEF_SIGN_THUMBPRINT set but signtool.exe not found (install the Windows SDK)." } - $tsUrl = if ($env:FLUTTER_CEF_SIGN_TIMESTAMP_URL) { $env:FLUTTER_CEF_SIGN_TIMESTAMP_URL } - else { 'http://timestamp.digicert.com' } - Info "[publish] signing cef_host.exe + cef_host.dll (thumbprint $($env:FLUTTER_CEF_SIGN_THUMBPRINT)) ..." + Info "[publish] signtool signing cef_host.exe + cef_host.dll (thumbprint $($env:FLUTTER_CEF_SIGN_THUMBPRINT)) ..." & $signtool sign /sha1 $env:FLUTTER_CEF_SIGN_THUMBPRINT /fd SHA256 /tr $tsUrl /td SHA256 $exe $dll if ($LASTEXITCODE -ne 0) { Fail "[publish] signtool sign failed (exit $LASTEXITCODE)." } & $signtool verify /pa $exe $dll if ($LASTEXITCODE -ne 0) { Fail "[publish] signtool verify failed after signing." } } else { - Info "[publish] signing skipped (no cert; unsigned artifact). Set FLUTTER_CEF_SIGN_THUMBPRINT to sign." + Info "[publish] signing skipped (no cert; unsigned artifact). Set FLUTTER_CEF_SIGN_GCLOUD_KEYSTORE (jsign+KMS) or FLUTTER_CEF_SIGN_THUMBPRINT (signtool) to sign." } # --- Provenance stamps beside the binaries (informational; the URL is the hash). ---