mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,926 @@
|
||||
# Built-in Browser Local HTTPS and Certificate Trust
|
||||
|
||||
## Status
|
||||
|
||||
Implemented reference design for
|
||||
[issue #8454](https://github.com/stablyai/orca/issues/8454).
|
||||
|
||||
The branch includes scheme-less local-dev URL classification, certificate-specific
|
||||
failure copy, `Try HTTPS`, exact per-WebContents certificate grants,
|
||||
`Proceed Anyway (Unsafe)`, local IPC, remote/headless runtime propagation, and
|
||||
focused unit and real-Electron coverage. A session-level request gate contains
|
||||
Chromium's cached certificate continuation so approval cannot leak to sibling
|
||||
tabs, assets, fetches, iframes, or WebSockets. The release gates below remain
|
||||
the acceptance contract for Electron upgrades and cross-platform validation.
|
||||
|
||||
## Summary
|
||||
|
||||
Orca's built-in browser cannot open a local HTTPS development server when the
|
||||
server presents an untrusted certificate. A scheme-less local address also
|
||||
defaults to HTTP, so entering `localhost:3000` does not discover an HTTPS-only
|
||||
server.
|
||||
|
||||
This design keeps HTTP as the default for the scheme-less local forms that
|
||||
already use it, adds a visible `Try HTTPS` recovery action after a failed local
|
||||
HTTP navigation, and adds an explicit `Proceed Anyway (Unsafe)` path for an
|
||||
untrusted local HTTPS certificate. Certificate approval is narrow and
|
||||
temporary: it is bound to one browser WebContents, one secure endpoint, the
|
||||
SHA-256 digest of the exact leaf certificate, and the specific certificate
|
||||
error. A secure endpoint treats `https` and its companion `wss` scheme as one
|
||||
TLS endpoint but still includes the canonical hostname and effective port. No
|
||||
approval state is persisted.
|
||||
|
||||
The certificate decision runs in the process that owns the browser page. Local
|
||||
desktop webviews use the desktop main process; SSH/headless pages use the remote
|
||||
runtime process and expose the decision through runtime RPC.
|
||||
|
||||
## Problem
|
||||
|
||||
There are three independent behaviors behind the current failure.
|
||||
|
||||
1. `normalizeBrowserNavigationUrl` and the tab-create entry classifier prepend
|
||||
`http://` to scheme-less loopback input. Explicit `https://` input is already
|
||||
preserved.
|
||||
2. Electron rejects an untrusted server certificate by default. Orca has no
|
||||
`certificate-error` decision flow, so a self-signed development certificate
|
||||
fails with a Chromium certificate error such as
|
||||
`ERR_CERT_AUTHORITY_INVALID (-202)`.
|
||||
3. Browser load errors are presented as generic connectivity failures. For a
|
||||
loopback URL, Orca advises the user to check that the server is running even
|
||||
when the server responded and only certificate verification failed.
|
||||
|
||||
The same trust limitation exists in the offscreen browser backend used by
|
||||
headless and SSH-owned browser pages. A desktop-only handler would fix the
|
||||
visible local webview while leaving remote browser ownership inconsistent.
|
||||
|
||||
## Goals
|
||||
|
||||
- Show certificate-specific failure copy for main-frame certificate errors.
|
||||
- Let a user explicitly approve an untrusted local development certificate.
|
||||
- Bind approval to the exact browser surface, secure endpoint, leaf certificate
|
||||
digest, and certificate error.
|
||||
- Keep approval in memory only and clear it with the owning browser surface.
|
||||
- Support both desktop webviews and SSH/headless offscreen browser pages.
|
||||
- Give scheme-less local input a discoverable path to HTTPS without silently
|
||||
changing the existing HTTP default.
|
||||
- Preserve the current browser sandbox, navigation allowlist, session profiles,
|
||||
and mixed-content policy.
|
||||
- Keep browser failure state accurate across event-order races, early webview
|
||||
attachment, reload, stale user actions, and remote latency.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not automatically trust a self-signed certificate.
|
||||
- Do not disable `webSecurity`, enable insecure mixed content, or add
|
||||
`--ignore-certificate-errors`.
|
||||
- Do not install a certificate into the operating-system trust store.
|
||||
- Do not persist certificate exceptions across app restarts, browser surface
|
||||
recreation, or session-profile changes.
|
||||
- Do not default every scheme-less localhost URL to HTTPS.
|
||||
- Do not automatically probe HTTPS and then downgrade to HTTP.
|
||||
- Do not add a general-purpose exception flow for public websites in v1.
|
||||
- Do not make every Chromium certificate error overridable in v1.
|
||||
- Do not extend localhost worktree-label proxying to HTTPS targets.
|
||||
|
||||
## Product Decisions
|
||||
|
||||
### Scheme-less local input continues to use HTTP
|
||||
|
||||
`localhost:3000` remains `http://localhost:3000/`. HTTP is still the common
|
||||
development-server default, and changing the inference globally would turn
|
||||
working HTTP servers into certificate or TLS failures.
|
||||
|
||||
When an HTTP navigation to a loopback URL fails, the browser failure overlay
|
||||
offers `Try HTTPS`. The action replaces only the URL scheme and preserves the
|
||||
host, explicit non-default port, path, query, and fragment. An absent port, or
|
||||
an explicit `:80` normalized away by the URL parser, becomes HTTPS's default
|
||||
port 443. Orca does not automatically retry because an invisible
|
||||
cross-protocol retry would add latency, send an unexpected second request, and
|
||||
make downgrade behavior opaque.
|
||||
|
||||
Explicit input remains authoritative:
|
||||
|
||||
- `http://localhost:3000` stays HTTP.
|
||||
- `https://localhost:3000` stays HTTPS.
|
||||
- URLs advertised by a workspace process keep their advertised scheme.
|
||||
|
||||
The shared browser URL domain becomes the source of truth for the existing
|
||||
scheme-less local-dev classification. The tab-create entry classifier calls
|
||||
that shared logic instead of keeping a second regular expression and scheme
|
||||
rule. This consolidation must not reuse the stricter certificate-eligibility
|
||||
predicate: doing so would change current behavior for inputs such as
|
||||
`0.0.0.0`, bracketed non-loopback IPv6 addresses, and `.localhost` subdomains.
|
||||
|
||||
### V1 approval covers untrusted local authorities only
|
||||
|
||||
The first version offers `Proceed Anyway (Unsafe)` only when all of these are
|
||||
true:
|
||||
|
||||
- the failure is for the main frame;
|
||||
- the attempted URL is HTTPS;
|
||||
- Electron reports `net::ERR_CERT_AUTHORITY_INVALID`, corresponding to
|
||||
Chromium load error `-202`;
|
||||
- the host is an eligible loopback host;
|
||||
- the WebContents is a browser surface managed by Orca; and
|
||||
- Orca retained a live pending challenge for that browser surface.
|
||||
|
||||
Other certificate errors receive accurate copy but no bypass action. In
|
||||
particular, v1 does not bypass revoked certificates, malformed certificates,
|
||||
weak keys, date errors, or hostname/SAN mismatches. Expanding the allowlist
|
||||
requires a separate security decision and dedicated tests.
|
||||
|
||||
### Eligible loopback hosts
|
||||
|
||||
The shared predicate accepts:
|
||||
|
||||
- `localhost`, case-insensitively, with an optional trailing dot;
|
||||
- valid subdomains of `.localhost`, also with an optional trailing dot;
|
||||
- valid IPv4 addresses in `127.0.0.0/8`; and
|
||||
- IPv6 loopback `::1`.
|
||||
|
||||
The predicate does not treat `0.0.0.0`, `::`, arbitrary bracketed IPv6
|
||||
addresses, private LAN addresses, or a DNS name that merely resolves to
|
||||
loopback as certificate-bypass eligible. Wildcard bind addresses should already
|
||||
be normalized to a connectable loopback address before navigation.
|
||||
|
||||
SSH local forwards are eligible when their browser-facing URL uses one of these
|
||||
loopback hosts. Forwarding does not fix certificate names: a URL using
|
||||
`127.0.0.1` still needs an IP SAN for `127.0.0.1`; a certificate containing only
|
||||
`localhost` will fail with `ERR_CERT_COMMON_NAME_INVALID`, which v1 does not
|
||||
bypass. Custom advertised DNS names remain subject to normal system trust in
|
||||
v1, even if the name maps to loopback.
|
||||
|
||||
### Approval lifetime
|
||||
|
||||
An accepted grant is scoped to:
|
||||
|
||||
```text
|
||||
browser WebContents ID + secure endpoint + leaf SHA-256 digest + error name
|
||||
```
|
||||
|
||||
The secure endpoint canonicalizes `wss:` to its companion `https:` endpoint and
|
||||
includes the normalized hostname and effective port. This lets an approved
|
||||
local page load same-endpoint HTTPS assets and its WSS development socket. A
|
||||
grant for `https://localhost:3000` does not apply to port 3001, to
|
||||
`https://127.0.0.1:3000`, to another browser tab, or to a replacement
|
||||
certificate.
|
||||
|
||||
Construct the secure endpoint from the parsed request URL: map `https:` and
|
||||
`wss:` to `https:`, lowercase the hostname, remove one optional trailing dot,
|
||||
use the URL parser's canonical IP representation, and use port 443 when the
|
||||
parsed port is empty. Exclude credentials, path, query, and fragment. Reject
|
||||
every other scheme.
|
||||
|
||||
Accepted grants survive reloads and same-surface navigation back to the exact
|
||||
endpoint. They are cleared when the WebContents commits a main-frame navigation
|
||||
whose canonical secure endpoint differs from the grant's endpoint (reusing the
|
||||
per-WebContents navigation-sequence tracking), when the WebContents is destroyed
|
||||
or replaced, the session profile changes, the browser-owning process exits, or
|
||||
the app restarts. Binding grant lifetime to the granting main-frame document
|
||||
prevents a later top-level document in the same tab — for example a navigation
|
||||
to a public site — from silently reusing the loopback grant for a same-endpoint
|
||||
iframe, fetch, or WSS subrequest.
|
||||
Clearing a grant affects future requests; it cannot retroactively terminate a
|
||||
document that already loaded. Grants are not written to settings, browser
|
||||
history, workspace session state, or logs.
|
||||
|
||||
Chromium caches an accepted `certificate-error` continuation across the
|
||||
Electron session partition. Orca therefore retains every endpoint/certificate
|
||||
identity accepted in that partition until the browser-owning process exits and
|
||||
gates all later HTTPS/WSS requests with `webRequest.onBeforeRequest`. Only the
|
||||
WebContents holding the matching grant may reach a cached endpoint. A sibling
|
||||
main-frame navigation receives a synthetic challenge for the known identity;
|
||||
sibling subresources, fetches, iframes, workers, and WSS requests fail silently.
|
||||
|
||||
Because a preflight request gate cannot inspect the certificate that will be
|
||||
presented after Chromium's cache is consulted, one endpoint is locked to the
|
||||
first accepted untrusted leaf identity for that process lifetime. If that
|
||||
endpoint later presents a different untrusted leaf, strict verification rejects
|
||||
it and Orca does not offer another bypass until the browser-owning process
|
||||
restarts. A replacement system-trusted certificate may load normally; the
|
||||
conservative request gate still keeps sibling WebContents blocked until restart.
|
||||
|
||||
## User Experience
|
||||
|
||||
### Local HTTP failure
|
||||
|
||||
For a failed `http://` loopback navigation, keep the current connectivity
|
||||
title and recovery hint. Add `Try HTTPS` when an HTTPS form can be constructed.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Can't reach localhost:3000
|
||||
We couldn't connect to your local server.
|
||||
|
||||
[Try HTTPS] [Retry] [Copy Address] [Open Externally]
|
||||
```
|
||||
|
||||
`Try HTTPS` is the default button because it is the targeted recovery action
|
||||
for a local HTTP failure. Keep the existing `Copy Address` recovery action;
|
||||
`Retry` uses `outline`, while `Copy Address` and `Open Externally` stay quiet
|
||||
secondary actions. `Try HTTPS` does not appear for an already-HTTPS URL or a
|
||||
non-eligible host.
|
||||
|
||||
### Certificate failure
|
||||
|
||||
For `ERR_CERT_AUTHORITY_INVALID (-202)` on an eligible local endpoint, replace
|
||||
the generic connectivity content with:
|
||||
|
||||
```text
|
||||
Connection isn't secure
|
||||
Orca doesn't trust the authority that issued the certificate for localhost:3000.
|
||||
|
||||
For local development, use a trusted local certificate when possible.
|
||||
|
||||
[Open Externally] [Retry] [Copy Address] [Proceed Anyway (Unsafe)]
|
||||
```
|
||||
|
||||
- `Open Externally` is the default safe recovery action when the URL is
|
||||
reachable from the desktop.
|
||||
- When `Open Externally` is unavailable, `Retry` becomes the default action.
|
||||
- `Proceed Anyway (Unsafe)` uses `outline`; `Copy Address` stays quiet.
|
||||
- `Proceed Anyway (Unsafe)` is always visibly labeled; it is not hidden behind
|
||||
a tooltip.
|
||||
- The unsafe action does not use the destructive color. It does not delete or
|
||||
irreversibly mutate user data, and the explicit label carries the warning.
|
||||
- Use a muted `ShieldAlert` icon from `lucide-react`; do not add a warning color
|
||||
or new token.
|
||||
|
||||
For a remote-owned page whose URL is remote localhost, `Open Externally` is
|
||||
hidden because the desktop system browser cannot reach the remote loopback
|
||||
address. It remains available for a desktop-owned page or a locally forwarded
|
||||
URL.
|
||||
|
||||
For certificate errors that v1 does not permit bypassing, show the same
|
||||
certificate-specific title with error-specific body text, `Retry`,
|
||||
`Copy Address`, and an eligible `Open Externally` action. `Copy Address` remains
|
||||
present as a quiet secondary action in every certificate-failure branch,
|
||||
eligible or not, so only `Try HTTPS` and `Proceed Anyway (Unsafe)` toggle in and
|
||||
out across cases. The same default-action rule applies as for `-202`:
|
||||
`Open Externally` is the default when the URL is reachable from the desktop,
|
||||
otherwise `Retry` becomes the default. Do not show the local-server-running
|
||||
hint.
|
||||
|
||||
Use this presentation mapping, inserting the display host into the copy:
|
||||
|
||||
| Chromium code | Body copy | Proceed in v1 |
|
||||
| ----------------------- | -------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `-200` | `The certificate doesn't match {host}.` | No |
|
||||
| `-201` | `The certificate for {host} isn't valid at the current date and time.` | No |
|
||||
| `-202` | `Orca doesn't trust the authority that issued the certificate for {host}.` | Eligible local endpoints only |
|
||||
| Other certificate error | `Orca couldn't verify the certificate for {host}.` | No |
|
||||
|
||||
Keep the raw error name/code available to diagnostics and optional details, but
|
||||
do not make users interpret it to understand the primary failure.
|
||||
|
||||
### Interaction behavior
|
||||
|
||||
- The overlay is persistent inline UI because the user must read and act on the
|
||||
failure; do not use a toast.
|
||||
- The overlay does not steal focus when it appears.
|
||||
- Actions are reachable by Tab and have visible focus rings.
|
||||
- Use `aria-live="polite"`; repeated background failures must not repeatedly
|
||||
announce the same message.
|
||||
- Clicking `Proceed Anyway (Unsafe)` disables the action immediately, and also
|
||||
disables the other overlay actions (`Retry`, `Try HTTPS`, `Copy Address`,
|
||||
`Open Externally`) for the duration of the approval round-trip so a stale
|
||||
click cannot race the controller's own `loadURL`. Re-enable them once a
|
||||
success or typed-failure response returns.
|
||||
- Map every `proceedCertificate` failure reason to an overlay outcome:
|
||||
`expired`, `changed`, `ineligible`, and `missing` keep the overlay open and
|
||||
show the inline recovery message below; `navigated` shows no message because
|
||||
the overlay for that navigation is already cleared. In every non-success case
|
||||
`Proceed Anyway (Unsafe)` stays disabled until a fresh pending challenge is
|
||||
announced rather than re-enabling on its own.
|
||||
- Delay the spinner by 200 ms, matching the style guide's remote-latency rule.
|
||||
After that delay, show the canonical `Loader2` with `Connecting…` and use a
|
||||
fixed button width so the label swap cannot move adjacent actions.
|
||||
- If approval expired or the certificate changed, keep the overlay open and
|
||||
show `The certificate changed or the approval expired. Retry the page.`
|
||||
- A successful main-frame navigation clears the visible certificate failure.
|
||||
|
||||
The UI must use existing `background`, `foreground`, `muted-foreground`,
|
||||
`border`, and `ring` tokens plus the existing shadcn `Button` primitive. No new
|
||||
color, radius, or shadow tier is required. Do not apply a blanket opacity to
|
||||
the overlay's interactive content; use semantic muted text classes so button
|
||||
and focus-ring contrast remains intact. Put every new user-visible string
|
||||
through the existing localization catalog.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Ownership
|
||||
|
||||
The browser-owning main process is the only authority that can accept a
|
||||
certificate challenge.
|
||||
|
||||
- Desktop-owned `<webview>` pages are controlled by the desktop main process.
|
||||
- Headless/SSH-owned pages are controlled by the remote `orca serve` main
|
||||
process through `OffscreenBrowserBackend`.
|
||||
- The renderer only presents a pending challenge and requests approval. It
|
||||
never decides whether a certificate is trusted.
|
||||
|
||||
### New main-process controller
|
||||
|
||||
Add `src/main/browser/browser-certificate-trust-controller.ts` to manage pending
|
||||
certificate challenges, and
|
||||
`src/main/browser/browser-certificate-request-guard.ts` to manage grants plus
|
||||
the session-level cached-certificate request boundary.
|
||||
|
||||
The controller owns:
|
||||
|
||||
```ts
|
||||
type BrowserCertificateFailure = {
|
||||
challengeId: string
|
||||
browserPageId: string
|
||||
errorCode: number | null
|
||||
error: string
|
||||
origin: string
|
||||
displayHost: string
|
||||
canProceed: boolean
|
||||
observedAt: number
|
||||
}
|
||||
|
||||
type PendingBrowserCertificateChallenge = {
|
||||
challengeId: string
|
||||
guestWebContentsId: number
|
||||
browserPageId: string | null
|
||||
navigationSequence: number
|
||||
navigationUrl: string
|
||||
origin: string
|
||||
secureEndpoint: string
|
||||
leafCertificateSha256: string
|
||||
errorCode: number | null
|
||||
error: string
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
type BrowserCertificateGrant = {
|
||||
guestWebContentsId: number
|
||||
secureEndpoint: string
|
||||
leafCertificateSha256: string
|
||||
error: string
|
||||
}
|
||||
```
|
||||
|
||||
Generate challenge IDs with `randomUUID()`. Compute the certificate identity in
|
||||
the browser-owning main process as SHA-256 over the DER bytes of Electron's leaf
|
||||
`certificate.data`; do not depend on the undocumented formatting or algorithm
|
||||
of `certificate.fingerprint`. Renderer and runtime payloads expose no
|
||||
certificate bytes or digest—only the challenge ID, display host, origin, error
|
||||
code, error name, `canProceed`, and observation time.
|
||||
|
||||
Pending challenges expire after five minutes and are bounded to 32 entries.
|
||||
Accepted grants are bounded to 32 entries per browser-owning process and are
|
||||
also removed with their WebContents. Bounds evict the oldest entry and notify
|
||||
the affected UI when a live challenge is evicted. Ordinary cleanup should keep
|
||||
both collections much smaller. Accepted endpoint identities are not evicted:
|
||||
Chromium's corresponding cache cannot be cleared narrowly, so forgetting one
|
||||
would reopen cross-WebContents access until process exit.
|
||||
|
||||
### Certificate event handling
|
||||
|
||||
Register exactly one app-level `certificate-error` listener after Electron is
|
||||
ready but before either a desktop window or the offscreen backend can create a
|
||||
browser page. Every branch must call Electron's callback exactly once; an
|
||||
unexpected exception fails closed with `callback(false)`.
|
||||
|
||||
Electron supplies the certificate error as a string on this event, while
|
||||
`did-fail-load` supplies the Chromium number later. Normalize the string to one
|
||||
canonical error name and map only known names to numbers for presentation. An
|
||||
unknown name keeps `errorCode: null` and is never bypass-eligible.
|
||||
|
||||
For every event:
|
||||
|
||||
1. Resolve the WebContents through `BrowserManager` and reject unmanaged,
|
||||
retired, or popup WebContents.
|
||||
2. Parse the request URL, normalize its secure endpoint, and compute the leaf
|
||||
certificate SHA-256 digest. Invalid data fails closed.
|
||||
3. Compare the normalized error name, endpoint, and digest against an accepted
|
||||
grant for this exact WebContents. On an exact match, first retain the accepted
|
||||
identity in the owning Electron session, then call `event.preventDefault()`
|
||||
and `callback(true)`.
|
||||
4. If no grant matches, reject non-main-frame failures without creating a
|
||||
challenge. Subresources and iframes can consume an existing grant but cannot
|
||||
mint one.
|
||||
5. For a main-frame failure, record a pending challenge only when the URL and
|
||||
error are eligible. Notify the owning page when its page ID is known, then
|
||||
call `callback(false)`.
|
||||
|
||||
Track a monotonically increasing main-frame navigation sequence per
|
||||
WebContents. A new main-frame navigation invalidates its pending challenge in
|
||||
both main and renderer state before the next certificate decision. Repeated
|
||||
identical events within one sequence reuse the challenge ID; a retry or a
|
||||
different navigation gets a new ID. This prevents an old button or compromised
|
||||
renderer from approving a page the user has already left.
|
||||
|
||||
The first failure is always rejected. User approval records a grant and reloads
|
||||
the page; the next certificate event is the one that succeeds.
|
||||
|
||||
Do not leave Electron's callback pending while waiting for user input. Rejecting
|
||||
first gives Orca a normal load-failure lifecycle, avoids an unbounded blocked
|
||||
navigation, and makes stale approval cleanup deterministic.
|
||||
|
||||
Do not use `session.setCertificateVerifyProc`. It applies to an entire session
|
||||
partition, cannot bind a decision to one browser page, and verification results
|
||||
can be cached by Chromium's network service.
|
||||
|
||||
### Session request gate and Electron upgrade guard
|
||||
|
||||
The pinned Electron 43 binary caches `callback(true)` broadly enough that a
|
||||
second WebContents in the same partition can load the endpoint without another
|
||||
`certificate-error` event. The certificate callback alone is therefore not an
|
||||
isolation boundary.
|
||||
|
||||
Install exactly one `webRequest.onBeforeRequest` listener on every Orca browser
|
||||
session before its first page loads. Electron permits only one listener for this
|
||||
event, so the browser session registry owns installation and removal. Once an
|
||||
endpoint identity has been accepted, the listener blocks every HTTPS/WSS
|
||||
request to that endpoint unless `details.webContentsId` has the matching grant.
|
||||
Requests without a WebContents ID, including background/worker traffic, fail
|
||||
closed. A blocked main-frame request recreates the challenge and certificate
|
||||
load error; other resource types are canceled without replacing the current
|
||||
page's overlay.
|
||||
|
||||
The real-Electron integration suite must prove same-tab reload, same-endpoint
|
||||
HTTPS assets and WSS, different-tab documents and subresources, and
|
||||
different-port isolation against the exact Electron version in
|
||||
`pnpm-lock.yaml`. Any Electron upgrade that breaks those tests blocks release
|
||||
until the design is revised; unit mocks are insufficient.
|
||||
|
||||
### BrowserManager integration
|
||||
|
||||
Add a narrow BrowserManager query API instead of exposing its internal maps:
|
||||
|
||||
```ts
|
||||
getManagedBrowserGuestContext(webContentsId: number): {
|
||||
browserPageId: string | null
|
||||
worktreeId: string | null
|
||||
sessionProfileId: string | null
|
||||
owner: 'desktop-webview' | 'offscreen'
|
||||
} | null
|
||||
```
|
||||
|
||||
For guest lifecycle, follow the existing extracted-controller precedent
|
||||
(`browser-grab-session-controller.ts`, `browser-guest-ui.ts`,
|
||||
`browser-download-destination.ts`), all of which BrowserManager drives by
|
||||
calling a plain method on the controller directly from `registerGuest` /
|
||||
`unregisterGuest`. Have BrowserManager call
|
||||
`certificateTrustController.onGuestRegistered({ browserPageId, webContentsId })`
|
||||
and `.onGuestRetired(webContentsId)` from those same bodies, rather than adding a
|
||||
new listener/emitter registration API to BrowserManager — there is no such
|
||||
pub-sub surface in `src/main/browser` today and this single consumer does not
|
||||
warrant introducing one.
|
||||
|
||||
`getManagedBrowserGuestContext` must recognize an attached primary desktop
|
||||
guest before renderer registration, returning a null page ID, while rejecting
|
||||
popup descendants that merely inherited browser policies. It must also
|
||||
recognize offscreen pages after `registerOffscreenGuest` and stop recognizing a
|
||||
guest immediately when BrowserManager retires or unregisters it. The controller
|
||||
re-checks this ownership on every certificate event even if it still has an
|
||||
in-memory grant.
|
||||
|
||||
An attached desktop webview can encounter a certificate error before its
|
||||
document reaches `dom-ready`, which is when BrowserPane currently registers the
|
||||
guest. Keep pending challenges keyed by WebContents ID until registration
|
||||
supplies the page ID, then flush the failure to the renderer.
|
||||
|
||||
BrowserPane should also register idempotently on the webview's `did-attach`
|
||||
event, with `dom-ready` retained as a fallback. `registerGuest` should report
|
||||
whether the main process accepted the registration so a rare attach-policy race
|
||||
can retry at `dom-ready`; the renderer must not cache the WebContents ID as
|
||||
registered until it receives `true`.
|
||||
|
||||
Offscreen pages already register before `loadURL`, so their page ID is normally
|
||||
available when the certificate event fires. Bring their main-frame navigation
|
||||
and `did-fail-load` observation under the same BrowserManager lifecycle used by
|
||||
desktop guests; the current offscreen loader only logs a rejected `loadURL`, so
|
||||
it cannot by itself populate runtime failure snapshots.
|
||||
|
||||
### Renderer state
|
||||
|
||||
Certificate challenges are transient runtime state and must not be added to
|
||||
the persisted `BrowserPage` schema.
|
||||
|
||||
Add a transient map to the browser store:
|
||||
|
||||
```ts
|
||||
browserCertificateFailuresByPageId: Record<string, BrowserCertificateFailure>
|
||||
```
|
||||
|
||||
The workspace session serializer must omit this map. Clear an entry when:
|
||||
|
||||
- the page completes a successful main-frame navigation;
|
||||
- the user starts a different navigation;
|
||||
- the page or workspace closes;
|
||||
- the owning browser WebContents is replaced; or
|
||||
- the main process reports that the challenge expired.
|
||||
|
||||
The existing `BrowserLoadError` remains the persisted diagnostic source. Add a
|
||||
pure Chromium-error classifier for Electron 43's certificate range (`-200`
|
||||
through `-219`, excluding the unused values) so certificate failures still
|
||||
render accurate copy after restore even when no live challenge exists. A live
|
||||
certificate failure's error code takes presentation precedence over a
|
||||
synthesized `-1` Chromium-error-page fallback. Independently, only a live,
|
||||
matching challenge controls whether `Proceed Anyway (Unsafe)` is enabled.
|
||||
|
||||
Use both the page ID and attempted origin when combining the live challenge
|
||||
with `BrowserLoadError`. A stale challenge from a prior navigation must never
|
||||
add a proceed action to a different failure. Main-process navigation-sequence
|
||||
invalidation is the security boundary; renderer cleanup is defense in depth and
|
||||
keeps the UI honest.
|
||||
|
||||
### Local IPC
|
||||
|
||||
Extend the browser preload API with:
|
||||
|
||||
```ts
|
||||
onCertificateFailureChanged(
|
||||
callback: (event: {
|
||||
browserPageId: string
|
||||
failure: BrowserCertificateFailure | null
|
||||
}) => void
|
||||
): () => void
|
||||
|
||||
proceedCertificate(args: {
|
||||
browserPageId: string
|
||||
challengeId: string
|
||||
}): Promise<
|
||||
| { ok: true }
|
||||
| {
|
||||
ok: false
|
||||
reason: 'expired' | 'changed' | 'ineligible' | 'missing' | 'navigated'
|
||||
}
|
||||
>
|
||||
```
|
||||
|
||||
Register the handler with the existing browser IPC group and reuse
|
||||
`isTrustedBrowserRenderer`. The main process resolves the page to its current
|
||||
WebContents and then consumes the exact pending challenge. Renderer-provided
|
||||
origin, error code, or certificate identity are never trusted.
|
||||
|
||||
On success, main consumes the challenge, records the grant, and calls
|
||||
`loadURL` with the challenge's main-owned `navigationUrl`. It must not use
|
||||
`webContents.getURL()`, which may be `chrome-error://chromewebdata/` or a
|
||||
previously committed page. It also must not reload if the page's current
|
||||
navigation sequence no longer matches the challenge. The renderer does not
|
||||
assign `webview.src` as a second side effect.
|
||||
|
||||
### Remote runtime RPC
|
||||
|
||||
The remote browser owner exposes equivalent behavior through:
|
||||
|
||||
```text
|
||||
browser.certificate.proceed
|
||||
```
|
||||
|
||||
with the runtime worktree selector, remote browser page ID, and challenge ID.
|
||||
Advertise support through a new `browser.certificate-trust.v1` capability only
|
||||
when the runtime has both a browser backend and the certificate controller.
|
||||
Keep the method out of the mobile-scope RPC allowlist; it is for an
|
||||
authenticated runtime-scope Orca client presenting browser chrome, not a guest
|
||||
page or unauthenticated caller.
|
||||
|
||||
Extend `RuntimeMobileSessionBrowserTab` compatibly with optional `loadError`
|
||||
and `certificateFailure` fields. Both are emitted from live runtime state.
|
||||
`loadError` keeps the existing `BrowserPage` last-known diagnostic semantics,
|
||||
including workspace persistence; `certificateFailure` is transient and is
|
||||
never written to workspace persistence. Controller changes, challenge expiry,
|
||||
navigation, and offscreen `did-fail-load` must mark the runtime session snapshot
|
||||
dirty so clients do not wait for unrelated state to change. Web clients and
|
||||
desktop clients reconcile these fields through the existing remote page-handle
|
||||
mapping. Each full snapshot replaces the prior transient certificate failure
|
||||
for that page, including clearing it when the field is absent. An older runtime
|
||||
without the capability always clears any previously mirrored proceed action.
|
||||
|
||||
When the active page has a remote owner, BrowserPane routes approval to that
|
||||
environment. It must not call local desktop IPC. If the runtime lacks the new
|
||||
capability, the UI still classifies the certificate failure accurately but does
|
||||
not render `Proceed Anyway (Unsafe)`. `Try HTTPS`, retry, and other navigation
|
||||
actions continue through the existing owner-aware navigation path rather than
|
||||
touching a local webview for a remote-owned page.
|
||||
|
||||
### Scheme recovery
|
||||
|
||||
Add pure functions in the shared browser URL domain:
|
||||
|
||||
```ts
|
||||
classifySchemeLessLocalDevAddress(rawInput: string): URL | null
|
||||
isEligibleLocalCertificateHost(hostname: string): boolean
|
||||
toHttpsRecoveryUrl(rawUrl: string): string | null
|
||||
```
|
||||
|
||||
`classifySchemeLessLocalDevAddress` preserves the exact current HTTP-default
|
||||
input set and is shared by address-bar normalization and tab-create entry. It
|
||||
is deliberately broader than certificate eligibility.
|
||||
|
||||
`isEligibleLocalCertificateHost` accepts a parsed/canonical hostname; normalize
|
||||
case and an optional trailing dot and handle the brackets returned by
|
||||
`URL.hostname` for IPv6. Validate IPv4 octets and DNS label boundaries instead
|
||||
of using a substring or loose suffix check.
|
||||
|
||||
`toHttpsRecoveryUrl` returns a value only for an HTTP URL with an eligible
|
||||
loopback host. It changes `protocol` to `https:` on a parsed `URL` object and
|
||||
preserves credentials, hostname, an explicit non-default port, path, query, and
|
||||
fragment. When no port is present, including when the parsed HTTP URL normalized
|
||||
`:80` away, the result uses HTTPS's default port 443. It never probes either
|
||||
protocol.
|
||||
|
||||
Both address-bar submission and tab-create entry use the shared local-address
|
||||
classifier. Existing advertised workspace-port URLs remain authoritative and
|
||||
require no new probing.
|
||||
|
||||
## Security Model
|
||||
|
||||
### Invariants
|
||||
|
||||
- Default certificate verification remains strict.
|
||||
- Only a trusted Orca renderer or authenticated runtime-scope Orca client can
|
||||
request approval.
|
||||
- The renderer cannot choose the origin, certificate digest, error,
|
||||
WebContents, or eligibility result.
|
||||
- A grant is exact-match only and never applies across tabs, ports,
|
||||
certificates, or browser-owning processes.
|
||||
- Session-cached accepted endpoints remain request-gated until process exit,
|
||||
even after their original WebContents or bounded grant is removed.
|
||||
- Certificate PEM/DER data and certificate digests do not enter workspace
|
||||
state, history, telemetry, or ordinary logs.
|
||||
- Subresource and iframe certificate failures cannot mint an approval action;
|
||||
they can consume an already-approved exact-endpoint grant on the same
|
||||
WebContents.
|
||||
- The proceed affordance exists only in Orca chrome; guest content has no
|
||||
direct approval channel. Main-process challenge validation remains the
|
||||
security boundary and does not assume proof of a physical click.
|
||||
- A remote client cannot approve a certificate in a different runtime
|
||||
environment.
|
||||
- Starting another main-frame navigation invalidates the pending approval in
|
||||
the browser-owning process, not only in renderer state.
|
||||
|
||||
### Threat cases
|
||||
|
||||
| Threat | Required behavior |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Compromised web page sends forged IPC | Guest WebContents has no preload API; trusted-renderer check rejects it. |
|
||||
| Compromised renderer invents a challenge | Main consumes only a server-created pending challenge tied to the current page and WebContents. |
|
||||
| Certificate changes after approval | Leaf-digest mismatch rejects; a second untrusted leaf for that endpoint cannot be bypassed until restart. |
|
||||
| Same host uses a different port | Secure-endpoint mismatch rejects. |
|
||||
| Another tab visits the approved endpoint | WebContents mismatch rejects. |
|
||||
| Approved tab later loads a foreign top document | Grant is invalidated when the main frame navigates to a different endpoint; the new document cannot reuse it. |
|
||||
| Challenge is replayed | Challenge ID is single-use and expires. |
|
||||
| User leaves the failed page before clicking | Navigation-sequence mismatch rejects without reloading the stale URL. |
|
||||
| Approved page opens same-endpoint WSS/HMR | Existing endpoint grant permits it; WSS cannot create a grant by itself. |
|
||||
| Public site presents an untrusted certificate | Host eligibility fails; no proceed action is available. |
|
||||
| Remote page approval is sent locally | Ownership routing and remote page handles select the browser-owning runtime. |
|
||||
| App restarts after approval | No persisted grant exists. |
|
||||
|
||||
## Failure Handling
|
||||
|
||||
- If a certificate event arrives before guest registration, retain it by
|
||||
WebContents ID and flush after registration.
|
||||
- If guest registration never completes, expire the pending challenge without
|
||||
notifying unrelated pages.
|
||||
- If `Proceed` arrives after expiry, navigation, or WebContents replacement,
|
||||
return a typed failure and leave strict verification in place.
|
||||
- If reload fails for a different reason, clear the certificate challenge and
|
||||
present the new load error.
|
||||
- If the same rejected challenge fires repeatedly in one navigation, coalesce
|
||||
it without rotating the challenge ID or stacking announcements.
|
||||
- If the certificate-error listener cannot parse the URL or leaf certificate,
|
||||
reject normally and do not offer approval.
|
||||
- If the remote connection drops during approval, keep the local UI stable and
|
||||
surface the runtime error inline after the latency threshold.
|
||||
- If an unexpected controller error occurs, call Electron's callback once with
|
||||
`false`; never leave the request suspended.
|
||||
- If an accepted main-frame page later opens a new same-endpoint TLS connection,
|
||||
apply the exact grant to that HTTPS/WSS subrequest. Reject unmatched
|
||||
subrequests silently without replacing the main-frame overlay.
|
||||
- If an accepted endpoint presents a different untrusted leaf, reject it without
|
||||
replacing the process-lifetime accepted identity or offering a second bypass.
|
||||
|
||||
## Data and Privacy
|
||||
|
||||
No migration is required.
|
||||
|
||||
Add no new product analytics in v1. Do not record URLs, certificate digests,
|
||||
challenge IDs, certificate subjects, or approval results. If product analytics
|
||||
becomes a requirement later, define coarse actions in the existing typed
|
||||
feature-interaction catalog as a separate reviewed change.
|
||||
|
||||
Existing diagnostics may retain the Chromium numerical error code, as they do
|
||||
for other browser load failures. Debug logging must contain at most the error
|
||||
name, page ID, owner kind, and normalized loopback host class; omit query
|
||||
strings, full URLs, and certificate digests.
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Shared URL tests
|
||||
|
||||
- Explicit HTTP and HTTPS local URLs remain unchanged.
|
||||
- Scheme-less `localhost`, IPv4 loopback, and IPv6 loopback still normalize to
|
||||
HTTP.
|
||||
- Tab-create and address-bar local-address classification stay in parity for
|
||||
the full legacy set, including wildcard and bracketed IPv6 inputs.
|
||||
- The broader scheme-less classifier is not accidentally substituted for the
|
||||
stricter certificate-eligibility predicate.
|
||||
- `toHttpsRecoveryUrl` preserves host, an explicit non-default port, path,
|
||||
query, and fragment; a URL with no parsed port changes from HTTP's default 80
|
||||
to HTTPS's default 443.
|
||||
- `Try HTTPS` is unavailable for HTTPS, file URLs, public hosts, wildcard bind
|
||||
hosts, private LAN hosts, and invalid URLs.
|
||||
- Loopback eligibility covers `localhost`, trailing-dot localhost,
|
||||
`.localhost` subdomains with and without a trailing dot, the full valid IPv4
|
||||
127/8 range, and bracketed or unbracketed `::1` after URL parsing.
|
||||
- Reject malformed IPv4, arbitrary bracketed IPv6, `0.0.0.0`, `::`, and names
|
||||
that only contain the word `localhost`.
|
||||
|
||||
### Main-process trust-controller tests
|
||||
|
||||
- Unmanaged, retired, and popup WebContents are rejected without pending
|
||||
challenges.
|
||||
- A managed `-202` loopback failure creates one pending challenge and calls
|
||||
`callback(false)`.
|
||||
- Every event path calls its callback exactly once; only an accepted grant calls
|
||||
`preventDefault()` and `callback(true)`.
|
||||
- The digest is SHA-256 over DER leaf bytes and is stable across PEM formatting.
|
||||
- An exact accepted grant covers same-endpoint HTTPS assets, WSS, and iframes on
|
||||
the same WebContents without allowing those requests to mint a challenge.
|
||||
- Wrong page, WebContents, origin, port, certificate digest, error type, and
|
||||
challenge ID fail closed.
|
||||
- A token is single-use and expires after five minutes.
|
||||
- Repeated identical failures in one navigation coalesce; a new navigation
|
||||
invalidates the token and rotates the challenge ID.
|
||||
- Approving after navigation returns `navigated` and never reloads the old URL.
|
||||
- Approval loads the main-owned challenged URL, not the current Chromium error
|
||||
URL or a renderer-supplied URL.
|
||||
- Certificate rotation fails closed and cannot replace the process-lifetime
|
||||
accepted endpoint identity.
|
||||
- WebContents destruction removes pending challenges and grants.
|
||||
- Bounds evict the oldest pending challenge/grant.
|
||||
- Registering a page after an early certificate event flushes the event to the
|
||||
correct renderer.
|
||||
- Concurrent tabs and session profiles remain isolated.
|
||||
|
||||
### IPC and runtime tests
|
||||
|
||||
- Browser certificate IPC rejects untrusted senders and malformed arguments.
|
||||
- Renderer-supplied origin or certificate-identity fields are neither accepted
|
||||
nor used.
|
||||
- Local approval reloads only the currently mapped guest.
|
||||
- Remote approval calls the owning environment's RPC with the remote page ID.
|
||||
- Local IPC is not called for a remote-owned page.
|
||||
- Mobile-scope runtime clients cannot call the approval method.
|
||||
- Older runtimes without `browser.certificate-trust.v1` never show an enabled
|
||||
proceed action.
|
||||
- Runtime snapshots carry live load errors and certificate failure state,
|
||||
publish immediately on changes, and do not add certificate challenges to
|
||||
persisted session schemas.
|
||||
- A snapshot that omits or clears a challenge removes the mirrored proceed
|
||||
action rather than leaving a stale one behind.
|
||||
- Two runtime environments with identical page IDs cannot approve each other's
|
||||
challenge.
|
||||
|
||||
### Renderer tests
|
||||
|
||||
- `-202` renders certificate-specific copy and no server-running hint.
|
||||
- A live certificate challenge still wins when Chromium's error-page fallback
|
||||
produced diagnostic code `-1`.
|
||||
- A live eligible challenge renders `Proceed Anyway (Unsafe)`.
|
||||
- A restored `-202` load error without a live challenge remains accurate but
|
||||
cannot proceed.
|
||||
- Other certificate errors render accurate copy without v1 bypass.
|
||||
- Local HTTP failures render `Try HTTPS`; HTTPS and non-loopback failures do not.
|
||||
- Starting a new navigation clears stale certificate state.
|
||||
- Success clears the overlay.
|
||||
- Proceed disables immediately, delays the spinner, and preserves its width.
|
||||
- Expired/changed responses show the inline recovery message.
|
||||
- Keyboard focus and `aria-live` behavior do not regress the address bar.
|
||||
- `Copy Address` remains available, interactive content is not blanket-dimmed,
|
||||
and all new strings are present in the localization catalog.
|
||||
|
||||
### Electron integration
|
||||
|
||||
Run a local HTTPS server with a generated certificate whose SAN includes
|
||||
`localhost` but whose authority is not trusted.
|
||||
|
||||
1. `https://localhost:<port>` fails with `-202` and shows certificate copy.
|
||||
2. `Proceed Anyway (Unsafe)` reloads and renders the page.
|
||||
3. HTTPS assets and a WSS echo/HMR endpoint on the same host and port load after
|
||||
approval; a different-port asset remains blocked.
|
||||
4. Reload in the same tab remains allowed without broadening the grant.
|
||||
5. A second tab using the same session profile, origin, and certificate remains
|
||||
blocked. This is a release-blocking assertion against the pinned Electron
|
||||
binary, not a mocked controller test.
|
||||
6. Navigating away before clicking makes the old challenge unusable and does
|
||||
not pull the tab back to the failed URL.
|
||||
7. Replacing the certificate keeps the original tab blocked and offers no
|
||||
second bypass until the browser-owning process restarts.
|
||||
8. Closing and recreating the browser surface removes the grant.
|
||||
9. Restarting Orca removes the grant.
|
||||
10. A system-trusted local certificate loads without an interstitial.
|
||||
11. A public/non-loopback untrusted origin has no proceed action.
|
||||
12. A hostname/SAN mismatch shows certificate copy but no proceed action.
|
||||
13. `localhost:<port>` first attempts HTTP and its failure offers `Try HTTPS`.
|
||||
|
||||
Repeat the ownership-sensitive cases for:
|
||||
|
||||
- a local desktop webview;
|
||||
- an SSH local port forward opened in the desktop browser, using a certificate
|
||||
whose SAN matches the browser-facing forwarded hostname; and
|
||||
- a headless/offscreen browser page owned by `orca serve` under Linux/Xvfb.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Add the shared scheme-less local classifier, strict certificate-host
|
||||
predicate, secure-endpoint canonicalizer, and HTTPS-recovery function with
|
||||
focused tests.
|
||||
2. Consolidate address-bar and tab-create local classification without changing
|
||||
current scheme defaults.
|
||||
3. Add BrowserManager ownership/registration/retirement hooks, early
|
||||
`did-attach` registration with a `dom-ready` fallback, and offscreen load
|
||||
failure observation.
|
||||
4. Add shared certificate failure/result types and the main-process trust
|
||||
controller, including navigation-sequence invalidation and the app-level
|
||||
listener. Its certificate-event handling resolves ownership through the
|
||||
BrowserManager hooks from step 3, so those must land first.
|
||||
5. Add local IPC, preload APIs, and transient renderer state.
|
||||
6. Add the certificate-specific classification and copy as new exported
|
||||
functions/branches in the existing `browser-notices.ts` (which already owns
|
||||
load-failure copy via `formatLoadFailureDescription` /
|
||||
`formatLoadFailureRecoveryHint`), and extract only the overlay JSX into a
|
||||
focused `browser-load-failure-overlay.tsx` module instead of growing the
|
||||
already grandfathered `BrowserPane.tsx`; do not add or extend a max-lines
|
||||
disable.
|
||||
7. Add certificate-specific and `Try HTTPS` UI, preserve existing recovery
|
||||
actions, and update the localization catalog.
|
||||
8. Add the offscreen/runtime capability, live snapshot fields and publication,
|
||||
and approval RPC.
|
||||
9. Add unit, IPC, renderer, runtime, and real-Electron integration coverage.
|
||||
10. Run formatting, typecheck, lint, localization verification, targeted tests,
|
||||
and cross-platform manual validation.
|
||||
|
||||
## Rollout
|
||||
|
||||
No feature flag is required for accurate error copy or `Try HTTPS` because both
|
||||
paths preserve strict certificate verification.
|
||||
|
||||
The proceed path should ship only after desktop and offscreen ownership tests
|
||||
pass. If remote runtime support cannot land in the same release, gate the
|
||||
button on `browser.certificate-trust.v1`; local desktop support may ship while
|
||||
older/remote owners retain accurate error copy and trusted-CA guidance.
|
||||
|
||||
Do not close issue #8454 until a packaged build has been validated against a
|
||||
real self-signed local HTTPS server. Unit mocks alone do not establish Electron
|
||||
certificate-event ordering.
|
||||
|
||||
## UI Quality Bar
|
||||
|
||||
- Follow `docs/STYLEGUIDE.md` and the adjacent browser failure overlay.
|
||||
- Use only existing semantic tokens, shadcn buttons, and lucide icons.
|
||||
- Keep the overlay quiet and monochrome; certificate failure must not introduce
|
||||
an amber warning treatment.
|
||||
- Copy must distinguish connection, DNS, and certificate failures without
|
||||
claiming more than the Chromium error proves.
|
||||
- Buttons must remain on one line at supported pane widths or wrap as one
|
||||
deliberate group without overlap.
|
||||
- Validate light/dark mode, macOS/Windows/Linux font metrics, and 200 ms remote
|
||||
latency.
|
||||
- Capture review evidence for local HTTP failure, eligible certificate failure,
|
||||
ineligible certificate failure, accepted certificate, and the same browser
|
||||
chrome after recovery.
|
||||
|
||||
## Release Gates and Open Risks
|
||||
|
||||
- The pinned Electron binary must prove that the session request gate blocks a
|
||||
second tab, sibling HTTPS subresources/WSS, and a second port even when
|
||||
Chromium does not re-emit `certificate-error`. If any isolation assertion
|
||||
fails, omit the proceed path or redesign it around a dedicated ephemeral
|
||||
partition; do not silently widen the grant.
|
||||
- Same-endpoint WSS and fresh HTTPS subresource connections must work after
|
||||
approval. A main document that renders while HMR/API traffic remains blocked
|
||||
does not fix the local-development use case.
|
||||
- A remote/headless certificate failure must reach the client as both accurate
|
||||
load-error copy and a live challenge. Logging a rejected offscreen `loadURL`
|
||||
is not sufficient.
|
||||
- Stale approval must be rejected in the browser-owning process after any new
|
||||
main-frame navigation, including when renderer cleanup or a remote snapshot
|
||||
is delayed.
|
||||
- SSH-forward validation must use a certificate whose SAN matches the
|
||||
browser-facing hostname. Authority bypass must not mask a hostname mismatch.
|
||||
- The app-level listener and real-certificate tests must pass on macOS, Windows,
|
||||
and Linux/Xvfb. Platform trust-store differences do not justify enabling a
|
||||
broader error allowlist.
|
||||
|
||||
## Review Decisions and Follow-ups
|
||||
|
||||
### From 2026-07-12 review
|
||||
|
||||
- The integrated browser is part of the development workflow (including
|
||||
automation, annotations, and remote ownership), so `Open Externally` and trusted
|
||||
local-certificate guidance remain safer alternatives but do not replace an
|
||||
explicit in-browser decision.
|
||||
- V1 intentionally limits approval to `ERR_CERT_AUTHORITY_INVALID (-202)`.
|
||||
Hostname/SAN, date, revoked, and malformed-certificate failures remain blocked;
|
||||
broadening that set requires a separate security review and real-certificate
|
||||
tests.
|
||||
- Raw error details remain available to existing diagnostics rather than adding a
|
||||
new disclosure affordance to the quiet recovery overlay.
|
||||
Reference in New Issue
Block a user