Files
buzz/docs/app-attest-compatibility.md
Tom Brow 5397ea49e2 fix(push): verify extended App Attest proofs with a focused owned module (#8329)
## Summary

Apple SDK 27.1 device captures include authenticator extension data that
the current verifier rejects. Replace the general appattest dependency
with a private gateway module that verifies the complete original bytes,
frames enrollment and assertions separately, and accepts well-formed
optional extensions without SDK-specific length or flag allowlists.

The module retains established signature, P-256, CBOR, and
certificate-chain libraries. App identity, exact enrollment environment,
credential/key binding, nonce, challenge, and increasing-counter checks
remain enforced. Bounded parsing rejects ambiguous duplicate keys,
truncation, trailing data, and oversized inputs. Fixture helpers are
test-only. Adapted code retains both upstream MIT notices and source
provenance.

Category/version properties remain authenticated observations under the
chosen compatibility-release policy. This does not implement Apple's
additional distribution-category/version enforcement; introducing those
restrictions requires a separately defined deployment policy. The
[verification
contract](https://github.com/block/buzz/blob/app-attest-owned-verifier/docs/app-attest-compatibility.md)
records this and other limits.

Related: https://github.com/block/buzz/pull/8020 addresses assertion
extension handling. This change also covers enrollment extensions,
credential-key parsing, and certificate validation.

### Validation

- Gateway suites: 77 default tests and 79 personal-development tests
passed, plus startup policy and Clippy.
- Added signed enrollment/certificate, generated CBOR mutation,
maximum-counter, and PostgreSQL race/replay coverage. All 12 isolated
Postgres tests passed. Apple's historical fixture is tested
structurally, not accepted as a current enrollment. Positive synthetic
leaf certificates are generated at test time.
- Protected SDK 26.5/27.1 enrollments and assertions pass the verifier,
including the same key across SDK upgrades; development enrollment
rejects under production policy. Captures are personal-development
identities, not supported production-identity evidence.
- Full local CI passed Rust tests and reached 6,828 passing desktop JS
tests before an unchanged 10 ms focus-timer test failed; its five-test
file passed in isolation. The pre-push native suite had 3,421 passes and
an unchanged 300 ms child-PID test failure, also passing in isolation.
Prior ACP timing flakes and these unrelated hook exceptions do not waive
required CI.
- Windows CI exposed a missing system OpenSSL installation for the test
fixture generator. The test dependency now builds bundled OpenSSL; all
30 focused App Attest tests pass locally. [Current-head
CI](https://github.com/block/buzz/actions/runs/38636062347) passed,
including Windows Rust, PostgreSQL, and desktop integration tests.
- [Prior-head Codex
review](https://github.com/block/buzz/pull/8329#issuecomment-6177515598)
found no major issues. Carl subsequently identified a dropped
credential-length workaround; fixed in
[27f9f26](27f9f2624f),
with 34 focused/protected tests and Clippy passing. [Current-head Codex
review](https://github.com/block/buzz/pull/8329#issuecomment-6178361143)
found no major issues; physical iOS 26 testing was explicitly waived by
Tom on October 14, 2026. No iOS 26 device test was performed. The SDK
comparison used the same iOS 27.0.1 device, so it demonstrates
SDK-linked variation without establishing OS-version causation.
- Isolated candidate HTTP/device validation passed with separate
authority state and no APNs delivery: SDK 26.5 enrollment/delegation,
SDK 27.1 delegation with the exact same key and installation, and fresh
SDK 27.1 enrollment/delegation. Assertions changed from 37 to 102 bytes
across the upgrade; accepted counter increased from 1 to 3 and
delegation generation from 1 to 2. All three runs rejected tampering and
replay (401) and consumed challenges (404). Existing Buzz app builds
were preserved. No production rollout is included.

- Existing-installation validation passed against the current-head Dev
gateway: a guarded SDK 27.1 diagnostic reused the stored key and
installation, advanced the assertion counter from 88 to 89 and
delegation from 89 to 90, and published the existing active relay lease
at generation 9 (previously 8). Independent database reads confirmed the
changes and preserved key, epoch, and expiry. The original app build was
restored in place with current data and keys intact.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-15 07:15:19 -07:00

115 lines
6.6 KiB
Markdown

# 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.
Credential IDs use the fixed 32-byte SHA-256 size, preserving appattest 0.1.1's
workaround for reported iOS 26.x nonstandard credential-length fields. The two
length bytes remain authenticated; the credential ID must match the supplied
key ID and the certificate key hash, and the COSE key at offset 87 must match
the certificate. Signed synthetic tests cover nonstandard lengths, actual
missing/extra credential bytes, key mismatch, and unsigned length mutations.
No enrollment captured on iOS 26.x has yet validated this reported OS variant.
SDK 26.5 tests on iOS 27.0.1 do not substitute for that evidence.
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.
## Resource characterization
A manual debug-build run on arm64 macOS (1,000 parses per case) measured about
1.61 ms for a 49,131-byte map near the 4,096-node limit, 247 microseconds for a
4,097-byte array at that node limit, 0.93 microseconds for a 65,536-byte byte
string, and 3.8 microseconds for depth 32. The test process peaked at 10.4 MB
resident memory; that includes the test harness and is not a parser-allocation
measurement. These are local observations, not latency guarantees or production
benchmarks. The manual `characterize_capacity_work` test reproduces the cases
without imposing a timing-sensitive CI assertion.
Node and depth limits bound traversal and key-comparison work; byte limits bound
copied payloads. Container capacity grows only after successfully decoding each
item, never from an untrusted declared length. The gateway's smaller transport
caps apply before these parser limits. Future valid objects can exceed service
capacity and receive the distinct internal capacity diagnostic; raising limits
requires review and repeating this characterization.
Generated tests cover 256 nested optional-value/signature-mutation cases and
512 arbitrary byte inputs per run. They complement the explicit boundary and
encoding-equivalence corpus. Apple's public historical attestation fixture also
exercises a complete credential key and extension tail with ED clear; its
certificate is not treated as a currently valid enrollment.