feat(browser): enable the Touch ID passkey authenticator on signed macOS builds

Sites gate passkey sign-in on isUserVerifyingPlatformAuthenticatorAvailable(),
which Electron reports as false until the app opts into the Touch ID
authenticator. GitHub's two-factor page then shows 'partial passkey support'
and the credential request waits on USB keys for Chromium's three-minute floor.

Configure the authenticator after ready from the keychain group in the signed
entitlements, render that entitlement at packaging time from APPLE_TEAM_ID, and
embed the Developer ID provisioning profile that macOS requires before it will
launch a binary claiming keychain-access-groups. Builds without the profile keep
the plain entitlements and today's behavior.
This commit is contained in:
Jinwoo-H
2026-09-07 01:47:34 -04:00
parent f1d8545024
commit febd6a4e64
12 changed files with 635 additions and 3 deletions
+12
View File
@@ -205,6 +205,18 @@ jobs:
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
# Why: the Touch ID passkey authenticator needs a restricted keychain entitlement
# that only an embedded Developer ID provisioning profile can authorise. Without
# the profile the packager keeps the plain entitlements and passkeys stay off.
- name: Materialize macOS provisioning profile
if: env.MAC_PROVISIONING_PROFILE_BASE64 != ''
shell: bash
run: |
printf '%s' "$MAC_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/orca.provisionprofile"
echo "ORCA_MAC_PROVISIONING_PROFILE=$RUNNER_TEMP/orca.provisionprofile" >> "$GITHUB_ENV"
env:
MAC_PROVISIONING_PROFILE_BASE64: ${{ secrets.MAC_PROVISIONING_PROFILE }}
# Why: signing is what makes an adhoc build installable over an existing
# Orca, so a missing cert must fail here rather than after a 20-minute build.
- name: Verify macOS signing environment
+12
View File
@@ -176,6 +176,18 @@ jobs:
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
# Why: the Touch ID passkey authenticator needs a restricted keychain entitlement
# that only an embedded Developer ID provisioning profile can authorise. Without
# the profile the packager keeps the plain entitlements and passkeys stay off.
- name: Materialize macOS provisioning profile
if: steps.freshness.outputs.should_build == 'true' && env.MAC_PROVISIONING_PROFILE_BASE64 != ''
shell: bash
run: |
printf '%s' "$MAC_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/orca.provisionprofile"
echo "ORCA_MAC_PROVISIONING_PROFILE=$RUNNER_TEMP/orca.provisionprofile" >> "$GITHUB_ENV"
env:
MAC_PROVISIONING_PROFILE_BASE64: ${{ secrets.MAC_PROVISIONING_PROFILE }}
# Why: signing is what makes a daily installable over an existing Orca, so
# a missing cert must fail here rather than after a 20-minute build.
- name: Verify macOS signing environment
+12
View File
@@ -182,6 +182,18 @@ jobs:
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
# Why: the Touch ID passkey authenticator needs a restricted keychain entitlement
# that only an embedded Developer ID provisioning profile can authorise. Without
# the profile the packager keeps the plain entitlements and passkeys stay off.
- name: Materialize macOS provisioning profile
if: env.MAC_PROVISIONING_PROFILE_BASE64 != ''
shell: bash
run: |
printf '%s' "$MAC_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/orca.provisionprofile"
echo "ORCA_MAC_PROVISIONING_PROFILE=$RUNNER_TEMP/orca.provisionprofile" >> "$GITHUB_ENV"
env:
MAC_PROVISIONING_PROFILE_BASE64: ${{ secrets.MAC_PROVISIONING_PROFILE }}
# Why: signing is what makes an hourly installable over an existing Orca, so
# a missing cert must fail here rather than after a 20-minute build.
- name: Verify macOS signing environment
+12
View File
@@ -72,6 +72,18 @@ jobs:
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
# Why: the Touch ID passkey authenticator needs a restricted keychain entitlement
# that only an embedded Developer ID provisioning profile can authorise. Without
# the profile the packager keeps the plain entitlements and passkeys stay off.
- name: Materialize macOS provisioning profile
if: env.MAC_PROVISIONING_PROFILE_BASE64 != ''
shell: bash
run: |
printf '%s' "$MAC_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/orca.provisionprofile"
echo "ORCA_MAC_PROVISIONING_PROFILE=$RUNNER_TEMP/orca.provisionprofile" >> "$GITHUB_ENV"
env:
MAC_PROVISIONING_PROFILE_BASE64: ${{ secrets.MAC_PROVISIONING_PROFILE }}
- name: Verify macOS signing environment
run: node config/scripts/verify-macos-release-env.mjs
env:
+13 -2
View File
@@ -20,6 +20,7 @@ const {
const { verifySkillsCliRuntime } = require('./scripts/verify-skills-cli-runtime.cjs')
const { verifyStaticAppImagePackage } = require('./scripts/static-appimage-package-contract.cjs')
const { signWindowsUninstallerViaSignPath } = require('./scripts/windows-uninstaller-signing.cjs')
const { resolveMacWebAuthnSigning } = require('./scripts/mac-webauthn-signing.cjs')
// Why: dev-channel builds must carry the *release* identity — same bundle id,
// Developer ID signature, and notarization ticket — or Squirrel.Mac refuses to
@@ -64,6 +65,15 @@ const devChannelRepo = isHourlyChannel
? 'orca-adhoc'
: null
const appId = 'com.stablyai.orca'
const MAC_BASE_ENTITLEMENTS = 'resources/build/entitlements.mac.plist'
// Why: the Touch ID passkey authenticator needs a restricted keychain entitlement that
// only a provisioning profile can authorise; see scripts/mac-webauthn-signing.cjs.
const macWebAuthnSigning = resolveMacWebAuthnSigning({
repoRoot: resolve(__dirname, '..'),
isMacRelease,
appId,
baseEntitlementsPath: MAC_BASE_ENTITLEMENTS
})
const featureWallResources = {
from: 'resources/onboarding/feature-wall',
to: 'onboarding/feature-wall'
@@ -460,8 +470,9 @@ module.exports = {
rank: 'Alternate'
})),
icon: 'resources/build/icon.icns',
entitlements: 'resources/build/entitlements.mac.plist',
entitlementsInherit: 'resources/build/entitlements.mac.plist',
entitlements: macWebAuthnSigning?.entitlements ?? MAC_BASE_ENTITLEMENTS,
entitlementsInherit: MAC_BASE_ENTITLEMENTS,
...(macWebAuthnSigning ? { provisioningProfile: macWebAuthnSigning.provisioningProfile } : {}),
extendInfo: {
NSAppleEventsUsageDescription:
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
@@ -150,3 +150,73 @@ describe('electron-builder mac channel config', () => {
})
})
})
describe('electron-builder mac passkey signing', () => {
const PROFILE_ENV = ['ORCA_MAC_PROVISIONING_PROFILE', 'APPLE_TEAM_ID']
function withProfileEnv(env, assert) {
const original = Object.fromEntries(PROFILE_ENV.map((key) => [key, process.env[key]]))
try {
for (const key of PROFILE_ENV) {
delete process.env[key]
}
withEnv(env, assert)
} finally {
for (const [key, value] of Object.entries(original)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
}
}
// Why: keychain-access-groups is a restricted entitlement, and a binary that claims
// it without an embedded profile is killed at launch. No profile means no claim.
it('keeps the plain entitlements when no provisioning profile is supplied', () => {
withProfileEnv({ ORCA_MAC_RELEASE: '1', APPLE_TEAM_ID: 'ABCDE12345' }, (config) => {
expect(config.mac.entitlements).toBe('resources/build/entitlements.mac.plist')
expect(config.mac.provisioningProfile).toBeUndefined()
})
})
it('signs release builds with the webauthn keychain group and embeds the profile', async () => {
const { mkdtemp, readFile, writeFile } = await import('node:fs/promises')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = await mkdtemp(join(tmpdir(), 'orca-profile-'))
const profile = join(dir, 'orca.provisionprofile')
await writeFile(profile, 'profile')
let rendered
withProfileEnv(
{
ORCA_MAC_RELEASE: '1',
APPLE_TEAM_ID: 'ABCDE12345',
ORCA_MAC_PROVISIONING_PROFILE: profile
},
(config) => {
expect(config.mac.provisioningProfile).toBe(profile)
expect(config.mac.entitlementsInherit).toBe('resources/build/entitlements.mac.plist')
rendered = config.mac.entitlements
}
)
expect(await readFile(rendered, 'utf8')).toContain('ABCDE12345.com.stablyai.orca.webauthn')
})
it('never claims the keychain group on local builds', async () => {
const { mkdtemp, writeFile } = await import('node:fs/promises')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = await mkdtemp(join(tmpdir(), 'orca-profile-'))
const profile = join(dir, 'orca.provisionprofile')
await writeFile(profile, 'profile')
withProfileEnv(
{ APPLE_TEAM_ID: 'ABCDE12345', ORCA_MAC_PROVISIONING_PROFILE: profile },
(config) => {
expect(config.mac.entitlements).toBe('resources/build/entitlements.mac.plist')
expect(config.mac.provisioningProfile).toBeUndefined()
}
)
})
})
+90
View File
@@ -0,0 +1,90 @@
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
// Why this exists: the Touch ID WebAuthn authenticator only works when the signed
// binary carries a `keychain-access-groups` entry for `<TEAM>.<bundle>.webauthn`,
// which `codesign` refuses to templatize (`$(TeamIdentifierPrefix)` is left
// verbatim), so the group is spliced in here from the team id at packaging time.
// The entry is restricted: macOS kills at launch any binary that claims it
// without an embedded provisioning profile authorising the group. Both inputs
// therefore travel together, and a build with neither keeps the base
// entitlements and simply never offers the authenticator.
const WEBAUTHN_KEYCHAIN_GROUP_SUFFIX = '.webauthn'
/** Inserts the identity and keychain-group keys before the closing `</dict>`. */
function renderWebAuthnEntitlements(baseEntitlementsXml, { teamId, appId }) {
if (!/^[A-Z0-9]{10}$/.test(teamId)) {
throw new Error(
`APPLE_TEAM_ID must be a 10-character Apple team id, got ${JSON.stringify(teamId)}`
)
}
if (baseEntitlementsXml.includes('keychain-access-groups')) {
throw new Error('base macOS entitlements must not already declare keychain-access-groups')
}
const closingIndex = baseEntitlementsXml.lastIndexOf('</dict>')
if (closingIndex === -1) {
throw new Error('base macOS entitlements plist has no closing </dict>')
}
const inserted = [
'\t<key>com.apple.application-identifier</key>',
`\t<string>${teamId}.${appId}</string>`,
'\t<key>com.apple.developer.team-identifier</key>',
`\t<string>${teamId}</string>`,
'\t<key>keychain-access-groups</key>',
'\t<array>',
`\t\t<string>${teamId}.${appId}${WEBAUTHN_KEYCHAIN_GROUP_SUFFIX}</string>`,
'\t</array>',
''
].join('\n')
return (
baseEntitlementsXml.slice(0, closingIndex) + inserted + baseEntitlementsXml.slice(closingIndex)
)
}
/**
* Resolves the mac signing inputs for one packaging run.
* Returns `null` when the build is not a signed release or the provisioning
* profile is absent, which keeps local and ad-hoc builds launchable.
*/
function resolveMacWebAuthnSigning({
repoRoot,
isMacRelease,
appId,
baseEntitlementsPath,
env = process.env,
outputDir = join(repoRoot, 'out', 'mac-signing')
}) {
if (!isMacRelease) {
return null
}
const profilePath = env.ORCA_MAC_PROVISIONING_PROFILE
const teamId = env.APPLE_TEAM_ID
if (!profilePath || !teamId) {
return null
}
const absoluteProfilePath = resolve(repoRoot, profilePath)
if (!existsSync(absoluteProfilePath)) {
throw new Error(
`ORCA_MAC_PROVISIONING_PROFILE points at a missing file: ${absoluteProfilePath}`
)
}
const entitlementsXml = renderWebAuthnEntitlements(
readFileSync(resolve(repoRoot, baseEntitlementsPath), 'utf8'),
{ teamId, appId }
)
mkdirSync(outputDir, { recursive: true })
const entitlementsPath = join(outputDir, 'entitlements.mac.webauthn.plist')
writeFileSync(entitlementsPath, entitlementsXml, 'utf8')
return {
entitlements: entitlementsPath,
provisioningProfile: absoluteProfilePath,
keychainAccessGroup: `${teamId}.${appId}${WEBAUTHN_KEYCHAIN_GROUP_SUFFIX}`
}
}
module.exports = {
WEBAUTHN_KEYCHAIN_GROUP_SUFFIX,
renderWebAuthnEntitlements,
resolveMacWebAuthnSigning
}
@@ -0,0 +1,134 @@
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
renderWebAuthnEntitlements,
resolveMacWebAuthnSigning
} = require('./mac-webauthn-signing.cjs')
const BASE_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
\t<key>com.apple.security.cs.allow-jit</key>
\t<true/>
</dict>
</plist>
`
const tempDirs = []
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
})
async function makeRepo() {
const root = await mkdtemp(join(tmpdir(), 'orca-mac-signing-'))
tempDirs.push(root)
await writeFile(join(root, 'entitlements.mac.plist'), BASE_PLIST)
await writeFile(join(root, 'orca.provisionprofile'), 'profile-bytes')
return root
}
describe('renderWebAuthnEntitlements', () => {
it('adds the team-scoped identity and webauthn keychain group to the base plist', () => {
const xml = renderWebAuthnEntitlements(BASE_PLIST, {
teamId: 'ABCDE12345',
appId: 'com.stablyai.orca'
})
expect(xml).toContain('<key>com.apple.security.cs.allow-jit</key>')
expect(xml).toContain(
'<key>com.apple.application-identifier</key>\n\t<string>ABCDE12345.com.stablyai.orca</string>'
)
expect(xml).toContain(
'<key>com.apple.developer.team-identifier</key>\n\t<string>ABCDE12345</string>'
)
expect(xml).toContain('<string>ABCDE12345.com.stablyai.orca.webauthn</string>')
expect(xml.trimEnd().endsWith('</plist>')).toBe(true)
})
it('rejects a team id that is not a 10-character Apple team id', () => {
expect(() =>
renderWebAuthnEntitlements(BASE_PLIST, { teamId: 'Lovecast LLC', appId: 'com.stablyai.orca' })
).toThrow(/APPLE_TEAM_ID/)
})
it('refuses to double-declare keychain-access-groups', () => {
const xml = renderWebAuthnEntitlements(BASE_PLIST, { teamId: 'ABCDE12345', appId: 'x' })
expect(() => renderWebAuthnEntitlements(xml, { teamId: 'ABCDE12345', appId: 'x' })).toThrow(
/already declare/
)
})
})
describe('resolveMacWebAuthnSigning', () => {
const baseOptions = (repoRoot, env) => ({
repoRoot,
isMacRelease: true,
appId: 'com.stablyai.orca',
baseEntitlementsPath: 'entitlements.mac.plist',
outputDir: join(repoRoot, 'out'),
env
})
it('writes a rendered entitlements file and resolves the profile path', async () => {
const root = await makeRepo()
const signing = resolveMacWebAuthnSigning(
baseOptions(root, {
APPLE_TEAM_ID: 'ABCDE12345',
ORCA_MAC_PROVISIONING_PROFILE: 'orca.provisionprofile'
})
)
expect(signing).toEqual({
entitlements: join(root, 'out', 'entitlements.mac.webauthn.plist'),
provisioningProfile: join(root, 'orca.provisionprofile'),
keychainAccessGroup: 'ABCDE12345.com.stablyai.orca.webauthn'
})
expect(await readFile(signing.entitlements, 'utf8')).toContain(
'ABCDE12345.com.stablyai.orca.webauthn'
)
})
// Why: the restricted entitlement without its profile is a launch-time SIGKILL,
// so a release missing the profile must fall back to the plain entitlements.
it('returns null when the profile env is absent', async () => {
const root = await makeRepo()
expect(resolveMacWebAuthnSigning(baseOptions(root, { APPLE_TEAM_ID: 'ABCDE12345' }))).toBeNull()
})
it('returns null when the team id env is absent', async () => {
const root = await makeRepo()
expect(
resolveMacWebAuthnSigning(
baseOptions(root, { ORCA_MAC_PROVISIONING_PROFILE: 'orca.provisionprofile' })
)
).toBeNull()
})
it('returns null for non-release builds even with every input present', async () => {
const root = await makeRepo()
expect(
resolveMacWebAuthnSigning({
...baseOptions(root, {
APPLE_TEAM_ID: 'ABCDE12345',
ORCA_MAC_PROVISIONING_PROFILE: 'orca.provisionprofile'
}),
isMacRelease: false
})
).toBeNull()
})
it('fails loudly when the profile path points nowhere', async () => {
const root = await makeRepo()
expect(() =>
resolveMacWebAuthnSigning(
baseOptions(root, {
APPLE_TEAM_ID: 'ABCDE12345',
ORCA_MAC_PROVISIONING_PROFILE: 'missing.provisionprofile'
})
)
).toThrow(/missing file/)
})
})
+9 -1
View File
@@ -15,7 +15,15 @@ Browser-use profiles let you run the Orca browser with a specific identity — a
Import cookies from Chrome or Edge (or from a cookie file) into a profile from Settings or the browser toolbar. Orca replaces existing cookies only for domains included in the import, so sign-ins for unrelated sites in the same profile stay intact. Google cookies are excluded — import menus show **Google logins aren't imported** and tell you to **Sign in to Google directly in Orca.** After an import that skipped Google cookies, a separate warning names the host that ran the import: open a browser in Orca on that host with the same profile, then sign in.
When a site requests a discoverable passkey from a USB security key and the key offers multiple accounts, Orca opens an account picker instead of silently canceling the sign-in. Choose the account you want or cancel the request. Platform passkeys stored by the operating system are not available in Orca yet; this flow is for external FIDO security keys.
## Passkeys
Sites can sign you in with a passkey inside Orca's browser. What is available depends on the platform:
- **macOS**: signed Orca builds offer a Touch ID passkey authenticator. When a site asks you to register a passkey, macOS shows the Touch ID sheet and stores the passkey in this Mac's Secure Enclave, scoped to the browser profile that created it. These passkeys are device-bound: they do not sync through iCloud Keychain, and passkeys you already keep in iCloud Keychain, Safari, or a password manager are not offered to sites in Orca. Register a new passkey for the site in Orca when it offers to, or use the site's other sign-in method.
- **Windows**: Windows handles passkey requests natively, so Windows Hello, security keys, and a phone via QR code all work as they do in other browsers.
- **Linux**: USB security keys only.
USB security keys work on every platform. When a security key or Touch ID offers more than one account for a site, Orca opens an account picker instead of silently canceling the sign-in. Choose the account you want or cancel the request.
## Use a profile
@@ -0,0 +1,166 @@
import { describe, expect, it, vi } from 'vitest'
import {
enablePlatformPasskeys,
readWebAuthnKeychainAccessGroup
} from './browser-platform-passkeys-macos'
const SIGNED_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>com.apple.application-identifier</key><string>TEAM123456.com.stablyai.orca</string>
<key>keychain-access-groups</key>
<array>
<string>TEAM123456.com.stablyai.orca</string>
<string>TEAM123456.com.stablyai.orca.webauthn</string>
</array>
<key>com.apple.security.cs.allow-jit</key><true/>
</dict></plist>`
type ConfigureWebAuthn = (options: {
touchID: { keychainAccessGroup: string; promptReason?: string }
}) => void
function packagedApp(): {
isPackaged: boolean
configureWebAuthn: ReturnType<typeof vi.fn<ConfigureWebAuthn>>
} {
return { isPackaged: true, configureWebAuthn: vi.fn<ConfigureWebAuthn>() }
}
describe('readWebAuthnKeychainAccessGroup', () => {
it('returns the group ending in .webauthn from a signed entitlement plist', () => {
expect(readWebAuthnKeychainAccessGroup(SIGNED_ENTITLEMENTS)).toBe(
'TEAM123456.com.stablyai.orca.webauthn'
)
})
it('returns null when the array holds no webauthn group', () => {
const xml = SIGNED_ENTITLEMENTS.replace(
'<string>TEAM123456.com.stablyai.orca.webauthn</string>',
''
)
expect(readWebAuthnKeychainAccessGroup(xml)).toBeNull()
})
it('returns null for an unsigned binary whose entitlement dump is empty', () => {
expect(readWebAuthnKeychainAccessGroup('')).toBeNull()
})
})
describe('enablePlatformPasskeys', () => {
it('configures the Touch ID authenticator with the signed group and a prompt reason', () => {
const app = packagedApp()
const outcome = enablePlatformPasskeys(app, {
platform: 'darwin',
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
readEntitlements: () => SIGNED_ENTITLEMENTS
})
expect(outcome).toEqual({
status: 'enabled',
keychainAccessGroup: 'TEAM123456.com.stablyai.orca.webauthn'
})
expect(app.configureWebAuthn).toHaveBeenCalledWith({
touchID: {
keychainAccessGroup: 'TEAM123456.com.stablyai.orca.webauthn',
promptReason: 'sign in to $1'
}
})
})
it('reads the entitlements of the running executable', () => {
const readEntitlements = vi.fn(() => SIGNED_ENTITLEMENTS)
enablePlatformPasskeys(packagedApp(), {
platform: 'darwin',
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
readEntitlements
})
expect(readEntitlements).toHaveBeenCalledWith('/Applications/Orca.app/Contents/MacOS/Orca')
})
it.each(['win32', 'linux'] as const)('does nothing on %s', (platform) => {
const app = packagedApp()
const readEntitlements = vi.fn(() => SIGNED_ENTITLEMENTS)
expect(enablePlatformPasskeys(app, { platform, readEntitlements })).toEqual({
status: 'skipped',
reason: 'not-macos'
})
expect(readEntitlements).not.toHaveBeenCalled()
expect(app.configureWebAuthn).not.toHaveBeenCalled()
})
it('skips unpackaged runs without inspecting the shared Electron binary', () => {
const app = { ...packagedApp(), isPackaged: false }
const readEntitlements = vi.fn(() => SIGNED_ENTITLEMENTS)
expect(enablePlatformPasskeys(app, { platform: 'darwin', readEntitlements })).toEqual({
status: 'skipped',
reason: 'unpackaged'
})
expect(readEntitlements).not.toHaveBeenCalled()
expect(app.configureWebAuthn).not.toHaveBeenCalled()
})
// Why: a build signed without the group would make Chromium log an entitlement
// error on every request; leaving the authenticator unconfigured keeps today's behavior.
it('skips a signed build that lacks the webauthn keychain group', () => {
const app = packagedApp()
expect(
enablePlatformPasskeys(app, {
platform: 'darwin',
readEntitlements: () => '<plist version="1.0"><dict></dict></plist>'
})
).toEqual({ status: 'skipped', reason: 'no-entitlement' })
expect(app.configureWebAuthn).not.toHaveBeenCalled()
})
it('skips when the running Electron has no configureWebAuthn', () => {
const app = { isPackaged: true }
expect(
enablePlatformPasskeys(app, {
platform: 'darwin',
readEntitlements: () => SIGNED_ENTITLEMENTS
})
).toEqual({ status: 'skipped', reason: 'api-unavailable' })
})
it('reports a codesign read failure without throwing', () => {
const app = packagedApp()
expect(
enablePlatformPasskeys(app, {
platform: 'darwin',
readEntitlements: () => {
throw new Error('codesign exited 1')
}
})
).toEqual({ status: 'failed', error: 'codesign exited 1' })
expect(app.configureWebAuthn).not.toHaveBeenCalled()
})
it('reports a configureWebAuthn failure without throwing', () => {
const app = packagedApp()
app.configureWebAuthn.mockImplementation(() => {
throw new TypeError('bad options')
})
expect(
enablePlatformPasskeys(app, {
platform: 'darwin',
readEntitlements: () => SIGNED_ENTITLEMENTS
})
).toEqual({ status: 'failed', error: 'bad options' })
})
})
describe('startup wiring', () => {
// Why: configureWebAuthn is process-wide and must land before the first guest can
// issue a passkey request, so it sits ahead of browser session initialization.
it('enables platform passkeys before browser sessions initialize', async () => {
const { readFileSync } = await import('node:fs')
const { join } = await import('node:path')
const source = readFileSync(
join(import.meta.dirname, '../startup/main-process-ready-foundation.ts'),
'utf8'
)
const enableIndex = source.indexOf('enablePlatformPasskeys(app)')
const sessionsIndex = source.indexOf('initializeBrowserSessionsForApp({')
expect(enableIndex).toBeGreaterThan(-1)
expect(sessionsIndex).toBeGreaterThan(enableIndex)
})
})
@@ -0,0 +1,97 @@
import { runProcessSync } from '../../shared/child-process/run-process'
// Why: GitHub and other relying parties gate passkey sign-in on
// PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(). Electron
// reports false until the app opts into the Touch ID authenticator, and a
// discoverable-credential request then waits on USB security keys for the
// three-minute Chromium floor. Opting in makes the check true, shows the Touch
// ID sheet, and lets a request with no matching passkey fail promptly.
export const ORCA_WEBAUTHN_KEYCHAIN_GROUP_SUFFIX = '.webauthn'
type PlatformPasskeyApp = {
isPackaged: boolean
configureWebAuthn?: (options: {
touchID: { keychainAccessGroup: string; promptReason?: string }
}) => void
}
export type PlatformPasskeyOutcome =
| { status: 'enabled'; keychainAccessGroup: string }
| { status: 'skipped'; reason: 'not-macos' | 'unpackaged' | 'api-unavailable' | 'no-entitlement' }
| { status: 'failed'; error: string }
/**
* Picks the WebAuthn keychain group out of the executable's signed entitlements.
* Why read the signature and not a build constant: the Touch ID authenticator only
* works when the running binary actually carries the group, so the signature is the
* single source of truth and unsigned local builds stay inert automatically.
*/
export function readWebAuthnKeychainAccessGroup(entitlementsXml: string): string | null {
const groupsMatch = /<key>keychain-access-groups<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(
entitlementsXml
)
if (!groupsMatch) {
return null
}
for (const match of groupsMatch[1].matchAll(/<string>([^<]*)<\/string>/g)) {
const group = match[1].trim()
if (group.endsWith(ORCA_WEBAUTHN_KEYCHAIN_GROUP_SUFFIX)) {
return group
}
}
return null
}
function readSignedEntitlements(execPath: string): string {
// Why: `codesign -d --entitlements :-` prints the signed entitlement plist to stdout.
const result = runProcessSync({
program: 'codesign',
args: ['-d', '--entitlements', ':-', execPath],
timeoutMs: 10_000
})
if (result.code !== 0) {
throw new Error(`codesign exited ${result.code ?? result.signal}: ${result.stderr.trim()}`)
}
return result.stdout
}
export function enablePlatformPasskeys(
app: PlatformPasskeyApp,
options: {
platform?: NodeJS.Platform
execPath?: string
readEntitlements?: (execPath: string) => string
} = {}
): PlatformPasskeyOutcome {
if ((options.platform ?? process.platform) !== 'darwin') {
return { status: 'skipped', reason: 'not-macos' }
}
if (!app.isPackaged) {
return { status: 'skipped', reason: 'unpackaged' }
}
if (typeof app.configureWebAuthn !== 'function') {
return { status: 'skipped', reason: 'api-unavailable' }
}
let keychainAccessGroup: string | null
try {
keychainAccessGroup = readWebAuthnKeychainAccessGroup(
(options.readEntitlements ?? readSignedEntitlements)(options.execPath ?? process.execPath)
)
} catch (error) {
return { status: 'failed', error: error instanceof Error ? error.message : String(error) }
}
if (!keychainAccessGroup) {
return { status: 'skipped', reason: 'no-entitlement' }
}
try {
// Why the explicit reason: Electron's default reads from a locale pak that can be
// missing, and an empty reason crashes the LAContext prompt (electron#53185).
app.configureWebAuthn({
touchID: { keychainAccessGroup, promptReason: 'sign in to $1' }
})
} catch (error) {
return { status: 'failed', error: error instanceof Error ? error.message : String(error) }
}
return { status: 'enabled', keychainAccessGroup }
}
@@ -39,6 +39,7 @@ import {
import { installDocPreviewProtocolHandler } from '../browser/doc-preview-protocol'
import { registerDocPreviewGrantHandlers } from '../ipc/doc-preview-grant-ipc'
import { initializeBrowserSessionsForApp } from '../browser/browser-session-startup'
import { enablePlatformPasskeys } from '../browser/browser-platform-passkeys-macos'
import { browserSessionRegistry } from '../browser/browser-session-registry'
import { logStartupMilestone } from './startup-diagnostics'
import { writeHttp1CompatibilityMarker } from './http1-compatibility-marker'
@@ -267,6 +268,13 @@ export async function initializeReadyFoundation(): Promise<void> {
// Why: the preview session is protocol-scoped, so the handler must exist before any preview webview attaches.
installDocPreviewProtocolHandler()
registerDocPreviewGrantHandlers()
// Why here: configureWebAuthn is process-wide and must run after `ready`; before any guest can issue a passkey request.
const platformPasskeys = enablePlatformPasskeys(app)
if (platformPasskeys.status === 'failed') {
console.warn('[browser] Touch ID passkey authenticator unavailable:', platformPasskeys.error)
} else if (platformPasskeys.status === 'enabled') {
console.log('[browser] Touch ID passkey authenticator enabled')
}
// Why: browser sessions serve desktop webviews and runtime profile commands, so init at app startup rather than via a renderer IPC path.
initializeBrowserSessionsForApp({
orcaProfileId: profile.profile.id,