fix(push): verify App Attest proofs with a focused owned module

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-14 12:39:44 -07:00
parent e68977f270
commit 8b573da916
18 changed files with 1900 additions and 212 deletions

View File

@@ -0,0 +1,81 @@
# App Attest compatibility boundary
The push gateway verifies enrollment and assertions through a private
[Buzz-owned module](../crates/buzz-push-gateway/src/app_attest.rs). Extended authenticators must
remain compatible without weakening the checks that bind a request to an app,
key, challenge, and increasing counter.
## Receiver policy
Enrollment requires the fixed authenticator prefix, AAGUID, credential ID, and a
complete COSE P-256 key. The COSE key must match the leaf certificate key. Any
remaining bytes form one complete extension map. Assertions require the fixed
37-byte prefix and, when present, one complete extension map. Neither operation
uses a total length or flags allowlist to recognize an Apple SDK version.
Every original authenticator byte participates in the certificate nonce or
assertion signature. Verification does not truncate or re-encode that byte
string. Assertion signatures and enrollment certificate nonces are checked
before decoding the extension payload. Unknown extension identifiers and opaque
optional property values do not authorize or reject a request by themselves.
Malformed structure, ambiguous duplicate keys, and trailing data are rejected.
CBOR accepts well-formed definite/indefinite containers and strings, nonpreferred
integer widths, and opaque tagged/simple values under bounded resource limits.
Map-key equality follows the generic CBOR model, including compound keys;
integers and floating-point values remain distinct. Schema-bearing maps require
their specified text or COSE labels.
The gateway retains its configured Apple root and exact server-selected app and
attestation environment. Certificate validation uses webpki; field extraction
uses x509-parser. The nonce extension must have its complete canonical structure.
The leaf key, credential ID, supplied key ID, zero enrollment counter, and app
hash must agree. Assertions retain signature, app hash, challenge, and increasing
counter checks. Existing atomic database updates remain the replay boundary.
The verifier consumes EKU OIDs without applying TLS purposes to App Attest and
honors supplied digital-signature/key-cert-sign usage restrictions. It does not
perform revocation checking or Apple's separate receipt risk assessment.
Version text and raw four-byte category values are bounded diagnostic
observations only. Unsupported optional representations remain opaque. No
version or category restriction is introduced.
## Capacity and tests
Parser limits are service policy: 64 KiB encoded input, 4096 decoded nodes,
depth 32, and 64 KiB copied strings/bytes. Declared lengths do not drive
preallocation. Gateway transport limits remain 16 KiB for enrollment and 1 KiB
for assertions. Tests cover boundaries, oversized declared lengths, truncations,
duplicate equivalence, and structurally invalid signed payloads.
Signed synthetic enrollment tests cover COSE parity/reordering, extension
variation, certificate constraints, nonce integrity, wrong identity/key binding,
and incomplete credentials. Gateway tests verify legacy and extended assertions,
tamper rejection, wrong transcript/key/app, and replay rejection. The signed-certificate suite runs with the gateway unit tests. Fixture helpers
are compiled only for tests, including in personal-development builds.
Physical-device validation used one probe identity on iOS 27.0.1:
| SDK | Enrollment authenticator | Assertion authenticator | Key relationship |
| --- | ---: | ---: | --- |
| 26.5 | 164 bytes | 37 bytes | Original probe key |
| 27.1 | Previously enrolled | 102 bytes | Same original key |
| 27.1 | 229 bytes | 102 bytes | Additional fresh probe key |
All three assertions and both enrollments pass the gateway verifier. Same-key
counters increase; repeated counters and altered transcripts fail. Development
enrollments fail under the production environment. These are local verifier
results, not evidence of a deployed gateway or APNs delivery. Raw device captures
stay outside the repository; optional ignored tests consume protected local
inputs for repeatable validation.
## Source provenance
Adapted portions originate in [appattest 0.1.1](https://crates.io/crates/appattest/0.1.1),
[upstream commit d1ffda7](https://github.com/PrismaPhonic/appattest/commit/d1ffda733f6e7f267aab79ae42b8ec7f60ceada1).
The package SHA-256 is `5d0a66767aaf7d483c556386fb68ca2fba9347684d8bb17a4bd8b755851870f7`.
Both upstream MIT notices are retained in the module's
[LICENSE](../crates/buzz-push-gateway/src/app_attest/LICENSE).
The gateway owns its verification policy and tracks Apple protocol and underlying
cryptographic-library updates directly. There is no remaining appattest dependency.