Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
.gitignore merge=generated-artifact
DESIGN.md merge=generated-artifact
ROADMAP.md merge=generated-artifact
apps/approve-ios/project.yml merge=generated-artifact
dag/gunbc/stage0/stage0_crate_layout_generated.dag merge=generated-artifact
dag/gunbc/stage0/stage0_crate_partition_generated.dag merge=generated-artifact
dag/gunbc/stage0/stage0_executable_assembly_generated.dag merge=generated-artifact
Expand Down
16 changes: 11 additions & 5 deletions apps/approve-ios/Approve/ApproveApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,20 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
}
}

func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
await state?.refresh()
// The UN* arguments are not Sendable and are not read: these callbacks are nonisolated so they
// never cross the main actor, and hop with no payload to refreshFromPush.
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
await refreshFromPush()
return [.banner, .sound]
}

/// The operator tapped the alert: re-list, then the inbox shows what is pending.
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
await state?.refresh()
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
await refreshFromPush()
}

@MainActor private func refreshFromPush() async {
_ = await state?.refresh()
}
}
2 changes: 1 addition & 1 deletion apps/approve-ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ held to the `.dag` folds by `dag/test/fixture/approval_device_redemption/vectors

| file | what it is |
|---|---|
| `project.yml` | XcodeGen spec; no `.xcodeproj` is committed |
| `project.yml` | XcodeGen spec, GENERATED from `gunbc.approve_ios_project` (drift-gated; never hand-edit); no `.xcodeproj` is committed |
| `Config/Team.xcconfig` | operator-filled: `DEVELOPMENT_TEAM`, `APPROVE_SERVER_HOST` (srv1 tailnet host), `APNS_ENVIRONMENT`, `APP_ATTEST_ENVIRONMENT` |
| `Approve/Protocol.swift` | `framed` (the injective `<n>:<field>,` rendering), `enrolment_transcript`, `device_redemption_signing_input`, `device_read_client_data`, the protocol records |
| `Approve/Wire.swift` | mirror of the modeled HTTP wire: emitter, strict readers, routes, headers; `URLSession` over the tailnet |
Expand Down
28 changes: 16 additions & 12 deletions apps/approve-ios/project.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# XcodeGen spec — no .xcodeproj is committed. On the MacBook: `brew install xcodegen && xcodegen generate`.
# Generated by gunbc.approve_ios_project expected_approve_ios_project_yml — do not hand-edit.
# Authority: gunbc.approve_ios_project approve_ios_project_spec; regen via tools.generated_artifact_gate main_wet.
# XcodeGen spec; no .xcodeproj is committed. On the Mac: `brew install xcodegen && xcodegen generate`.
name: Approve
options:
bundleIdPrefix: ai.gunb
deploymentTarget:
iOS: "17.0"
xcodeVersion: "16.0"
xcodeVersion: "26.6"
createIntermediateGroups: true
configFiles:
Debug: Config/Team.xcconfig
Expand All @@ -14,7 +16,10 @@ settings:
SWIFT_VERSION: "5.10"
SWIFT_STRICT_CONCURRENCY: complete
CODE_SIGN_STYLE: Automatic
# DEVELOPMENT_TEAM, APPROVE_SERVER_HOST and APNS_ENVIRONMENT come from Config/Team.xcconfig.
ENABLE_USER_SCRIPT_SANDBOXING: YES
STRING_CATALOG_GENERATE_SYMBOLS: YES
SWIFT_TREAT_WARNINGS_AS_ERRORS: YES
GCC_TREAT_WARNINGS_AS_ERRORS: YES
targets:
Approve:
type: application
Expand All @@ -24,33 +29,32 @@ targets:
base:
PRODUCT_BUNDLE_IDENTIFIER: ai.gunb.approve
MARKETING_VERSION: "1"
CURRENT_PROJECT_VERSION: 1
INFOPLIST_KEY_UILaunchScreen_Generation: YES
INFOPLIST_KEY_NSFaceIDUsageDescription: "Approving a request signs it with a key that only unlocks with Face ID."
INFOPLIST_KEY_CFBundleDisplayName: Approve
CURRENT_PROJECT_VERSION: "1"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
info:
path: Approve/Info.plist
properties:
UILaunchScreen: {}
NSFaceIDUsageDescription: Approving a request signs it with a key that only unlocks with Face ID.
CFBundleDisplayName: Approve
UISupportedInterfaceOrientations: [UIInterfaceOrientationPortrait, UIInterfaceOrientationPortraitUpsideDown, UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight]
UIBackgroundModes: [remote-notification]
# Read by ServerConfig at launch; the values are xcconfig-substituted.
ApproveServerHost: $(APPROVE_SERVER_HOST)
ApproveApnsEnvironment: $(APNS_ENVIRONMENT)
entitlements:
path: Approve/Approve.entitlements
properties:
aps-environment: $(APNS_ENVIRONMENT)
# extdeps.apple.app_attest AppAttestEnvironment: "development" for an Xcode build,
# "production" for TestFlight/App Store (the AAGUID the server checks follows this).
com.apple.developer.devicecheck.appattest-environment: $(APP_ATTEST_ENVIRONMENT)
ApproveTests:
type: bundle.unit-test
platform: iOS
sources:
- ApproveTests
# The fixture vectors are EMITTED by the .dag witness (test.claim.approval_device_redemption_witness_test),
# never hand-copied; the test bundle carries the committed file as a resource.
- path: ../../dag/test/fixture/approval_device_redemption/vectors.json
buildPhase: resources
settings:
base:
GENERATE_INFOPLIST_FILE: YES
dependencies:
- target: Approve
92 changes: 92 additions & 0 deletions dag/extdeps/apple/build_settings.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
module extdeps.apple.build_settings

import std.types { String, Bool }
import std.decl_ref { DeclarationRef, WholeDeclaration }
import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef }
import extdeps.uri { Uri, Https }

// Xcode build settings (Xcode > Build settings reference). Each setting this tree writes is one arm
// named by Apple's setting name, the spelling a project file and an .xcconfig carry.
data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri {
scheme: Https
locator: "developer.apple.com/documentation/xcode/build-settings-reference"
}
}

data extdeps_model_scope: ExternalModelScope = ExternalModelScope {
subject: ExternalSubjectRef {
declaration: DeclarationRef {
module_path: "extdeps.apple.build_settings",
decl_name: "BuildSettingName",
field: WholeDeclaration
}
},
first_citation: extdeps_external_authority_anchor,
further_citations: []
}

// ENABLE_USER_SCRIPT_SANDBOXING ("User Script Sandboxing"): run script phases sandboxed to their
// declared inputs and outputs. STRING_CATALOG_GENERATE_SYMBOLS ("Generate String Catalog Symbols"):
// generate Swift symbols for String Catalog keys. Both are defaults Xcode's "Update to recommended
// settings" check offers to turn on for a project last upgraded before them; setting them explicitly
// is what satisfies the check.
//
// SWIFT_TREAT_WARNINGS_AS_ERRORS ("Treat Warnings as Errors", Swift compiler) and
// GCC_TREAT_WARNINGS_AS_ERRORS ("Treat Warnings as Errors", Apple Clang): a warning fails the build.
// SWIFT_STRICT_CONCURRENCY ("Strict Concurrency Checking"): `complete` checks Sendable and actor
// isolation everywhere -- the checking the Swift 6 language mode enforces -- as diagnostics in the
// Swift 5 mode.
//
// GENERATE_INFOPLIST_FILE ("Generate Info.plist File"): Xcode synthesizes the target's Info.plist
// (and only then reads INFOPLIST_KEY_*); a target with neither it nor an INFOPLIST_FILE has no
// Info.plist and does not build.
//
// MARKETING_VERSION ("Marketing Version") becomes CFBundleShortVersionString and
// CURRENT_PROJECT_VERSION ("Current Project Version") becomes CFBundleVersion in the Info.plist
// XcodeGen writes.
//
// NO INFOPLIST_KEY_* ARM EXISTS, deliberately: the INFOPLIST_KEY_*
// settings are read only when Xcode generates the Info.plist, so a target that supplies its own
// Info.plist (INFOPLIST_FILE, as XcodeGen does for a target with `info`) must carry those keys in
// the file instead (extdeps.apple.info_plist); a setting it would ignore is not modeled.
type BuildSettingName
= SwiftVersion
| SwiftStrictConcurrency
| CodeSignStyle
| ProductBundleIdentifier
| AssetCatalogCompilerAppIconName
| EnableUserScriptSandboxing
| StringCatalogGenerateSymbols
| SwiftTreatWarningsAsErrors
| GccTreatWarningsAsErrors
| GenerateInfoPlistFile
| MarketingVersion
| CurrentProjectVersion

fn build_setting_name_token(n: BuildSettingName) -> String {
match n {
SwiftVersion => "SWIFT_VERSION"
SwiftStrictConcurrency => "SWIFT_STRICT_CONCURRENCY"
CodeSignStyle => "CODE_SIGN_STYLE"
ProductBundleIdentifier => "PRODUCT_BUNDLE_IDENTIFIER"
AssetCatalogCompilerAppIconName => "ASSETCATALOG_COMPILER_APPICON_NAME"
EnableUserScriptSandboxing => "ENABLE_USER_SCRIPT_SANDBOXING"
StringCatalogGenerateSymbols => "STRING_CATALOG_GENERATE_SYMBOLS"
SwiftTreatWarningsAsErrors => "SWIFT_TREAT_WARNINGS_AS_ERRORS"
GccTreatWarningsAsErrors => "GCC_TREAT_WARNINGS_AS_ERRORS"
GenerateInfoPlistFile => "GENERATE_INFOPLIST_FILE"
MarketingVersion => "MARKETING_VERSION"
CurrentProjectVersion => "CURRENT_PROJECT_VERSION"
}
}

type BuildSetting {
name: BuildSettingName
value: String
}

// Boolean build settings are spelled YES / NO.
fn build_setting_bool(name: BuildSettingName, on: Bool) -> BuildSetting {
BuildSetting { name: name, value: if on { "YES" } else { "NO" } }
}
157 changes: 157 additions & 0 deletions dag/extdeps/apple/info_plist.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
module extdeps.apple.info_plist

import std.types { String, NonEmptyStr, List }
import v2.std.optional { Present, Absent }
import std.decl_ref { DeclarationRef, WholeDeclaration }
import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef }
import extdeps.uri { Uri, Https }
import extdeps.apple.property_list { PlistEntry, PlistValue, PlistString, PlistArray, PlistDictionary, plist_entry }

// Apple's Information Property List keys (Bundle Resources > Information Property List). Each key
// this tree writes is one arm, carrying its documented value shape; the key's spelling is Apple's,
// derived here once. Per-key pages are cited by info_plist_key_reference.
data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri {
scheme: Https
locator: "developer.apple.com/documentation/bundleresources/information-property-list"
}
}

data extdeps_model_scope: ExternalModelScope = ExternalModelScope {
subject: ExternalSubjectRef {
declaration: DeclarationRef {
module_path: "extdeps.apple.info_plist",
decl_name: "InfoPlistKey",
field: WholeDeclaration
}
},
first_citation: extdeps_external_authority_anchor,
further_citations: []
}

// UILaunchScreen: a dictionary; an EMPTY dictionary selects the system launch screen (default
// background colour, no image). Declaring it at all is what tells iOS the app supports every screen
// size -- an app that has neither it nor UILaunchStoryboardName runs letterboxed in a legacy
// display mode. Sub-keys modeled: UIColorName and UIImageName (asset-catalog names), each omitted when
// absent; the rest (UINavigationBar, UITabBar, ...) land with their first consumer.
type LaunchScreen {
color_name: NonEmptyStr?
image_name: NonEmptyStr?
}

data system_default_launch_screen: LaunchScreen = LaunchScreen { color_name: Absent, image_name: Absent }

fn optional_string_entry(key: String, v: NonEmptyStr?) -> List<PlistEntry> {
match v {
Present { value: s } => [plist_entry(key: key, value: PlistString { value: s })]
Absent => []
}
}

// UIBackgroundModes values, Apple's spellings. `remote-notification` lets a push wake the app.
type BackgroundMode
= AudioMode
| LocationMode
| VoipMode
| ExternalAccessoryMode
| BluetoothCentralMode
| BluetoothPeripheralMode
| FetchMode
| RemoteNotificationMode
| ProcessingMode

fn background_mode_token(m: BackgroundMode) -> String {
match m {
AudioMode => "audio"
LocationMode => "location"
VoipMode => "voip"
ExternalAccessoryMode => "external-accessory"
BluetoothCentralMode => "bluetooth-central"
BluetoothPeripheralMode => "bluetooth-peripheral"
FetchMode => "fetch"
RemoteNotificationMode => "remote-notification"
ProcessingMode => "processing"
}
}

// AppDefinedKey is a key Apple does not define: the plist carries it verbatim for the app to read
// at runtime (Bundle.main.object(forInfoDictionaryKey:)).
type InfoPlistKey
= UILaunchScreenKey
| NSFaceIDUsageDescriptionKey
| CFBundleDisplayNameKey
| UIBackgroundModesKey
| UISupportedInterfaceOrientationsKey
| AppDefinedKey { name: String }

fn info_plist_key_token(k: InfoPlistKey) -> String {
match k {
UILaunchScreenKey => "UILaunchScreen"
NSFaceIDUsageDescriptionKey => "NSFaceIDUsageDescription"
CFBundleDisplayNameKey => "CFBundleDisplayName"
UIBackgroundModesKey => "UIBackgroundModes"
UISupportedInterfaceOrientationsKey => "UISupportedInterfaceOrientations"
AppDefinedKey { name: n } => n
}
}

fn apple_doc(locator: String) -> Uri {
Uri { scheme: Https, locator: locator }
}

fn info_plist_key_reference(k: InfoPlistKey) -> Uri {
match k {
UILaunchScreenKey => apple_doc(locator: "developer.apple.com/documentation/bundleresources/information-property-list/uilaunchscreen")
NSFaceIDUsageDescriptionKey => apple_doc(locator: "developer.apple.com/documentation/bundleresources/information-property-list/nsfaceidusagedescription")
CFBundleDisplayNameKey => apple_doc(locator: "developer.apple.com/documentation/bundleresources/information-property-list/cfbundledisplayname")
UIBackgroundModesKey => apple_doc(locator: "developer.apple.com/documentation/bundleresources/information-property-list/uibackgroundmodes")
UISupportedInterfaceOrientationsKey => apple_doc(locator: "developer.apple.com/documentation/bundleresources/information-property-list/uisupportedinterfaceorientations")
AppDefinedKey { name: _ } => extdeps_external_authority_anchor.uri
}
}

fn info_entry(k: InfoPlistKey, value: PlistValue) -> PlistEntry {
plist_entry(key: info_plist_key_token(k: k), value: value)
}

fn launch_screen_entry(s: LaunchScreen) -> PlistEntry {
info_entry(k: UILaunchScreenKey, value: PlistDictionary { entries: concat(optional_string_entry(key: "UIColorName", v: s.color_name), optional_string_entry(key: "UIImageName", v: s.image_name)) })
}

// NSFaceIDUsageDescription: the purpose string iOS shows the first time the app asks for Face ID. An
// app that evaluates a biometric policy without it is TERMINATED by the system at that call.
fn face_id_usage_entry(purpose: String) -> PlistEntry {
info_entry(k: NSFaceIDUsageDescriptionKey, value: PlistString { value: purpose })
}

fn display_name_entry(name: String) -> PlistEntry {
info_entry(k: CFBundleDisplayNameKey, value: PlistString { value: name })
}

fn background_modes_entry(modes: List<BackgroundMode>) -> PlistEntry {
info_entry(k: UIBackgroundModesKey, value: PlistArray { elements: map(modes, m => PlistString { value: background_mode_token(m: m) }) })
}

// UISupportedInterfaceOrientations values (UIInterfaceOrientation). An iPhone app that supports
// every screen size (UILaunchScreen) and declares no orientations is warned by Xcode that "All
// interface orientations must be supported" unless it requires full screen.
type InterfaceOrientation
= PortraitOrientation
| PortraitUpsideDownOrientation
| LandscapeLeftOrientation
| LandscapeRightOrientation

fn interface_orientation_token(o: InterfaceOrientation) -> String {
match o {
PortraitOrientation => "UIInterfaceOrientationPortrait"
PortraitUpsideDownOrientation => "UIInterfaceOrientationPortraitUpsideDown"
LandscapeLeftOrientation => "UIInterfaceOrientationLandscapeLeft"
LandscapeRightOrientation => "UIInterfaceOrientationLandscapeRight"
}
}

data all_interface_orientations: List<InterfaceOrientation> = [PortraitOrientation, PortraitUpsideDownOrientation, LandscapeLeftOrientation, LandscapeRightOrientation]

fn supported_orientations_entry(orientations: List<InterfaceOrientation>) -> PlistEntry {
info_entry(k: UISupportedInterfaceOrientationsKey, value: PlistArray { elements: map(orientations, o => PlistString { value: interface_orientation_token(o: o) }) })
}
Loading