Skip to content

Support any NIP-55 external signer (Amber, Cambium, ...)#1092

Open
TheCryptoDonkey wants to merge 2 commits into
PrimalHQ:mainfrom
TheCryptoDonkey:feat/nip55-multi-signer
Open

Support any NIP-55 external signer (Amber, Cambium, ...)#1092
TheCryptoDonkey wants to merge 2 commits into
PrimalHQ:mainfrom
TheCryptoDonkey:feat/nip55-multi-signer

Conversation

@TheCryptoDonkey

Copy link
Copy Markdown

Primal's Android app currently hardcodes Amber as the only NIP-55 external signer: the login button only appears when Amber is installed, signing intents are pinned to Amber's package, and content-provider calls use Amber's authority. Any other NIP-55 signer is invisible to it, even though the manifest already declares the generic nostrsigner: scheme in its <queries> block.

This PR generalises the external-signer client so any installed NIP-55 signer works — Amber, Cambium (which proxies requests to a hardware signer over NIP-46, so no keys ever live on the phone), and whatever ships next — while keeping the Amber-only experience identical to today.

What changes

  • Signer discovery via queryIntentActivities on the nostrsigner: scheme (the <queries> declaration already covers it — no manifest change). Primal's own signer activity is excluded, and the existing Amber version gate (>= 3.0.4) still applies to Amber specifically.
  • Login button: with one signer installed it reads "Login with " — so "Login with Amber", exactly as before. With several installed it reads "Login with external signer" and the get_public_key intent is left unpinned, so Android's own disambiguation dialog lets the user pick. The chosen signer reports its package in the package result extra (Amber already does this), which is then pinned for every subsequent request.
  • Per-credential signer: the signer package is persisted on the credential (signerPackageName, nullable). Existing external-signer logins deserialise unchanged and resolve to Amber. Since it is per credential, two accounts can even use two different signer apps.
  • Routing: NostrNotary and Nip04MessageCipher route content-provider queries and the visible-intent fallback to the credential's signer instead of the hardcoded Amber package/authority.
  • NIP-55 spec-compliance fix: content-provider responses are now read from the result column per the spec, falling back to Amber's legacy signature column. Previously nip04 encrypt/decrypt read only signature, which is an Amber-specific duplication — spec-compliant signers were silently treated as failures.
  • Renames: AmberLauncher.ktSignerLauncher.kt and AmberContentResolver.ktSignerContentResolver.kt, with function names to match (signEventWithSigner, etc.), since they no longer talk exclusively to Amber.

Behaviour notes

  • Amber-only users see no change: same button label, same flows, and previously persisted logins keep working.
  • Wire-contract compatibility for the multi-signer path was verified against both Amber's and Cambium's signer implementations (result extras, cursor columns, current_user handling).
  • Unit tests extended: credential store upsert + signer package persistence, legacy-credential fallback to Amber, and deserialisation of credentials persisted before this change.

The external-signer client previously hardcoded Amber: the login button
only appeared when Amber was installed, signing intents were pinned to
its package, and content-provider calls used its authority. Installed
NIP-55 signers are now discovered via the nostrsigner: scheme (already
declared in the manifest queries block), the signer that serves the
login is persisted on the credential, and all subsequent requests are
routed to it. Provider responses are read from the result column per
NIP-55, falling back to the legacy signature column that only Amber
duplicates. Amber-only behaviour is unchanged, including previously
persisted logins.
When several NIP-55 signers are installed the get_public_key intent is
launched unpinned so Android shows its chooser. Primal exports its own
nostrsigner: activity (its external-signer provider feature), so without
this it appears in that chooser and offers itself as a signer to sign
into itself. Exclude Primal own signer components via
EXTRA_EXCLUDE_COMPONENTS.
@TheCryptoDonkey

Copy link
Copy Markdown
Author

Tested end-to-end on an Android 13 (API 33) emulator against a real NIP-46 bunker, not just source-verified:

Single signer (Cambium only, Amber not installed) — the login button reads "Login with Cambium" (same shape Amber-only users see today). Login completes: Primal's get_public_key and the app-settings sign_event are proxied through Cambium to the remote signer, and Primal lands in the feed.

Multiple signers (Cambium + Amber both installed) — the button reads "Login with external signer", and tapping it shows Android's chooser. Login via Cambium from the chooser completes end-to-end (feed + wallet sign_events all signed remotely).

Self-exclusion (latest commit) — before the last commit, Primal appeared in its own chooser (it exports a nostrsigner: activity for its signer-provider feature), so it offered itself as a signer to sign into itself. EXTRA_EXCLUDE_COMPONENTS now keeps it out; the chooser lists only Amber and Cambium.

Two interop issues surfaced during testing were on the signer side, not this PR, and are fixed in Cambium (noted here for anyone testing against a spec-minimal signer): (1) Primal's get_public_key result is compared against the login input, which expects an npub, not raw hex; (2) Primal's sign_event payloads omit created_at (login) and tags (wallet op), which strict parsers reject. Amber tolerates both already. This PR's result-column fix is the third such interop point and lives here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant