From 73f6b82f58a6602e0123c551739f0aa0c0a97094 Mon Sep 17 00:00:00 2001 From: bordumb Date: Fri, 3 Apr 2026 19:03:30 -0700 Subject: [PATCH 1/6] ci: add Auths cryptographic commit verification (warn-only) --- .auths/allowed_signers | 23 +++ .github/workflows/auths-verify-commits.yml | 37 ++++ cookbook/security/README.md | 36 ++++ cookbook/security/auths_attack_simulation.py | 173 +++++++++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 .auths/allowed_signers create mode 100644 .github/workflows/auths-verify-commits.yml create mode 100644 cookbook/security/README.md create mode 100644 cookbook/security/auths_attack_simulation.py diff --git a/.auths/allowed_signers b/.auths/allowed_signers new file mode 100644 index 00000000000..21d68016ffb --- /dev/null +++ b/.auths/allowed_signers @@ -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: +# @auths.local namespaces="git" ssh-ed25519 +# +# 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: diff --git a/.github/workflows/auths-verify-commits.yml b/.github/workflows/auths-verify-commits.yml new file mode 100644 index 00000000000..bc66c723448 --- /dev/null +++ b/.github/workflows/auths-verify-commits.yml @@ -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 }} diff --git a/cookbook/security/README.md b/cookbook/security/README.md new file mode 100644 index 00000000000..7bc468507c5 --- /dev/null +++ b/cookbook/security/README.md @@ -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`. diff --git a/cookbook/security/auths_attack_simulation.py b/cookbook/security/auths_attack_simulation.py new file mode 100644 index 00000000000..3505ada8988 --- /dev/null +++ b/cookbook/security/auths_attack_simulation.py @@ -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() From cd508eb956bf2ce728e892b43293a332a1b34cb1 Mon Sep 17 00:00:00 2001 From: bordumb Date: Fri, 3 Apr 2026 19:16:08 -0700 Subject: [PATCH 2/6] fix: pin auths-verify-github-action to full SHA --- .auths/allowed_signers | 11 ++++++----- .github/workflows/auths-verify-commits.yml | 2 +- cookbook/security/README.md | 2 +- cookbook/security/auths_attack_simulation.py | 5 +++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.auths/allowed_signers b/.auths/allowed_signers index 21d68016ffb..34d0a6587c0 100644 --- a/.auths/allowed_signers +++ b/.auths/allowed_signers @@ -12,11 +12,12 @@ # @auths.local namespaces="git" ssh-ed25519 # # 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 +# 1. Install Auths CLI: +# Homebrew: brew tap auths-dev/auths-cli && brew install auths +# Cargo: cargo install auths_cli +# 2. Create identity: auths init +# 3. Export signers: auths signers sync --output .auths/allowed_signers +# 4. Commit and push this file # # Documentation: https://github.com/auths-dev/auths/blob/main/docs/guides/platforms/ci-cd.md # diff --git a/.github/workflows/auths-verify-commits.yml b/.github/workflows/auths-verify-commits.yml index bc66c723448..6e957fe3cbd 100644 --- a/.github/workflows/auths-verify-commits.yml +++ b/.github/workflows/auths-verify-commits.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - name: Verify commits with Auths - uses: auths-dev/auths-verify-github-action@v1 # TODO: pin to SHA once stable + uses: auths-dev/auths-verify-github-action@57e304ef368d30474e5b6a04106cacde6a8ce492 # v1 with: allowed-signers: .auths/allowed_signers fail-on-unsigned: 'false' diff --git a/cookbook/security/README.md b/cookbook/security/README.md index 7bc468507c5..5fd746bb0f2 100644 --- a/cookbook/security/README.md +++ b/cookbook/security/README.md @@ -26,7 +26,7 @@ The simulation script recreates the attack scenario and demonstrates how Auths verification catches the unauthorized commit: ```bash -pip install auths +brew tap auths-dev/auths-cli && brew install auths python auths_attack_simulation.py ``` diff --git a/cookbook/security/auths_attack_simulation.py b/cookbook/security/auths_attack_simulation.py index 3505ada8988..e24deda12a8 100644 --- a/cookbook/security/auths_attack_simulation.py +++ b/cookbook/security/auths_attack_simulation.py @@ -19,7 +19,7 @@ without the maintainer's private key stored in their device keychain. Usage: - pip install auths + brew tap auths-dev/auths-cli && brew install auths python auths_attack_simulation.py """ import os @@ -124,7 +124,8 @@ def main() -> None: print("The 'auths' CLI is not installed.") print() print("Install it with:") - print(" pip install auths") + print(" brew tap auths-dev/auths-cli && brew install auths") + print(" (or: cargo install auths_cli)") print() print("Or visit: https://github.com/auths-dev/auths") sys.exit(0) From c24d69b67485fb52b5f27cda2011413e4aca471b Mon Sep 17 00:00:00 2001 From: bordumb Date: Fri, 3 Apr 2026 19:18:26 -0700 Subject: [PATCH 3/6] fix: pin action to SHA, add contributor key to allowed_signers --- .auths/allowed_signers | 1 + 1 file changed, 1 insertion(+) diff --git a/.auths/allowed_signers b/.auths/allowed_signers index 34d0a6587c0..38fff92301a 100644 --- a/.auths/allowed_signers +++ b/.auths/allowed_signers @@ -22,3 +22,4 @@ # Documentation: https://github.com/auths-dev/auths/blob/main/docs/guides/platforms/ci-cd.md # # Maintainers — add your keys below this line: +z6MkhPJCPXd5A9VN4wScJkxTtz6de7egZQx78vsiAT1vg3PZ@auths.local namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuPK6OfYp7ngZp40Q+Dsrahhks472v6gPIMD0upCRnM From adaf2933af82497e2d4f53bddfbfc1283cc4472f Mon Sep 17 00:00:00 2001 From: bordumb Date: Fri, 3 Apr 2026 19:32:52 -0700 Subject: [PATCH 4/6] fix: clarify that simulation shows commit-signing layer, not full artifact chain --- cookbook/security/README.md | 17 +- cookbook/security/auths_attack_simulation.py | 259 +++++++++---------- 2 files changed, 138 insertions(+), 138 deletions(-) diff --git a/cookbook/security/README.md b/cookbook/security/README.md index 5fd746bb0f2..02d1e92a70c 100644 --- a/cookbook/security/README.md +++ b/cookbook/security/README.md @@ -20,10 +20,23 @@ KERI-based decentralized identifiers (DIDs). With Auths: - Stealing PyPI/npm credentials is insufficient without the signing key - Verification happens locally — no network calls to a central authority +## How Auths Addresses the Attack + +The real attack bypassed Git entirely — the attacker published directly to PyPI with +no corresponding commit. Commit-level signing alone would not have caught a +registry-only publish. However, Auths establishes a verifiable chain: every legitimate +release must trace back to a signed commit by an authorized maintainer. A package +published without a matching signed commit has no valid attestation chain and would be +flagged by consumers and CI pipelines that verify signatures. + +This workflow adds the commit-signing layer. A full deployment would also use +`auths artifact sign` in the release workflow to bind published packages to signed +commits, closing the gap completely. + ## Running the Simulation -The simulation script recreates the attack scenario and demonstrates how Auths -verification catches the unauthorized commit: +The simulation script demonstrates the commit-signing layer — it shows that commits +from unauthorized parties are detected: ```bash brew tap auths-dev/auths-cli && brew install auths diff --git a/cookbook/security/auths_attack_simulation.py b/cookbook/security/auths_attack_simulation.py index e24deda12a8..eaeb696b87d 100644 --- a/cookbook/security/auths_attack_simulation.py +++ b/cookbook/security/auths_attack_simulation.py @@ -1,8 +1,8 @@ """ 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. +Demonstrates how Auths cryptographic 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) @@ -12,106 +12,26 @@ 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. +How Auths closes this gap: + The real attack bypassed Git entirely — the attacker published directly to + PyPI with no corresponding commit. Auths establishes a policy that every + legitimate release must trace back to a signed action by an authorized + maintainer. A package published without a valid signature from a known + maintainer identity has no valid attestation and would be rejected. + + This simulation uses the Auths Python SDK to demonstrate the core + cryptographic primitive: sign an action with a maintainer's key, then + show that verification succeeds for the legitimate release and fails + for a tampered or unauthorized one. Usage: - brew tap auths-dev/auths-cli && brew install auths + pip install auths python auths_attack_simulation.py + +Requires: auths (Python SDK) """ -import os -import shutil -import subprocess +import json 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: @@ -120,54 +40,121 @@ def main() -> None: print("=" * 70) print() - if not check_auths_cli(): - print("The 'auths' CLI is not installed.") + try: + from auths import sign_action, verify_action_envelope + except ImportError: + print("The 'auths' Python SDK is not installed.") print() print("Install it with:") - print(" brew tap auths-dev/auths-cli && brew install auths") - print(" (or: cargo install auths_cli)") + 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() + # Derive the public key from the private seed for verification. + # In production, the maintainer's public key comes from their Auths + # identity (did:keri:...) and is listed in .auths/allowed_signers. + try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + MAINTAINER_SEED_HEX = "a" * 64 # Simulated maintainer private key seed + seed_bytes = bytes.fromhex(MAINTAINER_SEED_HEX) + private_key = Ed25519PrivateKey.from_private_bytes(seed_bytes) + MAINTAINER_PK_HEX = private_key.public_key().public_bytes_raw().hex() + except ImportError: + print("This simulation requires the 'cryptography' package.") + print("Install it with: pip install cryptography") + sys.exit(0) - # 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() + ATTACKER_SEED_HEX = "b" * 64 # Attacker has a different key + MAINTAINER_DID = "did:keri:EBfxc_LiteLLM_Maintainer" - # Verify the malicious commit - print("[3] Verifying attacker's commit (v1.82.7)...") - run_verification(repo) - print() + # ── Step 1: Legitimate maintainer signs a release ────────────────── + print("[1] Legitimate maintainer signs release v1.82.6...") + 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) + release_payload = json.dumps({ + "package": "litellm", + "version": "1.82.6", + "digest": "sha256:abc123def456...", + "registry": "pypi", + }) + + legitimate_envelope = sign_action( + MAINTAINER_SEED_HEX, + "release", + release_payload, + MAINTAINER_DID, + ) + + result = verify_action_envelope(legitimate_envelope, MAINTAINER_PK_HEX) + print(f" Signed by: {MAINTAINER_DID}") + print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") + print() + + # ── Step 2: Attacker publishes with stolen PyPI token ────────────── + print("[2] Attacker publishes v1.82.7 using stolen PyPI token...") + print(" (Attacker has registry credentials but NOT the signing key)") + print() + + malicious_payload = json.dumps({ + "package": "litellm", + "version": "1.82.7", + "digest": "sha256:malicious_payload_hash...", + "registry": "pypi", + }) + + # Attacker signs with their own key — NOT the maintainer's + attacker_envelope = sign_action( + ATTACKER_SEED_HEX, + "release", + malicious_payload, + "did:keri:EATTACKER_unknown_identity", + ) + + # Verify against the MAINTAINER's public key (the only trusted key) + result = verify_action_envelope(attacker_envelope, MAINTAINER_PK_HEX) + print(f" Signed by: did:keri:EATTACKER_unknown_identity") + print(f" Verification against maintainer key: {'PASSED' if result.valid else 'FAILED'}") + if result.error: + print(f" Reason: {result.error}") + print() + + # ── Step 3: Show tampered legitimate envelope also fails ─────────── + print("[3] Attacker tampers with a legitimately-signed envelope...") + print() + + envelope = json.loads(legitimate_envelope) + envelope["payload"]["version"] = "1.82.7" + envelope["payload"]["digest"] = "sha256:malicious_payload_hash..." + tampered_json = json.dumps(envelope) + + result = verify_action_envelope(tampered_json, MAINTAINER_PK_HEX) + print(f" Original signer: {MAINTAINER_DID}") + print(f" Tampered payload version: 1.82.7") + print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") + if result.error: + print(f" Reason: {result.error}") + print() + + # ── Summary ──────────────────────────────────────────────────────── + print("-" * 70) + print("SUMMARY") + print() + print(" v1.82.6 (legitimate, signed by maintainer): VERIFIED") + print(" v1.82.7 (attacker's key, not trusted): REJECTED") + print(" v1.82.7 (tampered legitimate envelope): REJECTED") + print() + print("NOTE: The real March 24 attack bypassed Git entirely — the attacker") + print("published directly to PyPI with no commit at all. This simulation") + print("demonstrates the cryptographic primitive that Auths provides: only") + print("the holder of the maintainer's private key can produce a valid") + print("signature. In a full deployment, the CI/CD pipeline would use") + print("'auths artifact sign' to bind the published package to the") + print("maintainer's identity, and consumers would verify before installing.") + print() + print("Learn more: https://github.com/auths-dev/auths") + print("=" * 70) if __name__ == "__main__": From 8f236f285f5aaa5365fc2510bcb5f404eb13ce7f Mon Sep 17 00:00:00 2001 From: bordumb Date: Sun, 5 Apr 2026 01:12:42 -0700 Subject: [PATCH 5/6] fix: use auths-dev/verify action and SDK-based demo scripts --- .github/workflows/auths-verify-commits.yml | 10 ++-- cookbook/security/README.md | 19 +++++--- cookbook/security/auths_attack_simulation.py | 48 ++++++-------------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/.github/workflows/auths-verify-commits.yml b/.github/workflows/auths-verify-commits.yml index 6e957fe3cbd..a205964cf8f 100644 --- a/.github/workflows/auths-verify-commits.yml +++ b/.github/workflows/auths-verify-commits.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 5 permissions: contents: read - pull-requests: write + pull-requests: ${{ github.event_name == 'pull_request' && 'write' || 'none' }} steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -28,10 +28,10 @@ jobs: persist-credentials: false - name: Verify commits with Auths - uses: auths-dev/auths-verify-github-action@57e304ef368d30474e5b6a04106cacde6a8ce492 # v1 + uses: auths-dev/verify@v1 with: - allowed-signers: .auths/allowed_signers + token: .auths/allowed_signers fail-on-unsigned: 'false' skip-merge-commits: 'true' - post-pr-comment: 'true' - github-token: ${{ secrets.GITHUB_TOKEN }} + post-pr-comment: ${{ github.event_name == 'pull_request' && 'true' || 'false' }} + github-token: ${{ github.event_name == 'pull_request' && secrets.GITHUB_TOKEN || '' }} diff --git a/cookbook/security/README.md b/cookbook/security/README.md index 02d1e92a70c..61f4c1cec3a 100644 --- a/cookbook/security/README.md +++ b/cookbook/security/README.md @@ -15,7 +15,7 @@ package and a verified maintainer identity**. [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 +- Every commit and artifact 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 @@ -29,20 +29,25 @@ release must trace back to a signed commit by an authorized maintainer. A packag published without a matching signed commit has no valid attestation chain and would be flagged by consumers and CI pipelines that verify signatures. -This workflow adds the commit-signing layer. A full deployment would also use -`auths artifact sign` in the release workflow to bind published packages to signed -commits, closing the gap completely. +This workflow adds the commit-signing layer via the +[`auths-dev/verify`](https://github.com/auths-dev/verify) GitHub Action. A full +deployment would also use `auths artifact sign` (via +[`auths-dev/sign`](https://github.com/auths-dev/sign)) in the release workflow to +bind published packages to signed commits. ## Running the Simulation -The simulation script demonstrates the commit-signing layer — it shows that commits -from unauthorized parties are detected: +The simulation script uses the Auths Python SDK to demonstrate the core cryptographic +primitive — it shows that only the holder of the maintainer's private key can produce +a valid signature: ```bash -brew tap auths-dev/auths-cli && brew install auths +pip install auths python auths_attack_simulation.py ``` +No CLI installation, git, or ssh-keygen needed — the script uses the SDK directly. + ## Adding Auths to Your Workflow See the GitHub Actions workflow at `.github/workflows/auths-verify-commits.yml` diff --git a/cookbook/security/auths_attack_simulation.py b/cookbook/security/auths_attack_simulation.py index eaeb696b87d..766d0b506eb 100644 --- a/cookbook/security/auths_attack_simulation.py +++ b/cookbook/security/auths_attack_simulation.py @@ -22,7 +22,7 @@ This simulation uses the Auths Python SDK to demonstrate the core cryptographic primitive: sign an action with a maintainer's key, then show that verification succeeds for the legitimate release and fails - for a tampered or unauthorized one. + for an unauthorized or tampered one. Usage: pip install auths @@ -41,7 +41,7 @@ def main() -> None: print() try: - from auths import sign_action, verify_action_envelope + from auths import generate_inmemory_keypair, sign_action, verify_action_envelope except ImportError: print("The 'auths' Python SDK is not installed.") print() @@ -51,23 +51,9 @@ def main() -> None: print("Or visit: https://github.com/auths-dev/auths") sys.exit(0) - # Derive the public key from the private seed for verification. - # In production, the maintainer's public key comes from their Auths - # identity (did:keri:...) and is listed in .auths/allowed_signers. - try: - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - - MAINTAINER_SEED_HEX = "a" * 64 # Simulated maintainer private key seed - seed_bytes = bytes.fromhex(MAINTAINER_SEED_HEX) - private_key = Ed25519PrivateKey.from_private_bytes(seed_bytes) - MAINTAINER_PK_HEX = private_key.public_key().public_bytes_raw().hex() - except ImportError: - print("This simulation requires the 'cryptography' package.") - print("Install it with: pip install cryptography") - sys.exit(0) - - ATTACKER_SEED_HEX = "b" * 64 # Attacker has a different key - MAINTAINER_DID = "did:keri:EBfxc_LiteLLM_Maintainer" + # Generate ephemeral identities — no filesystem, no keychain needed + maintainer_priv, maintainer_pub, maintainer_did = generate_inmemory_keypair() + attacker_priv, _attacker_pub, attacker_did = generate_inmemory_keypair() # ── Step 1: Legitimate maintainer signs a release ────────────────── print("[1] Legitimate maintainer signs release v1.82.6...") @@ -81,20 +67,17 @@ def main() -> None: }) legitimate_envelope = sign_action( - MAINTAINER_SEED_HEX, - "release", - release_payload, - MAINTAINER_DID, + maintainer_priv, "release", release_payload, maintainer_did, ) - result = verify_action_envelope(legitimate_envelope, MAINTAINER_PK_HEX) - print(f" Signed by: {MAINTAINER_DID}") + result = verify_action_envelope(legitimate_envelope, maintainer_pub) + print(f" Signed by: {maintainer_did}") print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") print() # ── Step 2: Attacker publishes with stolen PyPI token ────────────── print("[2] Attacker publishes v1.82.7 using stolen PyPI token...") - print(" (Attacker has registry credentials but NOT the signing key)") + print(" (Attacker has registry credentials but NOT the maintainer's signing key)") print() malicious_payload = json.dumps({ @@ -106,15 +89,12 @@ def main() -> None: # Attacker signs with their own key — NOT the maintainer's attacker_envelope = sign_action( - ATTACKER_SEED_HEX, - "release", - malicious_payload, - "did:keri:EATTACKER_unknown_identity", + attacker_priv, "release", malicious_payload, attacker_did, ) # Verify against the MAINTAINER's public key (the only trusted key) - result = verify_action_envelope(attacker_envelope, MAINTAINER_PK_HEX) - print(f" Signed by: did:keri:EATTACKER_unknown_identity") + result = verify_action_envelope(attacker_envelope, maintainer_pub) + print(f" Signed by: {attacker_did}") print(f" Verification against maintainer key: {'PASSED' if result.valid else 'FAILED'}") if result.error: print(f" Reason: {result.error}") @@ -129,8 +109,8 @@ def main() -> None: envelope["payload"]["digest"] = "sha256:malicious_payload_hash..." tampered_json = json.dumps(envelope) - result = verify_action_envelope(tampered_json, MAINTAINER_PK_HEX) - print(f" Original signer: {MAINTAINER_DID}") + result = verify_action_envelope(tampered_json, maintainer_pub) + print(f" Original signer: {maintainer_did}") print(f" Tampered payload version: 1.82.7") print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") if result.error: From 7906ee1390b4b89dabf38bb81d7c34f7583efcbb Mon Sep 17 00:00:00 2001 From: bordumb Date: Sun, 5 Apr 2026 01:21:13 -0700 Subject: [PATCH 6/6] ci: replace demo scripts with auths-dev/sign release workflow --- .github/workflows/auths-sign-release.yml | 59 ++++++++ cookbook/security/README.md | 54 ------- cookbook/security/auths_attack_simulation.py | 141 ------------------- 3 files changed, 59 insertions(+), 195 deletions(-) create mode 100644 .github/workflows/auths-sign-release.yml delete mode 100644 cookbook/security/README.md delete mode 100644 cookbook/security/auths_attack_simulation.py diff --git a/.github/workflows/auths-sign-release.yml b/.github/workflows/auths-sign-release.yml new file mode 100644 index 00000000000..fb61653064a --- /dev/null +++ b/.github/workflows/auths-sign-release.yml @@ -0,0 +1,59 @@ +name: Auths Sign Release Artifacts + +# Runs after the existing publish_to_pypi workflow builds artifacts. +# Signs the sdist (.tar.gz) and wheel (.whl) before they are published, +# creating .auths.json attestation files that consumers can verify. +# +# To activate: +# 1. Store AUTHS_CI_TOKEN as a repository secret +# (generate with: auths init --profile ci --github-action) +# 2. Remove the 'if: false' guard below + +on: + workflow_dispatch: + +jobs: + sign: + name: Sign release artifacts + if: false # Remove this line to activate + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + environment: pypi-publish + + steps: + - name: Checkout repo + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Build package + run: | + python -m pip install --upgrade pip build==1.4.2 + cp model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json + rm -rf build dist + python -m build + + - name: Sign artifacts with Auths + uses: auths-dev/sign@v1 + with: + token: ${{ secrets.AUTHS_CI_TOKEN }} + files: | + dist/*.tar.gz + dist/*.whl + verify: true + note: 'PyPI release ${{ github.sha }}' + + # After this step, each file in dist/ has a corresponding .auths.json + # attestation file. These can be: + # - Published alongside the package (e.g., as GitHub Release assets) + # - Uploaded to the Auths registry (auths artifact publish) + # - Verified by consumers: auths artifact verify dist/litellm-*.tar.gz + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/cookbook/security/README.md b/cookbook/security/README.md deleted file mode 100644 index 61f4c1cec3a..00000000000 --- a/cookbook/security/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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 and artifact 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 - -## How Auths Addresses the Attack - -The real attack bypassed Git entirely — the attacker published directly to PyPI with -no corresponding commit. Commit-level signing alone would not have caught a -registry-only publish. However, Auths establishes a verifiable chain: every legitimate -release must trace back to a signed commit by an authorized maintainer. A package -published without a matching signed commit has no valid attestation chain and would be -flagged by consumers and CI pipelines that verify signatures. - -This workflow adds the commit-signing layer via the -[`auths-dev/verify`](https://github.com/auths-dev/verify) GitHub Action. A full -deployment would also use `auths artifact sign` (via -[`auths-dev/sign`](https://github.com/auths-dev/sign)) in the release workflow to -bind published packages to signed commits. - -## Running the Simulation - -The simulation script uses the Auths Python SDK to demonstrate the core cryptographic -primitive — it shows that only the holder of the maintainer's private key can produce -a valid signature: - -```bash -pip install auths -python auths_attack_simulation.py -``` - -No CLI installation, git, or ssh-keygen needed — the script uses the SDK directly. - -## 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`. diff --git a/cookbook/security/auths_attack_simulation.py b/cookbook/security/auths_attack_simulation.py deleted file mode 100644 index 766d0b506eb..00000000000 --- a/cookbook/security/auths_attack_simulation.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Auths Attack Simulation: LiteLLM March 24, 2026 Supply Chain Incident - -Demonstrates how Auths cryptographic 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 - -How Auths closes this gap: - The real attack bypassed Git entirely — the attacker published directly to - PyPI with no corresponding commit. Auths establishes a policy that every - legitimate release must trace back to a signed action by an authorized - maintainer. A package published without a valid signature from a known - maintainer identity has no valid attestation and would be rejected. - - This simulation uses the Auths Python SDK to demonstrate the core - cryptographic primitive: sign an action with a maintainer's key, then - show that verification succeeds for the legitimate release and fails - for an unauthorized or tampered one. - -Usage: - pip install auths - python auths_attack_simulation.py - -Requires: auths (Python SDK) -""" -import json -import sys - - -def main() -> None: - print("=" * 70) - print("Auths Attack Simulation: LiteLLM Supply Chain Incident (March 24, 2026)") - print("=" * 70) - print() - - try: - from auths import generate_inmemory_keypair, sign_action, verify_action_envelope - except ImportError: - print("The 'auths' Python SDK 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) - - # Generate ephemeral identities — no filesystem, no keychain needed - maintainer_priv, maintainer_pub, maintainer_did = generate_inmemory_keypair() - attacker_priv, _attacker_pub, attacker_did = generate_inmemory_keypair() - - # ── Step 1: Legitimate maintainer signs a release ────────────────── - print("[1] Legitimate maintainer signs release v1.82.6...") - print() - - release_payload = json.dumps({ - "package": "litellm", - "version": "1.82.6", - "digest": "sha256:abc123def456...", - "registry": "pypi", - }) - - legitimate_envelope = sign_action( - maintainer_priv, "release", release_payload, maintainer_did, - ) - - result = verify_action_envelope(legitimate_envelope, maintainer_pub) - print(f" Signed by: {maintainer_did}") - print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") - print() - - # ── Step 2: Attacker publishes with stolen PyPI token ────────────── - print("[2] Attacker publishes v1.82.7 using stolen PyPI token...") - print(" (Attacker has registry credentials but NOT the maintainer's signing key)") - print() - - malicious_payload = json.dumps({ - "package": "litellm", - "version": "1.82.7", - "digest": "sha256:malicious_payload_hash...", - "registry": "pypi", - }) - - # Attacker signs with their own key — NOT the maintainer's - attacker_envelope = sign_action( - attacker_priv, "release", malicious_payload, attacker_did, - ) - - # Verify against the MAINTAINER's public key (the only trusted key) - result = verify_action_envelope(attacker_envelope, maintainer_pub) - print(f" Signed by: {attacker_did}") - print(f" Verification against maintainer key: {'PASSED' if result.valid else 'FAILED'}") - if result.error: - print(f" Reason: {result.error}") - print() - - # ── Step 3: Show tampered legitimate envelope also fails ─────────── - print("[3] Attacker tampers with a legitimately-signed envelope...") - print() - - envelope = json.loads(legitimate_envelope) - envelope["payload"]["version"] = "1.82.7" - envelope["payload"]["digest"] = "sha256:malicious_payload_hash..." - tampered_json = json.dumps(envelope) - - result = verify_action_envelope(tampered_json, maintainer_pub) - print(f" Original signer: {maintainer_did}") - print(f" Tampered payload version: 1.82.7") - print(f" Verification: {'PASSED' if result.valid else 'FAILED'}") - if result.error: - print(f" Reason: {result.error}") - print() - - # ── Summary ──────────────────────────────────────────────────────── - print("-" * 70) - print("SUMMARY") - print() - print(" v1.82.6 (legitimate, signed by maintainer): VERIFIED") - print(" v1.82.7 (attacker's key, not trusted): REJECTED") - print(" v1.82.7 (tampered legitimate envelope): REJECTED") - print() - print("NOTE: The real March 24 attack bypassed Git entirely — the attacker") - print("published directly to PyPI with no commit at all. This simulation") - print("demonstrates the cryptographic primitive that Auths provides: only") - print("the holder of the maintainer's private key can produce a valid") - print("signature. In a full deployment, the CI/CD pipeline would use") - print("'auths artifact sign' to bind the published package to the") - print("maintainer's identity, and consumers would verify before installing.") - print() - print("Learn more: https://github.com/auths-dev/auths") - print("=" * 70) - - -if __name__ == "__main__": - main()