Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pip-install = "==1.3.5"
aiohttp = "==3.8.5"
openai = "==1.75.0"
google-generativeai = "==0.8.5"
python-gnupg = "==0.5.3"

[requires]
python_version = "3.11.12"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The official [VLDC](https://vldc.org) telegram group bot.
* πŸ‘ smell like PRISM? nononono!
* πŸ’° kozula Don't argue with kozula rate!
* 🀫 buktopuha Let's play a game 🀑
* 🐦 chirp – warrant canary with PGP signature ([docs](WARRANT_CANARY.md))

### Modes
* 😼 smile mode – allow only stickers in the chat
Expand Down
163 changes: 163 additions & 0 deletions WARRANT_CANARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Warrant Canary - Chirp Command

## Overview

The warrant canary feature allows users to verify that the bot is operating normally and without interference. When a user sends the `/chirp` command, the bot responds with "meow" signed with its PGP key.

## How It Works

The warrant canary is implemented as follows:

1. **User sends `/chirp` command** - Anyone can verify the bot is operational
2. **Bot responds with signed "meow"** - The message is cryptographically signed with the bot's private PGP key
3. **Users can verify the signature** - Using the bot's public key, users can verify the response is authentic

If the bot doesn't respond or responds without a valid signature, it may indicate:
- The bot is down
- The bot has been compromised
- The bot's GPG keys have been lost or tampered with

## Setup

### 1. Install Dependencies

The warrant canary requires the `python-gnupg` package, which is already included in `Pipfile`:

```bash
pipenv install
```

### 2. Install GPG

The system needs GPG installed:

```bash
# On Ubuntu/Debian (used in dev Docker)
apt-get install gnupg

# On Alpine (for Docker)
apk add gnupg
```

**Docker Setup**: Add the following line to your Dockerfile after the apt-get update:
```dockerfile
RUN apt-get -y update && apt-get install -y ffmpeg build-essential gnupg
```

### 3. Generate GPG Key

Run the key generation script to create a GPG key pair for the bot:

```bash
# Inside the container or environment
cd /app/bot
python generate_gpg_key.py
```

Or with a custom GPG home directory:

```bash
python generate_gpg_key.py --gpg-home /path/to/.gnupg
```

This will:
- Generate a 2048-bit RSA key pair
- Store the keys in `/app/.gnupg` (or specified directory)
- Export the public key to `nyan_bot_public.asc`
- Display the public key for sharing

### 4. Share Public Key

After generating the key, share the public key with your users so they can verify signed messages:

```bash
cat /app/.gnupg/nyan_bot_public.asc
```

Users can import this key with:

```bash
gpg --import nyan_bot_public.asc
```

## Usage

### For Users

To check if the bot is operational:

```
/chirp
```

Expected response:
```
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

meow
-----BEGIN PGP SIGNATURE-----
[signature data]
-----END PGP SIGNATURE-----
```

### Verifying the Signature

1. Copy the signed message
2. Save it to a file (e.g., `response.txt`)
3. Verify with GPG:

```bash
gpg --verify response.txt
```

You should see output indicating the signature is valid:
```
gpg: Good signature from "VLDC Nyan Bot <[email protected]>"
```

## Troubleshooting

### Bot responds with "meow" without signature

This means:
- GPG keys haven't been generated yet
- The `python-gnupg` package is not installed
- The GPG home directory is not accessible

### No response to `/chirp`

This means the bot is down or not receiving messages.

### Invalid signature

This may indicate:
- The bot has been compromised
- The keys have been replaced
- There's a bug in the signing implementation

## Security Considerations

1. **Private Key Security**: The bot's private key is stored without a passphrase to allow automated signing. Ensure the key directory (`/app/.gnupg`) has appropriate permissions (700).

2. **Key Rotation**: Consider rotating the GPG key periodically and announcing the new public key to users.

3. **Regular Testing**: Users should regularly test the `/chirp` command to ensure it's working as expected.

4. **Public Key Distribution**: Distribute the public key through multiple trusted channels (website, GitHub, etc.) to prevent MITM attacks.

## Implementation Details

- **Skill**: `bot/skills/chirp.py`
- **Key Generation**: `bot/generate_gpg_key.py`
- **Tests**: `bot/tests/chirp_test.py`
- **Key Storage**: `/app/.gnupg` (default)
- **Key Type**: RSA 2048-bit
- **Key Usage**: Signing only
- **Signature Format**: Clear-signed ASCII-armored

## References

- [Warrant Canary on Wikipedia](https://en.wikipedia.org/wiki/Warrant_canary)
- [GnuPG Documentation](https://gnupg.org/documentation/)
- [python-gnupg Documentation](https://gnupg.readthedocs.io/)
106 changes: 106 additions & 0 deletions bot/generate_gpg_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Generate GPG key for Nyan bot warrant canary.

This script generates a PGP key pair for the bot to sign warrant canary messages.
Run this script once during bot setup.
"""

import sys
from pathlib import Path

try:
import gnupg
except ImportError:
print("Error: python-gnupg is not installed")
print("Install it with: pip install python-gnupg")
sys.exit(1)


def generate_bot_key(gpg_home: str = "/app/.gnupg"):
"""
Generate a GPG key for the Nyan bot.

Args:
gpg_home: Directory to store GPG keys (default: /app/.gnupg)
"""
# Create GPG home directory if it doesn't exist
gpg_home_path = Path(gpg_home)
gpg_home_path.mkdir(parents=True, exist_ok=True, mode=0o700)

# Create gpg.conf for batch mode
gpg_conf = gpg_home_path / "gpg.conf"
with open(gpg_conf, "w", encoding="utf-8") as f:
f.write("pinentry-mode loopback\n")

# Initialize GPG
gpg = gnupg.GPG(gnupghome=str(gpg_home_path))
gpg.encoding = "utf-8"

# Check if key already exists
keys = gpg.list_keys()
if keys:
print("GPG key already exists:")
for key in keys:
print(f" Key ID: {key['keyid']}")
print(f" UID: {key['uids']}")
print(f" Fingerprint: {key['fingerprint']}")
return

# Generate key using batch mode (more reliable for automation)
print("Generating GPG key for Nyan bot...")
batch_input = """
%echo Generating key for VLDC Nyan Bot
Key-Type: RSA
Key-Length: 2048
Key-Usage: sign
Name-Real: VLDC Nyan Bot
Name-Email: [email protected]
Expire-Date: 0
%no-protection
%commit
%echo Done
"""

key = gpg.gen_key(batch_input)

if key:
print("\nβœ… GPG key generated successfully!")
print(f"Key ID: {key}")

# Export public key for verification
public_key = gpg.export_keys(str(key))
if public_key:
public_key_file = gpg_home_path / "nyan_bot_public.asc"
with open(public_key_file, "w", encoding="utf-8") as f:
f.write(public_key)
print(f"\nπŸ“„ Public key exported to: {public_key_file}")
print("\nPublic key (for verification):")
print("=" * 80)
print(public_key)
print("=" * 80)
print("\nShare this public key so users can verify signed messages!")
else:
print("❌ Failed to generate GPG key")
sys.exit(1)


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
description="Generate GPG key for Nyan bot warrant canary"
)
parser.add_argument(
"--gpg-home",
default="/app/.gnupg",
help="Directory to store GPG keys (default: /app/.gnupg)",
)

args = parser.parse_args()

try:
generate_bot_key(args.gpg_home)
except Exception as exc: # pylint: disable=broad-exception-caught
print(f"❌ Error: {exc}")
sys.exit(1)
3 changes: 3 additions & 0 deletions bot/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from skills.uwu import add_uwu
from skills.buktopuha import add_buktopuha
from skills.chat import add_chat_mode
from skills.chirp import add_chirp

logger = logging.getLogger(__name__)
VERSION = "0.10.0"
Expand Down Expand Up @@ -92,6 +93,7 @@ def _make_skill(add_handlers: Callable, name: str, hint: str) -> Dict:
_make_skill(add_kozula, "πŸ’° kozula", " Don't argue with kozula rate!"),
_make_skill(add_length, "πŸ† length", " length of your instrument"),
_make_skill(add_buktopuha, "🀫 start BukToPuHa", " let's play a game"),
_make_skill(add_chirp, "🐦 chirp", " warrant canary - meow!"),
# modes
_make_skill(add_trusted_mode, "πŸ‘β€πŸ—¨ in god we trust", " are you worthy hah?"),
_make_skill(add_aoc_mode, "πŸŽ„ AOC notifier", " kekV"),
Expand Down Expand Up @@ -127,6 +129,7 @@ def _make_skill(add_handlers: Callable, name: str, hint: str) -> Dict:
("longest", "size doesn't matter, or is it?"),
("buktopuha", "let's play a game 🀑"),
("znatoki", "top BuKToPuHa players"),
("chirp", "🐦 warrant canary - meow!"),
]


Expand Down
Loading