diff --git a/github-app-token/SKILL.md b/github-app-token/SKILL.md index a5bb038..d638610 100644 --- a/github-app-token/SKILL.md +++ b/github-app-token/SKILL.md @@ -19,19 +19,18 @@ The following environment variables MUST be set before invoking this skill: If any variable is missing, stop and tell the user which ones are required. +Requires `openssl`, `curl`, and `grep` (standard on macOS and Linux). + ## Steps ### 1. Generate a JWT -Create a JWT signed with the GitHub App's private key. You MUST use the helper script bundled with this skill: +Run the helper script bundled with this skill: ```bash -# generates a JWT valid for 10 minutes -TOKEN=$(python3 "$(dirname "$0")/../skills/github-app-token/scripts/generate_jwt.py") +TOKEN=$(bash /path/to/skills/github-app-token/scripts/generate_jwt.sh) ``` -If `python3` is not available, fall back to the inline openssl method described in the Fallback section below. - The JWT uses: - **Algorithm**: RS256 - **Header**: `{"alg": "RS256", "typ": "JWT"}` @@ -43,12 +42,13 @@ The JWT uses: ### 2. Exchange the JWT for an installation access token ```bash -INSTALL_TOKEN=$(curl -s -X POST \ +RESPONSE=$(curl -s -X POST \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}/access_tokens" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + "https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}/access_tokens") + +INSTALL_TOKEN=$(echo "$RESPONSE" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4) ``` If the response contains an error (e.g., `401 Unauthorized`), check: @@ -81,23 +81,6 @@ curl -s -X DELETE \ "https://api.github.com/installation/token" ``` -## Fallback: JWT generation without Python - -If `python3` is unavailable, generate the JWT using `openssl` and `bash`: - -```bash -header=$(printf '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') -now=$(date +%s) -iat=$((now - 60)) -exp=$((now + 600)) -payload=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$iat" "$exp" "$GITHUB_APP_ID" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') -unsigned="${header}.${payload}" -signature=$(printf '%s' "$unsigned" | openssl dgst -sha256 -sign "${GITHUB_APP_PEM_FILE}" -binary | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') -TOKEN="${unsigned}.${signature}" -``` - -Then continue from Step 2. - ## Security Notes - Never log or echo the PEM key, JWT, or installation token to stdout in production. diff --git a/github-app-token/scripts/generate_jwt.py b/github-app-token/scripts/generate_jwt.py deleted file mode 100755 index 376b31c..0000000 --- a/github-app-token/scripts/generate_jwt.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a JWT for GitHub App authentication. - -Reads from environment variables: - GITHUB_APP_ID - The GitHub App's numeric ID - GITHUB_APP_PEM_FILE - Path to the PEM-encoded private key file - -Prints the signed JWT to stdout. -""" - -import json -import os -import sys -import time -import base64 - -try: - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import padding - - USE_CRYPTOGRAPHY = True -except ImportError: - import subprocess - - USE_CRYPTOGRAPHY = False - - -def b64url(data: bytes) -> str: - return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") - - -def sign_with_cryptography(unsigned: str, pem_contents: str) -> str: - """Sign using the cryptography library. Handles both PKCS#1 and PKCS#8 PEM formats.""" - private_key = serialization.load_pem_private_key(pem_contents.encode(), password=None) - signature = private_key.sign(unsigned.encode(), padding.PKCS1v15(), hashes.SHA256()) - return b64url(signature) - - -def sign_with_openssl(unsigned: str, pem_file: str) -> str: - """Sign using the openssl CLI. Reads the key from the file path directly.""" - result = subprocess.run( - ["openssl", "dgst", "-sha256", "-sign", pem_file, "-binary"], - input=unsigned.encode(), - capture_output=True, - ) - if result.returncode != 0: - print(f"error: openssl signing failed: {result.stderr.decode()}", file=sys.stderr) - sys.exit(1) - return b64url(result.stdout) - - -def main(): - app_id = os.environ.get("GITHUB_APP_ID") - pem_file = os.environ.get("GITHUB_APP_PEM_FILE") - - if not app_id: - print("error: GITHUB_APP_ID is not set", file=sys.stderr) - sys.exit(1) - if not pem_file: - print("error: GITHUB_APP_PEM_FILE is not set", file=sys.stderr) - sys.exit(1) - if not os.path.isfile(pem_file): - print(f"error: PEM file not found: {pem_file}", file=sys.stderr) - sys.exit(1) - - now = int(time.time()) - header = b64url(json.dumps({"alg": "RS256", "typ": "JWT"}).encode()) - payload = b64url( - json.dumps({"iat": now - 60, "exp": now + 600, "iss": app_id}).encode() - ) - unsigned = f"{header}.{payload}" - - if USE_CRYPTOGRAPHY: - with open(pem_file, "r") as f: - pem_contents = f.read() - signature = sign_with_cryptography(unsigned, pem_contents) - else: - signature = sign_with_openssl(unsigned, pem_file) - - print(f"{unsigned}.{signature}") - - -if __name__ == "__main__": - main() diff --git a/github-app-token/scripts/generate_jwt.sh b/github-app-token/scripts/generate_jwt.sh new file mode 100755 index 0000000..31423e6 --- /dev/null +++ b/github-app-token/scripts/generate_jwt.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Generate a JWT for GitHub App authentication. +# +# Required environment variables: +# GITHUB_APP_ID - The GitHub App's numeric ID +# GITHUB_APP_PEM_FILE - Path to the PEM-encoded private key file +# +# Prints the signed JWT to stdout. + +set -euo pipefail + +if [[ -z "${GITHUB_APP_ID:-}" ]]; then + echo "error: GITHUB_APP_ID is not set" >&2 + exit 1 +fi + +if [[ -z "${GITHUB_APP_PEM_FILE:-}" ]]; then + echo "error: GITHUB_APP_PEM_FILE is not set" >&2 + exit 1 +fi + +if [[ ! -f "${GITHUB_APP_PEM_FILE}" ]]; then + echo "error: PEM file not found: ${GITHUB_APP_PEM_FILE}" >&2 + exit 1 +fi + +## Build JWT + +header=$(printf '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') + +now=$(date +%s) +iat=$((now - 60)) +exp=$((now + 600)) + +payload=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$iat" "$exp" "$GITHUB_APP_ID" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') + +unsigned="${header}.${payload}" + +signature=$(printf '%s' "$unsigned" | openssl dgst -sha256 -sign "${GITHUB_APP_PEM_FILE}" -binary | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') + +echo "${unsigned}.${signature}"