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 @@ How Auths closes this gap: 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: