You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: openspec/specs/external-sources/spec.md
+79-1Lines changed: 79 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -133,6 +133,78 @@ The system MUST verify the integrity of an externally-sourced artifact before in
133
133
-**THEN** the install MUST fail with "Multiple matching assets, set explicit assetPattern"
134
134
-**AND** no download MUST happen
135
135
136
+
### Requirement: SHA-256 recorded on first successful external install [Hardening]
137
+
138
+
On every successful external install, the system MUST record the artifact's SHA-256 in the app's source binding (`source.{appId}` payload, `sha256` map keyed by version). The digest MUST be taken from the verified `.sha256` sibling when one was checked, and otherwise computed locally from the downloaded archive. Recording MUST happen only after the install fully succeeded. The map MUST be capped (200 entries, oldest evicted first).
139
+
140
+
#### Scenario: Digest recorded from verified sibling
141
+
142
+
- GIVEN `openregister@2.5.0` is installed from `github:ConductionNL/openregister` and the release publishes a matching `.sha256` sibling
143
+
- WHEN the install succeeds
144
+
- THEN the binding for `openregister` MUST contain `sha256["2.5.0"]` equal to the verified digest
145
+
146
+
#### Scenario: Digest computed and recorded without sibling
147
+
148
+
- GIVEN the release for `openregister@2.4.0` has no `.sha256` sibling asset
149
+
- WHEN the install succeeds (with the existing `integrityWarning`)
150
+
- THEN the binding MUST contain `sha256["2.4.0"]` equal to the locally computed SHA-256 of the downloaded archive
151
+
152
+
#### Scenario: Failed install records nothing
153
+
154
+
- GIVEN an external install of `openregister@2.5.0` fails after download (e.g. appId mismatch in `appinfo/info.xml`)
155
+
- WHEN the install aborts
156
+
- THEN no `sha256["2.5.0"]` entry MUST be written for that attempt
157
+
158
+
### Requirement: Recorded SHA-256 enforced on reinstall [Hardening]
159
+
160
+
When the binding records a SHA-256 for the requested version, the system MUST compare it against the SHA-256 of the freshly downloaded artifact before extraction. On mismatch the install MUST fail with a message naming both digests and the machine-readable error code `sha_mismatch`, and no extraction, backup, or filesystem change MUST happen. The request parameter `acceptNewSha: true` MUST bypass the comparison for that single request and, on install success, replace the recorded digest; the replacement MUST be logged at warning level and audited when the audit-trail capability is available.
161
+
162
+
#### Scenario: Matching digest proceeds
163
+
164
+
- GIVEN the binding records `sha256["2.3.0"]` for `openregister`
165
+
- WHEN the admin rolls back to 2.3.0 and the downloaded artifact hashes to the recorded digest
166
+
- THEN the install MUST proceed through the existing checks
167
+
- AND the install response MUST indicate the artifact matched the first-install checksum
168
+
169
+
#### Scenario: Rewritten release fails closed
170
+
171
+
- GIVEN the binding records `sha256["2.3.0"]` and the upstream release asset has since been replaced with different bytes
172
+
- WHEN the admin attempts to reinstall 2.3.0 without `acceptNewSha`
173
+
- THEN the install MUST fail with error code `sha_mismatch` naming the expected and actual digests
174
+
- AND no extraction, backup, or change to the installed app MUST happen
175
+
- AND a co-published rewritten `.sha256` sibling MUST NOT cause the check to pass (the recorded digest takes precedence)
176
+
177
+
#### Scenario: Explicit acceptance replaces the recorded digest
178
+
179
+
- GIVEN a `sha_mismatch` failure for `openregister@2.3.0`
180
+
- WHEN the admin retries with `acceptNewSha: true` (password-confirmed install) and the install succeeds
181
+
- THEN `sha256["2.3.0"]` MUST be replaced with the new digest
182
+
- AND the replacement MUST be logged at warning level with both digests
183
+
- AND an audit entry MUST record the acceptance when the audit-trail capability is deployed
184
+
185
+
#### Scenario: acceptNewSha without a recorded digest is harmless
186
+
187
+
- GIVEN no digest is recorded for the requested version
188
+
- WHEN the admin installs with `acceptNewSha: true`
189
+
- THEN the install MUST behave exactly as a normal first install (record on success)
190
+
191
+
### Requirement: Recorded digests are binding-scoped and surfaced [Hardening]
192
+
193
+
Recorded digests MUST live inside the source binding payload so their lifecycle follows the binding: rebinding an app to a different source MUST discard the previous binding's digests, while rebinding to the same source MUST preserve them. The binding read API and the external version list MUST expose recorded digests (they are not secrets), and the version picker MUST badge versions that have a recorded digest.
194
+
195
+
#### Scenario: Rebinding to a different source discards digests
196
+
197
+
- GIVEN `openregister` is bound to `github:ConductionNL/openregister` with recorded digests
198
+
- WHEN the admin rebinds it to `github:myorg/openregister-fork`
199
+
- THEN the new binding MUST contain no `sha256` entries from the previous binding
200
+
201
+
#### Scenario: Digests visible in version list
202
+
203
+
- GIVEN the binding records `sha256["2.3.0"]`
204
+
- WHEN the admin loads the version list for `openregister`
205
+
- THEN the 2.3.0 entry MUST include the recorded digest
206
+
- AND the picker MUST badge 2.3.0 as having a first-install checksum on record
207
+
136
208
### Requirement: Source management API [MVP]
137
209
138
210
The system MUST provide HTTP endpoints for listing registered sources and the trusted-source allowlist, binding a source to an app, and **curating the trusted-source allowlist** (adding and removing forge-qualified patterns). Allowlist write operations MUST be admin-only and password-confirmed and MUST reject over-broad patterns.
@@ -286,9 +358,15 @@ The admin UI MUST surface source binding so an admin can view an app's current b
286
358
-[ ] Source binding survives Nextcloud restart (persisted via `IConfig::setAppValue`)
287
359
-[ ] App Store install path (`SelectedReleaseInstallerService`) is unchanged
288
360
-[ ] All `composer check:strict` passes; PHPUnit suite passes
361
+
-[x]`SourceBinding` carries a `sha256` version→digest map with typed accessors; round-trips through `fromArray`/`toArray`; 200-entry cap
362
+
-[x] Digest recorded on every successful external install (sibling-verified or locally computed), never on failure
363
+
-[x] Recorded digest checked before extraction; mismatch → `sha_mismatch`, no filesystem change, recorded digest outranks a sibling `.sha256`
364
+
-[x]`acceptNewSha: true` bypasses once, replaces on success, is warning-logged (and audited when available)
365
+
-[x] Rebind to different source discards digests; same source preserves them
366
+
-[x] Binding API + external version list expose digests; picker badges recorded versions; mismatch dialog offers the explicit acceptance path
289
367
290
368
## Notes
291
369
292
370
- This spec governs the install **mechanism**. The discovery / search UI for finding apps to install lives in proposal 3 (`add-app-discovery-search`).
293
371
- Private-repo support and PAT management live in proposal 2 (`add-github-pat-management`). For this proposal, only public GitHub releases are supported.
294
-
-Future hardening work: auto-pin observed SHA-256 to the binding so a maintainer rewriting the GitHub release cannot ship altered bytes silently. Tracked as a TODO; not in this proposal's MVP.
372
+
-SHA-256 auto-pinning (trust-on-first-use) is implemented: the **first** install of a never-observed artifact is exactly as trusted as before (allowlist + appId/version checks + optional sibling checksum) — this hardening adds no first-contact protection. The sibling `.sha256` check remains a transport check; the recorded digest is the history check and wins on conflict. Cosign/Sigstore verification remains the future cryptographically complete answer (separate change); App Store installs keep the NC code-signing chain and are out of scope.
0 commit comments