PasskeyBridge

NTN · 2026-09-03

Ground Truth for Space-Based Auth: Validating Doppler Challenges Against Public TLE Catalogs

By J. W. Bouckaert

Ground Truth for Space-Based Auth: Validating Doppler Challenges Against Public TLE Catalogs

Asking the device what it expected

The first version of the kinematic channel in Provenance Guard worked the way most first versions do. The device reported the Doppler shift it observed on a satellite downlink, and it also reported the shift it expected. We compared the two. Inside a tolerance, the channel passed.

That design has a hole you can see from orbit. The party being verified supplied both halves of the comparison. A device that wanted to pass could report any observed value at all, as long as it reported the same number as its expectation. The check measured internal consistency.

The hardened path merged on 2026-09-01 and is live behind a per-tenant flag. It changes one thing: the expected value now comes from a source the device cannot influence. We compute it from a public orbital catalog, for a satellite and an observer position that were fixed when the challenge was issued, at the instant the verdict is computed. This post is about that computation, the numbers that bound it, and the parts of the problem it does not solve.

Two-line element sets

A two-line element set, or TLE, is the standard public description of an object's orbit. Each set carries the mean orbital elements of one object at one reference time, the epoch, plus drag terms. CelesTrak documents the format down to the column positions and publishes current element sets for active satellites, including the Starlink group.

Two properties of a TLE matter for authentication.

First, a TLE is a mean-element set tied to one propagation model. The elements are fitted so that SGP4 reproduces the tracked orbit; hand them to a different propagator and you get a different, worse answer. The model was documented publicly in Vallado, Crawford, Hujsak and Kelso, "Revisiting Spacetrack Report #3" (AIAA 2006-6753), and the open implementations derive from that paper. We use the satellite.js implementation.

Second, a TLE ages. Every time-varying field is referenced to the epoch, and the fit is only as good as the tracking data behind it. The further the verdict instant sits from the epoch, the larger the error, and the catalog itself is refreshed as new tracking arrives. That is why freshness is a term in the error budget rather than a detail.

The pipeline

The computation lives in one shared module, so the public radar on our home page and the verdict path run identical math. In order:

  1. Load the current TLE for the declared satellite from the durable catalog cache. The cache is written from Space-Track.org, with CelesTrak as the backup, and an entry fetched more than 24 hours ago is rejected.
  2. Parse the two lines into an SGP4 record and propagate to the instant the device says it measured, falling back to the verdict instant for a client that sends none. This gives position and velocity in the Earth-centred inertial frame.
  3. Rotate both vectors into the Earth-fixed frame using sidereal time at that instant, and place the observer on the ellipsoid from its latitude and longitude.
  4. Compute slant range and range rate: the component of the satellite's velocity along the line of sight.
  5. Convert range rate to Doppler at the carrier. The relation is the classical one, f_d = -(range rate / c) * f_carrier, with c = 299,792.458 km/s.

The carrier is fixed on the server at 11.325 GHz, a Ku-band downlink value for the Starlink group, and the request cannot change it. That matters because the expected Doppler scales linearly with the carrier: let the caller pick the frequency and they can scale the expectation to match whatever they observed.

Two more gates sit around the comparison. A satellite below 10 degrees of elevation from the observer produces no expectation at all, and the channel fails closed rather than comparing against a value dominated by geometric error at the horizon. And the comparison uses an absolute floor of 1 kHz on the denominator, because line-of-sight Doppler crosses zero at closest approach. A purely relative tolerance would explode there and accept anything.

Binding the target at challenge time

The second half of the fix is about when the target gets chosen.

When we issue a provenance challenge, the caller declares the satellite by NORAD catalogue number and the observer by latitude and longitude. Both are stored on the challenge row. At verdict time we read them back from our own record and ignore anything the verify request says about the target. A device cannot observe one satellite, then ask to be judged against another that happens to fit its number.

The tolerance is fixed the same way. On the hardened path the width of the window is a server constant; a device can send a tolerance field, and we do not read it. This was not true when the hardened path first merged. The request accepted a caller-supplied tolerance, which meant a device could widen its own window to any value it liked. We closed that with the same release as this post. It is the kind of gap that a method piece exists to find, and we would rather write it down than pretend the first cut was complete.

This is the same idea as binding a WebAuthn challenge to a relying party: fix the parameters on the server first, then let the client produce evidence against them. The only novelty here is that the evidence is physical.

The verdict records what it compared. Each verdict row for the kinematic channel carries the NORAD id, the TLE epoch, the catalog age in seconds at verdict time, the expected Doppler and the observed Doppler. When a tenant asks why a channel failed, the answer is in the row.

The error budget

The point of this post is the numbers, so here they are as the code has them.

