-
Notifications
You must be signed in to change notification settings - Fork 55
docs: Add replay protection example (#9) #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,146 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| """Beacon Replay Protection Example. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| This example demonstrates how to protect Beacon message handlers from | ||||||||||||||||||||||||||||||||||||||||||||
| replay attacks by validating timestamp freshness and checking nonce | ||||||||||||||||||||||||||||||||||||||||||||
| uniqueness. It covers: | ||||||||||||||||||||||||||||||||||||||||||||
| 1. Timestamp TTL (Time-To-Live) window validation | ||||||||||||||||||||||||||||||||||||||||||||
| 2. In-memory duplicate nonce caching | ||||||||||||||||||||||||||||||||||||||||||||
| 3. Simulating replayed payload failures | ||||||||||||||||||||||||||||||||||||||||||||
| 4. Test vectors | ||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| from beacon_skill.identity import AgentIdentity | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Configuration for replay protection | ||||||||||||||||||||||||||||||||||||||||||||
| TIMESTAMP_TTL_SECONDS = 300 # 5 minutes | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # In production this could be Redis or Memcached | ||||||||||||||||||||||||||||||||||||||||||||
| class NonceCache: | ||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| # Maps `agent_id:nonce` to `timestamp` | ||||||||||||||||||||||||||||||||||||||||||||
| self._seen_nonces: Dict[str, int] = {} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def check_and_add(self, agent_id: str, nonce: str, current_time: int) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||
| """Returns True if successful, False if nonce was already seen.""" | ||||||||||||||||||||||||||||||||||||||||||||
| key = f"{agent_id}:{nonce}" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Cleanup expired nonces implicitly on check | ||||||||||||||||||||||||||||||||||||||||||||
| self._cleanup(current_time) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if key in self._seen_nonces: | ||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| self._seen_nonces[key] = current_time | ||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def _cleanup(self, current_time: int): | ||||||||||||||||||||||||||||||||||||||||||||
| """Remove nonces older than TTL window""" | ||||||||||||||||||||||||||||||||||||||||||||
| expired = [k for k, ts in self._seen_nonces.items() | ||||||||||||||||||||||||||||||||||||||||||||
| if current_time - ts > TIMESTAMP_TTL_SECONDS] | ||||||||||||||||||||||||||||||||||||||||||||
| for k in expired: | ||||||||||||||||||||||||||||||||||||||||||||
| del self._seen_nonces[k] | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| class SecureMessageHandler: | ||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| self.nonce_cache = NonceCache() | ||||||||||||||||||||||||||||||||||||||||||||
| # Holds verified payloads | ||||||||||||||||||||||||||||||||||||||||||||
| self.processed = [] | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def handle_message(self, pubkey_hex: str, raw_payload: bytes, signature_hex: str) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||
| """Validate an incoming message for authenticity and replay resilience.""" | ||||||||||||||||||||||||||||||||||||||||||||
| # 1. Authenticate signature | ||||||||||||||||||||||||||||||||||||||||||||
| if not AgentIdentity.verify(pubkey_hex, signature_hex, raw_payload): | ||||||||||||||||||||||||||||||||||||||||||||
| return {"error": "Invalid signature", "status": 401} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Parse payload | ||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||
| payload = json.loads(raw_payload.decode("utf-8")) | ||||||||||||||||||||||||||||||||||||||||||||
| except json.JSONDecodeError: | ||||||||||||||||||||||||||||||||||||||||||||
| return {"error": "Invalid JSON format", "status": 400} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # 2. Check for required replay protection fields | ||||||||||||||||||||||||||||||||||||||||||||
| ts = payload.get("ts") | ||||||||||||||||||||||||||||||||||||||||||||
| nonce = payload.get("nonce") | ||||||||||||||||||||||||||||||||||||||||||||
| sender = payload.get("agent_id") | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+68
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if not ts or not nonce or not sender: | ||||||||||||||||||||||||||||||||||||||||||||
| return {"error": "Missing replay protection fields (ts, nonce, agent_id)", "status": 400} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # 3. Validate Timestamp (TTL validation) | ||||||||||||||||||||||||||||||||||||||||||||
| now = int(time.time()) | ||||||||||||||||||||||||||||||||||||||||||||
| age = now - ts | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+69
to
+75
|
||||||||||||||||||||||||||||||||||||||||||||
| if not ts or not nonce or not sender: | |
| return {"error": "Missing replay protection fields (ts, nonce, agent_id)", "status": 400} | |
| # 3. Validate Timestamp (TTL validation) | |
| now = int(time.time()) | |
| age = now - ts | |
| # Explicitly check for missing fields; allow values like 0 for ts | |
| if ts is None or nonce is None or sender is None: | |
| return {"error": "Missing replay protection fields (ts, nonce, agent_id)", "status": 400} | |
| # Coerce timestamp to int and validate type | |
| try: | |
| ts_int = int(ts) | |
| except (TypeError, ValueError): | |
| return {"error": "Invalid ts field; must be integer Unix timestamp", "status": 400} | |
| # 3. Validate Timestamp (TTL validation) | |
| now = int(time.time()) | |
| age = now - ts_int |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example trusts agent_id from the signed payload for nonce-scoping. An attacker can sign with a valid key but vary agent_id to bypass duplicate-nonce detection (and/or poison the cache). Consider deriving agent_id from pubkey_hex (via agent_id_from_pubkey) and rejecting the message if the payload’s agent_id doesn’t match the derived value; then use the derived ID for the cache key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other scripts in
examples/include a#!/usr/bin/env python3shebang at the top; adding it here would keep the examples consistent and makes the file directly executable on Unix-like systems.