Home · docs · provider attestation

spec · v0.4.3

Provider attestation

Every model an Umbra provider serves is backed by a chain that proves the physical machine is the genuine, unmodified Apple device it claims to be — and that the binary running on it is the audited Umbra provider, not a fork.

The five layers

Layer 1

Secure Enclave P-256

The private key is generated and held inside the SE; it never leaves the chip, even to RAM. Every serving-statement is signed by this key. The coordinator and the consumer both verify the signature against the public key in the attestation blob.

Layer 2

MDM SecurityInfo

An MDM (e.g. MicroMDM, Mosyle, Jamf) pushes a profile that asserts the device is enrolled, not jailbroken, and has the expected posture. The profile is signed by the MDM's CA. The coordinator cross-checks the SE-asserted serial against the MDM-asserted serial.

Layer 3

Apple MDA X.509 chain

The attestation blob includes the full Managed Device Attestation certificate chain. The coordinator validates it against the Apple Enterprise Attestation Root CA, then extracts the SE public key + freshness nonce + Apple-arc OID extensions (serial, SepOS version, SIP, SecureBoot).

Layer 4

5-minute freshness challenge

Every 5 minutes the coordinator issues a fresh nonce. The provider binary's SE signs the nonce. A provider that can't keep up — or that changed state between challenges (e.g. debugger attached) — fails the freshness check and drops out of the network.

Layer 5

APNs code-identity

The Swift provider binary registers with APNs to prove it's the audited build (DeviceCheck + App Attest code-identity), not a same-look-alike. This is what stops a malicious operator from running a modified binary on real Apple hardware.

On a real Mac

The production path is the Swift binary at umbra/provider/Sources/UmbraProvider/. It calls, in-process:

// Generate the SE keypair (non-exportable, hardware-backed)
let priv = SecKeyCreateRandomKey([
  kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
  kSecAttrKeySizeInBits: 256,
  kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
], &error) as! SecKey

// Fetch the Apple Managed Device Attestation blob
let dc = DCDevice.current
let attestation = try await dc.generateAttestation()

// Submit to the coordinator
try await coordinator.register(
  attestation: attestation,           // the blob + cert chain
  sePublicKey: priv.publicKeyDER(),
  encryptionKey: x25519.publicKey
)

From a shell:

git clone https://github.com/owenisa/umbra
cd umbra/provider
swift build -c release
./.build/release/umbra-provider init \
    --handle your-handle \
    --token "$UMBRA_TOKEN"

On Linux / dev box

The scripts/umbra-provider Python CLI is a portable reference. In dev/sim mode it builds a synthetic blob that the coordinator's Go MDAValidator accepts when configured with test roots.

pip install cryptography    # optional — openssl fallback works

umbra-provider dev-sim --handle your-handle --token "$UMBRA_TOKEN"
umbra-provider status
umbra-provider attest     # prints the full blob

The script writes two files in ~/.umbra/:

The 5-minute freshness protocol

provider                              coordinator
  |                                       |
  |  GET /v1/attest/challenge              |
  |  nonce + deadline + challenge_id      |
  |  <- (response from coordinator)         |
  |                                       |
  |  SE_Sign(canonical({                   |
  |    challenge_id,                       |
  |    nonce,                              |
  |    publicKey: se_pub,                  |
  |    deadline                            |
  |  }))                                  |
  |                                       |
  |  POST /v1/attest/challenge/answer      |
  |  body: challenge_id + signature        |
  |                                       |
  |  verify: deadline / sig / pubkey      |
  |  -> 200 OK + grant_quota per 5 min     |

If the provider can't answer in 30s, or the signature is invalid, or the public key doesn't match the registration, the coordinator evicts the provider from the network.

Coordinator verification (Go)

The full verifier is in coordinator/internal/verifier/. The chain check is MDAValidator.Validate(report):

  1. Parse the MDA cert chain (leaf + intermediates).
  2. Verify against the Apple Enterprise Attestation Root CA pool using crypto/x509 Certificate.Verify.
  3. Extract the Apple-arc OID extensions from the leaf.
  4. Cross-check: leaf.SerialNumber == blob.serialNumber and leaf.FreshnessNonce == SHA-256(raw SE public key).
  5. Verify the SE signature over the canonical JSON of the blob (minus signature).

All checks fail-closed. Test vectors live in coordinator/internal/verifier/mda_test.go; sim-mode vectors in mda_sim_test.go (the synthetic blob from umbra-provider dev-sim validates against the sim root).

What this page is. A condensed spec for the attestation chain. The source of truth is umbra/docs/attestation.md in the repo. The umbra/provider/ Swift code is the production implementation; the scripts/umbra-provider Python CLI is the dev reference. The Go verifier in coordinator/internal/verifier/ is the load-bearing cryptographic check.