Skip to content

HMAC-only JWT provider to shrink binaries - #1171

Merged
pblazej merged 3 commits into
mainfrom
blaze/uniffi-size-trim
Jun 17, 2026
Merged

HMAC-only JWT provider to shrink binaries#1171
pblazej merged 3 commits into
mainfrom
blaze/uniffi-size-trim

Conversation

@pblazej

@pblazej pblazej commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

LiveKit access tokens are HS256, yet livekit-api pulled jsonwebtoken's full
rust_crypto backend (RSA/EC/EdDSA). This swaps it for a minimal in-crate HMAC
CryptoProvider, dropping ~200 KiB of unreachable crypto from every shipped
binary. No public API or behavior change.

Also removes the now-obsolete nightly/build-std CI step for tvOS/visionOS —
both are Tier 2 with prebuilt std now.

Size impact

Slice Before After Δ
RustLiveKitUniFFI iOS arm64 framework ~900 KiB 672 KiB −227 KiB (−25%)
liblivekit_uniffi Android arm64-v8a .so ~1157 KiB 816 KiB −340 KiB (−29%)

iOS bloaty diff is almost entirely code: __TEXT,__text −186 KiB, __TEXT,__const
(EC/RSA constant tables) −44 KiB. livekit-ffi/livekit benefit too.

Size guards tightened to 5% over the measured artifacts: iOS
SPM_SIZE_LIMIT_BYTES 1 MiB → 723467; Android ANDROID_SIZE_LIMIT_BYTES
1.5 MiB → 877918.

Why a custom provider

jsonwebtoken 10 resolves its backend through a process-global CryptoProvider,
and new_signer/new_verifier statically reference every algorithm — so the
linker can't dead-strip RSA/EC/EdDSA; only feature removal works. The
EncodingKey/DecodingKey::try_get_hmac_secret() accessors and the
JwtSigner/JwtVerifier traits are public, so an HS256/384/512 provider is
~40 lines, installed idempotently on first sign/verify.

Alternatives considered

  • Per-crate feature gate (livekit-uniffi requests livekit-api without
    rust_crypto): doesn't work — cargo compiles livekit-api once with the
    workspace-union of features, and livekit/livekit-ffi request the defaults,
    so rust_crypto stays linked regardless.
  • livekit-uniffi as its own workspace: excluding it from the root workspace
    would isolate feature resolution, making a per-crate gate work without
    changing the published livekit-api default. Rejected — a larger structural
    change (explicit path deps instead of workspace = true, separate
    Cargo.lock) for no extra savings over the default swap, which is safe here
    since everything is HS256.
  • Drop services-tokio/reqwest, gate os_info/device-info, trim
    livekit-protocol
    : ~0 binary impact — bloaty shows these are already
    dead-stripped (nothing in the token path references them).
  • build-std for iOS: ~27 KiB more (the __eh_frame unwinding tables from
    the prebuilt panic=unwind std), but needs nightly for the primary slice.
    Deferred.

Safety

  • The access-token API is HS256-hardcoded and jsonwebtoken is not
    re-exported
    , so RSA/EC/EdDSA were never reachable — removing them changes no
    public behavior.
  • No "missing provider" crash: the provider is installed via Once at all
    three entry points (to_jwt, verify, from_unverified) before any
    encode/decode. Unsupported algorithms return a clean Err, never a panic.
  • Only residual: the process-global install_default() could conflict with an
    app that separately uses jsonwebtoken-10 for non-HS256 JWTs in the same
    process — narrow, and documented in jwt_provider.rs.

Validation

  • cargo test -p livekit-api passes (sign / verify / unverified-decode).
  • livekit-ffi compiles unchanged.
  • Real iOS framework + Android AAR built and measured; both size gates pass.
  • Full xcframework build on the pinned stable 1.96.0 confirms tvOS/visionOS use
    prebuilt std (no -Zbuild-std, no recompiled core/std on any slice).

🤖 Generated with Claude Code

