Provenance Guard (PBPG)
Console-only three-channel device verification: sensor fingerprint, kinematic handshake, and a quorum attestation from a second enrolled device.
Overview
Provenance Guard checks that a request really comes from a specific piece of enrolled hardware. A device answers a challenge with three kinds of evidence: a composite hash of its sensor readings, a kinematic measurement, and an attestation from a second device in the same user's constellation. All three must pass. There is no partial credit and no probabilistic score.
Where it sits. The verifier is the shield-provenance-guard edge function, backed by three tables: shield_constellation_devices (enrolled devices and their rank), shield_provenance_challenges (one row per challenge and its verdict) and shield_succession_tokens.
Who can use it. The Enterprise plan, and a signed-in tenant admin. The function is console-only: it is absent from the public function allowlist, so https://api.passkeybridge.io/v1/shield-provenance-guard answers 404 with x-pb-reason: unknown-function. It requires the admin scope, which no API key the platform will mint can carry, because admin is excluded from the provisionable scope list. The one way in is supabase.functions.invoke from a dashboard session whose user passes is_tenant_admin for the tenant_id in the request body.
What has a screen. Dashboard, Security, Device provenance lists enrolled devices and challenge history. That is the whole UI. Enrolment, challenge and verdict have no screen and no public endpoint, so a deployment that uses them drives them from its own console-side integration.
Three-channel architecture
A verdict is authorized only when all three channels pass, and nothing substitutes for a channel. How strictly Channels 2 and 3 are checked depends on two columns on shield_tenants. Both default to false, and no screen writes either one today, so an operator sets them in the database:
provenance_hardening_enabledturns on server-derived Doppler in Channel 2 and mandatory signature verification in Channel 3.provenance_classb_standalone_enabledlets a Wi-Fi or cell co-presence pass satisfy Channel 3 on its own.
Channel 1, atomic fingerprint. The device submits emi_spectral_hash, thermal_variance_hash, accelerometer_hash, network_jitter_hash and capture_duration_ms. The server joins those four hashes with | in that order, takes SHA-256 of the result, and requires an exact match against the device_fingerprint_hash recorded at enrolment. Any difference fails the channel with similarity 0. Drift-aware matching appears in the patent application and is not implemented, so the enrolled value has to be the composite computed in exactly that order.
Channel 2, kinematic handshake. mode is ntn, terrestrial or hybrid.
terrestrialpasses whencarrier_jitter_entropyis above 0.01 and at most 1.0.ntnwith hardening on compares the device'sobserved_doppler_hzagainst a line-of-sight Doppler shift the server propagates with SGP4, for the satellite (norad_id) and observer bound into the challenge. Sendobserved_at, an ISO timestamp of when the device took the reading: the server propagates the ephemeris to that instant rather than to the verdict instant, which removes an unbounded error term. It is refused if it is more than 120 seconds old or more than 5 seconds in the future, and an unusable value fails the channel closed rather than falling back. The expected value is computed by the server, not taken from the request. It passes within 15 percent of that value, with the comparison denominator floored at 1,000 Hz so a near-zero expectation at closest approach cannot widen the window. The satellite must be at least 10 degrees above the horizon, and the carrier is fixed server-side at 11.325 GHz.ntnwith hardening off falls back to comparing the caller's owndoppler_varianceagainst the caller's ownexpected_doppler_variance, which establishes nothing about where the device is.hybridrequires both the NTN and the terrestrial check.
The ephemeris comes from one durable catalog row holding the Starlink group. Since 2026-09-21 it is refreshed hourly from Space-Track.org, the USSPACECOM publication channel, with CelesTrak kept as an automatic backup: CelesTrak's four-hourly job defers while Space-Track is keeping the row fresh and resumes on its own if Space-Track goes quiet for three hours. Rows older than 24 hours are refused and the request path never fetches an upstream itself, so a missing, stale or out-of-group catalog fails the channel closed. Background: Ground truth for space-based auth.
Channel 3, quorum attestation. A second device belonging to the same tenant vouches for the one being challenged. The attesting device must be enrolled, active and senior rank. Class A protocols carry a measured distance; Class B protocols compare sets of observed network landmarks.
| Protocol | Class | Passes when |
|---|---|---|
nfc | A | distance at most 0.1 m, and with hardening on the verdict is reached within 15 seconds of the challenge |
uwb | A | distance at most 5 m |
ble | A | distance at most 30 m |
doppler_correlation | A | distance at most 50 km |
wifi_copresence | B | at least 2 shared Wi-Fi landmarks, observed within a 60 second window |
cell_copresence | B | at least 1 shared cell landmark, observed within a 60 second window |
A missing proximity_distance_m is treated as infinite, so an omitted distance fails Class A rather than passing it. Raw BSSIDs and cell identities never reach a column or a log: the server keyed-hashes each one with HMAC-SHA-256 under the server-held pepper, compares digests, and stores only the count of shared landmarks.
With hardening off, Class A checks rank and distance only, and records signature_valid: null. With hardening on, the attesting device's enrolled ML-DSA-65 public key must verify a signature over a canonical payload that binds the live challenge nonce, so a replayed attestation fails; a device enrolled without a public key fails the channel outright. A Class B pass corroborates but does not satisfy Channel 3 unless provenance_classb_standalone_enabled is set for the tenant.
Actions and request bodies
Seven actions on one function. Every request carries action and tenant_id. A missing tenant_id answers 400 tenant_id required, and an unrecognised action answers 400 Unknown action.
const { data, error } = await supabase.functions.invoke("shield-provenance-guard", {
body: { action: "pg_devices", tenant_id: tenantId },
});| Action | Required fields | Optional fields |
|---|---|---|
pg_challenge | user_hash, device_id | norad_id, observer with lat and lon |
pg_verify | challenge_id | fingerprint, kinematic, quorum_attestation |
pg_enroll | user_hash, device_fingerprint_hash | device_label, enrolled_by_device_id, device_public_jwk |
pg_promote | device_id | none |
pg_succession | user_hash, new_device_id, vouching_device_ids, entropy_chain_hash | kinematic_context_hash, proximity_protocol, proximity_distance_m |
pg_devices | none | user_hash to filter to one subject |
pg_history | none | limit, default 50 and capped at 200 |
Omitting an evidence object on pg_verify is not a client error: that channel simply fails, so an empty submission returns a rejected verdict with 200 rather than a 400.
pg_challenge binds the satellite and observer for Channel 2 at issue time and returns the nonce the attesting device signs:
{
"ok": true,
"challenge_id": "uuid",
"challenge_nonce": "64 hex characters",
"expires_at": "2026-09-16T17:00:05Z",
"channels_required": ["atomic_fingerprint", "kinematic_handshake", "quorum_attestation"],
"correlation_id": "uuid"
}The challenge expires five seconds after it is created. Evidence collection has to be under way before the challenge is issued; a flow that starts capturing sensor data afterwards will miss the window and get 410.
pg_enroll records a junior device and returns { ok, device: { id, rank, enrolled_at }, correlation_id }. device_public_jwk must be an ML-DSA-65 public JWK (kty is AKP, public component only). It is validated whenever it is supplied, and it is required once hardening is on.
Device constellation and ranks
Every device enrolls as junior. Only a senior device can vouch in Channel 3, and pg_promote is the only path from one rank to the other.
pg_promote refuses while attestation_count is below promotion_threshold, which defaults to 5, answering 400 Device needs N more attestations before promotion. A device that is already senior returns 200 with Already senior. One caveat to plan around: no shipped code path increments attestation_count, and none writes last_attested_at. Today those counters move only if something outside this function writes them, so promotion in practice is a database step.
Succession. pg_succession records a hardware handover in shield_succession_tokens. It requires at least two named devices that are senior and active, and answers 400 Need >=2 senior vouching devices, found N otherwise. The row stores the caller-supplied entropy_chain_hash, the voucher ids, the arriving device's fingerprint and the proximity protocol, which defaults to ble. The token is unsigned, no chain is recomputed, and the departing device stays active. Recursive entropy chaining sets out exactly what that action does.
Revocation. There is no pg_revoke action. is_active and revoked_at on shield_constellation_devices are set outside this function, and an inactive device fails a challenge with 404 Device not found or inactive.
Constellation fracture detection appears in the patent application. No code in this build watches for Doppler discontinuity or reissues a challenge on its own.
Verdict and errors
pg_verify returns the per-channel results and the combined verdict. The verdict itself carries no signature: this function verifies signatures the devices supply and signs nothing of its own.
{
"ok": true,
"authorized": false,
"channel_1": { "pass": true, "similarity": 1 },
"channel_2": { "pass": true, "mode": "terrestrial", "trust_weight": 1 },
"channel_3": { "pass": false, "signature_valid": false, "reason": "Attesting device has no enrolled public key" },
"reason": "Provenance FAILED: Channel 3 (Quorum) did not pass. No fallback.",
"latency_ms": 42,
"correlation_id": "uuid"
}authorized is the AND of the three pass flags. reason appears only on a rejection and names each channel that failed. Every verdict also updates the challenge row with the per-channel results, the expected and observed Doppler when hardening computed them, the TLE epoch and age, the shared landmark count and the assurance tier.
| Status | Body or header | Cause |
|---|---|---|
| 400 | tenant_id required | no tenant_id in the body |
| 400 | user_hash and device_id required | incomplete pg_challenge |
| 400 | device_public_jwk is not a valid ML-DSA-65 public JWK | malformed key on pg_enroll |
| 401 | x-pb-reason: invalid-jwt | the session token did not verify |
| 403 | x-pb-reason: not-tenant-admin | signed in without admin rights on that tenant |
| 403 | code: plan_required | the tenant is below Enterprise |
| 404 | Device not found or inactive | unknown or revoked device_id |
| 404 | Challenge not found or already resolved | unknown challenge_id, or one already scored |
| 405 | Method not allowed | any method other than POST |
| 410 | Challenge expired | the verdict arrived more than five seconds after the challenge |
404 with x-pb-reason: unknown-function | at api.passkeybridge.io | the function is console-only |
Verify it worked
Dashboard, Security, Device provenance is the only screen, and it is read-only.
- The four tiles count senior devices, junior devices, and the authorized and rejected verdicts among the loaded challenges.
- Device constellation lists every active device with its rank badge, the first 16 characters of its fingerprint hash, its enrolment date and, for a junior device, a progress bar toward
promotion_threshold. - Challenge history shows the 20 newest challenges: an Authorized or Rejected badge, the Channel 2 mode rendered as Satellite, Terrestrial or Hybrid, the decision time the function recorded, and a tick or cross for Fingerprint, Handshake and Quorum.
- Refresh re-reads both lists. Neither polls, so a verdict issued from your integration appears on the next refresh.
Each verdict also writes an audit entry. Dashboard, Operations, Audit log holds provenance_authorized or provenance_rejected with the challenge id as the resource, alongside constellation_enroll, constellation_promote and succession_ceremony.
Related from the blog
- Ground Truth for Space-Based Auth: Validating Doppler Challenges Against Public TLE Catalogsntn · 11 min read
- The Missing Link in Identity Assurance: Cross-Referencing SIM Signals with Verifiable Credentialssecurity · 15 min read
- Anatomy of an A2A Handshake: How Two AI Agents Establish Trust Without a Shared Secretsecurity · 13 min read