Skip to content

Check the issuer of every stream the transmitter returns - #20

Merged
thomasdarimont merged 4 commits into
mainfrom
gh-19-adapt-to-easyssf-changes
Oct 30, 2026
Merged

thomasdarimont merged 4 commits into
mainfrom
gh-19-adapt-to-easyssf-changes

Conversation

@thomasdarimont

@thomasdarimont thomasdarimont commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Ports the hardening of easyssf 0.7.0, which came out of a review of the Go and Python ports.

  • Stream issuer: SsfStreamClient checks the issuer of every stream the transmitter returns, not only of a created one. The registrar deletes only a stream it created
    with the wrong issuer.
  • Poll endpoint: the access token goes only to a poll endpoint that passes the URI policy (https, or http on loopback addresses, unless AllowInsecureHttp is set). A
    configured Poll:EndpointUrl is checked at startup. A polled SET listed under a key other than its jti is reported as invalid_request instead of being handled.
  • Response size: responses from the transmitter are limited to 4 MiB by default (EasySsf:Receiver:Http:MaxResponseSize), applied as MaxResponseContentBufferSize of
    the receiver's HTTP clients.
  • Registrar stop/start: a registration that finishes after StopAsync() publishes nothing and leaves no stream behind. A stream it created goes to the registration that
    followed, or gets deleted. StopAsync() deletes only a stream published since the last stop. New: StopTimeout, and TestTransmitter.SeveralStreamsAllowed.

Not ported because .NET doesn't have the bugs: the loopback check (Uri.IsLoopback already rejects 127.attacker.example), the pending-acks gauge reading NaN, and the
poller thread surviving a restart.

Fixes #19

Signed-off-by: Thomas Darimont thomas.darimont@googlemail.com

A review of the Go and Python ports found that only a created stream had
its issuer checked. GetStreamAsync, GetStreamsAsync, UpdateStreamAsync
and ReplaceStreamAsync of SsfStreamClient now throw
SsfStreamIssuerMismatchException as CreateStreamAsync did, as the SETs
of a stream with another issuer would not verify (SSF 1.0, section
8.1.1).

SsfStreamRegistrar deletes a stream only when it created it with the
wrong issuer, and leaves an existing one alone; it fails without
retrying either way.

Ported from easyssf b9acc3a.

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
The poll request carries the access token, but the poll endpoint was
not checked: one configured or taken from the stream with plain http
on any host received the token. SsfPoller now holds it to the new
SsfPollerOptions.UriPolicy (https, or http on loopback addresses only)
in the one method that sends poll requests, so the acknowledgements
flushed on stop pass the same check. The hosting integration sets the
policy from AllowInsecureHttp and validates Poll:EndpointUrl at
startup.

A SET the poll response lists under a key other than its jti (RFC
8936, section 2.4) is reported as invalid_request under that key
instead of being handled and acknowledged under a key that is not its
own.

Ported from easyssf b9acc3a and 910cd24.

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
HttpClient buffers up to 2 GiB of whatever a response carries, so a
faulty or hostile transmitter could make the receiver hold a body of
its choosing. Metadata, JWK Set, token, stream configuration and poll
responses come nowhere near 4 MiB. The new setting
EasySsf:Receiver:Http:MaxResponseSize, 4 MiB by default and validated
on start, becomes MaxResponseContentBufferSize of the receiver's HTTP
clients; every call of the receiver buffers its response, so a larger
one fails the call as if the transmitter were unavailable. The README
tells a framework-free receiver to bound its own HttpClient.

Ported from easyssf b9acc3a.

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
A registration that completed after StopAsync(), because its
HttpMessageHandler did not observe the cancellation, still published
its stream, and StopAsync() deleted the stream the receiver knew even
when a previous stop had deleted it already. Each Start() now begins a
run of its own; a registration publishes only while it is the current
run, under a lock StopAsync() takes too, so it publishes before the
stop goes on or not at all.

A registration that outlives its stop deletes a stream it created on
the way, or, if the registrar was started again, leaves it to the
registration that followed, which adopts it if it finds it and deletes
it once it publishes another. A stream whose deletion is under way is
not adopted. Orphans are deleted outside the lock and without the
stop's cancellation. StopAsync() deletes only a stream published since
the last stop and warns after the new StopTimeout. TestTransmitter
gets SeveralStreamsAllowed. The README notes what stream management
means for several instances.

Ported from easyssf b9acc3a, 910cd24, cae740f, b18b62f, c8e0c9c,
d7b82ee, 5754b08 and 88c8d60.

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
@thomasdarimont
thomasdarimont merged commit 4129886 into main Oct 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden the receiver against a review of the Go and Python ports

1 participant