litellm - ✅(Solved) Fix [Bug]: Airgapped license verification fails when the license payload contains "." (e.g. a domain-like user_id) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#26231Fetched 2026-04-23 07:24:35
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×1

Root Cause

Root cause

Fix Action

Fixed

PR fix notes

PR #26234: fix(license): derive offline signature length from RSA public key

Description (problem / solution / changelog)

Relevant issues

Fixes #26231

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - [see details](https://docs.litellm.ai/docs/extras/ contributing_code)
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run Link:

  • CI run for the last commit Link:

  • Merge / cherry-pick CI run Links:

Screenshots / Proof of Fix

Before this change, local airgapped license verification could fail for valid licenses if the JSON payload contained a . byte. The previous implementation used decoded.split(b".", 1), which incorrectly split at the first . inside the JSON payload rather than at the separator between <message> and <signature>.

With this change, the verifier derives the expected RSA signature length from public_key.key_size // 8 and splits the decoded payload from the end using that length. This makes local verification robust even when fields like user_id contain dots.

Added regression coverage for:

  • a payload without .
  • a payload with . in user_id
  • a payload with multiple . characters
  • an invalid payload format that should be rejected cleanly

Local verification:

  • Command: uv run pytest tests/test_litellm/proxy/auth/test_litellm_license.py -q
  • Result: 6 passed

Type

🐛 Bug Fix ✅ Test

Changes

  • Fixed airgapped license parsing in LicenseCheck.verify_license_without_api_request().
  • Replaced the unsafe first-dot split logic with parsing based on the RSA public key size.
  • Derived the expected signature length dynamically from the provided public key instead of relying on hardcoded signature sizes.
  • Added regression tests covering payloads with no dots, one dot, multiple dots, and invalid formatting.

I wasn’t sure which branch this PR should target, so please let me know if I selected the wrong one.

Changed files

  • litellm/proxy/auth/litellm_license.py (modified, +18/-1)
  • tests/test_litellm/proxy/auth/test_litellm_license.py (modified, +130/-2)

Code Example

decoded = base64.b64decode(license_key)
  message, signature = decoded.split(b".", 1)   # <-- bug

---

# config.yaml (minimal)
  model_list:
    - model_name: dummy
      litellm_params: { model: openai/dummy, api_key: dummy }
  general_settings:
    master_key: os.environ/LITELLM_MASTER_KEY

---

export LITELLM_LICENSE='<your license key whose user_id contains ".">'
  export LITELLM_MASTER_KEY='sk-local'
  docker run --rm -e LITELLM_LICENSE -e LITELLM_MASTER_KEY \
    --entrypoint python \
    ghcr.io/berriai/litellm-non_root:main-v1.82.3 \
    -c "from litellm.proxy.auth.litellm_license import LicenseCheck; \
        lc = LicenseCheck(); \
        print('is_premium:', lc.is_premium()); \
        print('airgapped_data:', lc.airgapped_license_data)"

---
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

Summary

LicenseCheck._verify_license_str splits the base64-decoded license on the first . byte:

https://github.com/BerriAI/litellm/blob/main/litellm/proxy/auth/litellm_license.py#L177-L178

decoded = base64.b64decode(license_key)
message, signature = decoded.split(b".", 1)   # <-- bug

The format is <JSON payload>.<RSA signature>, but the JSON payload itself can contain . bytes (0x2E) — most commonly inside user_id (domain-like IDs, e.g. "example.co.jp-license-litellm"). When it does, split(b".", 1) cuts the payload in the middle, so message is truncated JSON and signature contains the rest of the payload plus the real signature. Both JSON parsing and RSA verification then fail, and is_premium() returns False for what is a perfectly valid license.

Scope of impact

  • All airgapped/offline licenses whose JSON payload contains . (dot) bytes.
  • This is not rare: any user_id that looks like a domain (company.com-…, *.co.jp-…) or contains a version/decimal will hit it.
  • The remote https://license.litellm.ai/verify_license/<key> endpoint currently returns 404 for every license we have tried (both trial and production), so the local public-key path is the only thing keeping Enterprise features working — and that local path is what this bug breaks.

Actual

is_premium() returns False; airgapped_license_data is {}; Enterprise features (SSO settings, audit logs, etc.) are disabled.

Root cause

The license format <raw_json>.<raw_signature> uses a byte that legitimately appears in JSON as the separator, without any escaping. This is essentially a home-grown JWS that is not safe against payload content.

Steps to Reproduce

with a real license, set it on the proxy and call is_premium():

# config.yaml (minimal)
model_list:
  - model_name: dummy
    litellm_params: { model: openai/dummy, api_key: dummy }
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
export LITELLM_LICENSE='<your license key whose user_id contains ".">'
export LITELLM_MASTER_KEY='sk-local'
docker run --rm -e LITELLM_LICENSE -e LITELLM_MASTER_KEY \
  --entrypoint python \
  ghcr.io/berriai/litellm-non_root:main-v1.82.3 \
  -c "from litellm.proxy.auth.litellm_license import LicenseCheck; \
      lc = LicenseCheck(); \
      print('is_premium:', lc.is_premium()); \
      print('airgapped_data:', lc.airgapped_license_data)"

Relevant log output

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.82.3

Twitter / LinkedIn details

No response

extent analysis

TL;DR

The issue can be fixed by modifying the LicenseCheck._verify_license_str method to correctly split the base64-decoded license string, considering the JSON payload can contain . bytes.

Guidance

  • Identify a suitable alternative separator that does not appear in the JSON payload, or implement a more robust parsing mechanism.
  • Consider using a standard format like JSON Web Signature (JWS) that is designed to handle such cases.
  • Modify the LicenseCheck._verify_license_str method to handle the new separator or parsing mechanism.
  • Test the modified method with licenses containing . bytes in the JSON payload to ensure correct splitting and verification.

Example

import base64
import json

def _verify_license_str(self, license_key):
    decoded = base64.b64decode(license_key)
    # Assuming a new separator, e.g., '---'
    message, signature = decoded.split(b'---', 1)
    try:
        json_payload = json.loads(message)
        # Proceed with verification using the json_payload and signature
    except json.JSONDecodeError:
        # Handle invalid JSON payload
        pass

Notes

The current implementation assumes a simple separator (.) that can appear in the JSON payload, leading to incorrect splitting. A more robust solution would involve using a standard format or a separator that is guaranteed not to appear in the payload.

Recommendation

Apply a workaround by modifying the LicenseCheck._verify_license_str method to handle the correct splitting of the license string, using a suitable alternative separator or parsing mechanism. This will ensure correct verification of licenses containing . bytes in the JSON payload.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING