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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
bounded, redirect-aware downloader. Interrupted or cancelled downloads
remove their partial file as reliably as ordinary failures.

- Authenticate signed update-manifest payload bytes before parsing their JSON.
Malformed, unauthenticated payloads now fail at the Ed25519 boundary instead
of entering semantic validation and contradicting the documented trust
order. `read-signed-manifest` remains an explicitly unauthenticated
inspection API; update clients use `verify-signed-manifest`.
- Add a Windows `rivet::system::TrayController` that owns tray window
subclassing, localized native menu presentation, command dispatch, and
automatic `TaskbarCreated` recovery. The reference Taskboard now exercises
Expand Down
4 changes: 3 additions & 1 deletion docs/release-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ run; the family update feed and locked-down machines consume it directly.

HTTPS protects transport, but it is not the root of update trust. A manifest contains an exact byte payload and an Ed25519 signature. The signed payload identifies the application, version, build, stable/beta/dev channel, staged rollout percentage, minimum updatable version, rollback predecessor, and every platform artifact's URL, byte size, SHA-256, installer kind, and arguments.

The updater verifies in this order:
The updater verifies in this order. The signed payload stays as opaque bytes
until the Ed25519 check succeeds; unauthenticated input never reaches the
manifest JSON parser.

1. Ed25519 signature and expected key ID.
2. Application identity, channel, SemVer precedence, minimum version, rollout bucket, platform, and architecture.
Expand Down
7 changes: 5 additions & 2 deletions rivet/distribution/manifest.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,15 @@
(values (payload-bytes->manifest payload) payload key-id signature))

(define (verify-signed-manifest input public-key #:key-id [expected-key-id #f])
(define-values (manifest payload key-id signature) (read-signed-manifest input))
;; The payload is attacker-controlled until the signature succeeds. Parse
;; only the small outer wrapper first, authenticate the exact payload bytes,
;; and cross the semantic parsing boundary afterwards.
(define-values (payload key-id signature) (read-wrapper input))
(when (and expected-key-id (not (string=? expected-key-id key-id)))
(raise-arguments-error 'verify-signed-manifest
"manifest was signed by an unexpected key"
"expected" expected-key-id
"actual" key-id))
(unless (ed25519-verify public-key payload signature)
(error 'verify-signed-manifest "Ed25519 manifest signature verification failed"))
manifest)
(payload-bytes->manifest payload))
8 changes: 7 additions & 1 deletion rivet/scribblings/rivet.scrbl
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,17 @@ policy plus a list of artifacts.
[out output-port? (current-output-port)])
void?]{
Validates, serializes, and signs a manifest payload with Ed25519.}
@defproc[(read-signed-manifest [input input-port?])
(values update-manifest? bytes? string? bytes?)]{
Decodes a signed wrapper for inspection and returns its parsed manifest, exact
payload bytes, key ID, and signature bytes. This procedure does not
authenticate them; update clients should use @racket[verify-signed-manifest].}
@defproc[(verify-signed-manifest [input input-port?]
[public-key any/c]
[#:key-id expected-key-id (or/c #f string?) #f])
update-manifest?]{
Verifies the exact signed payload before exposing the parsed manifest.}
Verifies the expected key ID and Ed25519 signature over the exact payload bytes
before parsing or exposing the manifest.}

@subsection{Selecting and Installing an Update}

Expand Down
23 changes: 23 additions & 0 deletions tests/distribution.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@
(verify-signed-manifest
(open-input-bytes (get-output-bytes tampered-out)) public-key)))

;; Unauthenticated payload bytes never reach the JSON/manifest parser. A
;; malformed payload with a well-formed but mismatched signature must fail at
;; the cryptographic boundary, not disclose a downstream parse diagnostic.
(define untrusted-out (open-output-bytes))
(write-json
(hasheq
'schema manifest-schema-version
'payload (bytes->base64-string #"not JSON")
'signature
(hasheq
'algorithm "ed25519"
'key_id "release-2026"
'value
(bytes->base64-string
(ed25519-sign private-key #"different authenticated payload"))))
untrusted-out)
(check-exn #rx"signature verification failed"
(lambda ()
(verify-signed-manifest
(open-input-bytes (get-output-bytes untrusted-out))
public-key
#:key-id "release-2026")))

(define config
(updater-config "dev.rivet.test" "1.1.0" 'stable 'windows 'x64
public-key "release-2026" 0 (* 1024 1024)))
Expand Down
Loading