Onym Moderation ↔ Device Mark: Apple DeviceCheck Implementation
Implementation profile draft 0.2 — August 2026
This profile maps the abstract device-mark rail onto Apple DeviceCheck: two per-device bits scoped to the interface vendor's Apple developer account, written and read server-side, surviving app reinstallation.
This document is a concrete implementation of
Moderation.md §5.7 (device marks and enforcement
binding). The abstract contract remains authoritative for mandates, reports,
cases, verdicts, obligations, and invariants. The merged
onym-moderation main
defines the concrete v1 Rust wire and implemented lifecycle. This document
defines the Apple platform mapping and records where that implementation falls
short of the abstract rail.
The document distinguishes:
- platform behavior, which Apple's DeviceCheck service provides;
- profile requirements, which a conforming interface vendor's enforcement backend must implement; and
- gaps, where the platform or current code cannot yet meet the abstract contract and a declared mitigation applies.
The current Onym iOS implementation is a client-side conformance
slice, not a deployed DeviceCheck enforcement system. It implements the
domain objects, authenticated consent boundary, backend protocol, device
token provider, verdict validator, gate state machine, persistence seams,
and honest development stub described in §9. It does not yet include the
vendor backend inside the app repository. The merged Rust service implements
the .p8-holding backend, DeviceCheck reads and writes, countersigning, verdict
receipt, reconciliation, and audit log, but is reference code with no
production deployment or credentials.
1. Conformance declaration
| Abstract concept | DeviceCheck mapping |
|---|---|
| Enforcement profile | onym:moderation-enforcement-profile:apple-devicecheck-v1, version 1 |
| Gate notice schema | onym-moderation-apple-devicecheck-case-notice-v1 |
| Device-mark platform | Apple DeviceCheck service |
| Mark scope (per interface vendor) | Bits are per device per Apple developer account — vendor A's bits are invisible to vendor B, matching Moderation.md §13 |
case-open mark |
bit0 |
banned mark |
bit1 |
| Mark write path | Vendor backend → update_two_bits, authenticated by the vendor's DeviceCheck key |
| Mark read path | Vendor backend → query_two_bits |
deviceBinding (mandate/verdict field) |
An opaque vendor-local identifier for the enrollment; not the DeviceCheck token (tokens are ephemeral and unlinkable by design) |
| Device attestation of "this app on this device" | DCDevice.generateToken() on the device, validated by Apple when the backend calls the API |
| Mark persistence | Bits persist across app reinstallation as documented platform behavior; persistence across device erase and restore paths is Apple's fraud-state design intent and must be verified and disclosed per deployment (§8) |
The table instantiates this signed enforcement-profile object:
{
"profileVersion": 1,
"profileId": "onym:moderation-enforcement-profile:apple-devicecheck-v1",
"platform": "apple-devicecheck",
"bindings": {
"enroll-device": {"requestSchema": "onym-moderation-apple-devicecheck-enrollment-request-v1", "resultSchema": "onym-moderation-apple-devicecheck-device-enrollment-v1"},
"countersign-mandate": {"requestSchema": "onym-moderation-mandate-v1", "resultSchema": "onym-moderation-apple-devicecheck-interface-signature-v1"},
"register-mandate": {"requestSchema": "onym-moderation-mandate-v1", "resultSchema": "onym-moderation-mandate-receipt-v1"},
"gate-check": {"requestSchema": "onym-moderation-apple-devicecheck-gate-request-v1", "resultSchema": "onym-moderation-apple-devicecheck-gate-result-v1"},
"deliver-verdict": {"requestSchema": "onym-moderation-verdict-submission-v1", "resultSchema": "onym-moderation-verdict-receipt-v1"}
},
"caseNoticeSchema": "onym-moderation-apple-devicecheck-case-notice-v1",
"markBindings": {"case-open": "bit0", "banned": "bit1"},
"specification": "<content-address-of-this-profile-specification>"
}
Sections 4–6 define the registered schema and mark meanings. The Interface directory authenticates this object as Moderation.md §6.4 requires, and the signed mandate pins its ID/version pair. Reissuing that pair with different bindings is nonconforming.
The account scope is broader than one app. To preserve the abstract contract's exclusive interface-vendor write boundary, a conforming deployment must either dedicate its Apple developer account to interfaces that share this exact bit contract or prove that no sibling app or backend in the account can write the bits. An App ID does not supply a separate moderation-bit namespace.
2. Ownership mapping
| Component | Owner | Holds |
|---|---|---|
| iOS interface app | Interface vendor | DCDevice token generation; ban-state UX (verdict reference, appeal path) |
| Enforcement backend | Interface vendor | Apple DeviceCheck private key (.p8), key ID, team ID; verdict validation; the mark write path |
| DeviceCheck service | Apple | Bit storage and device-token validation; judges nothing |
| Moderation authority | Independent operator | Verdict signing keys; no Apple credentials, no write path |
The Apple developer account credentials are the physical reason the abstract contract routes all mark writes through the interface vendor: parties outside that account cannot write. The profile turns that constraint into the separation-of-powers the contract requires — the authority signs, the vendor executes, Apple stores — while the account-isolation rule above prevents a sibling app from becoming an undeclared write path.
3. Physical topology
┌──────────────────────┐ DCDevice.generateToken() ┌──────────────────────┐
│ iOS interface app │─────────────────────────────>│ Enforcement backend │
│ (device) │ ephemeral device token │ (interface vendor) │
│ shows ban/appeal UX │<─────────────────────────────│ holds .p8 key │
└──────────────────────┘ proceed / refuse + verdict └──────┬───────▲───────┘
signed Verdict │ │ signed
executed here │ │ Verdict
v │
┌──────────────────────────┐ │
│ api.devicecheck.apple.com│ │
│ query_two_bits │ │
│ update_two_bits │ │
└──────────────────────────┘ │
┌───────────────┴──────┐
│ Moderation authority │
│ (no Apple access) │
└──────────────────────┘
4. Bit mapping
| Bit | Abstract mark | Set when | Cleared when |
|---|---|---|---|
bit0 |
case-open |
Backend validates an interim verdict opening a case against this device's enrollment | Dismissal, superseding ban, decision-deadline default, or designation revocation |
bit1 |
banned |
Backend validates a ban verdict whose executeAfter has arrived; a non-suspensive ban may execute before final becomes true |
banExpires, reversal/new-holder remedy, or designation revocation where no forum survives |
Profile requirements:
- bits change only inside verdict execution and declared
deadline/expiry/revocation reconciliation; no
administrative tool, support desk, sibling-app backend, or store-pressure
path may touch
update_two_bits(Moderation.md §8, interface obligation 6); - every
update_two_bitscall is logged against the verdict hash (or the expiry/deadline/revocation rule) that authorized it, and the log is auditable by the audit seat — this is the profile's substitute for a platform-level proof that the vendor wrote faithfully; and last_update_time(returned by Apple atYYYY-MMgranularity) is treated as a consistency check only, never as an authorization or expiry source — real timing lives in the verdict.
5. Enrollment and the gate check
5.1 Reviewed terms and enrollment
Consent is one transaction with a pause for human review, not two manifest fetches:
- the client asks its moderation repository for
manifestForReview(listing); - inside that repository boundary, the authority-directory key is pinned, the exact manifest bytes and detached signature are fetched, the signature and directory bindings are evaluated, the consent-time conditions are validated, and the hash is computed. A conforming build rejects failed authenticity checks; the current soft-enforcement caveat is recorded in §8.9;
- the repository returns an opaque
ReviewedManifest. Its initializer is not public, so application code can display its decoded manifest and hash but cannot manufacture the value accepted by signing or pair one manifest's decoded fields with another manifest's raw bytes; and - after agreement,
consent(to:reviewedManifest:)consumes that same value, re-runs the time-sensitive validity conditions, and never refetches. The mandate's classes,manifestHash, and persisted manifest bytes therefore come from exactly what the user reviewed.
The device enrollment then proceeds:
- the app calls
DCDevice.generateToken()when DeviceCheck is supported; it sendsnilwhen attestation is unavailable and never fabricates a token; - it sends an
EnrollmentRequestcontaining the optional token,userKey, UTC timestamp, and the user's signature over those same transmitted fields; - the backend validates the session signature, freshness, and token,
calls
query_two_bitsas required, and returns an opaque vendor-localdeviceBinding; - the client builds and signs the mandate from its retained reviewed artifact and that binding, including this enforcement-profile ID/version;
- the backend returns only its detached countersignature. The client appends that signature to its own mandate, so the countersigning round-trip cannot replace authority, classes, hash, user, or binding; and
- the interface registers the exact finalized two-signature mandate with the named
authority as Moderation.md §6.2 requires. Registration is idempotent by
mandateRef; failure leaves authority enrollment incomplete. The signed consent artifact remains immutable, but the gate must fail closed and the interface retries only transient failure. If the Authority's published manifest changed, the app must present the new authenticated artifact for review and obtain fresh consent/signatures rather than retrying forever or substituting terms.
These steps bind the abstract operation names as follows: steps 1–3 are
enroll-device, steps 4–5 are countersign-mandate, and step 6 is
register-mandate. The gate flow below is gate-check. Signed interim and
terminal verdicts enter this backend through deliver-verdict, after which
the gate returns CaseNotice or ban state to the app. The first three
operations belong to the interface-enforcement profile; registration and
delivery are the two cross-owner facets of the authority profile.
The current iOS session-signing form is provisional until the backend wire
contract is fixed. Each field is prefixed with a big-endian 32-bit byte
length. Enrollment uses the domain string
onym-moderation-enroll-v1; gate checks use
onym-moderation-gate-v1 and additionally cover mandateRef. Both cover
the user key and UTC ISO-8601 timestamp. The different domains prevent an
enrollment signature from being replayed as a gate-check signature, and
including every transmitted signed field lets the backend reconstruct the
payload it verifies.
5.2 Gate checks
At launch, on foreground, on explicit retry, and at least once per the declared interval while the app runs:
- the client sends a
GateCheckRequestcontaining a fresh optional device token, user key, activemandateRef, timestamp, and session signature; - the backend calls
query_two_bits(ES256 JWT from the.p8key;device_token, freshtransaction_id,timestamp) and reconciles the bit state with its verdict records; bit1set → it returnsbannedwith the governing verdict reference and full display state; the app refuses to operate and displays the authority contact, expiry, ordinary appeal, and new-holder path (a silent brick is nonconforming);bit1clear,bit0set → it returnscaseOpenwith notices; the app operates normally and displays the open case (procedural state, no service degradation);- both bits clear, including Apple's "bit state not found" response → it
returns
clear; and - an invalid or unavailable attestation, unresolved holder, exhausted
offline grace, or otherwise untrustworthy answer → it returns or the
client derives
checkRequired, which blocks operation.
No active mandate is not an operational answer: it routes to the consent gate. Losing local mandate storage must therefore lead to re-consent and an immediate fresh bit check, never past the gate into an unmoderated app.
The backend associates the enrollment with the mandate's
deviceBinding identifier at first token validation. DeviceCheck
tokens are ephemeral and cannot serve as identifiers; the binding is a
vendor-local record, which keeps the abstract contract's promise that
no global device identifier is created.
Because tokens are unlinkable, the profile is explicit about what needs linkage and what does not:
- Reading is stateless. The bits are the sole state the refusal
decision consults: any fresh token can be queried. A set
bit1refuses service regardless of whether the backend can resolve the enrollment. This is what survives reinstall. - Writing and reconciliation are session-mediated. Every gate check runs inside a session authenticated by an identity key (mandate signing, launch, notice delivery). The (identity signature, device token) pair presented together in one session is the only token-to-enrollment linkage, and it is refreshed at every session — including the first session after a reinstall, when the user restores a mandated identity. The backend never links a token to an enrollment any other way.
- Unresolvable state routes to re-identification. A device whose bits are set but whose session identity resolves to no active verdict (fresh identity after a wipe, or a new device holder) is shown the re-identification path: the ban UX displays the governing verdict when the session resolves one. Otherwise it displays the authority's new-holder/re-identification procedure, whose outcome is a reversal verdict (clearing the bits) or a confirmation that names the holder's session identity and restores linkage.
Profile requirement — offline and gate-evasion window: the gate check
runs at launch and at least once per declared interval (default P1D)
while the app runs. A device that cannot reach the backend operates on
its last known state for a declared grace window (default P3D), then
degrades to gate-check-required. The verdict's identity refusal applies
at the backend regardless, so a banned identity gains nothing from
keeping the app offline.
The iOS gate repository persists the last successful result and its wall
clock time. An unreachable backend within grace serves that last result;
no history, expired grace, or a clock earlier than the last success blocks
with neverChecked, offlineGraceExpired, or clockRollback
respectively. Concurrent checks carry a monotonically increasing
generation so a stale slow clear cannot overwrite a newer banned
answer. Its internal interval loop re-anchors to wall-clock deadlines;
the application composition must still call checkNow() on foreground,
because iOS may suspend the loop.
6. Verdict execution
On receiving a verdict from the designated authority, the backend:
- validates shape per Moderation.md §5.6 — authority signature against
the operator key in the mandate's exact consented manifest; consented
manifest hash, authority, mandate reference, accused user,
deviceBinding, and class bindings; mandatory reasoning; marks and fields consistent with the disposition;appealDeadlineequal todecidedAt + appealWindow;executeAfterconsistent with the consented appeal effect; andbanExpiresequal toexecuteAfter + banTermfor duration classes or absent for permanent classes; - resolves
deviceBindingto the enrollment; if the device has no live session, the write is queued and executes in the next session that presents a token together with an identity the enrollment's mandate names (mark_write_failedsemantics: retry, with the identity refusal already in force at the backend). Thecase-openwrite has a natural execution point: notice service (Moderation.md §5.5) requires a connected session of the accused, and the write executes in that session; - calls
update_two_bitswith the target bit state; and - stores enough state to reconcile
banExpires, dismissal/reversal, decision-deadline default, and designation revocation. The merged reference currently implements only expiry plus delivered verdicts; §8 records the missing independent defaults.
Merged-reference gap. Authority-signature failure is enforced only when the deployment enables the reference service's signature-enforcement switch; it defaults off. This deployment setting does not weaken requirement 1.
The concrete Rust/Apple write log stores the abstract provenance field as
authorized_by: normally the governing verdictRef, or expiry for expiry
reconciliation. Its audit response exposes authorizedBy with the requested
aggregate state, write outcome, and previous/current hashes in the
tamper-evident chain. These names and the expiry sentinel are specific to
this profile.
Clearing on expiry, a delivered dismissal/reversal, or another declared default
requires a live device token, which the banned app cannot always supply (the
user may have deleted it). Profile requirement: the backend clears bits
lazily — the next time any app install on that device presents a token, the
backend reconciles verdict state before answering the gate check. Resolution is
session-mediated (§5): when the session's identity resolves the enrollment, a
conforming backend applies expiry, later dismissal/reversal, and
deadline/revocation defaults directly; when it resolves nothing, the reference
returns checkRequired(reidentificationRequired). No implemented
re-identification flow turns that state into a reversal or restored linkage. A
device that never returns keeps stale bits in Apple's storage, but no conforming
gate ever acts on them without reconciliation, so the stale state is inert.
The iOS package provides this validation as a pure VerdictValidator
returning either execute or storeUntilExecuteAfter. It compares
derived timestamps with one second of serialization tolerance. Client
integration must invoke it before rendering a returned verdict as
legitimate; the validator is not the mark executor, and the Rust reference
backend performs the same checks before every Apple write.
7. Error mapping
| Abstract error | DeviceCheck condition | Backend behavior |
|---|---|---|
mark_write_failed |
4xx/5xx from update_two_bits, token rejected |
Retry with fresh token on next presentation; verdict remains valid; identity refusal in force |
verdict_invalid |
— (pre-platform) | Refuse execution; never call Apple |
| Clean state | 200 "bit state not found" on query | Treat as both marks clear |
| Token invalid | validate_device_token failure / 401 on query |
Re-request token from app; repeated failure → gate-check-required state |
| Rate limited | 429 | Backoff; gate checks serve last reconciled state within grace window |
new_holder_claim |
Holder asserts device transfer | Route to the manifest-declared new-holder procedure; validate and execute its resulting reversal or confirmation through the normal verdict path |
8. Known gaps
- The bits survive the person. DeviceCheck state persists across device resale and hand-me-downs by design. The abstract contract's new-holder claim is the mitigation, but the reference endpoint cannot authenticate ownership and its eight storage slots are exhaustible. It also depends on the new holder encountering the ban UX and acting. A friendlier detection heuristic (for example, a fresh mandate signature from an unrelated identity on a banned device fast-tracks the new-holder path) is a profile requirement not yet implemented anywhere.
- Two bits, month-granular timestamp. The platform cannot store verdict references, expiries, or class information; all real state lives in the vendor backend, and the bits are a cache of its conclusions. A vendor that loses its verdict database cannot reconstruct meaning from Apple's bits alone; backup obligations belong to the interface's own contract.
- No third-party proof of faithful writes. Apple attests the device, not the vendor's honesty. The signed write log (§4) plus audit-seat attestation is the declared substitute; until an auditor actually attests a deployed backend, this is a paper control.
- Apple is a silent party. Developer-account termination or DeviceCheck service changes can erase or orphan mark state outside this contract's lifecycle. The failure is safe-by-default (lost bits read as clean, and gates reconcile against backend state), but the dependency must be disclosed in the interface's manifest.
- Simulator and enterprise-signed builds have no DeviceCheck support; conforming builds must fail toward gate-check-required, not toward unmoderated operation.
- A queued ban write can be outrun. A ban verdict against a device with no live session executes only when a mandated identity next authenticates from it (§6). A user who wipes the app before the write lands and never again presents a mandated identity from that device keeps its bits clean; the identity refusal at the backend still holds for the named keys, so what escapes is the device mark, not access under the banned identity. This is the honest cost of unlinkable tokens, and manifests must not claim device marking is unconditional.
- Reset persistence is asserted, not yet tested. Apple documents the bits as per-device fraud state; whether they survive every erase and restore path (full device wipe, migration to a replacement device, refurbishment) has not been verified by this project. Conformance testing must exercise these paths and the deployment must disclose the result — the same caveat the Android sibling carries for factory reset (Moderation-Device-Recall.md §8.4). Until verified, no manifest may present reset survival as unconditional; the abstract contract's evasion-cost rationale (Moderation.md §3.3) is stated with the same qualification.
- The backend is reference code, not a production deployment. The current
iOS code stops at
EnforcementBackendClientand ships an honest stub. Mergedonym-moderationimplements the Rust network service, DeviceCheck calls, countersigning, lazy reconciliation, and write log, but no deployed Onym service currently holds production DeviceCheck credentials. The stub'sclearresponse to a nil token is a development-only exception; a real backend must return gate-check-required. - Cryptographic enforcement and client transport remain provisional. The iOS package implements detached authority-directory and manifest verification plus verdict verification, but the relevant enforcement switches remain off, and the Rust backend likewise defaults authority verdict-signature enforcement off. The merged reference fixes the current Rust HTTP shapes and UTF-8-byte-order canonical JSON form, and the Swift authority client now ships a URLSession transport whose signing forms are pinned to the same ordering. Turning enforcement on requires real keys plus cross-language fixtures.
- Ban recourse is not wired end to end. The abstract contract requires
the gate to display working ordinary-appeal and new-holder paths. The Rust
BanStatehasappealUrlandnewHolderUrl, but the merged Apple backend leaves both absent; the iOS client now has Authority transport, so the remaining gap is the backend's missing recourse URLs. A ban screen without usable recourse remains nonconforming. - Authority-failure defaults are missing. The Rust Interface clears
case-openonly after the Authority sweep delivers a dismissal and has no designation-revocation or successor-forum state. Permanent Authority disappearance can therefore strand a procedural mark, contrary to the abstract deadline and revocation requirements.
9. Current Onym iOS implementation
The OnymModeration package introduced by
onym-ios PR #216 maps the
abstract contract to the following client components. The authority-client
claims below are directly checkable in
ModerationAuthorityClient.swift,
and the gate result in
EnforcementBackendClient.swift:
| Contract/profile responsibility | Current iOS status |
|---|---|
| Authority designation | KnownAuthoritiesFetcher with signed-asset verification for the interface's authority directory |
| Manifest authenticity | Exact-byte fetch and SHA-256 pin; directory/manifest component and operator-key binding; detached Ed25519 verification path, currently in soft-enforcement mode |
| Consent transaction | Repository-minted ReviewedManifest; the signer accepts the retained artifact and cannot refetch or accept a caller-built SignedManifest |
| Consent-time constraints | Expiry, supported profile ID, mandatory new-holder procedure, and parsed external appellate for permanent classes |
| Mandate lifecycle | One identity-device mandate, user signature, signature-only countersigning response, exact manifest snapshot persistence, immutable history on authority switch |
| Device attestation | DCDevice.generateToken() provider seam; nil represents unsupported attestation and is never fabricated into a token |
| Backend boundary | Typed enrollment, countersignature, and gate-check protocol; the iOS package still uses a development stub, while merged onym-moderation supplies a separate Rust reference backend |
| Gate behavior | Launch/interval state machine, persisted last-known result, P3D grace, fail-closed clock rollback, stale-completion guard, and blocking checkRequired reasons |
| Verdict handling | Domain objects and mechanical validator for mandate/manifest bindings, marks, appeal timing, execution timing, and consented ban duration |
| Authority client boundary | ModerationAuthorityClient exposes registerMandate, fileReport, uploadEvidenceImage, respond (including additional evidence), appeal (ordinary or new-holder), queryStatus, and the device-recovery claim operations; URLSessionModerationAuthorityClientFactory resolves a URLSession transport per directory-selected authority, with the honest throwing stub retained for previews |
| Authority outbound delivery | GateCheckResult.caseOpen carries CaseNotice values and .banned carries ban/verdict display state from the interface backend; the authority has no client-side mark-write method |
| Mandate registration | Implemented: the client appends the interface countersignature and registers the finalized two-signature mandate with the named authority via registerMandate, verifying the returned mandateRef against its own content hash, with single-flight deduplication and retry of interrupted registrations |
| Authority reference service | Merged onym-moderation main defines the concrete v1 request/response shapes and exposes mandate registration, the four client operations, local-model triage, moderator review, verdict delivery, and a DeviceCheck backend; it remains undeployed |
| App composition | Package is linked but PR #216 does not wire onboarding, foreground checks, root gating, or ban/case UI; those integrations belong to later stack layers |
| Authority service and Apple writes | Merged work through PR #10 implements the Authority, triage/review, reconciliation, write log, and update_two_bits path; no production deployment exists, verdict-signature enforcement defaults off, and external appellate routing is absent; the iOS app now calls mandate registration from its consent flow |
The iOS reviewed-manifest path is stricter than the v1 Rust reference: it
requires a new-holder procedure and parses an external appellate for permanent
classes. The merged Rust reference treats both manifest fields as optional metadata. Authority
startup validates the five per-class terms, while the Apple service validates
mandate, manifest-hash, class, and verdict bindings; neither rejects a permanent
class because appellate is absent, malformed, or names the issuing authority.
The Authority records local appeals but does not route them to an external
component. Those iOS checks implement the abstract contract; the merged Rust
services' failure to repeat or route them is a documented conformance gap.
This table is descriptive and deliberately narrower than the profile's acceptance criteria. A package or stub passing local tests is not evidence that Apple state is durable, that a backend writes faithfully, or that a real authority follows the abstract procedure.
10. Conformance tests
Fixtures must cover: JWT construction and key rotation against Apple's development endpoint; query/update round-trip including the "bit state not found" clean state; verdict-to-bit execution for every disposition; lazy reconciliation (expiry passed while device absent, later dismissal/reversal, decision default, and revocation); queued writes on token absence and their execution at the next mandated-identity session; identity-mediated re-linking after reinstall; routing of a bit-set device with an unresolvable session identity to re-identification; grace-window and gate-check-required degradation; developer-account sibling-app write isolation; and the bounded, indistinguishable unauthenticated new-holder-claim responses implemented by the reference Authority.
11. Acceptance criteria
This profile is successfully implemented when:
- a ban verdict against an enrolled device results in
bit1set and the app refusing service with the full ban UX, using only the vendor's DeviceCheck credentials; - deleting and reinstalling the app on the banned device restores the refusal from the bits alone — the refusal decision consults no backend account state, though the vendor's verdict and enrollment records (sanction state under the interface contract's disclosed carve-out) exist and are consulted for reconciliation and appeal routing;
- expiry, dismissal/reversal, decision-deadline default, and applicable revocation clear marks without authority or user discretion beyond a token presentation;
- no code path outside verdict execution and reconciliation — including a
sibling app or its backend — can reach
update_two_bits, and the write log accounts for every call; and - a second interface vendor can implement this profile against its own Apple account without any coordination with the first, and neither can read or write the other's bits.
References
- Onym moderation contract boundary: Moderation.md
- Apple DeviceCheck documentation: https://developer.apple.com/documentation/devicecheck
- Accessing and modifying per-device data: https://developer.apple.com/documentation/devicecheck/accessing-and-modifying-per-device-data
- Onym audit seat (write-log attestation): ../audit/Audit.md
- Current Onym iOS client implementation: onym-ios PR #216
This document is maintained in the public onym-system repository and rendered here from a pinned commit. The repository is the authoritative source — latest version on main →