mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
ci: add Auths cryptographic commit verification (warn-only)
This commit is contained in:
parent
127149c263
commit
73f6b82f58
4 changed files with 269 additions and 0 deletions
23
.auths/allowed_signers
Normal file
23
.auths/allowed_signers
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Auths Allowed Signers
|
||||
#
|
||||
# This file lists the Ed25519 public keys of maintainers authorized to sign
|
||||
# commits for this repository using Auths (https://github.com/auths-dev/auths).
|
||||
#
|
||||
# With Auths, every commit carries an Ed25519 signature bound to the
|
||||
# maintainer's KERI-based decentralized identifier (DID). This creates a
|
||||
# cryptographic chain of custody that cannot be forged with stolen registry
|
||||
# credentials alone.
|
||||
#
|
||||
# Format:
|
||||
# <DID>@auths.local namespaces="git" ssh-ed25519 <base64-public-key>
|
||||
#
|
||||
# To add your key:
|
||||
# 1. Install Auths: pip install auths
|
||||
# 2. Create identity: auths init
|
||||
# 3. Configure Git: auths git setup
|
||||
# 4. Export signers: auths git allowed-signers --output .auths/allowed_signers
|
||||
# 5. Commit and push this file
|
||||
#
|
||||
# Documentation: https://github.com/auths-dev/auths/blob/main/docs/guides/platforms/ci-cd.md
|
||||
#
|
||||
# Maintainers — add your keys below this line:
|
||||
37
.github/workflows/auths-verify-commits.yml
vendored
Normal file
37
.github/workflows/auths-verify-commits.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Auths Commit Verification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify commit signatures
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify commits with Auths
|
||||
uses: auths-dev/auths-verify-github-action@v1 # TODO: pin to SHA once stable
|
||||
with:
|
||||
allowed-signers: .auths/allowed_signers
|
||||
fail-on-unsigned: 'false'
|
||||
skip-merge-commits: 'true'
|
||||
post-pr-comment: 'true'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
36
cookbook/security/README.md
Normal file
36
cookbook/security/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Security Cookbook: Auths Commit Verification
|
||||
|
||||
## Background
|
||||
|
||||
On March 24, 2026, LiteLLM was the target of a supply chain attack. The attacker
|
||||
compromised the Trivy GitHub Action, which exfiltrated the `PYPI_PUBLISH` token from
|
||||
LiteLLM's CI/CD pipeline. The stolen token was used to publish malicious versions
|
||||
(v1.82.7 and v1.82.8) directly to PyPI. The source code on GitHub was never modified.
|
||||
|
||||
The attack succeeded because **there was no cryptographic binding between the published
|
||||
package and a verified maintainer identity**.
|
||||
|
||||
## What is Auths?
|
||||
|
||||
[Auths](https://github.com/auths-dev/auths) provides Ed25519 signatures bound to
|
||||
KERI-based decentralized identifiers (DIDs). With Auths:
|
||||
|
||||
- Every commit carries a signature from the maintainer's cryptographic identity
|
||||
- The signature is bound to the maintainer's device keychain (not a registry account)
|
||||
- Stealing PyPI/npm credentials is insufficient without the signing key
|
||||
- Verification happens locally — no network calls to a central authority
|
||||
|
||||
## Running the Simulation
|
||||
|
||||
The simulation script recreates the attack scenario and demonstrates how Auths
|
||||
verification catches the unauthorized commit:
|
||||
|
||||
```bash
|
||||
pip install auths
|
||||
python auths_attack_simulation.py
|
||||
```
|
||||
|
||||
## Adding Auths to Your Workflow
|
||||
|
||||
See the GitHub Actions workflow at `.github/workflows/auths-verify-commits.yml`
|
||||
and the allowed signers configuration at `.auths/allowed_signers`.
|
||||
173
cookbook/security/auths_attack_simulation.py
Normal file
173
cookbook/security/auths_attack_simulation.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""
|
||||
Auths Attack Simulation: LiteLLM March 24, 2026 Supply Chain Incident
|
||||
|
||||
Demonstrates how Auths cryptographic commit verification would have detected
|
||||
the unauthorized PyPI publish that compromised LiteLLM v1.82.7 and v1.82.8.
|
||||
|
||||
What happened:
|
||||
1. Attacker compromised the Trivy GitHub Action (March 19)
|
||||
2. LiteLLM's CI ran Trivy without version pinning
|
||||
3. Compromised Trivy exfiltrated the PYPI_PUBLISH token from GitHub Actions
|
||||
4. Attacker used the stolen token to publish malicious versions to PyPI
|
||||
5. The malicious packages contained a credential stealer in a .pth file
|
||||
6. Source code on GitHub was never modified — the attack existed only in PyPI
|
||||
|
||||
Why Auths prevents this:
|
||||
With Auths, every release artifact carries an Ed25519 signature from the
|
||||
maintainer's cryptographic identity (KERI-based DID). Stealing the PyPI
|
||||
token is insufficient — the attacker cannot produce a valid signature
|
||||
without the maintainer's private key stored in their device keychain.
|
||||
|
||||
Usage:
|
||||
pip install auths
|
||||
python auths_attack_simulation.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def check_auths_cli() -> bool:
|
||||
"""Check if the auths CLI is available."""
|
||||
return shutil.which("auths") is not None
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command and return the result."""
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
)
|
||||
|
||||
|
||||
def setup_test_repo(tmpdir: str) -> str:
|
||||
"""Create a temporary git repo with signed and unsigned commits."""
|
||||
repo = os.path.join(tmpdir, "litellm-simulation")
|
||||
os.makedirs(repo)
|
||||
|
||||
# Initialize repo
|
||||
run(["git", "init"], cwd=repo)
|
||||
run(["git", "config", "user.email", "maintainer@example.com"], cwd=repo)
|
||||
run(["git", "config", "user.name", "LiteLLM Maintainer"], cwd=repo)
|
||||
|
||||
# Generate a test Ed25519 keypair for the "legitimate maintainer"
|
||||
key_path = os.path.join(tmpdir, "test_key")
|
||||
run(
|
||||
["ssh-keygen", "-t", "ed25519", "-f", key_path, "-N", "", "-q"],
|
||||
cwd=tmpdir,
|
||||
)
|
||||
|
||||
# Configure git to sign with this key
|
||||
run(["git", "config", "gpg.format", "ssh"], cwd=repo)
|
||||
run(["git", "config", "user.signingkey", key_path], cwd=repo)
|
||||
|
||||
# Create allowed_signers file
|
||||
pub_key_content = open(f"{key_path}.pub").read().strip()
|
||||
signers_path = os.path.join(repo, ".auths")
|
||||
os.makedirs(signers_path)
|
||||
with open(os.path.join(signers_path, "allowed_signers"), "w") as f:
|
||||
f.write(f"maintainer@example.com {pub_key_content}\n")
|
||||
|
||||
run(["git", "config", "gpg.ssh.allowedSignersFile", os.path.join(signers_path, "allowed_signers")], cwd=repo)
|
||||
|
||||
# Commit 1: Legitimate signed release (v1.82.6)
|
||||
with open(os.path.join(repo, "litellm_version.py"), "w") as f:
|
||||
f.write('version = "1.82.6"\n')
|
||||
run(["git", "add", "."], cwd=repo)
|
||||
run(["git", "commit", "-S", "-m", "release: v1.82.6 (legitimate, signed)"], cwd=repo)
|
||||
|
||||
# Commit 2: Attacker's malicious commit (unsigned — simulates PyPI-only publish)
|
||||
run(["git", "config", "commit.gpgSign", "false"], cwd=repo)
|
||||
with open(os.path.join(repo, "litellm_version.py"), "w") as f:
|
||||
f.write('version = "1.82.7"\n')
|
||||
with open(os.path.join(repo, "litellm_init.pth"), "w") as f:
|
||||
f.write("# Simulated malicious payload — credential stealer\n")
|
||||
f.write("import os; os.environ.get('AWS_SECRET_ACCESS_KEY') # exfiltrate\n")
|
||||
run(["git", "add", "."], cwd=repo)
|
||||
run(["git", "commit", "-m", "release: v1.82.7 (MALICIOUS — unsigned)"], cwd=repo)
|
||||
|
||||
return repo
|
||||
|
||||
|
||||
def run_verification(repo: str) -> None:
|
||||
"""Run auths verification and display results."""
|
||||
result = run(
|
||||
["auths", "verify", "HEAD~1..HEAD", "--allowed-signers", ".auths/allowed_signers"],
|
||||
cwd=repo,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(" BLOCKED: Unsigned commit detected")
|
||||
if result.stdout:
|
||||
print(f" Output: {result.stdout.strip()}")
|
||||
if result.stderr:
|
||||
print(f" Detail: {result.stderr.strip()}")
|
||||
else:
|
||||
print(" PASSED: All commits verified")
|
||||
if result.stdout:
|
||||
print(f" Output: {result.stdout.strip()}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 70)
|
||||
print("Auths Attack Simulation: LiteLLM Supply Chain Incident (March 24, 2026)")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
if not check_auths_cli():
|
||||
print("The 'auths' CLI is not installed.")
|
||||
print()
|
||||
print("Install it with:")
|
||||
print(" pip install auths")
|
||||
print()
|
||||
print("Or visit: https://github.com/auths-dev/auths")
|
||||
sys.exit(0)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
print("[1] Setting up simulation repository...")
|
||||
repo = setup_test_repo(tmpdir)
|
||||
print(" Created repo with 2 commits:")
|
||||
print(" - v1.82.6: Legitimate release, signed by maintainer")
|
||||
print(" - v1.82.7: Attacker's malicious version, unsigned")
|
||||
print()
|
||||
|
||||
# Verify the legitimate commit
|
||||
print("[2] Verifying legitimate commit (v1.82.6)...")
|
||||
result = run(
|
||||
["auths", "verify", "HEAD~2..HEAD~1", "--allowed-signers", ".auths/allowed_signers"],
|
||||
cwd=repo,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(" PASSED: Commit is signed by an authorized maintainer")
|
||||
else:
|
||||
print(" Result: ", result.stdout.strip() if result.stdout else result.stderr.strip())
|
||||
print()
|
||||
|
||||
# Verify the malicious commit
|
||||
print("[3] Verifying attacker's commit (v1.82.7)...")
|
||||
run_verification(repo)
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("-" * 70)
|
||||
print("RESULT: The attacker's unsigned commit would have been flagged.")
|
||||
print()
|
||||
print("In the real attack, the attacker used a stolen PyPI token to publish")
|
||||
print("malicious packages directly to the registry. The source code on GitHub")
|
||||
print("was never modified. With Auths, even if registry credentials are stolen,")
|
||||
print("the attacker cannot produce a valid Ed25519 signature — the private key")
|
||||
print("is bound to the maintainer's device keychain and never leaves it.")
|
||||
print()
|
||||
print("Learn more: https://github.com/auths-dev/auths")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue