Device-facing gRPC
ReadyThe device-facing surface is the gRPC contract every authenticator speaks. It is two listeners: an onboarding endpoint over ordinary TLS, where each of two enrollment calls carries its own cryptographic proof, and a mutually-authenticated gateway of eight calls, where the device certificate proves the hardware and a per-call signature proves the user. The Swift and Kotlin SDKs are generated from it.
Who this is for: anyone building a client from the wire directly — a platform we don’t yet ship an SDK for, or a security team auditing exactly what the phone sends. If you use the Swift or Kotlin SDK, you never touch this; it is the ground truth underneath them.
Most integrators embed the Swift or Kotlin SDK and never implement this surface by hand — the SDKs generate their bindings straight from these definitions. Read on if you are bringing a platform we don’t yet ship, or auditing exactly what the device transmits.
Two listeners, two trust models#
The device talks to two separate endpoints. They differ in how a caller is trusted — which is the whole story of the surface.
- A device that has not enrolled yet has no certificate to present, so this endpoint asks for none.
- Each call proves itself with its own cryptographic primitive — a single-use invite token, or a platform attestation bound to a server nonce.
- Two calls, and no more, are reachable here.
- Transport — the device’s mTLS certificate proves “I am this hardware,” verified before the handler runs.
- Application — state-changing calls also carry a per-user signature proving “I am this user,” checked against the key stored at enrollment: an ECDSA-P256 signature, or a hybrid ES256 + ML-DSA-65 signature on devices that commit to post-quantum signing at enrollment. The device produces it under biometric unlock.
- Everything after enrollment lives here.
Onboarding — two calls#
The enrollment ceremony. A device arrives with an invite, proves it is a genuine phone, and leaves with a certificate it keeps. Each name links to its full request/response reference.
| RPC | What it does | Authenticated by |
|---|---|---|
| BeginEnrollment | Claim a single-use invite, and receive a fresh attestation nonce plus the deployment’s root-CA chain. | invite token |
| CompleteEnrollment | Prove platform attestation and the new keys; receive the signed device certificate (and the bundled TOTP secret). | attestation |
Gateway — eight calls#
Everything an enrolled device does. device means the mTLS certificate is proof enough; device + user means a per-user signature rides along.
| RPC | What it does | Authenticated by |
|---|---|---|
| Ping | Reachability, certificate, and device-status check in one — drives the “check status now” affordance. | device |
| RenewCertificate | Replace the device’s mTLS certificate; the server signs a fresh one-year leaf. Active devices do this monthly. | device |
| DeactivateDevice | Device-initiated self-deactivation of this enrollment. | device |
| FetchChallenge | Pull a pending challenge by its ID — the one encoded in a scanned QR. | device |
| SubmitChallengeResponse | The user’s signed terminal decision — approve, deny, or dismiss. | device + user |
| SubscribeChallengesstream | A long-lived stream of incoming challenges — the push channel, with no APNs or FCM in the path. | device |
| SubscribeAccountDevicesstream | The account’s whole device set, sent on open and again whenever a device is added, removed or changes status. | device |
| FetchAccountTOTPSecret | Fetch the current TOTP secret when the phone detects its copy has drifted. | device |
SubscribeChallenges is a plain server-stream: challenges arrive over the same direct, mutually-authenticated connection, with no Apple or Google push service in the path. That is what lets a deployment run entirely on its own — the sovereignty point covered in Why gRPC-only.
Straight from the proto#
Skip the SDK entirely: generate gRPC stubs from the public .proto files and drive both listeners yourself. You then own what the SDK otherwise handles for you — the hardware-backed keys, the canonical payload below, and the signature over it. This is the escape hatch for a platform we don’t yet ship, or a team that wants the transport in its own hands.
// No SDK: generate grpc-swift-2 stubs from the public .proto files. $ buf generate // or protoc --swift_out --grpc-swift_out // You then open the mTLS channel, present the device certificate, // build the canonical payload (see "The signed decision"), sign it // with your Secure Enclave key, and call the generated client. // The RPCs mirror the tables above.
// No SDK: generate grpc-kotlin stubs from the public .proto files. $ buf generate // or protoc --kotlin_out --grpckt_out // You then open the mTLS channel, present the device certificate, // build the canonical payload (see "The signed decision"), sign it // with your StrongBox key, and call the generated client. // The RPCs mirror the tables above.
The generated client mirrors the calls in the tables above; the full worked example is published with the public .proto files at launch.
The signed decision#
The one call that commits a user’s intent — SubmitChallengeResponse — is signed over a canonical byte payload: a fixed digest assembled from the challenge, the outcome, and the context, in a layout every client builds identically. Because the format is pinned, a signature from a Swift phone, a Kotlin phone, or a hand-rolled client all verify the same way on the server. Get that layout wrong and the signature simply fails to verify — the server never guesses.
The full device-facing surface ships as a public .proto file — generate a client in any gRPC language with buf or protoc: download device_gateway_service.proto and device_onboard_service.proto. Each RPC above also links to its detailed endpoint reference. The relying-party surface your own backend calls is separate — see the API reference.