# Kiwy-Signage Player - HTTPS/SSL Certificate Analysis ## Executive Summary The Kiwy-Signage player is a Python-based digital signage application built with Kivy that communicates with the DigiServer v2 backend. **The player currently has NO custom SSL certificate verification mechanism and relies entirely on Python's `requests` library default behavior.** This means: - ✅ HTTPS connections to production servers work because they have valid CA-signed certificates - ❌ Self-signed certificates or custom certificate authorities will **FAIL** without code modifications - ❌ No `verify` parameter is passed to any requests calls (uses default `verify=True`) - ❌ No support for custom CA certificates or certificate bundles --- ## 1. HTTP Client Library & Dependencies ### Library Used - **requests** (version 2.32.4) - Python HTTP library with SSL verification enabled by default - **aiohttp** (version 3.9.1) - Not currently used for player authentication/API calls ### Dependency Chain ``` requirements.txt: - kivy>=2.3.0 - ffpyplayer - requests==2.32.4 ← Used for ALL HTTPS requests - bcrypt==4.2.1 - aiohttp==3.9.1 - asyncio==3.4.3 ``` --- ## 2. Main Connection Files & Locations ### Core Authentication Module **File:** [src/player_auth.py](../../tmp/Kiwy-Signage/src/player_auth.py) **Lines:** 352 lines total **Responsibility:** Handles all server authentication and API communication ### Playlist Management **File:** [src/get_playlists_v2.py](../../tmp/Kiwy-Signage/src/get_playlists_v2.py) **Lines:** 352 lines total **Responsibility:** Fetches and manages playlists, uses PlayerAuth for communication ### Network Monitoring **File:** [src/network_monitor.py](../../tmp/Kiwy-Signage/src/network_monitor.py) **Lines:** 235 lines total **Responsibility:** Monitors connectivity using ping (not HTTPS), manages WiFi restarts ### Main GUI Application **File:** [src/main.py](../../tmp/Kiwy-Signage/src/main.py) **Lines:** 1,826 lines total **Responsibility:** Kivy GUI, server connection settings, calls PlayerAuth for authentication ### Configuration File **File:** [config/app_config.json](../../tmp/Kiwy-Signage/config/app_config.json) **Responsibility:** Stores server IP, port, player credentials, and settings --- ## 3. HTTPS Connection Architecture ### Authentication Flow ``` 1. Player Configuration (config/app_config.json) ├─ server_ip: "digi-signage.moto-adv.com" ├─ port: "443" ├─ screen_name: "player-name" └─ quickconnect_key: "QUICK123" 2. URL Construction (src/main.py, lines 696-703) ├─ If server_ip has http:// or https:// prefix, use as-is ├─ Otherwise: protocol = "https" if port == "443" else "http" └─ server_url = f"{protocol}://{server_ip}:{port}" 3. Authentication Request (src/player_auth.py, lines 95-98) ├─ POST /api/auth/player ├─ Payload: {hostname, password, quickconnect_code} └─ Returns: {auth_code, player_id, player_name, playlist_id, ...} 4. Authenticated API Calls (src/player_auth.py, lines 159-163, etc.) ├─ Headers: Authorization: Bearer {auth_code} └─ GET/POST to various /api/... endpoints ``` ### All HTTPS Request Points in Code #### 1. **Authentication** (src/player_auth.py) **Location:** [Line 95](../../tmp/Kiwy-Signage/src/player_auth.py#L95) ```python response = requests.post(auth_url, json=payload, timeout=timeout) ``` - **URL:** `{server_url}/api/auth/player` - **Method:** POST - **Auth:** None (initial auth) - **SSL Verify:** DEFAULT (True, no custom handling) **Location:** [Line 157](../../tmp/Kiwy-Signage/src/player_auth.py#L157) ```python response = requests.post(verify_url, json=payload, timeout=timeout) ``` - **URL:** `{server_url}/api/auth/verify` - **Method:** POST - **Auth:** None - **SSL Verify:** DEFAULT (True) #### 2. **Playlist Fetching** (src/player_auth.py) **Location:** [Line 178](../../tmp/Kiwy-Signage/src/player_auth.py#L178) ```python response = requests.get(playlist_url, headers=headers, timeout=timeout) ``` - **URL:** `{server_url}/api/playlists/{player_id}` - **Method:** GET - **Auth:** Bearer token in Authorization header - **Headers:** `Authorization: Bearer {auth_code}` - **SSL Verify:** DEFAULT (True, **NO `verify=` parameter**) #### 3. **Heartbeat/Status** (src/player_auth.py) **Location:** [Line 227](../../tmp/Kiwy-Signage/src/player_auth.py#L227) ```python response = requests.post(heartbeat_url, headers=headers, json=payload, timeout=timeout) ``` - **URL:** `{server_url}/api/players/{player_id}/heartbeat` - **Method:** POST - **Auth:** Bearer token - **SSL Verify:** DEFAULT (True, **NO `verify=` parameter**) #### 4. **Player Feedback** (src/player_auth.py) **Location:** [Line 254](../../tmp/Kiwy-Signage/src/player_auth.py#L254) ```python response = requests.post(feedback_url, headers=headers, json=payload, timeout=timeout) ``` - **URL:** `{server_url}/api/player-feedback` - **Method:** POST - **Auth:** Bearer token - **SSL Verify:** DEFAULT (True, **NO `verify=` parameter**) #### 5. **Media Download** (src/get_playlists_v2.py) **Location:** [Line 159](../../tmp/Kiwy-Signage/src/get_playlists_v2.py#L159) ```python response = requests.get(file_url, timeout=30) ``` - **URL:** Direct to media file URLs from playlist - **Method:** GET - **Auth:** None (public download URLs) - **SSL Verify:** DEFAULT (True, **NO `verify=` parameter**) --- ## 4. Certificate Verification Current Configuration ### Current SSL/Certificate Behavior **Summary:** Relies entirely on Python's `requests` library defaults. **Default requests behavior:** - `verify=True` (implicitly used when not specified) - Uses system CA certificate store - Validates certificate chain, hostname, and expiration - Rejects self-signed certificates with error ### Hardcoded Certificate Settings 🔴 **NONE** - No hardcoded SSL certificate settings exist in the codebase. ### Certificate Verification Code Locations **Search Results for "verify", "ssl", "cert", "certificate":** Only `verify_auth()` method found (authenticates with server, not certificate verification): - [src/player_auth.py, Line 137](../../tmp/Kiwy-Signage/src/player_auth.py#L137) - `def verify_auth(self, timeout: int = 10)` - [src/player_auth.py, Line 153](../../tmp/Kiwy-Signage/src/player_auth.py#L153) - `verify_url = f"{server_url}/api/auth/verify"` **No SSL/certificate configuration found in:** - ❌ requests library verify parameter - ❌ Custom CA bundle paths - ❌ SSL context configuration - ❌ Certificate pinning - ❌ urllib3 certificate settings --- ## 5. Self-Signed Certificate Support ### Current State: ❌ NOT SUPPORTED When connecting to a server with a self-signed certificate: ```python # Current code (player_auth.py, Line 95): response = requests.post(auth_url, json=payload, timeout=timeout) # Will raise: # requests.exceptions.SSLError: # ("certificate verify failed: self signed certificate (_ssl.c:...) ``` ### Exception Handling The code catches exceptions but doesn't differentiate SSL errors: ```python # player_auth.py, lines 111-127 except requests.exceptions.ConnectionError: error_msg = "Cannot connect to server" except requests.exceptions.Timeout: error_msg = "Connection timeout" except Exception as e: error_msg = f"Authentication error: {str(e)}" # Will catch SSL errors here but label them as generic "Authentication error" ``` --- ## 6. Required Changes for Self-Signed Certificate Support ### Option 1: Disable Certificate Verification (⚠️ INSECURE - Development Only) **Not Recommended for Production** Add to each `requests` call: ```python verify=False # Disables SSL certificate verification ``` **Example modification:** ```python # OLD (player_auth.py, Line 95): response = requests.post(auth_url, json=payload, timeout=timeout) # NEW: response = requests.post(auth_url, json=payload, timeout=timeout, verify=False) ``` **Locations requiring modification (5 places):** 1. [src/player_auth.py, Line 95](../../tmp/Kiwy-Signage/src/player_auth.py#L95) - authenticate() method 2. [src/player_auth.py, Line 157](../../tmp/Kiwy-Signage/src/player_auth.py#L157) - verify_auth() method 3. [src/player_auth.py, Line 178](../../tmp/Kiwy-Signage/src/player_auth.py#L178) - get_playlist() method 4. [src/player_auth.py, Line 227](../../tmp/Kiwy-Signage/src/player_auth.py#L227) - send_heartbeat() method 5. [src/player_auth.py, Line 254](../../tmp/Kiwy-Signage/src/player_auth.py#L254) - send_feedback() method 6. [src/get_playlists_v2.py, Line 159](../../tmp/Kiwy-Signage/src/get_playlists_v2.py#L159) - download_media_files() method --- ### Option 2: Custom CA Certificate Bundle (✅ RECOMMENDED) **Production-Ready Approach** #### Step 1: Create certificate configuration ```python # New file: src/ssl_config.py import os import requests class SSLConfig: """Manage SSL certificate verification for self-signed certs""" @staticmethod def get_ca_bundle(): """Get path to CA certificate bundle Returns: str: Path to CA bundle or True for default system certs """ # Priority order: # 1. Custom CA bundle in config directory # 2. CA bundle path from environment variable # 3. System default CA bundle (requests uses certifi) custom_ca = 'config/ca_bundle.crt' if os.path.exists(custom_ca): return custom_ca env_ca = os.environ.get('REQUESTS_CA_BUNDLE') if env_ca and os.path.exists(env_ca): return env_ca return True # Use system/certifi default @staticmethod def get_verify_setting(): """Get SSL verification setting Returns: bool or str: Path to CA bundle or True/False """ return SSLConfig.get_ca_bundle() ``` #### Step 2: Modify PlayerAuth to use custom certificates ```python # player_auth.py modifications: from ssl_config import SSLConfig # Add import class PlayerAuth: def __init__(self, config_file='player_auth.json'): self.config_file = config_file self.auth_data = self._load_auth_data() self.verify_ssl = SSLConfig.get_verify_setting() # Add this def authenticate(self, ...): # Add verify parameter to requests call: response = requests.post( auth_url, json=payload, timeout=timeout, verify=self.verify_ssl # ADD THIS ) def verify_auth(self, ...): response = requests.post( verify_url, json=payload, timeout=timeout, verify=self.verify_ssl # ADD THIS ) def get_playlist(self, ...): response = requests.get( playlist_url, headers=headers, timeout=timeout, verify=self.verify_ssl # ADD THIS ) def send_heartbeat(self, ...): response = requests.post( heartbeat_url, headers=headers, json=payload, timeout=timeout, verify=self.verify_ssl # ADD THIS ) def send_feedback(self, ...): response = requests.post( feedback_url, headers=headers, json=payload, timeout=timeout, verify=self.verify_ssl # ADD THIS ) ``` #### Step 3: Handle media downloads ```python # get_playlists_v2.py modifications: from ssl_config import SSLConfig def download_media_files(playlist, media_dir): verify_ssl = SSLConfig.get_verify_setting() # Add this for media in playlist: ... response = requests.get( file_url, timeout=30, verify=verify_ssl # ADD THIS ) ... ``` #### Step 4: Prepare CA certificate 1. **Export certificate from self-signed server:** ```bash openssl s_client -connect server.local:443 -showcerts < /dev/null | \ openssl x509 -outform PEM > ca_bundle.crt ``` 2. **Place in player config:** ```bash cp ca_bundle.crt /path/to/Kiwy-Signage/config/ca_bundle.crt ``` 3. **Or set environment variable:** ```bash export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt ``` --- ### Option 3: Certificate Pinning (⚠️ Advanced) For maximum security when using self-signed certificates: ```python import ssl import certifi import requests from requests.adapters import HTTPAdapter from urllib3.util.ssl_ import create_urllib3_context class SSLPinningAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): ctx = create_urllib3_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE # Or use specific certificate: # ctx.load_verify_locations('config/server_cert.pem') kwargs['ssl_context'] = ctx return super().init_poolmanager(*args, **kwargs) # Usage in PlayerAuth: session = requests.Session() session.mount('https://', SSLPinningAdapter()) response = session.post(auth_url, json=payload, timeout=timeout) ``` --- ## 7. Testing Self-Signed Certificate Connections ### Before Modification (Current Behavior) Test connection to self-signed server: ```bash cd /tmp/Kiwy-Signage python3 -c " import requests url = 'https://your-self-signed-server:443/api/health' try: response = requests.get(url) print('Connection successful') except requests.exceptions.SSLError as e: print(f'SSL Error: {e}') " # Output: SSL Error: certificate verify failed ``` ### After Modification (With Custom CA) ```bash cd /tmp/Kiwy-Signage # Place ca_bundle.crt in config/ python3 -c " import requests url = 'https://your-self-signed-server:443/api/health' response = requests.get(url, verify='config/ca_bundle.crt') print(f'Connection successful: {response.status_code}') " # Output: Connection successful: 200 ``` --- ## 8. Summary Table | Aspect | Current State | Support Level | |--------|---------------|----------------| | **HTTP Client** | requests 2.32.4 | ✅ Production-ready | | **HTTPS Support** | Yes (standard URLs) | ✅ Full | | **Self-Signed Certs** | ❌ NO | ❌ NOT SUPPORTED | | **Custom CA Bundle** | ❌ NO | ❌ NOT SUPPORTED | | **Certificate Pinning** | ❌ NO | ❌ NOT SUPPORTED | | **SSL Verify Parameter** | Default (True) | ⚠️ All requests use default | | **Hardcoded Settings** | None | - | | **Environment Variables** | Not checked | ⚠️ Could be added | | **Configuration File** | app_config.json (no SSL options) | ⚠️ Could be extended | --- ## 9. Integration with DigiServer v2 ### Current Communication Protocol The player communicates with DigiServer v2 using: 1. **Initial Authentication (HTTP/HTTPS)** - Endpoint: `POST /api/auth/player` - Payload: `{hostname, password, quickconnect_code}` - Response: `{auth_code, player_id, player_name, ...}` 2. **All Subsequent Requests (HTTP/HTTPS)** - Header: `Authorization: Bearer {auth_code}` - Endpoints: - `GET /api/playlists/{player_id}` - `POST /api/players/{player_id}/heartbeat` - `POST /api/player-feedback` 3. **Media Downloads (HTTP/HTTPS)** - Direct URLs from playlist: `{server_url}/uploads/...` ### Server Configuration (config/app_config.json) ```json { "server_ip": "digi-signage.moto-adv.com", "port": "443", "screen_name": "tv-terasa", "quickconnect_key": "8887779", "orientation": "Landscape", "touch": "True", "max_resolution": "1920x1080", "edit_feature_enabled": true } ``` ### ⚠️ NOTE: No SSL/certificate options in config The application accepts server_ip, port, hostname, and credentials, but: - ❌ No way to specify CA certificate path - ❌ No way to disable SSL verification - ❌ No way to enable certificate pinning --- ## 10. Recommended Implementation Plan ### For Self-Signed Certificate Support: **Step 1: Add SSL Configuration Module** (5-10 min) - Create `src/ssl_config.py` with SSLConfig class - Support for custom CA bundle path **Step 2: Modify PlayerAuth** (10-15 min) - Add `verify_ssl` parameter to `__init__` - Update all 5 `requests` calls to include `verify=self.verify_ssl` - Improve SSL error handling/reporting **Step 3: Update Configuration** (5 min) - Extend `config/app_config.json` to include optional `ca_bundle_path` - Or use environment variable `REQUESTS_CA_BUNDLE` **Step 4: Documentation** (5 min) - Add README section on SSL certificate configuration - Document how to export and place CA certificates **Step 5: Testing** (10-15 min) - Test with self-signed certificate - Verify backward compatibility with valid CA certs **Total Time Estimate:** 35-50 minutes for complete implementation --- ## 11. Code References ### All requests calls in codebase: ``` src/player_auth.py: Line 95: requests.post(auth_url, json=payload, timeout=timeout) Line 157: requests.post(verify_url, json=payload, timeout=timeout) Line 178: requests.get(playlist_url, headers=headers, timeout=timeout) Line 227: requests.post(heartbeat_url, headers=headers, json=payload, timeout=timeout) Line 254: requests.post(feedback_url, headers=headers, json=payload, timeout=timeout) src/get_playlists_v2.py: Line 159: requests.get(file_url, timeout=30) working_files/test_direct_api.py: Line 32: requests.get(url, headers=headers, timeout=10) working_files/get_playlists.py: Line 101: requests.post(feedback_url, json=feedback_data, timeout=10) Line 131: requests.get(server_url, params=params) Line 139: requests.get(file_url, timeout=10) ``` All calls use default `verify=True` (implicit). --- ## Conclusion The Kiwy-Signage player is a well-structured Python application that properly uses the `requests` library for HTTPS communication. However, it currently **does not support self-signed certificates or custom certificate authorities** without code modifications. To support self-signed certificates, implementing Option 2 (Custom CA Certificate Bundle) is recommended as it: - ✅ Maintains security for production deployments - ✅ Allows flexibility for self-signed/internal CAs - ✅ Requires minimal code changes (5-6 request calls) - ✅ Follows Python best practices - ✅ Is backward compatible with existing deployments