Docs/Mobile SDK/API Reference

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 for what it can and cannot establish.

Signature

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

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

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
}
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
}
  • 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 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. It is removed in 4.0.
  • callerMetadata (new in 3.1) checks a content commitment. See 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.
  • 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, pass the manifest you received with the proof, and the content you want to check, in callerMetadata.

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
}
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 }
}

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.

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) }
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)

Example

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)")
    }
}
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}") }
}

See also

TermsPrivacy© 2026 Understone, Inc. d/b/a Octet