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/mobile/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` and does not 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 var requireVerdict: RequiredVerdict? // fail-closed on the signed verdict tier; default nil
    public var callerMetadata: CallerMetadataCheck? // manifest + content for caller-metadata; default nil
    public var requireCallerMetadata: Bool     // fail-closed: the proof must commit content; default false
    public var expectedSessionNonce: Data?     // the sessionNonce you asked with; nil => NOT-CHECKED
    public init(expectedRegion: ProofRegion? = nil, maxAgeSeconds: TimeInterval = 300,
                requireAttestation: Bool = false, attestationBundle: Data? = nil,
                /* ... */ requireVerdict: RequiredVerdict? = nil,
                callerMetadata: CallerMetadataCheck? = nil, requireCallerMetadata: Bool = false,
                expectedSessionNonce: Data? = nil)
}

public enum RequiredVerdict: Sendable, Hashable {
    case verified    // only a signed VERIFIED verdict passes
    case plausible   // a signed VERIFIED or PLAUSIBLE verdict passes
}
```

  </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
    val requireVerdict: RequiredVerdict? = null, // fail-closed on the signed verdict tier; default null
    val callerMetadata: CallerMetadataCheck? = null, // manifest + content for caller-metadata; default null
    val requireCallerMetadata: Boolean = false,  // fail-closed: the proof must commit content; default false
    val expectedSessionNonce: ByteArray? = null, // the sessionNonce you asked with; null => NOT-CHECKED
)

enum class RequiredVerdict {
    VERIFIED,    // only a signed VERIFIED verdict passes
    PLAUSIBLE,   // a signed VERIFIED or PLAUSIBLE verdict passes
}
```

  </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.
- **`requireVerdict`** (new in 3.0, deprecated in 3.1) fails a proof whose signed [assurance tier](/docs/mobile/api-reference/octet-verdict/#the-assurance-tier) is below the one you require. It also fails closed when the verdict is not signed into the proof. `.verified` / `VERIFIED` refuses a `PLAUSIBLE` disc answer, which otherwise verifies like a `VERIFIED` one. It matches `--require-verdict` in `octet-verify` 1.6.0. Since 3.1 it is ignored by default, and its check reports `NOT-CHECKED`. See [the assurance tier](/docs/mobile/api-reference/octet-verdict/#the-assurance-tier). It is removed in 4.0.
- **`callerMetadata`** (new in 3.1) checks a [content commitment](/docs/mobile/concepts/content-commitments/). See [Caller metadata](#caller-metadata).
- **`requireCallerMetadata`** (new in 3.1) fails a proof that commits no content, so a presenter can't avoid the check by handing over a different proof.
- **`expectedSessionNonce`** (new in 3.1) is the `sessionNonce` you passed when you asked for the proof. The `session-binding` check passes only for a proof bound to this nonce, and fails for a proof bound to another nonce or to none, so a proof captured from another session can't be replayed to you. Checking that each nonce is used once stays with you. It matches `--session-nonce` in `octet-verify`. See [Proof Binding](/docs/mobile/concepts/proof-binding/).
- **`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 attested 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`, `stage-signatures` is `PASS`, and no check is `INCONCLUSIVE`. |
| `spoofingVerdict` | `LocationProofVerdict?` | New in 3.0. The proof's signed assurance tier (`VERIFIED`, `PLAUSIBLE`, and so on), or `nil` / `null` when the tier is not signed into the proof. Since 3.1, `nil` / `null` by default. |

- **`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 taxonomy below. Most names match `octet-verify`. The on-device `hardware-attestation` check covers what the CLI reports as `attestation-root` (Android) and `app-attest` (iOS).
- **`CheckStatus`**: `pass` / `fail` / `warn` / `notChecked` / `inconclusive` (`PASS` / `FAIL` / `WARN` / `NOT_CHECKED` / `INCONCLUSIVE` on Android). The wire tags are `PASS` / `FAIL` / `WARN` / `NOT-CHECKED` / `INCONCLUSIVE`. `inconclusive` is new in 3.1. A `switch` or `when` over `CheckStatus` without a default branch must handle it.

### 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`, `stage-signatures` is `PASS`, and no check is `INCONCLUSIVE`. `NOT-CHECKED` never makes a proof authentic. New in 3.1: an `INCONCLUSIVE` check makes `isAuthentic` false.
- `verdict` is `invalid` if any check is `FAIL`, else `valid` if `isAuthentic`, else `inconclusive`.
- A skipped check is always reported as `NOT-CHECKED`.

## 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, so the proof is hardware-attested. | PASS / FAIL when anchorable, else NOT-CHECKED |
| `attestation-required` | Fail-closed gate on `hardware-attestation`. | PASS / FAIL only when `requireAttestation` |
| `session-binding` | The proof is bound to `expectedSessionNonce`. New on-device in 3.1. | PASS / FAIL when `expectedSessionNonce` set, else NOT-CHECKED |
| `verdict-tier` | On a disc answer, the tier the verdict was signed with. New in 3.0. | Disc answers whose proof carries a signed tier |
| `caller-manifest` | The content list the proof carries recomputes its signed commitment, and a presented manifest matches that list. New in 3.1. | PASS / FAIL (`MANIFEST_MISMATCH`, `MANIFEST_INVALID`) when the proof lists content, else NOT-CHECKED |
| `caller-manifest-locator` | A listed locator looks like a credential: longer than 256 bytes, or a URL with a query string or user info. New in 3.1. | WARN, never FAIL. Present only when a locator is flagged |
| `caller-metadata` | The presented manifest matches the proof's content commitment, and each presented value matches its digest. New in 3.1. | PASS / FAIL / INCONCLUSIVE, else NOT-CHECKED |
| replay-uniqueness, revocation | Backend-only state. | Always NOT-CHECKED on-device |

## Caller metadata

New in 3.1. To check a [content commitment](/docs/mobile/concepts/content-commitments/), pass the manifest you received with the proof, and the content you want to check, in `callerMetadata`.

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

```swift
public struct CallerMetadataCheck: Sendable {
    public enum Content: Sendable { case bytes(Data), file(URL), unavailable(String) }
    public enum RequiredContent: Sendable, Equatable { case none, keys([String]), all }
    public var manifestJson: Data?            // the manifest, as received
    public var content: [String: Content]      // per manifest key
    public var requireContent: RequiredContent // default .none
}
```

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

```kotlin
class CallerMetadataCheck(
    manifestJson: ByteArray?,                  // the manifest, as received
    val content: Map<String, Content> = emptyMap(),
    val requireContent: RequiredContent = RequiredContent.None,
) {
    sealed class Content { class Bytes(value: ByteArray); data class FromFile(val file: File); data class Unavailable(val reason: String) }
    sealed class RequiredContent { object None; data class Keys(val keys: List<String>); object All }
}
```

  </TabItem>
</Tabs>

The manifest is JSON, `{"v":1,"entries":[{"key","contentType","size","salt","sha256"}]}`, with one entry per item in the order of the proof's `contentCommitments`, and `salt` and `sha256` in lower-case hex. Build it from `contentCommitments` and the salts your app kept. An item you leave out of `content` is accepted on its digest. Use `requireContent` to insist on seeing particular items. Use `.unavailable` for content you were told about but could not fetch.

The `caller-metadata` check reports:

| Result | When |
|---|---|
| `PASS` | The manifest matches the commitment, and every presented value matches. The detail counts items matched by content and by digest. |
| `FAIL` | A value or a size does not match (`CONTENT_MISMATCH`), the manifest differs from the list the proof carries (`MANIFEST_MISMATCH`), the manifest is malformed (`MANIFEST_INVALID`), or content was presented for a proof that commits none. Also a proof without a commitment when `requireCallerMetadata` is set. |
| `INCONCLUSIVE` | Content was referenced but could not be read, was required and not presented, or was presented without a manifest (`CONTENT_UNRESOLVED`). |
| `NOT-CHECKED` | Nothing was presented. |

`INCONCLUSIVE` makes the verdict `inconclusive` and `isAuthentic` false, so a proof whose required content went unchecked is never accepted on either.

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

```swift
let check = CallerMetadataCheck(manifestJson: manifest,
                                content: ["note": .bytes(note), "video": .file(videoURL)],
                                requireContent: .all)
let result = Octet.verify(proof, options: VerifyOptions(callerMetadata: check,
                                                        requireCallerMetadata: true))
if result.verdict == .valid { accept(proof) }
```

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

```kotlin
val check = CallerMetadataCheck(manifest,
    mapOf("note" to Content.Bytes(note), "video" to Content.FromFile(videoFile)),
    RequiredContent.All)
val result = Octet.verify(proof, VerifyOptions(callerMetadata = check, requireCallerMetadata = true))
if (result.verdict == ProofVerification.Verdict.VALID) accept(proof)
```

  </TabItem>
</Tabs>

## Example

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

```swift
var options = VerifyOptions(maxAgeSeconds: 120)
options.requireVerdict = .verified        // refuse a PLAUSIBLE disc answer
let result = Octet.verify(verdict.proof!, options: options)
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, requireVerdict = RequiredVerdict.VERIFIED),
)
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/mobile/concepts/on-device-verification/). What the verdict means and its limits.
- [Verifying Proofs](/docs/mobile/concepts/verifying-proofs/). The `octet-verify` tool and the full trust model.
- [`OctetVerdict`](/docs/mobile/api-reference/octet-verdict/). The proof you pass in rides on `verdict.proof`.