TermValueEffect on the comparison
Carrier11.325 GHz, fixed server-sideSets the scale. A LEO range rate of 7 km/s produces a shift of about 264 kHz.
Catalog freshnessCache entry rejected if fetched more than 24 hours agoBounds propagation error from an aging element set. The set's own epoch is recorded on the verdict row.
Elevation floor10 degreesBelow it, no expectation is computed and the channel fails.
Tolerance15 percent of the expected magnitude, fixed server-sideAt an expected shift of 100 kHz the window is 15 kHz, a range-rate band of about 397 m/s.
Denominator floor1 kHzNear the zero crossing the window is never narrower than 15 percent of 1 kHz. One kilohertz at this carrier is a range rate of about 26.5 m/s.
Observer positionDeclared at challenge time, latitude and longitude onlyAltitude defaults to the ellipsoid. A wrong observer position moves the expected value; the challenge fixes it before any observation exists.
Verdict instantServer clock at verifyThe device's clock plays no part in the expectation.

Three consequences follow from the table.

The tolerance is wide on purpose. Fifteen percent is a first bound rather than the precision of a receiver, chosen so that fabricated observations fail while catalog age, observer rounding and receiver error do not reject honest devices. Narrowing it is a decision to make on field data, which is the honest part of the next section.

The floor changes the meaning of the test at closest approach. A satellite passing overhead has a Doppler shift that sweeps from strongly positive to strongly negative through zero. Around the crossing the expected value is small and a relative test would be meaningless, so the absolute floor takes over. A device that reports a near-zero shift at the moment the catalog also predicts near zero passes; that is correct, and it is also the least informative moment of a pass. Verdicts that matter should be taken away from the crossing.

Freshness is a gate, not a weight. A stale catalog does not degrade the verdict, it removes the channel. That is deliberate. A degraded expectation is worse than none, because it produces confident wrong answers.

Limits

These are the limits as they stand today.

The catalog scope is one group. The durable cache holds the Starlink group that the public radar uses. A NORAD id outside it resolves to nothing, and the channel fails closed. Broadening the catalog is a follow-on; until then, a tenant declaring a different constellation gets a failed channel instead of a wrong pass.

The cache is refreshed on a schedule. Traffic never refreshes it. Since 2026-09-21 the primary source is Space-Track.org, the channel USSPACECOM publishes through and the one CelesTrak republishes. An hourly job pulls the Starlink group, asks only for propagable on-orbit objects, and adopts the answer only when it parses as a complete feed. CelesTrak remains as an automatic backup on its original four-hourly schedule: it stands down while Space-Track is keeping the row fresh and resumes by itself if Space-Track goes quiet for three hours, with no flag to flip. The public radar endpoint reads the cache and never fetches an upstream itself. Until 2026-09-16 the radar did the fetching as a side effect of being visited, so when CelesTrak rate-limited our address the cache expired and every visit became another refused request; the channel was closed for a week. If every refresh fails for a day, the 24 hour gate closes the channel. That is the safe failure.

There is no field data behind the tolerance. The 15 percent bound and the 1 kHz floor came from the physics and the catalog behaviour, with no population of real receivers behind them. That is why they are constants in one place. The first production tenant with satellite-capable devices will produce the distribution that tightens them.

One satellite, one observation. The channel compares a single observed value against a single expected value. It does not yet ask for a second satellite, a second sample a few seconds later, or the sign change across a pass, any of which would make fabrication much harder. Replay is narrower than it was: a reading now carries the time it was taken, that time is refused beyond a bounded window, and the attesting device in Channel 3 signs it, so a genuine observation from another moment needs that second device to be complicit rather than merely fooled. What remains is that the expected value is arithmetic over a public catalog, so a motivated party who can run SGP4 can compute it without receiving anything. Channel 2 is a check against careless forgery; the unforgeable part is the post-quantum attestation in Channel 3.

The carrier is an assumption about the link. Fixing it server-side removes the caller's ability to scale the expectation, but it also means the channel is only meaningful for downlinks near that frequency. A device observing a different band fails, correctly, until the catalog and carrier are extended together.

The observer position is declared. The device says where it is at challenge time. The channel then tests whether the observation is consistent with that declaration and the catalog. It does not independently establish the position; that is the job of the other channels in the verdict, which is why the kinematic channel contributes a weight rather than a decision.

Future sources of ground truth

Public catalogs are the right foundation because they are the one input in this system that neither the device nor we control. Everything else in the pipeline is a fixed, auditable transformation of that input: the model is published, the carrier is a constant, the observer is bound at challenge time, the tolerance is a server constant, and the verdict row records every value used.

The work that follows is measurement. Collect expected and observed pairs from real devices, plot the residuals against catalog age and elevation, and set the tolerance from the tail rather than from a round number. Add a second sample so the sign of the sweep has to match. Extend the catalog beyond one group and give the cache its own refresh. Each of those is a parameter change on a pipeline whose shape is already fixed, which is the reason to fix the shape first.

Start free · Test the API