import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# `Octet.verify`

On-device proof verification. Static, synchronous, offline. Verifies a `LocationProof` (or its raw bytes) against the keys inside it, with no network call. See [On-device Verification](/docs/concepts/on-device-verification/) for what it can and cannot establish.

## Signature

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
public enum Octet {
    public static func verify(_ proof: LocationProof,
                              options: VerifyOptions = VerifyOptions()) -> ProofVerification
    public static func verify(proofBytes: Data,
                              options: VerifyOptions = VerifyOptions()) -> ProofVerification
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
object Octet {
    fun verify(proof: LocationProof,
               options: VerifyOptions = VerifyOptions()): ProofVerification
    fun verify(proofBytes: ByteArray,
               options: VerifyOptions = VerifyOptions()): ProofVerification
}
```

  </TabItem>
</Tabs>

The two overloads are equivalent: `verify(proof:)` reads `proof.proofBytes`. A structurally undecodable input returns `verdict == invalid` with a `wire-format` `FAIL`, never a throw.

## `VerifyOptions`

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
public struct VerifyOptions: Sendable {
    public var expectedRegion: ProofRegion?   // enables region checks; nil => NOT-CHECKED
    public var maxAgeSeconds: TimeInterval     // freshness window; default 300
    public var requireAttestation: Bool        // fail-closed on hardware-attestation; default false
    public var attestationBundle: Data?        // iOS: enrolment bundle to anchor assertion-only proofs
    public init(expectedRegion: ProofRegion? = nil, maxAgeSeconds: TimeInterval = 300,
                requireAttestation: Bool = false, attestationBundle: Data? = nil)
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
data class VerifyOptions(
    val expectedRegion: ProofRegion? = null,   // enables region checks; null => NOT-CHECKED
    val maxAgeSeconds: Long = 300,             // freshness window; default 300
    val requireAttestation: Boolean = false,   // fail-closed on hardware-attestation; default false
    val attestationBundle: ByteArray? = null,  // iOS: enrolment bundle to anchor assertion-only proofs
)
```

  </TabItem>
</Tabs>

- **`expectedRegion`** turns on the `region-claim`, `region-type`, and `contains` checks. When `nil`/`null`, those report `NOT-CHECKED`.
- **`maxAgeSeconds`** is the freshness window, judged against the signed timestamp.
- **`requireAttestation`** adds the `attestation-required` gate: the result is `FAIL` unless `hardware-attestation` passed.
- **`attestationBundle`** (iOS) is a serialized enrolment bundle (`attestationEnrolmentBundle().protoData()`) that anchors an assertion-only proof to Apple's App Attest root. On Android it is ignored, because every Android proof carries its own key-attestation chain.

## `ProofVerification`

The result. A flat, log-safe record: no coordinates, no proof bytes echoed, no PII.

| Field | Type | Meaning |
|---|---|---|
| `verdict` | `Verdict` | Tri-state headline: `valid` / `invalid` / `inconclusive`. |
| `checks` | `[ProofCheck]` | Every check that ran, in recipe order. |
| `isValid` | `Bool` | No check is `FAIL`. |
| `isAuthentic` | `Bool` | `isValid` and `stage-signatures` is `PASS`. |

- **`Verdict`** -- `valid` / `invalid` / `inconclusive` (`VALID` / `INVALID` / `INCONCLUSIVE` on Android).
- **`ProofCheck`** -- `name: String`, `status: CheckStatus`, `detail: String` (log-safe). `name` is a stable string from the check vocabulary, aligned to `octet-verify`.
- **`CheckStatus`** -- `pass` / `fail` / `warn` / `notChecked` (`PASS` / `FAIL` / `WARN` / `NOT_CHECKED` on Android; the wire tags are `PASS` / `FAIL` / `WARN` / `NOT-CHECKED`).

### Verdict rules

These mirror `octet-verify` verbatim:

- `isValid` is true when no check is `FAIL`. `NOT-CHECKED` and `WARN` never reject.
- `isAuthentic` is true when `isValid` and `stage-signatures` is `PASS`. `NOT-CHECKED` never makes a proof authentic.
- `verdict` is `invalid` if any check is `FAIL`, else `valid` if `isAuthentic`, else `inconclusive`.
- A skipped check is always surfaced as `NOT-CHECKED`, never silently a `PASS`.

## Check taxonomy

| Check | What it confirms | On-device |
|---|---|---|
| `wire-format` | The bytes decode to a well-formed proof with no smuggled duplicate fields. | PASS / FAIL |
| `stage-signatures` | Every stage is signed by the hardware-backed P-256 key the proof carries. | PASS / FAIL |
| `device-attestation-sig` | The device-key signature over the proof's committed fields. | PASS / FAIL |
| `binding` / `freshness` | Field bindings match their signed hashes; the proof falls within `maxAgeSeconds`. | PASS / FAIL |
| `region-claim` / `region-type` / `contains` | The claimed region matches `expectedRegion`. | PASS / FAIL when `expectedRegion` set, else NOT-CHECKED |
| `hardware-attestation` | The device-attestation chain anchors to a bundled Apple / Google root (Tier 3). | PASS / FAIL when anchorable, else NOT-CHECKED |
| `attestation-required` | Fail-closed gate on `hardware-attestation`. | PASS / FAIL only when `requireAttestation` |
| replay-uniqueness, revocation | Backend-only state. | Always NOT-CHECKED on-device |

## Example

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
let result = Octet.verify(verdict.proof!, options: VerifyOptions(maxAgeSeconds: 120))
if result.isAuthentic {
    accept(verdict.proof!)
} else {
    for check in result.checks where check.status == .fail {
        log("verify failed: \(check.name) -- \(check.detail)")
    }
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
val result = Octet.verify(verdict.proof!!, VerifyOptions(maxAgeSeconds = 120))
if (result.isAuthentic) {
    accept(verdict.proof!!)
} else {
    result.checks.filter { it.status == CheckStatus.FAIL }
        .forEach { log("verify failed: ${it.name} -- ${it.detail}") }
}
```

  </TabItem>
</Tabs>

## See also

- [On-device Verification](/docs/concepts/on-device-verification/). What the verdict means and its limits.
- [Verifying Proofs](/docs/concepts/verifying-proofs/). The `octet-verify` service and the full trust model.
- [`OctetVerdict`](/docs/api-reference/octet-verdict/). The proof you pass in rides on `verdict.proof`.