pblazej and others added 2 commits June 16, 2026 10:50
Replace jsonwebtoken's rust_crypto backend with an in-crate HMAC-only
CryptoProvider (HS256/384/512). LiveKit access tokens are HS256 and the
access_token API is HS256-hardcoded (jsonwebtoken isn't re-exported), so
the dropped RSA/EC/EdDSA algorithms were never reachable. Installed as the
process-global provider on first sign/verify.

Shrinks the shipped binaries ~25-29% (also benefits livekit-ffi/livekit):
- RustLiveKitUniFFI ios-arm64 framework: ~900 -> 672 KiB
- livekit-uniffi android arm64-v8a .so:  ~1157 -> 816 KiB

Tighten CI size guards to 5% over the measured real artifacts:
- SPM_SIZE_LIMIT_BYTES:     1 MiB   -> 723467
- ANDROID_SIZE_LIMIT_BYTES: 1.5 MiB -> 877918

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aarch64-apple-tvos and aarch64-apple-visionos (+ -sim) are now Tier 2 with
prebuilt std (tvOS since 1.84; visionOS after), so cargo-swift builds them on
stable. Add them to the regular target list and remove the nightly + rust-src
install. Verified by a full xcframework build on the pinned 1.96.0: all slices
use prebuilt std (no -Zbuild-std, no recompiled core/std).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pblazej
pblazej requested a review from ladvoc as a code owner June 16, 2026 08:52
@github-actions

Copy link
Copy Markdown
Contributor

Changeset

The following package versions will be affected by this PR:

Package Bump
livekit-api patch

Fixes the License Check (missing Apache header on the new file) and Rust
Formatting CI jobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pblazej pblazej changed the title perf(livekit-api): HMAC-only JWT provider to shrink binaries ~25% HMAC-only JWT provider to shrink binaries Jun 16, 2026

@ladvoc ladvoc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✅

@pblazej
pblazej merged commit f8c7506 into main Jun 17, 2026
23 checks passed
@pblazej
pblazej deleted the blaze/uniffi-size-trim branch June 17, 2026 06:18
pblazej added a commit that referenced this pull request Jul 30, 2026
…1300)

- build-android-*: cargo-make's `extend` replaces the parent env map
rather than merging it, so `env = { TARGET = ... }` silently dropped
ANDROID_RELEASE_FLAG and RUSTFLAGS — `--profile release` produced debug
.so files and android-copy-jniLibs found nothing in target/*/release.
Derive the flag from CARGO_MAKE_PROFILE in the script instead.
- android-bindgen-kotlin: TARGET leaks across cargo-make tasks, so
bindgen-kotlin's `build` dependency cross-compiled for Android with the
host linker ("cannot find -llog"). Pin TARGET back to the host triple
and keep symbols, which library-mode bindgen needs on Linux.
- Raise both size gates to measured values: ios-arm64 is 1088 KiB and
arm64-v8a 1174 KiB, having grown past the limits set in #1171 when the
data-track UniFFI surface landed in #1034.
- Check out inputs.tag_name in both reusable workflows;
workflow_dispatch was building the dispatch ref (main) rather than the
requested tag.

### Before you submit your PR

Make sure the following is true before submitting your PR:

- [ ] I have read the [contributing
guidelines](https://github.com/livekit/rust-sdks/blob/main/CONTRIBUTING.md)
and validated that this PR will be accepted.
- [ ] I have read and followed the principles regarding breaking
changes, testing, and code quality.

### PR description

Describe the changes in this PR. Explain what the PR is meant to solve
and how to reproduce the issue in the first place.

### Breaking changes

If this PR introduces breaking changes, list them here and document the
rationale for introducing such a change.

### MSRV

If the PR modifies the crate's MSRV (Minimum Supported Rust Version),
document it here.

### Testing

Ideally, unit test the code you add, but ensure you're not repeating
existing test cases. Use as many already written scaffolding, utilities
as possible; write your own, when needed. If external services, APIs,
tokens are required (e.g., running an LK server instance), provide the
necessary information. Make sure your tests perform useful,
context-aware assertions and do not simply emulate "happy paths".

### Async

We want the project to be runtime-agnostic, so please reuse what's
already in
[livekit-runtime](https://github.com/livekit/rust-sdks/blob/main/livekit-runtime/)
and feel free to add anything missing. It's ok to use Tokio directly,
when writing unit tests, if necessary. When testing, do not use
artificial delays for the state to "catch up"; instead, respect the
event flow and subscribe properly using channels or other mechanisms.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants