Skip to content

nip55: nonce expiry uses monotonic Instant, which pauses during Android suspend #597

Description

@kwsantiago

Surfaced during keep-android PR review of the nip55-caller-verification-android branch (pin keep@5fc0942, keep-android #to-link).

Summary

Nip55NonceStore in keep-mobile/src/nip55_caller.rs measures the 5-minute nonce freshness window with std::time::Instant (generate_at/consume_at, NONCE_EXPIRY at line 14). On Android, Instant maps to CLOCK_MONOTONIC, which is paused while the device is suspended (deep sleep).

The previous Kotlin implementation used SystemClock.elapsedRealtime() (CLOCK_BOOTTIME), which counts time spent in suspend. So the migration changed the effective freshness bound: a device that sleeps for more than 5 wall-clock minutes between a NIP-55 request notification being shown (nonce issued) and the user tapping to approve (nonce consumed) can still observe the nonce as Valid, widening the intended 5-minute window.

The current code documents this as an intentional tradeoff at nip55_caller.rs:164-167 (monotonic resists wall-clock manipulation; the Android second expiresAt - now > NONCE_EXPIRY clause was deliberately dropped). The two properties are in tension:

  • Pro monotonic (current): a wall-clock change cannot move expires_at. Wall-clock manipulation cannot extend or shorten the window.
  • Con monotonic (this issue): suspend pauses the clock, so the real-world freshness window can exceed 5 minutes across deep sleep.

Impact

Low. The nonce is single-use (consume removes the entry unconditionally) and package-bound (the approval activity rejects a nonce whose bound package != the calling package). The 5-minute bound is defense-in-depth on an already-narrow path, not the primary control. No exploit is enabled by the widening on its own; an attacker would still need to intercept the nonce and impersonate the bound package.

Options

  1. Accept and document the relaxed wall-clock semantics (close as wontfix; update the comment to state the suspend-pause behavior explicitly so it is not mistaken for a strict 5-minute wall-clock bound).
  2. Use a boot-time clock for nonce expiry so the window holds across suspend. Instant cannot express this portably; would need a platform-supplied CLOCK_BOOTTIME timestamp passed in (the consume_at/generate_at seams already take an injected now, so the store logic need not change, only the clock source the Android layer feeds it).

Test gap

consume_at/generate_at take an injected now, but there is no test asserting the intended wall-clock bound or documenting the relaxed suspend semantics. Whichever option is chosen, add a test (or a comment) pinning the decision.

Refs

  • keep-mobile/src/nip55_caller.rs:14 (NONCE_EXPIRY), :140-157 (generate_at), :164-177 (consume_at + tradeoff comment)
  • keep-android CallerVerificationStore.consumeNonce delegates here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    mobilekeep-mobile UniFFI bindingsp3Lowest PrioritysecuritySecurity-related issues

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions