signTransaction
Sign Soroban transactions and authorization entries with a passkey — WebAuthn assertion, low-S normalization, and contract-verifiable signature assembly.
Turn a Soroban transaction or a single authorization entry into a passkey-signed envelope. The @soropass/core/sign subpath obtains a WebAuthn assertion, low-S-normalizes it, and assembles the contract signature your __check_auth re-derives. @stellar/stellar-sdk is a peer dependency — it is never bundled into the SDK output.
Address-credential auth
Signs every address-credential Soroban auth entry carried by the InvokeHostFunction operations of a transaction.
Always low-S
Every assertion is low-S-normalized downstream — Soroban's secp256r1_verify does not enforce it, so the SDK must.
Pluggable signer
browserPasskeySigner adapts navigator.credentials.get; inject a custom WebAuthnClient for tests or non-browser hosts.
signTransaction
Sign every address-credential Soroban auth entry carried by the InvokeHostFunction operations of a transaction (base64 XDR envelope). Returns the signed envelope XDR.
signTransaction(txXdr: string, options: SorobanSignOptions): Promise<string>| Param | Type | Description |
|---|---|---|
txXdr | string | Base64 XDR transaction envelope to sign. |
options | SorobanSignOptions | Network passphrase + WebAuthn signer (see below). |
Returns Promise<string> — the signed envelope as base64 XDR.
import { signTransaction, browserPasskeySigner } from '@soropass/core/sign';
const sign = browserPasskeySigner({ rpId, allowCredentials: [credentialId] });
const signedXdr = await signTransaction(txXdr, { networkPassphrase, sign });signAuthEntry
Obtain a WebAuthn assertion, low-S-normalize, and assemble the contract signature. Returns the signed entry as base64 XDR.
signAuthEntry(entryXdr: string, options: SorobanSignOptions): Promise<string>| Param | Type | Description |
|---|---|---|
entryXdr | string | Base64 XDR of a single SorobanAuthorizationEntry. |
options | SorobanSignOptions | Network passphrase + WebAuthn signer (see below). |
Returns Promise<string> — the signed entry as base64 XDR.
import { signAuthEntry, browserPasskeySigner } from '@soropass/core/sign';
const sign = browserPasskeySigner({ rpId, allowCredentials: [credentialId] });
const signedEntry = await signAuthEntry(entryXdr, { networkPassphrase, sign });SorobanSignOptions
The options object shared by signTransaction and signAuthEntry.
| Field | Type | Description |
|---|---|---|
networkPassphrase | string | Network passphrase bound into the auth challenge (networkId = SHA256(networkPassphrase)). |
sign | WebAuthnSigner | The signer that produces the assertion — usually browserPasskeySigner(...). |
browserPasskeySigner
Adapt navigator.credentials.get into the WebAuthnSigner that signTransaction / signAuthEntry expect. Hands the signer a base64url challenge (the Soroban auth preimage), decodes it, runs the assertion, returns the fields the auth assembler needs. The DER signature is low-S normalized downstream.
browserPasskeySigner(options: BrowserPasskeySignerOptions): WebAuthnSignerReturns WebAuthnSigner — pass it as options.sign to the sign functions above.
BrowserPasskeySignerOptions
| Field | Type | Description |
|---|---|---|
rpId | string | Relying Party ID (registrable domain). |
allowCredentials? | string[] | base64url credential ids; omit for a discoverable prompt. |
userVerification? | 'discouraged' | 'preferred' | 'required' | WebAuthn user-verification requirement. |
webauthn? | WebAuthnClient | Inject a custom client (tests / non-browser). |
Omit allowCredentials to drive a discoverable (resident-key) prompt where the authenticator surfaces the credential itself.
import { browserPasskeySigner, signTransaction } from '@soropass/core/sign';
const sign = browserPasskeySigner({
rpId: 'app.example.com',
allowCredentials: [credentialId], // omit for a discoverable prompt
userVerification: 'required',
});
const signedXdr = await signTransaction(txXdr, { networkPassphrase, sign });Low-S normalization
Invariant #2. Roughly 50% of Apple Touch ID / Face ID assertions come back high-S, and Soroban's secp256r1_verify does not enforce low-S — so the SDK must emit low-S before any contract sees the signature.
A high-S signature is malleable: the same message has two valid encodings. Always normalize to canonical low-S (S ≤ n/2) client-side.
isLowS
True if a 64-byte compact signature is canonical low-S (S ≤ n/2).
isLowS(compactSignature: Uint8Array): booleannormalizeLowS
If S > n/2, replace S with n − S. Idempotent.
normalizeLowS(compactSignature: Uint8Array): Uint8ArrayderToCompact
ASN.1 DER → 64-byte raw R‖S (noble parser). Does not enforce low-S.
derToCompact(der: Uint8Array): Uint8ArrayderToCompactLowS
DER → 64-byte canonical low-S compact (what the ceremony uses).
derToCompactLowS(der: Uint8Array): Uint8Arrayimport { derToCompactLowS, isLowS, normalizeLowS } from '@soropass/core/sign';
// DER from the authenticator → canonical 64-byte low-S compact
const compact = derToCompactLowS(assertion.signatureDer);
isLowS(compact); // true — ready for the contract
normalizeLowS(compact); // idempotent: returns compact unchangedSoroban auth
The primitives that derive the challenge the authenticator signs and assemble the assertion back into a SorobanAuthorizationEntry the contract can verify.
reconstructSignedPayload
SHA256(authData ‖ SHA256(clientDataJSON)) — the 32-byte payload __check_auth re-derives.
reconstructSignedPayload({ authenticatorData, clientDataJSON }): Uint8ArrayauthEntryChallenge
base64url challenge (43 chars) the authenticator signs.
authEntryChallenge(entry, networkPassphrase): stringauthEntryChallengeBytes
SHA256(XDR(HashIdPreimage::SorobanAuthorization{...})); networkId = SHA256(networkPassphrase).
authEntryChallengeBytes(entry, networkPassphrase): Uint8ArrayapplyAssertionToEntry
Sets the entry signature to ScVal::Map { authenticator_data, client_data_json, signature: BytesN<64> } (alphabetical keys). The signature MUST already be 64-byte low-S.
applyAssertionToEntry(entry, assertion): SorobanAuthorizationEntryThe signature passed to applyAssertionToEntry must already be the 64-byte low-S compact form. Use derToCompactLowS first.
import {
authEntryChallenge,
reconstructSignedPayload,
applyAssertionToEntry,
} from '@soropass/core/sign';
// 1. the base64url challenge the authenticator signs
const challenge = authEntryChallenge(entry, networkPassphrase);
// 2. after the assertion, the 32-byte payload __check_auth re-derives
const payload = reconstructSignedPayload({
authenticatorData,
clientDataJSON,
});
// 3. signature must already be 64-byte low-S
const signed = applyAssertionToEntry(entry, {
authenticatorData,
clientDataJSON,
signature, // BytesN<64>, low-S
});Production note
simulateTransaction does not run secp256r1_verify, so it under-budgets __check_auth. Raise the instruction budget + resource fee before submit, or use a managed submitter.
For the matching error codes raised by these functions — including INVALID_SIGNATURE_DER and challenge/origin mismatches — see the KitError taxonomy.
createPasskey
Register an ES256-only passkey, extract its SEC-1 public key, deploy a smart account, and persist the credential via the @soropass/core/create subpath.
recover & connect
Bring a returning SoroPass user back to their Stellar smart account — silent connect() on a known device, discoverable recover() for new devices.