diff --git a/.github/skills/ctf-testing/SKILL.md b/.github/skills/ctf-testing/SKILL.md index 3df82ff..58c84ea 100644 --- a/.github/skills/ctf-testing/SKILL.md +++ b/.github/skills/ctf-testing/SKILL.md @@ -45,15 +45,17 @@ Run from repository root: 2. **Challenge setup** - Files exist, services running, permissions correct 3. **Solution commands** - Each challenge returns valid flag 4. **Flag submission** - All 18 flags accepted by `verify` -5. **Reboot resilience** (with `--with-reboot`) - Services restart, progress persists +5. **Verification token system** - Instance secrets, token generation, token format validation +6. **Reboot resilience** (with `--with-reboot`) - Services restart, progress persists ## Expected Results -A successful run shows **69 tests passing**: +A successful run shows **~84 tests passing**: - 7 verify subcommand tests - 24 challenge setup verifications - 18 solution command tests - 20 flag verification tests +- 15 verification token tests ## Troubleshooting diff --git a/.github/skills/ctf-testing/test_ctf_challenges.sh b/.github/skills/ctf-testing/test_ctf_challenges.sh index 2283de9..3452757 100755 --- a/.github/skills/ctf-testing/test_ctf_challenges.sh +++ b/.github/skills/ctf-testing/test_ctf_challenges.sh @@ -476,6 +476,102 @@ verify_captured_flag 18 run_test_output "verify progress shows 18/18" \ "verify progress" "18/18" +# ============================================================================ +section "VERIFICATION TOKEN TESTS" +# ============================================================================ + +echo "Testing the verification token export system..." + +# Test that verification secrets were created +run_test "Verification secrets: instance_id exists" \ + "test -f /etc/ctf/instance_id && test -s /etc/ctf/instance_id" + +run_test "Verification secrets: verification_secret exists" \ + "test -f /etc/ctf/verification_secret && test -s /etc/ctf/verification_secret" + +run_test "Verification secrets: instance_id is 32 hex chars" \ + "test \$(cat /etc/ctf/instance_id | wc -c) -eq 33" # 32 chars + newline + +run_test "Verification secrets: verification_secret is 64 hex chars (SHA256)" \ + "test \$(cat /etc/ctf/verification_secret | wc -c) -eq 65" # 64 chars + newline + +# Test export command now that all challenges are complete +echo "Testing verify export command..." + +EXPORT_OUTPUT=$(verify export testuser 2>&1) || true +# Save to file to avoid issues with special characters in ASCII art +echo "$EXPORT_OUTPUT" > /tmp/ctf_export_output.txt + +# Check export output contains expected content (using file to avoid shell escaping issues) +if grep -q "COMPLETION CERTIFICATE" /tmp/ctf_export_output.txt 2>/dev/null; then + pass "verify export creates certificate" +else + fail "verify export creates certificate" +fi + +if grep -q "testuser" /tmp/ctf_export_output.txt 2>/dev/null; then + pass "verify export shows GitHub username" +else + fail "verify export shows GitHub username" +fi + +if grep -q "BEGIN L2C CTF TOKEN" /tmp/ctf_export_output.txt 2>/dev/null; then + pass "verify export generates verification token" +else + fail "verify export generates verification token" +fi + +# Extract and validate token format (using file to avoid shell escaping) +TOKEN=$(sed -n '/BEGIN L2C CTF TOKEN/,/END L2C CTF TOKEN/p' /tmp/ctf_export_output.txt | grep -v 'L2C CTF TOKEN' | tr -d '\n ') +if [ -n "$TOKEN" ]; then + pass "verify export: Token extracted" + + # Token should be valid base64 + DECODED=$(echo "$TOKEN" | base64 -d 2>/dev/null) || DECODED="" + if [ -n "$DECODED" ]; then + pass "verify export: Token is valid base64" + + # Check token contains expected JSON fields + if echo "$DECODED" | grep -q '"payload"'; then + pass "verify export: Token contains payload" + else + fail "verify export: Token missing payload field" + fi + + if echo "$DECODED" | grep -q '"signature"'; then + pass "verify export: Token contains signature" + else + fail "verify export: Token missing signature field" + fi + + if echo "$DECODED" | grep -q '"github_username":"testuser"'; then + pass "verify export: Token contains correct github_username" + else + fail "verify export: Token has wrong or missing github_username" + fi + + if echo "$DECODED" | grep -q '"challenges":18'; then + pass "verify export: Token shows 18 challenges" + else + fail "verify export: Token has wrong challenge count" + fi + + if echo "$DECODED" | grep -q '"instance_id"'; then + pass "verify export: Token contains instance_id" + else + fail "verify export: Token missing instance_id" + fi + else + fail "verify export: Token is not valid base64" + fi +else + fail "verify export: No token found in output" +fi + +# Test that export without username shows usage +run_test_output "verify export without username shows usage" \ + "verify export 2>&1" "Usage:" + # ============================================================================ section "TEST SUMMARY" # ============================================================================ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f87ed5..d93e8db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,6 +80,7 @@ For thorough testing (includes reboot verification): - Services are running and accessible - Flags can be discovered and submitted - Progress tracking works +- Verification token generation and format - (With `--with-reboot`) Services survive VM restart See [.github/skills/ctf-testing/SKILL.md](.github/skills/ctf-testing/SKILL.md) for detailed documentation. diff --git a/README.md b/README.md index 5252ad1..c7664f0 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,30 @@ Deploy your CTF lab using your preferred cloud provider: | Azure | ~$0.05 | [Azure Setup](./azure/README.md) | | GCP | ~$0.03 | [GCP Setup](./gcp/README.md) | +## Completing the CTF + +Once you've solved all 18 challenges, export your completion certificate: + +```bash +verify export +``` + +> [!IMPORTANT] +> Enter your GitHub username **exactly** as it appears on GitHub—no `@` symbol, no extra spaces, no special characters. For example: `verify export octocat` not `verify export @octocat`. The verification system will reject tokens with incorrect usernames. + +This generates a cryptographically signed token. To verify your completion: + +1. Go to [learntocloud.guide/phase2](https://learntocloud.guide/phase2) +2. Sign in with the **same GitHub account** you used in the export command +3. Copy **only the token** (the long string of characters between the markers): + ``` + --- BEGIN L2C CTF TOKEN --- + eyJwYXlsb2FkIjp7...your-unique-token-here...fQ== + --- END L2C CTF TOKEN --- + ``` + > **Copy everything between the markers, but NOT the `--- BEGIN/END ---` lines themselves.** +4. Paste the token into the verification form + ## Tips - Use `man` pages to learn commands (e.g., `man find`) diff --git a/VERIFICATION.md b/VERIFICATION.md new file mode 100644 index 0000000..349618f --- /dev/null +++ b/VERIFICATION.md @@ -0,0 +1,415 @@ +# CTF Completion Verification + +This document describes how the Learn to Cloud CTF verification token system works and how to implement verification in your application. + +## Overview + +When users complete all 18 challenges and run `verify export `, they receive: +1. A visual certificate displayed in the terminal +2. A **signed verification token** they can copy-paste to verify their completion + +## Security Design + +The verification system uses **GitHub OAuth** as the primary security mechanism: + +1. **User completes CTF** and runs `verify export ` +2. **Token is generated** containing their GitHub username +3. **User visits verification app** at https://learntocloud.guide/phase2 and signs in with GitHub +4. **App verifies**: `token.github_username === OAuth_user.login` + +This means: +- Users must sign in with the **same GitHub account** they specified when exporting +- Even if someone forges a token, they can only claim it for their own GitHub account +- No value in forging tokens for other users (can't log in as them) + +Additionally, tokens are signed with HMAC-SHA256 using a derived secret: +- `VERIFICATION_SECRET = SHA256(MASTER_SECRET:INSTANCE_ID)` +- This allows the app to verify the token structure is valid + +## Token Format + +The token is a base64-encoded JSON object containing: + +```json +{ + "payload": { + "github_username": "octocat", + "date": "2026-01-13", + "time": "02:30", + "challenges": 18, + "timestamp": 1736784000, + "instance_id": "a1b2c3d4e5f6..." + }, + "signature": "abc123..." +} +``` + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `github_username` | string | User's GitHub username (verified via OAuth) | +| `date` | string | Completion date (YYYY-MM-DD) | +| `time` | string | Total time to complete (HH:MM) | +| `challenges` | number | Number of challenges completed (always 18) | +| `timestamp` | number | Unix timestamp when token was generated | +| `instance_id` | string | Unique identifier for this VM instance (32 hex chars) | +| `signature` | string | HMAC-SHA256 signature of the payload | + +## Verification App Implementation + +### Master Secret + +``` +L2C_CTF_MASTER_2024 +``` + +> ⚠️ **IMPORTANT**: This master secret must be stored securely in your verification app (environment variable, secrets manager, etc.). Never expose it to the client/frontend. + +### Verification Algorithm + +1. **User signs in** with GitHub OAuth → get `oauth_user.login` +2. **Decode** the token from base64 +3. **Parse** the JSON to extract `payload` and `signature` +4. **Check GitHub username**: `payload.github_username === oauth_user.login` ⚠️ **Critical step!** +5. **Extract** the `instance_id` from the payload +6. **Derive** the verification secret: `SHA256(MASTER_SECRET + ":" + instance_id)` +7. **Stringify** the payload (exactly as received) +8. **Compute** HMAC-SHA256 of the payload using the derived secret +9. **Compare** computed signature with the provided signature +10. **Validate** the payload fields (challenges === 18, reasonable timestamp, etc.) + +### Example Implementations + +#### Python + +```python +import base64 +import json +import hmac +import hashlib +from datetime import datetime + +MASTER_SECRET = "L2C_CTF_MASTER_2024" + +def derive_secret(instance_id: str) -> str: + """Derive the verification secret from master secret and instance ID.""" + data = f"{MASTER_SECRET}:{instance_id}" + return hashlib.sha256(data.encode()).hexdigest() + +def verify_token(token: str, oauth_github_username: str) -> dict: + """ + Verify a CTF completion token. + + Args: + token: The base64-encoded token from the user + oauth_github_username: The GitHub username from OAuth sign-in + + Returns: + dict with 'valid' (bool) and 'data' (payload) or 'error' (message) + """ + try: + # Decode base64 + decoded = base64.b64decode(token).decode('utf-8') + token_data = json.loads(decoded) + + payload = token_data.get('payload') + signature = token_data.get('signature') + + if not payload or not signature: + return {"valid": False, "error": "Invalid token structure"} + + # CRITICAL: Verify GitHub username matches OAuth user + token_username = payload.get('github_username', '').lower() + if token_username != oauth_github_username.lower(): + return {"valid": False, "error": "GitHub username mismatch"} + + # Get instance ID and derive the secret + instance_id = payload.get('instance_id') + if not instance_id: + return {"valid": False, "error": "Missing instance ID"} + + verification_secret = derive_secret(instance_id) + + # Recreate the payload string exactly as it was signed + payload_str = json.dumps(payload, separators=(',', ':')) + + # Compute expected signature + expected_sig = hmac.new( + verification_secret.encode(), + payload_str.encode(), + hashlib.sha256 + ).hexdigest() + + # Constant-time comparison to prevent timing attacks + if not hmac.compare_digest(signature, expected_sig): + return {"valid": False, "error": "Invalid signature"} + + # Validate payload + if payload.get('challenges') != 18: + return {"valid": False, "error": "Incomplete challenges"} + + # Check timestamp is reasonable (not in future, not too old) + timestamp = payload.get('timestamp', 0) + now = datetime.now().timestamp() + if timestamp > now + 3600: # Allow 1 hour clock skew + return {"valid": False, "error": "Invalid timestamp"} + + return { + "valid": True, + "data": { + "github_username": payload.get('github_username'), + "date": payload.get('date'), + "completion_time": payload.get('time'), + "challenges": payload.get('challenges') + } + } + + except Exception as e: + return {"valid": False, "error": f"Token parsing failed: {str(e)}"} + + +# Example usage (in your Flask/FastAPI route after OAuth) +if __name__ == "__main__": + # In real app, oauth_username comes from GitHub OAuth callback + oauth_username = input("Your GitHub username: ").strip() + test_token = input("Paste token: ").strip() + result = verify_token(test_token, oauth_username) + print(json.dumps(result, indent=2)) +``` + +#### JavaScript/Node.js + +```javascript +const crypto = require('crypto'); + +const MASTER_SECRET = 'L2C_CTF_MASTER_2024'; + +function deriveSecret(instanceId) { + const data = `${MASTER_SECRET}:${instanceId}`; + return crypto.createHash('sha256').update(data).digest('hex'); +} + +function verifyToken(token, oauthGithubUsername) { + try { + // Decode base64 + const decoded = Buffer.from(token, 'base64').toString('utf-8'); + const tokenData = JSON.parse(decoded); + + const { payload, signature } = tokenData; + + if (!payload || !signature) { + return { valid: false, error: 'Invalid token structure' }; + } + + // CRITICAL: Verify GitHub username matches OAuth user + const tokenUsername = (payload.github_username || '').toLowerCase(); + if (tokenUsername !== oauthGithubUsername.toLowerCase()) { + return { valid: false, error: 'GitHub username mismatch' }; + } + + // Get instance ID and derive the secret + const instanceId = payload.instance_id; + if (!instanceId) { + return { valid: false, error: 'Missing instance ID' }; + } + + const verificationSecret = deriveSecret(instanceId); + + // Recreate the payload string exactly as it was signed + const payloadStr = JSON.stringify(payload); + + // Compute expected signature + const expectedSig = crypto + .createHmac('sha256', verificationSecret) + .update(payloadStr) + .digest('hex'); + + // Constant-time comparison + if (!crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSig) + )) { + return { valid: false, error: 'Invalid signature' }; + } + + // Validate payload + if (payload.challenges !== 18) { + return { valid: false, error: 'Incomplete challenges' }; + } + + return { + valid: true, + data: { + githubUsername: payload.github_username, + date: payload.date, + completionTime: payload.time, + challenges: payload.challenges + } + }; + + } catch (e) { + return { valid: false, error: `Token parsing failed: ${e.message}` }; + } +} + +module.exports = { verifyToken }; +``` + +#### Go + +```go +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "strings" +) + +const masterSecret = "L2C_CTF_MASTER_2024" + +type Payload struct { + GithubUsername string `json:"github_username"` + Date string `json:"date"` + Time string `json:"time"` + Challenges int `json:"challenges"` + Timestamp int64 `json:"timestamp"` + InstanceID string `json:"instance_id"` +} + +type TokenData struct { + Payload Payload `json:"payload"` + Signature string `json:"signature"` +} + +type VerificationResult struct { + Valid bool `json:"valid"` + Data map[string]interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +func deriveSecret(instanceID string) string { + data := fmt.Sprintf("%s:%s", masterSecret, instanceID) + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:]) +} + +func verifyToken(token string, oauthGithubUsername string) VerificationResult { + // Decode base64 + decoded, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return VerificationResult{Valid: false, Error: "Base64 decode failed"} + } + + var tokenData TokenData + if err := json.Unmarshal(decoded, &tokenData); err != nil { + return VerificationResult{Valid: false, Error: "JSON parse failed"} + } + + // CRITICAL: Verify GitHub username matches OAuth user + if strings.ToLower(tokenData.Payload.GithubUsername) != strings.ToLower(oauthGithubUsername) { + return VerificationResult{Valid: false, Error: "GitHub username mismatch"} + } + + // Derive secret from instance ID + if tokenData.Payload.InstanceID == "" { + return VerificationResult{Valid: false, Error: "Missing instance ID"} + } + verificationSecret := deriveSecret(tokenData.Payload.InstanceID) + + // Recreate payload string + payloadBytes, _ := json.Marshal(tokenData.Payload) + + // Compute expected signature + h := hmac.New(sha256.New, []byte(verificationSecret)) + h.Write(payloadBytes) + expectedSig := hex.EncodeToString(h.Sum(nil)) + + // Compare signatures + if !hmac.Equal([]byte(tokenData.Signature), []byte(expectedSig)) { + return VerificationResult{Valid: false, Error: "Invalid signature"} + } + + // Validate challenges + if tokenData.Payload.Challenges != 18 { + return VerificationResult{Valid: false, Error: "Incomplete challenges"} + } + + return VerificationResult{ + Valid: true, + Data: map[string]interface{}{ + "githubUsername": tokenData.Payload.GithubUsername, + "date": tokenData.Payload.Date, + "completionTime": tokenData.Payload.Time, + "challenges": tokenData.Payload.Challenges, + }, + } +} +``` + +## Security Considerations + +1. **GitHub OAuth is the Primary Security**: The key security mechanism is that users must sign in with the same GitHub account specified in their token. Even if someone forges a token, they can only claim it for their own GitHub account. + +2. **Master Secret Storage**: The master secret (`L2C_CTF_MASTER_2024`) should be stored securely in your verification app (environment variable or secrets manager), but note that the real security comes from GitHub OAuth verification. + +3. **Case-Insensitive Username Matching**: GitHub usernames are case-insensitive, so always compare with `.toLowerCase()` / `.lower()`. + +4. **Timing Attacks**: Always use constant-time comparison functions when comparing signatures. + +5. **Token Expiration**: Consider adding expiration validation if tokens should only be valid for a certain period. + +6. **Rate Limiting**: Implement rate limiting on your verification endpoint. + +7. **HTTPS Only**: Always serve the verification API over HTTPS. + +## Token Lifecycle + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ VM Setup │ │ User │ │ Verification │ +│ │ │ │ │ App │ +└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + │ Generate INSTANCE_ID │ │ + │ Derive SECRET from │ │ + │ MASTER + INSTANCE_ID │ │ + │ │ │ + │ │ verify export "user" │ + │ │<──────────────────────│ + │ │ │ + │ Sign with SECRET │ │ + │ Include github_user │ │ + │ in token │ │ + │──────────────────────>│ │ + │ │ │ + │ │ Sign in with GitHub │ + │ │──────────────────────>│ + │ │ │ + │ │ Paste token │ + │ │──────────────────────>│ + │ │ │ + │ │ token.github_user │ + │ │ == oauth.login? │ + │ │ ✅ Verified! │ + │ │<──────────────────────│ + │ │ │ +``` + +## Updating the Master Secret + +If the master secret is compromised: + +1. Generate a new master secret +2. Update `ctf_setup.sh` with the new master secret +3. Update the verification app with the new master secret +4. **Note**: All previously issued tokens will become invalid + +## Questions? + +Open an issue in the [linux-ctfs repository](https://github.com/learntocloud/linux-ctfs). diff --git a/ctf_setup.sh b/ctf_setup.sh index 63f7d78..a1c4961 100644 --- a/ctf_setup.sh +++ b/ctf_setup.sh @@ -58,6 +58,15 @@ for i in {0..18}; do FLAG_HASHES[$i]=$(echo -n "${FLAGS[$i]}" | sha256sum | cut -d' ' -f1) done +# ============================================================================= +# VERIFICATION TOKEN SECRET +# ============================================================================= +# Generate a unique instance ID and derive a secret for this deployment +# The verification app uses the master secret + instance ID to derive the same secret +INSTANCE_ID=$(head -c 16 /dev/urandom | xxd -p) +MASTER_SECRET="L2C_CTF_MASTER_2024" +VERIFICATION_SECRET=$(echo -n "${MASTER_SECRET}:${INSTANCE_ID}" | sha256sum | cut -d' ' -f1) + # ============================================================================= # SYSTEM SETUP # ============================================================================= @@ -129,6 +138,11 @@ HASHEOF sudo mv /tmp/ctf_hashes /etc/ctf/flag_hashes sudo chmod 644 /etc/ctf/flag_hashes +# Store verification token secrets +echo "$INSTANCE_ID" | sudo tee /etc/ctf/instance_id > /dev/null +echo "$VERIFICATION_SECRET" | sudo tee /etc/ctf/verification_secret > /dev/null +sudo chmod 644 /etc/ctf/instance_id /etc/ctf/verification_secret + # Create verify script sudo tee /usr/local/bin/verify > /dev/null << 'EOFVERIFY' #!/bin/bash @@ -143,6 +157,10 @@ fi # Read hashes into array mapfile -t ANSWER_HASHES < "$HASH_FILE" +# Load verification token secrets +INSTANCE_ID=$(cat /etc/ctf/instance_id 2>/dev/null || echo "") +VERIFICATION_SECRET=$(cat /etc/ctf/verification_secret 2>/dev/null || echo "") + CHALLENGE_NAMES=( "Example Challenge" "Hidden File Discovery" @@ -295,13 +313,16 @@ export_certificate() { return 1 fi - # Now check for name argument + # Now check for GitHub username argument if [ -z "$1" ]; then - echo "Usage: verify export " - echo "Example: verify export John Doe" + echo "Usage: verify export " + echo "Example: verify export octocat" + echo "" + echo "⚠️ Use your GitHub username! This will be verified when you" + echo " submit your token at https://learntocloud.guide/phase2" return 1 fi - local custom_name="$1" + local github_username="$1" local completion_time="Unknown" if [ -f "$START_TIME_FILE" ]; then @@ -321,9 +342,9 @@ export_certificate() { echo " LEARN TO CLOUD - CTF COMPLETION CERTIFICATE " | lolcat echo "============================================================" | lolcat echo "" - echo " This certifies that" + echo " This certifies that GitHub user" echo "" - figlet -c "$custom_name" | lolcat + figlet -c "$github_username" | lolcat echo "" echo " has successfully completed all 18 Linux CTF challenges" echo "" @@ -351,9 +372,9 @@ export_certificate() { LEARN TO CLOUD - CTF COMPLETION CERTIFICATE ============================================================ - This certifies that + This certifies that GitHub user - $custom_name + $github_username has successfully completed all 18 Linux CTF challenges @@ -376,7 +397,44 @@ export_certificate() { ============================================================ CERTEOF echo "" - echo "Certificate exported to: $cert_file" + echo "Certificate saved to: $cert_file" + + # Generate signed verification token + local timestamp=$(date +%s) + local date_str=$(date +%Y-%m-%d) + + # Create JSON payload (includes github_username for verification app to match against OAuth) + local payload=$(cat << JSONEOF +{"github_username":"$github_username","date":"$date_str","time":"$completion_time","challenges":18,"timestamp":$timestamp,"instance_id":"$INSTANCE_ID"} +JSONEOF +) + + # Generate HMAC-SHA256 signature + local signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$VERIFICATION_SECRET" | cut -d' ' -f2) + + # Combine payload and signature, then base64 encode + local token_data=$(cat << TOKENEOF +{"payload":$payload,"signature":"$signature"} +TOKENEOF +) + local token=$(echo -n "$token_data" | base64 -w 0) + + echo "" + echo "============================================================" | lolcat + echo " 🎫 VERIFICATION TOKEN " | lolcat + echo "============================================================" | lolcat + echo "" + echo "To verify your completion:" + echo " 1. Go to https://learntocloud.guide/phase2" + echo " 2. Sign in with GitHub (as: $github_username)" + echo " 3. Paste the token below" + echo "" + echo "--- BEGIN L2C CTF TOKEN ---" + echo "$token" + echo "--- END L2C CTF TOKEN ---" + echo "" + echo "📋 Tip: Triple-click to select the entire token, then copy!" + echo "" } case "$1" in @@ -407,9 +465,10 @@ case "$1" in echo " verify list - List all challenges with status" echo " verify hint [n] - Show hint for challenge n" echo " verify time - Show elapsed time" - echo " verify export - Export completion certificate with your name" + echo " verify export - Export certificate with your GitHub username" echo echo "Example: verify 0 CTF{example}" + echo " verify export octocat" ;; esac EOFVERIFY diff --git a/verify_token.py b/verify_token.py new file mode 100644 index 0000000..b788ffc --- /dev/null +++ b/verify_token.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +CTF Token Verification Script + +This script mimics what the verification app at https://learntocloud.guide/phase2 +would do to verify a CTF completion token. +""" + +import base64 +import json +import hmac +import hashlib +from datetime import datetime + +# Master secret (same as in ctf_setup.sh) +MASTER_SECRET = "L2C_CTF_MASTER_2024" + + +def derive_secret(instance_id: str) -> str: + """Derive the verification secret from master secret and instance ID.""" + data = f"{MASTER_SECRET}:{instance_id}" + return hashlib.sha256(data.encode()).hexdigest() + + +def verify_token(token: str, oauth_github_username: str) -> dict: + """ + Verify a CTF completion token. + + Args: + token: The base64-encoded token from the user + oauth_github_username: The GitHub username from OAuth sign-in + + Returns: + dict with 'valid' (bool) and 'data' (payload) or 'error' (message) + """ + try: + # Step 1: Decode base64 + print(f"[1] Decoding base64 token...") + decoded = base64.b64decode(token).decode('utf-8') + token_data = json.loads(decoded) + print(f" ✓ Decoded successfully") + + payload = token_data.get('payload') + signature = token_data.get('signature') + + if not payload or not signature: + return {"valid": False, "error": "Invalid token structure"} + + print(f"\n[2] Parsed payload:") + print(f" - github_username: {payload.get('github_username')}") + print(f" - date: {payload.get('date')}") + print(f" - time: {payload.get('time')}") + print(f" - challenges: {payload.get('challenges')}") + print(f" - instance_id: {payload.get('instance_id')}") + + # Step 2: CRITICAL - Verify GitHub username matches OAuth user + print(f"\n[3] Verifying GitHub username...") + token_username = (payload.get('github_username') or '').lower() + oauth_username_lower = oauth_github_username.lower() + + if token_username != oauth_username_lower: + print(f" ✗ MISMATCH: Token has '{token_username}', OAuth user is '{oauth_username_lower}'") + return {"valid": False, "error": f"GitHub username mismatch. Token is for '{token_username}', but you signed in as '{oauth_github_username}'"} + print(f" ✓ Username match: {token_username}") + + # Step 3: Get instance ID and derive the secret + print(f"\n[4] Deriving verification secret...") + instance_id = payload.get('instance_id') + if not instance_id: + return {"valid": False, "error": "Missing instance ID"} + + verification_secret = derive_secret(instance_id) + print(f" ✓ Secret derived from MASTER_SECRET + instance_id") + + # Step 4: Recreate the payload string and compute signature + print(f"\n[5] Verifying signature...") + payload_str = json.dumps(payload, separators=(',', ':')) + + expected_sig = hmac.new( + verification_secret.encode(), + payload_str.encode(), + hashlib.sha256 + ).hexdigest() + + # Step 5: Constant-time comparison + if not hmac.compare_digest(signature, expected_sig): + print(f" ✗ Signature mismatch!") + print(f" Expected: {expected_sig}") + print(f" Got: {signature}") + return {"valid": False, "error": "Invalid signature"} + print(f" ✓ Signature valid!") + + # Step 6: Validate payload fields + print(f"\n[6] Validating payload...") + if payload.get('challenges') != 18: + return {"valid": False, "error": f"Incomplete challenges: {payload.get('challenges')}/18"} + print(f" ✓ All 18 challenges completed") + + # Step 7: Check timestamp is reasonable + timestamp = payload.get('timestamp', 0) + now = datetime.now().timestamp() + if timestamp > now + 3600: # Allow 1 hour clock skew + return {"valid": False, "error": "Invalid timestamp (in the future)"} + print(f" ✓ Timestamp valid") + + return { + "valid": True, + "data": { + "github_username": payload.get('github_username'), + "date": payload.get('date'), + "completion_time": payload.get('time'), + "challenges": payload.get('challenges') + } + } + + except Exception as e: + return {"valid": False, "error": f"Token parsing failed: {str(e)}"} + + +def main(): + print("=" * 60) + print(" Learn to Cloud - CTF Token Verification") + print("=" * 60) + print() + + # In the real app, this comes from GitHub OAuth + oauth_username = input("Enter GitHub username (simulating OAuth login): ").strip() + print() + + # Token from the user + token = input("Paste your verification token: ").strip() + print() + + print("-" * 60) + print(" VERIFICATION PROCESS") + print("-" * 60) + print() + + result = verify_token(token, oauth_username) + + print() + print("=" * 60) + print(" RESULT") + print("=" * 60) + print() + + if result["valid"]: + print("✅ VERIFICATION SUCCESSFUL!") + print() + print(f" GitHub User: {result['data']['github_username']}") + print(f" Completion Date: {result['data']['date']}") + print(f" Completion Time: {result['data']['completion_time']}") + print(f" Challenges: {result['data']['challenges']}/18") + print() + print(" 🎉 Congratulations on completing the Linux CTF!") + else: + print("❌ VERIFICATION FAILED!") + print() + print(f" Error: {result['error']}") + + print() + print("=" * 60) + + +if __name__ == "__main__": + main()