@latchway/react-native 0.0.0-bootstrap.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/LatchwayReactNative.podspec +33 -0
- package/NOTICE +7 -0
- package/README.md +293 -3
- package/SECURITY.md +69 -0
- package/android/build.gradle.kts +62 -0
- package/android/consumer-rules.pro +2 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/android/gradle.properties +4 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle.kts +33 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/latchway/reactnative/LatchwayReactNativePackage.kt +25 -0
- package/android/src/main/java/dev/latchway/reactnative/NativeLatchwayModule.kt +964 -0
- package/android/src/test/java/dev/latchway/reactnative/NativeLatchwayModuleTest.kt +521 -0
- package/babel.cjs +22 -0
- package/babel.d.cts +7 -0
- package/contract.lock +7 -0
- package/docs/architecture.md +145 -0
- package/docs/conformance.md +68 -0
- package/docs/langchain.md +135 -0
- package/docs/native-installation.md +172 -0
- package/docs/physical-device-evidence.md +559 -0
- package/docs/releasing.md +360 -0
- package/docs/security.md +55 -0
- package/ios/LatchwayNativeBridge.swift +1551 -0
- package/ios/RCTNativeLatchway.h +9 -0
- package/ios/RCTNativeLatchway.mm +175 -0
- package/lib/client.d.ts +29 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +939 -0
- package/lib/client.js.map +1 -0
- package/lib/component-client.d.ts +15 -0
- package/lib/component-client.d.ts.map +1 -0
- package/lib/component-client.js +141 -0
- package/lib/component-client.js.map +1 -0
- package/lib/config.d.ts +25 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +261 -0
- package/lib/config.js.map +1 -0
- package/lib/coordinator.d.ts +19 -0
- package/lib/coordinator.d.ts.map +1 -0
- package/lib/coordinator.js +167 -0
- package/lib/coordinator.js.map +1 -0
- package/lib/errors.d.ts +5 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +201 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/native/NativeLatchway.d.ts +25 -0
- package/lib/native/NativeLatchway.d.ts.map +1 -0
- package/lib/native/NativeLatchway.js +3 -0
- package/lib/native/NativeLatchway.js.map +1 -0
- package/lib/native/bridge.d.ts +5 -0
- package/lib/native/bridge.d.ts.map +1 -0
- package/lib/native/bridge.js +17 -0
- package/lib/native/bridge.js.map +1 -0
- package/lib/native-output.d.ts +3 -0
- package/lib/native-output.d.ts.map +1 -0
- package/lib/native-output.js +43 -0
- package/lib/native-output.js.map +1 -0
- package/lib/polyfills.d.ts +8 -0
- package/lib/polyfills.d.ts.map +1 -0
- package/lib/polyfills.js +56 -0
- package/lib/polyfills.js.map +1 -0
- package/lib/request-id.d.ts +2 -0
- package/lib/request-id.d.ts.map +1 -0
- package/lib/request-id.js +5 -0
- package/lib/request-id.js.map +1 -0
- package/lib/runtime-symbols.d.ts +2 -0
- package/lib/runtime-symbols.d.ts.map +1 -0
- package/lib/runtime-symbols.js +9 -0
- package/lib/runtime-symbols.js.map +1 -0
- package/lib/testing.d.ts +7 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +9 -0
- package/lib/testing.js.map +1 -0
- package/lib/types.d.ts +200 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +8 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +8 -0
- package/lib/version.js.map +1 -0
- package/package.json +152 -6
- package/react-native.config.cjs +7 -0
- package/release-compatibility.json +64 -0
- package/src/client.ts +1022 -0
- package/src/component-client.ts +158 -0
- package/src/config.ts +368 -0
- package/src/coordinator.ts +195 -0
- package/src/errors.ts +225 -0
- package/src/index.ts +53 -0
- package/src/native/NativeLatchway.ts +75 -0
- package/src/native/bridge.ts +23 -0
- package/src/native-output.ts +43 -0
- package/src/polyfills.ts +50 -0
- package/src/request-id.ts +5 -0
- package/src/runtime-symbols.ts +9 -0
- package/src/testing.ts +11 -0
- package/src/types.ts +241 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# Physical-device release evidence
|
|
2
|
+
|
|
3
|
+
The React Native release gate proves that the published JavaScript surface
|
|
4
|
+
actually reaches the locked native iOS and Android SDKs on production-eligible
|
|
5
|
+
devices. A simulator, emulator, debug build, testing process, debugger-attached
|
|
6
|
+
process, software-key fallback, sideloaded Android build, or testing attestation
|
|
7
|
+
environment can never produce passing release evidence.
|
|
8
|
+
|
|
9
|
+
The optional iOS development Firebase bootstrap is likewise excluded. Its
|
|
10
|
+
bridge exists only in `DEBUG`, its plist phase refuses Release/candidate builds,
|
|
11
|
+
and its one-read grant is launched outside the protected collector. Its local
|
|
12
|
+
runner does require a fresh sign-in, replacement App Attest session, Responses,
|
|
13
|
+
quota, exact trusted diagnostics, terminal revocation/sign-out, and a run-bound
|
|
14
|
+
app-container marker. Even so, a successful development-signed `dev.latchway`
|
|
15
|
+
run is source verification, not production App Attest, immutable-candidate, or
|
|
16
|
+
publication evidence.
|
|
17
|
+
|
|
18
|
+
This is an external release gate. Repository CI validates the collectors and
|
|
19
|
+
their failure cases, but it does not claim a physical-device success. Only the
|
|
20
|
+
protected, manually dispatched `Physical React Native evidence` workflow can
|
|
21
|
+
collect a candidate report, and the final cross-repository adapter requires all
|
|
22
|
+
four independently validated native and React Native reports.
|
|
23
|
+
|
|
24
|
+
Every authorization, lease, identity grant, candidate handoff, and retained
|
|
25
|
+
artifact is bound to one workflow run and attempt. Do not use **Re-run failed
|
|
26
|
+
jobs** to recover a partial physical proof: a successful authorization or
|
|
27
|
+
candidate job from an earlier attempt is intentionally unusable by a later
|
|
28
|
+
collector. Start a fresh complete workflow dispatch so all single-use grants
|
|
29
|
+
and attempt-bound evidence are reissued together.
|
|
30
|
+
|
|
31
|
+
The raw React Native device record is
|
|
32
|
+
`latchway.react-native-device-run.v2`. It contains only checks the opaque
|
|
33
|
+
production bridge can prove without exporting credentials. The protected
|
|
34
|
+
finalizer imports replay rejection, proof-tamper rejection, refresh-token
|
|
35
|
+
rotation, protocol-version rejection, and post-revocation enforcement from the
|
|
36
|
+
exact linked native report. A JavaScript record that supplies any of those
|
|
37
|
+
native-only proof IDs is rejected.
|
|
38
|
+
|
|
39
|
+
## What the run proves
|
|
40
|
+
|
|
41
|
+
Each platform run uses the real example application and verifies:
|
|
42
|
+
|
|
43
|
+
- the React Native New Architecture bridge reached the locked native SDK;
|
|
44
|
+
- a production App Attest session with a Secure Enclave key on iOS, or a Play
|
|
45
|
+
Integrity session with hardware-backed Android Keystore key material;
|
|
46
|
+
- on iOS, an initial App Attest registration followed by session-only local
|
|
47
|
+
retirement and an App Attest assertion that reuses the same installation;
|
|
48
|
+
- a DPoP-authorized request through the opaque native dispatch boundary and the
|
|
49
|
+
concrete React Native `LatchwayError` mapping for authorization-first HTTP
|
|
50
|
+
403 `component_feature_not_granted`;
|
|
51
|
+
- exact HTTP 401 replay/tamper rejection, redacted refresh-credential rotation
|
|
52
|
+
for one stable installation, HTTP 426 protocol-version-zero rejection, and
|
|
53
|
+
HTTP 403 post-revocation enforcement imported unchanged, field-for-field,
|
|
54
|
+
from the linked, release-eligible native report;
|
|
55
|
+
- a bounded, non-empty SSE stream and a quota response through the pinned
|
|
56
|
+
gateway image and configuration;
|
|
57
|
+
- one short-lived, P-256-signed deployment statement fetched from the same
|
|
58
|
+
gateway origin before and after each run, with the exact native/RN client
|
|
59
|
+
policy requiring request-hash-bound production trust;
|
|
60
|
+
- exact app/package version, build, team or cloud project, signing-certificate,
|
|
61
|
+
distribution/Play track, source, native SDK, core, and contract hashes;
|
|
62
|
+
- linkage to a separately passing native iOS or Android physical-device report;
|
|
63
|
+
- absence of identity/session/refresh tokens, DPoP proofs, raw attestation,
|
|
64
|
+
private keys, and provider credentials from every retained document.
|
|
65
|
+
|
|
66
|
+
The Android gate additionally requires the Play installer, a locked production
|
|
67
|
+
device with green Verified Boot, a Play-recognized app, and a licensed account.
|
|
68
|
+
Installing an APK with `adb` is deliberately insufficient. The iOS gate checks
|
|
69
|
+
a private snapshot of the exact signed `.app`, production App Attest
|
|
70
|
+
entitlement, application identifier, team, signing certificate, executable
|
|
71
|
+
hash, canonical per-file manifest hash, and deterministic whole-bundle tree
|
|
72
|
+
hash before inspection and again
|
|
73
|
+
immediately before installing that same snapshot.
|
|
74
|
+
|
|
75
|
+
## Prepare immutable candidates
|
|
76
|
+
|
|
77
|
+
First produce and retain the native reports with the sibling SDK workflows:
|
|
78
|
+
|
|
79
|
+
- iOS: `latchway-ios-sdk/.github/workflows/physical-app-attest.yml`
|
|
80
|
+
- Android: `latchway-android/.github/workflows/physical-play-integrity.yml`
|
|
81
|
+
|
|
82
|
+
Stage each native evidence/profile pair on its matching protected runner. Pin
|
|
83
|
+
its evidence SHA-256 in the React Native environment. A React Native candidate
|
|
84
|
+
must embed all physical-run values listed in `example/.env.example`, with
|
|
85
|
+
`LATCHWAY_CONFORMANCE_AUTORUN=true`. Those values are release identifiers, not
|
|
86
|
+
credentials, but the build configuration itself remains a protected release
|
|
87
|
+
input. Produce the candidate with
|
|
88
|
+
`scripts/stage-physical-react-native-candidate.py ios|android`. The producer
|
|
89
|
+
requires clean exact React Native, JavaScript SDK, native SDK, and core
|
|
90
|
+
worktrees and checks their commits against `release-compatibility.json` and
|
|
91
|
+
`contract.lock`. It materializes the exact React Native and JavaScript commits
|
|
92
|
+
as fresh sibling worktrees, regenerates their dependencies and JavaScript build
|
|
93
|
+
output from protected locks, uses external Firebase configuration, performs a
|
|
94
|
+
Release build, inspects the result, and emits canonical source/candidate
|
|
95
|
+
manifests plus `SHA256SUMS`. The platform candidate necessarily embeds only its
|
|
96
|
+
non-secret Firebase client configuration. Provider credentials, provider
|
|
97
|
+
secrets, and signing passwords are never included anywhere in candidate
|
|
98
|
+
output. Its output directory must not already exist.
|
|
99
|
+
|
|
100
|
+
Run the producer from the clean React Native repository. Common protected
|
|
101
|
+
inputs are `LATCHWAY_SOURCE_COMMIT`, `LATCHWAY_CORE_COMMIT`,
|
|
102
|
+
`LATCHWAY_CORE_SOURCE_PATH`, `LATCHWAY_JAVASCRIPT_SDK_PATH`,
|
|
103
|
+
`LATCHWAY_CONTRACT_VERSION`,
|
|
104
|
+
`LATCHWAY_CONTRACT_BUNDLE_SHA256`, `LATCHWAY_RN_SDK_VERSION`,
|
|
105
|
+
`LATCHWAY_NATIVE_SDK_VERSION`, `LATCHWAY_NATIVE_EVIDENCE_PATH` and its
|
|
106
|
+
`LATCHWAY_NATIVE_EVIDENCE_SHA256`, all gateway coordinates listed in
|
|
107
|
+
`example/.env.example`, and a new `LATCHWAY_CANDIDATE_OUTPUT_DIR`. The producer
|
|
108
|
+
requires the repository-pinned Node 24.19.0 and pnpm 10.15.0
|
|
109
|
+
toolchain and records both versions in `source-inputs.json`. iOS also
|
|
110
|
+
requires `LATCHWAY_IOS_SDK_PATH`, `LATCHWAY_IOS_COMMIT`,
|
|
111
|
+
`LATCHWAY_BUNDLE_ID`, version/build/distribution pins,
|
|
112
|
+
`LATCHWAY_FIREBASE_IOS_CONFIG_PATH`, `LATCHWAY_IOS_PODFILE_LOCK_PATH`,
|
|
113
|
+
`LATCHWAY_TEAM_ID`, `LATCHWAY_IOS_APP_ID_PREFIX`,
|
|
114
|
+
`LATCHWAY_IOS_CODE_SIGN_IDENTITY`,
|
|
115
|
+
`LATCHWAY_IOS_PROVISIONING_PROFILE_UUID`,
|
|
116
|
+
`LATCHWAY_IOS_APPINTENTS_BUNDLE_ID`,
|
|
117
|
+
`LATCHWAY_IOS_APPINTENTS_PROVISIONING_PROFILE_UUID`,
|
|
118
|
+
`LATCHWAY_IOS_SHARED_KEYCHAIN_ACCESS_GROUP`, and the production App Attest and
|
|
119
|
+
`LATCHWAY_SIGNING_CERTIFICATE_SHA256` pins. Android requires
|
|
120
|
+
`LATCHWAY_ANDROID_SDK_PATH`, `LATCHWAY_ANDROID_COMMIT`,
|
|
121
|
+
`LATCHWAY_PACKAGE_NAME`, `LATCHWAY_APP_VERSION`, `LATCHWAY_VERSION_CODE`,
|
|
122
|
+
`LATCHWAY_PLAY_TRACK`, `LATCHWAY_REQUIRE_LICENSED`,
|
|
123
|
+
`LATCHWAY_CLOUD_PROJECT_NUMBER`, `LATCHWAY_FIREBASE_ANDROID_CONFIG_PATH`, and
|
|
124
|
+
the expected Play App Signing `LATCHWAY_SIGNING_CERTIFICATE_SHA256`. The
|
|
125
|
+
checkout and Gradle are strictly unsigned-only and reject every
|
|
126
|
+
keystore/password/alias/upload-certificate input. The repository producer emits
|
|
127
|
+
only a closed unsigned AAB/APK handoff and a canonical pre-sign AAB payload
|
|
128
|
+
manifest. A protected fresh signer job receives that data without a checkout or
|
|
129
|
+
Gradle. Before any step receives key material it separately downloads the AAB
|
|
130
|
+
verifier, checks its protected SHA-256, regenerates the canonical AAB payload
|
|
131
|
+
manifest and byte-compares it with the carried manifest. It also strictly parses
|
|
132
|
+
the unsigned APK, binds its package/version, embedded configuration and
|
|
133
|
+
JavaScript bundle, rejects every `META-INF` signature control and requires
|
|
134
|
+
`apksigner` to reject it as unsigned. The secret-bearing step invokes only
|
|
135
|
+
system signing tools; APK signing fixes minimum SDK 24 and disables v1/JAR
|
|
136
|
+
signatures. A second fresh job has no secret and no checkout; it accepts the
|
|
137
|
+
verifier source only when its SHA-256 matches the same protected pin. The
|
|
138
|
+
verifier independently parses local and central ZIP records,
|
|
139
|
+
rejects structural mismatches, unsafe modes, overlaps, trailing/polyglot bytes,
|
|
140
|
+
ambiguous names and extra signature metadata, fully reads every payload, pins
|
|
141
|
+
one common leaf signer, and requires exact continuity with the separately
|
|
142
|
+
carried pre-sign manifest. The exact independently verified AAB/APK bytes are
|
|
143
|
+
the only publishable signed output. The signer also retains the source-built
|
|
144
|
+
unsigned APK. The fresh verifier hash-binds it to the unsigned candidate
|
|
145
|
+
manifest, rejects `META-INF` signature controls in both APKs, requires the
|
|
146
|
+
signed APK's complete ZIP payload to be identical (only the v2/v3 APK signing
|
|
147
|
+
block may differ outside the ZIP payload), and independently rechecks package,
|
|
148
|
+
version, embedded Firebase and candidate-configuration hashes, JavaScript
|
|
149
|
+
bundle, and upload signer. This
|
|
150
|
+
prevents a correctly signed APK substituted inside the key-bearing boundary
|
|
151
|
+
from becoming a release output. Passwords and keystores never enter the
|
|
152
|
+
repository build or candidate output. Runtime identity grants are rejected if
|
|
153
|
+
present during candidate production.
|
|
154
|
+
|
|
155
|
+
Use `.github/workflows/react-native-android-candidate.yml` for the Android
|
|
156
|
+
release handoff. Its build job is the only job with a checkout and is isolated
|
|
157
|
+
in the credential-free `react-native-android-candidate-build` environment; it
|
|
158
|
+
emits only unsigned data. `sign-isolated` uses only
|
|
159
|
+
`react-native-android-upload-signing`, has no checkout or Gradle, and
|
|
160
|
+
`verify-signed` uses only the no-secret
|
|
161
|
+
`react-native-android-candidate-verification` environment, with neither signing
|
|
162
|
+
secrets nor a checkout. Each exact environment policy sentinel is the first
|
|
163
|
+
step, before checkout, artifact download, tool execution, secret access, or
|
|
164
|
+
job-token use. Configure the protected verifier
|
|
165
|
+
environment with the exact SHA-256 of
|
|
166
|
+
`scripts/VerifyReactNativeAabSignature.java`; changing the verifier therefore
|
|
167
|
+
requires an explicit protected-coordinate review. Also set the protected
|
|
168
|
+
`LATCHWAY_ANDROID_UPLOAD_SIGNATURE_ALGORITHM` coordinate to the upload key's
|
|
169
|
+
exact `SHA256withRSA` or `SHA256withECDSA` algorithm.
|
|
170
|
+
|
|
171
|
+
Before importing native-only proofs, the finalizer validates the native profile
|
|
172
|
+
and report against its platform-authoritative checked-in schema, requires
|
|
173
|
+
`release_eligible=true`, checks the platform and repository, and binds native
|
|
174
|
+
SDK, core, contract, gateway image/configuration/origin/deployment-statement
|
|
175
|
+
coordinates. The native
|
|
176
|
+
report's byte SHA-256 must match all three independent bindings: the value
|
|
177
|
+
embedded in the signed React Native candidate, the protected React Native
|
|
178
|
+
profile, and the externally signed one-job collector lease. Only the five exact
|
|
179
|
+
allowlisted native tests are copied. Missing, failed, renamed, extended,
|
|
180
|
+
cross-platform, rehashed, or coordinate-substituted reports fail closed.
|
|
181
|
+
|
|
182
|
+
Android native and React Native output evidence use the shared v1 contract. The
|
|
183
|
+
pinned iOS SDK uses the component-aware v2 contract; this repository carries an
|
|
184
|
+
exact reviewed snapshot at
|
|
185
|
+
`Conformance/linked-ios-physical-device-evidence.schema.json` together with its
|
|
186
|
+
validator at `scripts/linked-ios-device-evidence.py`. The finalizer test pins the
|
|
187
|
+
raw reviewed bytes (validator SHA-256
|
|
188
|
+
`8d12b2beb887cebb10f1fcc634cd9ebad839e3b40372a03f5f558ad5f41bc0d4`, schema
|
|
189
|
+
SHA-256 `b0f399ff16ff21e80ac1528af143e3834d0ef80e8a8dbeb9c7d4a2e354ead8c6`),
|
|
190
|
+
so an unreviewed snapshot change fails CI. This prevents genuine v2 iOS evidence
|
|
191
|
+
from being treated as v1 while preserving the v1 React Native output consumed
|
|
192
|
+
by the current cross-repository release adapter.
|
|
193
|
+
|
|
194
|
+
The linked iOS report must contain the exact 13-test component-observation v2
|
|
195
|
+
set. Widget, Share, and Action must each record a distinct successful delegated
|
|
196
|
+
request; sibling server credentials must be rejected; an Action attempt to read
|
|
197
|
+
a Widget or Share private key must record `SecItemCopyMatching` returning
|
|
198
|
+
`errSecMissingEntitlement` without key material; and two overlapping refreshes
|
|
199
|
+
must independently return the same rotated credentials and session. The fresh
|
|
200
|
+
GitHub-hosted signer rechecks those concrete fields before attesting the React
|
|
201
|
+
Native evidence bundle. The finalizer still imports only the five allowlisted
|
|
202
|
+
native security proofs into the outer report.
|
|
203
|
+
|
|
204
|
+
The React Native example does not reproduce or claim the linked SDK's
|
|
205
|
+
Widget/Share/Action operations: its App Intents target has no React Native
|
|
206
|
+
runtime or component-request bridge and fails closed when invoked. The React
|
|
207
|
+
Native iOS run proves the root App Attest path plus the exact, hash-bound native
|
|
208
|
+
Installation Family evidence.
|
|
209
|
+
|
|
210
|
+
For iOS, the producer archives a non-debug, production-signed New Architecture
|
|
211
|
+
`.app` from the exact candidate commit. It requires a protected `Podfile.lock`,
|
|
212
|
+
Firebase plist, signing identity, distinct root and App Intents bundle IDs and
|
|
213
|
+
provisioning-profile UUIDs, the protected shared Keychain access group,
|
|
214
|
+
certificate hash, Team ID, and App ID Prefix. App ID Prefix is independently
|
|
215
|
+
protected because it can differ from Team ID; it binds `application-identifier`
|
|
216
|
+
and both Keychain access groups. The signed root target must have exactly the
|
|
217
|
+
private app-ID group first and the shared component group second. The signed
|
|
218
|
+
App Intents extension must have only the shared group, so it cannot read root
|
|
219
|
+
default Keychain state. Each modern provisioning profile is verified with
|
|
220
|
+
OpenSSL CMS, then its UUID, devices, Team ID, App ID Prefix, bundle ID, and
|
|
221
|
+
App Attest/get-task-allow policy are checked. Every signed Keychain group must
|
|
222
|
+
be authorized by an exact profile entry or a well-formed terminal `*` prefix;
|
|
223
|
+
unrelated profile grants are allowed, while malformed wildcards and
|
|
224
|
+
unauthorized signed groups fail closed. Root signing and its profile must both
|
|
225
|
+
carry the exact App Attest `app-attest-opt-in=[CDhash]` value, and the profile
|
|
226
|
+
must authorize the production environment. The App Intents extension and its
|
|
227
|
+
profile must carry neither App Attest entitlement key.
|
|
228
|
+
`LATCHWAY_IOS_INSTALL_MODE` must be `install`:
|
|
229
|
+
the runner uninstalls any old application, verifies absence, and installs the
|
|
230
|
+
inspected candidate immediately before launch. The executable and
|
|
231
|
+
`main.jsbundle` SHA-256 pins must both match. The producer also emits a
|
|
232
|
+
canonical per-file `ios-app-files.sha256` manifest and the native-compatible
|
|
233
|
+
`latchway.ios-app-bundle-tree.v1` digest, which additionally binds directory
|
|
234
|
+
and file modes. Both hashes are protected lease coordinates and are copied to
|
|
235
|
+
the final profile/evidence. The collector copies the `.app` into a private
|
|
236
|
+
snapshot, verifies both hashes there, re-verifies both immediately before
|
|
237
|
+
handoff, and installs that exact snapshot. The bundle cannot contain its own
|
|
238
|
+
JavaScript hash, so that non-secret, lease-bound digest is supplied at launch
|
|
239
|
+
through the native child environment.
|
|
240
|
+
|
|
241
|
+
For Android, publish the same Release AAB through the pinned Play track,
|
|
242
|
+
install it from `com.android.vending` on the locked physical device, and keep a
|
|
243
|
+
licensed Play account on that device. This Play licensing account is only for
|
|
244
|
+
Play Integrity recognition; it is not the application's Firebase identity.
|
|
245
|
+
The serial is a secret. The package,
|
|
246
|
+
version, version code, canonical installed base/split APK-set manifest hash,
|
|
247
|
+
certificate hash, Play track, and cloud project number are protected
|
|
248
|
+
environment variables. Every split is signature/package/version-code checked;
|
|
249
|
+
the complete installed split manifest is captured before launch and again
|
|
250
|
+
after the observed run, and both the digest and byte-for-byte manifest must be
|
|
251
|
+
identical before the observed digest is written to the evidence profile;
|
|
252
|
+
an added, removed, or substituted split changes the protected set hash.
|
|
253
|
+
|
|
254
|
+
The protected GitHub environments are:
|
|
255
|
+
|
|
256
|
+
- `react-native-ios-production`, on a newly booted repository-scoped JIT
|
|
257
|
+
runner registered with `--ephemeral`, labelled `macOS`,
|
|
258
|
+
`latchway-physical-ios`, and `latchway-ephemeral-jit`, and named exactly
|
|
259
|
+
`latchway-rn-ios-<run-id>-<run-attempt>`;
|
|
260
|
+
- `react-native-android-production`, on the equivalent one-job runner labelled
|
|
261
|
+
`Linux`, `latchway-physical-android`, and `latchway-ephemeral-jit`, and named
|
|
262
|
+
exactly `latchway-rn-android-<run-id>-<run-attempt>`.
|
|
263
|
+
|
|
264
|
+
The application identity bootstrap is a Firebase custom token minted for the
|
|
265
|
+
protected run. It is not a Latchway session/admin grant and crosses only the
|
|
266
|
+
example-specific `LatchwayEvidence` bridge; it never crosses the public
|
|
267
|
+
`@latchway/react-native` SDK bridge. The token must be 32–65,536 bytes and exactly three
|
|
268
|
+
non-empty base64url JWT segments. The signed collector lease and issuer record,
|
|
269
|
+
not the Firebase JWT's provider-defined `aud` field, authoritatively bind its
|
|
270
|
+
SHA-256, repository, source commit, run/attempt, exact Latchway
|
|
271
|
+
`application_id`, exact iOS bundle ID or Android package name in
|
|
272
|
+
`package_or_bundle_identifier`, `identity_provider="firebase"`, issuance,
|
|
273
|
+
expiry, and collector audience. Both the source-free collector gate and the
|
|
274
|
+
fresh candidate-code-free attestation signer revalidate all three
|
|
275
|
+
identity/application fields.
|
|
276
|
+
The example exchanges it exactly once with
|
|
277
|
+
`signInWithCustomToken` to obtain a Firebase ID token; native Latchway code
|
|
278
|
+
continues to own App Attest/Play Integrity, DPoP, session, and refresh state.
|
|
279
|
+
|
|
280
|
+
Reusable runners, runners able to accept a second job, surviving workspaces,
|
|
281
|
+
and shared devices are ineligible. Each environment configures the public
|
|
282
|
+
collector trust root and SHA-256 plus its platform-specific one-use device
|
|
283
|
+
grant SHA-256. The grant is provisioned after the run ID and attempt exist,
|
|
284
|
+
records its issuance and expiry, remains valid for at most five minutes and no
|
|
285
|
+
later than the runner lease, is accepted once, and is bound
|
|
286
|
+
to repository, source commit, run/attempt, application, and
|
|
287
|
+
collector audience `latchway-physical-evidence/rn-ios-app-attest` or
|
|
288
|
+
`latchway-physical-evidence/rn-android-play-integrity`. No reusable identity
|
|
289
|
+
session, organization/admin token, PAT, registry/cloud credential, or OIDC
|
|
290
|
+
authority may be present on a collector. If platform application setup cannot
|
|
291
|
+
consume that one-use Firebase custom token, the physical gate cannot run.
|
|
292
|
+
|
|
293
|
+
Only the platform collection step receives the actual custom token. It first
|
|
294
|
+
hash-verifies the value against the signed lease coordinate and removes it from
|
|
295
|
+
the runner environment immediately after launch/staging. iOS inherits it
|
|
296
|
+
through `DEVICECTL_CHILD_*`; the app captures it before React Native starts,
|
|
297
|
+
unsets the process environment, and has one terminal in-memory read. This is a
|
|
298
|
+
bounded lifetime, not a claim that immutable Swift string bytes are
|
|
299
|
+
cryptographically zeroized; uninstall is authoritative. Android streams it on
|
|
300
|
+
stdin through a shell-only `android.permission.DUMP` content provider into one
|
|
301
|
+
process-memory slot—never an intent extra, argv value, log, or file—and clears
|
|
302
|
+
the mutable byte buffers. Fresh install/app-data clear forbids a persisted
|
|
303
|
+
Firebase `currentUser` from satisfying either platform run.
|
|
304
|
+
|
|
305
|
+
The variable names are `LATCHWAY_COLLECTOR_TRUST_ROOT_PEM`,
|
|
306
|
+
`LATCHWAY_COLLECTOR_TRUST_ROOT_SHA256`, `LATCHWAY_APPLICATION_ID`, and
|
|
307
|
+
respectively `LATCHWAY_IOS_BUNDLE_ID` plus
|
|
308
|
+
`LATCHWAY_IOS_DEVICE_GRANT_SHA256` and the producer-emitted
|
|
309
|
+
`LATCHWAY_IOS_APP_FILES_MANIFEST_SHA256` and
|
|
310
|
+
`LATCHWAY_IOS_APP_BUNDLE_TREE_SHA256`, or `LATCHWAY_ANDROID_PACKAGE_NAME` plus
|
|
311
|
+
`LATCHWAY_ANDROID_DEVICE_GRANT_SHA256`. The public trust root, expected hashes,
|
|
312
|
+
and non-secret application identifiers are repeated in
|
|
313
|
+
`physical-evidence-signing`; no private supervisor key, device selector,
|
|
314
|
+
application credential, or grant value is present there.
|
|
315
|
+
|
|
316
|
+
Create a third reviewed environment, `physical-evidence-signing`, in this
|
|
317
|
+
repository. It must contain no device, identity, application, native-evidence,
|
|
318
|
+
or runner credentials; the exact application identifiers above are coordinates,
|
|
319
|
+
not credentials. Require independent reviewers and restrict deployments to
|
|
320
|
+
`main`.
|
|
321
|
+
|
|
322
|
+
The GitHub-hosted `authorize-source` job and both final attestation jobs reuse
|
|
323
|
+
`physical-evidence-signing`; only the two collectors use their corresponding
|
|
324
|
+
`react-native-ios-production` or `react-native-android-production` environment.
|
|
325
|
+
Every job that names one of these protected environments has the exact
|
|
326
|
+
environment policy sentinel as its first step, before checkout, artifact
|
|
327
|
+
access, device work, credentials, job-token use, OIDC, or attestation.
|
|
328
|
+
|
|
329
|
+
Environment protection should require an independent reviewer and prevent
|
|
330
|
+
untrusted pull-request code from reaching the devices or application accounts.
|
|
331
|
+
Toolchain identities are compared byte-for-byte with the protected expected
|
|
332
|
+
Xcode, `adb`, and `apksigner` versions before collection.
|
|
333
|
+
|
|
334
|
+
## Ephemeral runner identity and teardown
|
|
335
|
+
|
|
336
|
+
The GitHub-hosted `authorize-source` job checks out the React Native candidate
|
|
337
|
+
only as data, executes no repository code, records its exact commit and Git
|
|
338
|
+
tree for both platform audiences and this run/attempt, and creates a GitHub
|
|
339
|
+
Sigstore attestation. Each collector verifies that bundle with
|
|
340
|
+
`--deny-self-hosted-runners` before checking out or executing candidate code.
|
|
341
|
+
|
|
342
|
+
Every JIT image exposes root-owned, non-writable
|
|
343
|
+
`/etc/latchway/physical-collector/lease.json` and `lease.sig`, and the
|
|
344
|
+
root-owned client `/usr/local/libexec/latchway-physical-collector-finalize`.
|
|
345
|
+
The ECDSA/SHA-256 lease is signed outside the candidate VM and binds repository,
|
|
346
|
+
source commit and authorization hash, workflow run/attempt/job/audience,
|
|
347
|
+
runner name/image/boot identity, one-job/fresh/JIT flags, the exact signed app
|
|
348
|
+
or installed APK-set, the iOS per-file manifest and whole-tree digests,
|
|
349
|
+
JavaScript bundle and linked-native evidence hashes, and
|
|
350
|
+
the one-use grant hash/issuance/expiry. Its credential declaration must deny
|
|
351
|
+
long-lived, organization, administration, registry, and OIDC credentials.
|
|
352
|
+
The signed lease uses `latchway.physical-collector-lease.v2`, requires the exact
|
|
353
|
+
canonical grant key set, and asserts that the supervisor enforces the protected
|
|
354
|
+
grant digest once. The signed `latchway.physical-collector-teardown.v2` must
|
|
355
|
+
assert one consumption and gateway-receipt binding, and its
|
|
356
|
+
`observations.identity_grant_sha256` must equal that protected digest. Both the
|
|
357
|
+
collector and fresh signer enforce these fields for iOS and Android.
|
|
358
|
+
|
|
359
|
+
The finalizer is a client for an authenticated privileged supervisor, not a
|
|
360
|
+
general signing utility. The signing key and gateway observer capability stay
|
|
361
|
+
outside candidate control. The service accepts file paths rather than
|
|
362
|
+
caller-supplied digest claims, independently hashes and validates the source,
|
|
363
|
+
evidence, and wipe receipt, independently observes the device and the
|
|
364
|
+
gateway's server-side App Attest or Play Integrity run receipt, and permits one
|
|
365
|
+
invocation per lease. It deregisters the runner, prevents another job, and
|
|
366
|
+
schedules VM destruction within ten minutes. Candidate code can force a
|
|
367
|
+
failure, but it cannot obtain a signature over arbitrary hashes or a synthetic
|
|
368
|
+
device/provider verdict.
|
|
369
|
+
|
|
370
|
+
The supervisor's out-of-band watchdog must revoke the JIT registration,
|
|
371
|
+
invalidate the one-use grant, wipe/reset the attached device, and destroy the
|
|
372
|
+
VM after cancellation, timeout, runner crash, network loss, or a missing
|
|
373
|
+
finalizer receipt. This path cannot depend on candidate code or a final Actions
|
|
374
|
+
step executing.
|
|
375
|
+
|
|
376
|
+
Platform app-data wipe and supervisor finalization are separate unconditional
|
|
377
|
+
`if: always()` steps. iOS uninstalls the conformance app and confirms absence;
|
|
378
|
+
Android runs `pm clear` and confirms no process remains. Finalization still
|
|
379
|
+
runs if lease, source, toolchain, grant, collection, or wipe validation fails.
|
|
380
|
+
Only a signed teardown with `evidence_eligible=true`, independent
|
|
381
|
+
device/provider and gateway-receipt verification, successful wipe, JIT
|
|
382
|
+
deregistration, no further jobs, and a bounded destruction deadline can be
|
|
383
|
+
handed to the signer.
|
|
384
|
+
|
|
385
|
+
Both environments configure `LATCHWAY_GATEWAY_ORIGIN`, the deployment key ID,
|
|
386
|
+
the exact statement SHA-256, the local P-256 public-key path and SPKI SHA-256,
|
|
387
|
+
and `LATCHWAY_GATEWAY_MINIMUM_TRUST_LEVEL`. The gateway publishes canonical
|
|
388
|
+
`/.well-known/latchway/deployment-statement-v1.json` plus its detached DER
|
|
389
|
+
ECDSA/SHA-256 `.sig`. Statements last no more than 24 hours. Android policy
|
|
390
|
+
requires `PLAY_RECOGNIZED` and `LICENSED`; both platforms deny testing/debug
|
|
391
|
+
clients and require request-hash binding. The candidate embeds the origin,
|
|
392
|
+
environment, key ID, statement digest, and public-key digest.
|
|
393
|
+
The iOS client policy pins the core-normalized App Attest trust level exactly
|
|
394
|
+
to `app_verified`; the Android policy pins `device_verified` or
|
|
395
|
+
`strong_device_verified` according to its protected device requirement. A
|
|
396
|
+
provider's normalized trust level is never accepted as evidence for the other
|
|
397
|
+
provider. Both environments also pin `LATCHWAY_ERROR_MAPPING_FEATURE` to a
|
|
398
|
+
syntactically valid feature that is intentionally absent from the root
|
|
399
|
+
component's grant; the same non-secret value is embedded in the signed
|
|
400
|
+
candidate. It may also be absent from gateway configuration. The canonical
|
|
401
|
+
result remains HTTP 403 `component_feature_not_granted`, proving authorization
|
|
402
|
+
precedes feature lookup and does not disclose whether the feature exists.
|
|
403
|
+
This is the React Native wrapper's canonical mapping proof. The independently
|
|
404
|
+
versioned native iOS and Android reports retain their HTTP 404
|
|
405
|
+
`feature_not_found` mapping contract; the finalizer validates those reports
|
|
406
|
+
before importing only their five native-only security proofs.
|
|
407
|
+
|
|
408
|
+
## Collect and validate
|
|
409
|
+
|
|
410
|
+
Dispatch `.github/workflows/physical-device-evidence.yml` with the full
|
|
411
|
+
40-character React Native commit. The runners invoke:
|
|
412
|
+
|
|
413
|
+
```sh
|
|
414
|
+
scripts/run-physical-react-native-ios.sh
|
|
415
|
+
scripts/run-physical-react-native-android.sh
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Each protected environment also pins `LATCHWAY_SOURCE_COMMIT` to that exact
|
|
419
|
+
40-character candidate. The workflow refuses a dispatch input that differs
|
|
420
|
+
from the environment pin. The example retires its dedicated prior Latchway
|
|
421
|
+
installation before the measured calls, so a session created by an older app
|
|
422
|
+
build cannot be reused as current App Attest or Play Integrity proof.
|
|
423
|
+
|
|
424
|
+
The scripts refuse dirty source trees, symbolic-link inputs, mismatched hashes,
|
|
425
|
+
unsafe devices, untrusted builds, stale run IDs, and malformed records. The
|
|
426
|
+
example-native sinks rebuild v2 JSON from exact platform runtime allowlists
|
|
427
|
+
(eight tests on iOS and seven on Android), reject native-only proof fields and
|
|
428
|
+
IDs, and expose only a fixed protected file;
|
|
429
|
+
they do not accept an arbitrary output path. Successful
|
|
430
|
+
artifacts contain the profile, sanitized observation, schema-validated evidence,
|
|
431
|
+
JUnit, validation summary, device inventory, linked native report/profile,
|
|
432
|
+
signed statement, signature, public key, exact client policy, verification
|
|
433
|
+
result, and `SHA256SUMS`. A failed run never becomes a passing report.
|
|
434
|
+
|
|
435
|
+
Each protected JIT collector runs with only repository-scoped `actions: read`
|
|
436
|
+
and `contents: read`: candidate
|
|
437
|
+
checkout, native/device execution, and access to app/device credentials have no
|
|
438
|
+
OIDC, attestation, or artifact-metadata authority. It uploads a bounded one-day
|
|
439
|
+
`react-native-<platform>-physical-unsigned-<run>-<attempt>` handoff. A fresh
|
|
440
|
+
GitHub-hosted Ubuntu job behind `physical-evidence-signing` downloads that
|
|
441
|
+
handoff without checking out source and uses only fixed inline shell, `jq`, and
|
|
442
|
+
hash checks to enforce the exact file set, size limits, manifest, source
|
|
443
|
+
commit, run/attempt, platform, physical-device, production-provider,
|
|
444
|
+
passing-test, and redaction coordinates. Only that job receives OIDC and
|
|
445
|
+
attestation permissions. It emits the observer-compatible final artifacts
|
|
446
|
+
`react-native-ios-physical-<run>-<attempt>` and
|
|
447
|
+
`react-native-android-physical-<run>-<attempt>`, with the GitHub Sigstore bundle
|
|
448
|
+
at exactly `github-attestation.sigstore.json` beside the attested profile,
|
|
449
|
+
evidence, and `SHA256SUMS`.
|
|
450
|
+
|
|
451
|
+
The signer additionally verifies the GitHub-hosted source authorization,
|
|
452
|
+
trust-root signatures on the lease and teardown, exact run/grant/artifact
|
|
453
|
+
coordinates, device-wipe receipt, evidence-manifest binding, independent
|
|
454
|
+
supervisor verdict, and destruction deadline. It attests a
|
|
455
|
+
`collector-isolation-validation.json` subject and retains separate
|
|
456
|
+
`react-native-<platform>-collector-isolation-<run>-<attempt>` artifacts for 30
|
|
457
|
+
days. The observer-compatible physical artifact file sets remain unchanged.
|
|
458
|
+
|
|
459
|
+
Repository validation cannot create hardware, a JIT provisioning service, the
|
|
460
|
+
root supervisor, isolated signing key/observer capability, one-use grant
|
|
461
|
+
issuer, protected environments, or a post-job hypervisor destruction record.
|
|
462
|
+
Those remain external release prerequisites. Operators must independently
|
|
463
|
+
retain a destruction log bound to the same lease/run as each final artifact;
|
|
464
|
+
the signed teardown proves deregistration and scheduled destruction, not that
|
|
465
|
+
the hypervisor later completed it.
|
|
466
|
+
|
|
467
|
+
Repository-only validation is safe to run anywhere:
|
|
468
|
+
|
|
469
|
+
```sh
|
|
470
|
+
python3 scripts/test-device-evidence.py
|
|
471
|
+
python3 scripts/test-physical-evidence-workflow.py
|
|
472
|
+
python3 scripts/test-finalize-react-native-device-run.py
|
|
473
|
+
python3 scripts/test-export-core-physical-evidence.py
|
|
474
|
+
python3 scripts/test-verify-gateway-deployment.py
|
|
475
|
+
bash -n scripts/gateway-deployment-evidence.sh
|
|
476
|
+
bash -n scripts/run-physical-react-native-ios.sh
|
|
477
|
+
bash -n scripts/run-physical-react-native-android.sh
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
These tests use synthetic *rejection* fixtures and schema-valid offline
|
|
481
|
+
documents. They are not physical-device evidence.
|
|
482
|
+
|
|
483
|
+
## Emit the core external domain document
|
|
484
|
+
|
|
485
|
+
After all four reports exist for one release, create a protected coordinates
|
|
486
|
+
file with exactly this shape:
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"schema_version": 1,
|
|
491
|
+
"core_commit": "<40 lowercase hex>",
|
|
492
|
+
"core_release": "v<semver>",
|
|
493
|
+
"contract_version": "<semver>",
|
|
494
|
+
"bundle_sha256": "<64 lowercase hex>",
|
|
495
|
+
"oci_image_digest": "ghcr.io/latchway/latchway@sha256:<64 lowercase hex>",
|
|
496
|
+
"gateway_configuration_sha256": "<64 lowercase hex>",
|
|
497
|
+
"repositories": {
|
|
498
|
+
"core": { "commit": "<40 lowercase hex>", "tag": "v<semver>", "version": "<semver>" },
|
|
499
|
+
"javascript": { "commit": "<40 lowercase hex>", "tag": "v<semver>", "version": "<semver>" },
|
|
500
|
+
"ios": { "commit": "<40 lowercase hex>", "tag": "v<semver>", "version": "<semver>" },
|
|
501
|
+
"android": { "commit": "<40 lowercase hex>", "tag": "v<semver>", "version": "<semver>" },
|
|
502
|
+
"react_native": { "commit": "<40 lowercase hex>", "tag": "v<semver>", "version": "<semver>" }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
This file is prepared before publication: `tag` values are the intended
|
|
508
|
+
release names and need not exist yet. The adapter never resolves or fetches a
|
|
509
|
+
tag; it binds each report to the exact candidate commit and SDK version, while
|
|
510
|
+
requiring each future tag to equal `v<version>`.
|
|
511
|
+
|
|
512
|
+
Then run the deterministic offline adapter into a new, empty output directory:
|
|
513
|
+
|
|
514
|
+
```sh
|
|
515
|
+
python3 scripts/export-core-physical-evidence.py \
|
|
516
|
+
--schema Conformance/physical-device-evidence.schema.json \
|
|
517
|
+
--coordinates /protected/release-coordinates.json \
|
|
518
|
+
--ios-profile /protected/ios/app-attest-profile.json \
|
|
519
|
+
--ios-evidence /protected/ios/app-attest-evidence.json \
|
|
520
|
+
--ios-attestation /protected/ios/github-attestation.sigstore.json \
|
|
521
|
+
--ios-manifest /protected/ios/SHA256SUMS \
|
|
522
|
+
--android-profile /protected/android/play-integrity-profile.json \
|
|
523
|
+
--android-evidence /protected/android/play-integrity-evidence.json \
|
|
524
|
+
--android-attestation /protected/android/github-attestation.sigstore.json \
|
|
525
|
+
--android-manifest /protected/android/SHA256SUMS \
|
|
526
|
+
--rn-ios-profile /protected/rn-ios/react-native-ios-profile.json \
|
|
527
|
+
--rn-ios-evidence /protected/rn-ios/react-native-ios-evidence.json \
|
|
528
|
+
--rn-ios-attestation /protected/rn-ios/github-attestation.sigstore.json \
|
|
529
|
+
--rn-ios-manifest /protected/rn-ios/SHA256SUMS \
|
|
530
|
+
--rn-android-profile /protected/rn-android/react-native-android-profile.json \
|
|
531
|
+
--rn-android-evidence /protected/rn-android/react-native-android-evidence.json \
|
|
532
|
+
--rn-android-attestation /protected/rn-android/github-attestation.sigstore.json \
|
|
533
|
+
--rn-android-manifest /protected/rn-android/SHA256SUMS \
|
|
534
|
+
--output-root /protected/core-physical-evidence
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Run this on a trusted release host with the GitHub CLI (`gh`) available. The
|
|
538
|
+
adapter verifies each profile and evidence file against its retained GitHub
|
|
539
|
+
Sigstore bundle, exact repository, protected workflow path, and candidate
|
|
540
|
+
source commit before parsing its claims. It then validates every report again,
|
|
541
|
+
requires the exact SDK version and shared gateway configuration/release
|
|
542
|
+
coordinates, requires all four reports to bind the same signed deployment
|
|
543
|
+
origin/key/statement, verifies both React Native-to-native evidence hashes, enforces a
|
|
544
|
+
seven-day collection/freshness window, copies all sixteen hashed source
|
|
545
|
+
artifacts. The gateway configuration hash remains mandatory in the protected coordinates
|
|
546
|
+
and every raw report, and is retained through the copied artifacts and their
|
|
547
|
+
hashes. It is intentionally not a top-level member of `physical_devices.json`,
|
|
548
|
+
whose envelope exactly matches the core consumer contract. Only then does the
|
|
549
|
+
adapter write that domain with these four claims:
|
|
550
|
+
|
|
551
|
+
- `app_attest_production_verified`
|
|
552
|
+
- `play_integrity_play_distributed_verified`
|
|
553
|
+
- `react_native_ios_verified`
|
|
554
|
+
- `react_native_android_verified`
|
|
555
|
+
|
|
556
|
+
All four claims are written together and only as `true`; there is no switch or
|
|
557
|
+
manual override that can manufacture a passing domain document. Preserve the
|
|
558
|
+
output as an immutable release artifact and run the core cross-repository
|
|
559
|
+
conformance validator against that directory.
|