Skip to main content

Identity and external signers

A local identity is an account-scoped authority. Import exactly 32 secret bytes once, clear the caller buffer, retain the opaque identity for the authenticated session, and call the idempotent erase() only on logout or authority loss. Normal language ownership handles ordinary cleanup.

Local identity lifecycle

let identity = LocalIdentity::from_secret_bytes(&secret)?;
secret.fill(0);
let author = identity.public_key();

// Use throughout this account session.
drop(identity); // logout; Rust ownership drops the secret-bearing state

Use LocalIdentity in Rust. It is not cloneable and relies on deterministic Rust ownership rather than an erase() method. Put it in an Option when an account session needs to take and drop authority explicitly. LocalIdentityHandle is the coarse generated-binding owner, not the preferred native API.

The public-key accessor is cached in foreign facades. Its benchmark is below the useful clock floor and is recorded only as an API-shape check.

External signing

Use an external signer when a secret must remain in hardware, a host service, an OS key store, or a browser extension. Capability discovery is explicit: event signing, NIP-44 encryption, and NIP-44 decryption are independent.

The host callback receives a bounded request and returns a signature or cryptographic result. Softchat completes and validates the event or message; the callback must not duplicate event-ID construction.

Implement ExternalSigner, declare ExternalSignerCapabilities, then call sign_event_with_external, encrypt_with_external, or decrypt_with_external. Return ExternalSignerFailure::Cancelled for user cancellation and Rejected for a provider refusal.

let event = sign_event_with_external(&provider, draft)?;
let encrypted = encrypt_with_external(&provider, &recipient, "hello")?;
let plaintext = decrypt_with_external(&provider, &encrypted)?;

Safety checklist

  • Never log, stringify, clone broadly, or persist secret bytes.
  • Never use example fixture keys for an account.
  • Never cache derived conversation keys to optimize a benchmark.
  • Treat an erased identity as permanently invalid; create a new one after reauthentication.
  • Keep one consistent caller/actor when an application permits concurrent access.