Architecture
This is the high-level architecture of the Stellar Passkey Kit — the SCF-43 RFP submission for adding Soroban smart-account passkey support to the Stellar wallet ecosystem.
Diagram
Plain-English walkthrough
What each box does
- WebAuthn API — Built into every modern browser. Talks to the platform authenticator (Touch ID, Windows Hello, security key) over CTAP2. The kit never sees the user's biometric data; only the public half of a P-256 keypair and the per-ceremony assertion bytes ever leave the secure enclave.
- @stellar-passkey/core — Browser-side TypeScript SDK. Drives the five canonical ceremonies —
createPasskey,connectPasskey,signTransaction,signAuthEntry,recoverPasskey. Does the heavy lifting on the WebAuthn ↔ Soroban auth boundary: CBOR-decodes authenticator data, extracts the SEC-1 uncompressed P-256 key, normalises Apple's high-S signatures to low-S client-side (CAP-0051 on-chainsecp256r1_verifydeliberately rejects high-S, see Stellar protocol #1435), and packs the WebAuthn assertion into the contract'sSignatureScVal. - @stellar-passkey/ui — Three framework-agnostic Lit Web Components (
<passkey-create-button>,<passkey-sign-tx>,<passkey-recover>) consuming the core SDK. Themable via CSS custom properties, no React/Vue dependency. - @creit.tech/stellar-wallets-kit (fork) — The community SEP-43 multi-wallet aggregator. Our fork adds a
PasskeyModulethat sits alongside Freighter / Albedo / WalletConnect etc.; consumers wire it with two callbacks (signTransaction,signAuthEntry) pointing at the reference SDK above. The upstream Draft PR lands at the end of Day 7 (YK-288). - soroban-rpc — Stellar testnet's Soroban RPC endpoint. Used for
getLatestLedger(to computesignatureExpirationLedger),simulateTransaction(to populate resource footprints andSorobanAuthorizationEntrys before signing) andsendTransaction. - Smart-Account Contract — The Rust soroban-sdk 23 contract (
packages/contract) deployed once per WASM and instantiated per user. Its__check_authentry point reconstructs the WebAuthn preimage hash fromauthenticatorData ‖ sha256(clientDataJSON)and feeds it through CAP-0051's host-providedsecp256r1_verify. Per user, exactly one instance lives on-chain.
Cryptographic flow
- Register. Browser calls
navigator.credentials.create({ rpId, pubKeyCredParams: [{ type: "public-key", alg: -7 }] })—-7is ES256 (P-256 + SHA-256); RS256 is hard-rejected, the on-chain verifier only supports secp256r1. The authenticator returnsattestationObjectwhose CBOR-encodedauthDatacontains the public key in COSE form. The SDK extracts the SEC-1 uncompressed bytes0x04 ‖ X ‖ Y(65 bytes) and hands them to the wallet contract's bootstrap-admin call. - Deploy. A
DeployerCallbacks(wired by the consumer — backend, Launchtube relay, browser-wallet, etc.) issuesCreateContract(walletWasmHash, salt)and thenadd(credentialId, pk, admin=true)on the freshly-deployed wallet. The SDK never holds a Stellar signing key. - Sign. For each Soroban auth entry whose
credentials.addressmatches the active wallet, the SDK:- sets
signatureExpirationLedger = currentLedger + 60(rpc.prepareTransactionleaves this zero — required-to-set, see CHANGELOG entry on YK-253); - hashes the
HashIdPreimage::SorobanAuthorizationenvelope (this is thesignature_payload: Hash<32>the contract expects); - calls
navigator.credentials.get({ allowCredentials: [credentialId] })with that hash aschallenge; - verifies the assertion's
clientDataJSON.challengematches (replay defence); - converts DER → raw
r ‖ s, applies low-S normalisation, packs into the contract'sSignatureScVal in canonical map order; - mutates the auth entry in-place.
- re-simulates the transaction (
SorobanRpc.assembleTransaction) so the resource footprint reflects the real signature size.
- sets
- Recover. When localStorage is gone (new device, cleared cache), the SDK drives a discoverable WebAuthn ceremony (no
allowCredentials), receives the user-chosen credentialId, and queriesgetEventsfor("sw_v1","add", credentialId)topics. Each matching event's emitting contract is a wallet bound to that passkey — the user picks one.
Where user data lives
| Data | Location | Notes |
|---|---|---|
| Biometric template | User device secure enclave | Never leaves the device. |
| Private key (P-256) | Authenticator secure enclave (TPM / Secure Element) | Non-extractable. |
| Public key (P-256, 65 B) | On-chain in the smart-account contract's storage | Indexed by credentialId. |
credentialId | localStorage (key @stellar-passkey/lastSession) and on-chain in contract events | Storage record carries only (credentialId, rpId, createdAt); NOT the contract address — that is re-derived from chain state on every connectPasskey to prevent storage-tampering attacks (see storage.ts). |
Contract C… address | Re-derived from ("sw_v1","add", credentialId) events on-chain | Never persisted client-side by the kit. |
| Stellar signing key | Never held by the kit. | Wired by the caller via DeployerCallbacks (backend, Launchtube, browser-wallet, test-harness). |
Network surface
The SDK talks to one off-chain endpoint: https://soroban-testnet.stellar.org (or its mainnet equivalent). No analytics, no telemetry, no central coordination server. Transaction submission can additionally hit Horizon or Launchtube if the consumer wires those into DeployerCallbacks; both are optional.
Authoritative references
- WebAuthn Level 3 — https://www.w3.org/TR/webauthn-3/
- CAP-0051 secp256r1_verify host function — https://stellar.org/protocol/cap-51
- Stellar protocol issue #1435 (high-S host behaviour) — https://github.com/stellar/stellar-protocol/issues/1435
- SEP-43 wallet interface — https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md
- @creit.tech/stellar-wallets-kit ModuleInterface — https://github.com/Creit-Tech/Stellar-Wallets-Kit/blob/main/src/types/mod.ts