18 KiB
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
verifyparameter is passed to any requests calls (uses defaultverify=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
Lines: 352 lines total
Responsibility: Handles all server authentication and API communication
Playlist Management
File: 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
Lines: 235 lines total
Responsibility: Monitors connectivity using ping (not HTTPS), manages WiFi restarts
Main GUI Application
File: 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
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
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
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
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
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
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
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 -
def verify_auth(self, timeout: int = 10) - src/player_auth.py, Line 153 -
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:
# 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:
# 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:
verify=False # Disables SSL certificate verification
Example modification:
# 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):
- src/player_auth.py, Line 95 - authenticate() method
- src/player_auth.py, Line 157 - verify_auth() method
- src/player_auth.py, Line 178 - get_playlist() method
- src/player_auth.py, Line 227 - send_heartbeat() method
- src/player_auth.py, Line 254 - send_feedback() method
- src/get_playlists_v2.py, Line 159 - download_media_files() method
Option 2: Custom CA Certificate Bundle (✅ RECOMMENDED)
Production-Ready Approach
Step 1: Create certificate configuration
# 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
# 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
# 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
- Export certificate from self-signed server:
openssl s_client -connect server.local:443 -showcerts < /dev/null | \
openssl x509 -outform PEM > ca_bundle.crt
- Place in player config:
cp ca_bundle.crt /path/to/Kiwy-Signage/config/ca_bundle.crt
- Or set environment variable:
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt
Option 3: Certificate Pinning (⚠️ Advanced)
For maximum security when using self-signed certificates:
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:
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)
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:
-
Initial Authentication (HTTP/HTTPS)
- Endpoint:
POST /api/auth/player - Payload:
{hostname, password, quickconnect_code} - Response:
{auth_code, player_id, player_name, ...}
- Endpoint:
-
All Subsequent Requests (HTTP/HTTPS)
- Header:
Authorization: Bearer {auth_code} - Endpoints:
GET /api/playlists/{player_id}POST /api/players/{player_id}/heartbeatPOST /api/player-feedback
- Header:
-
Media Downloads (HTTP/HTTPS)
- Direct URLs from playlist:
{server_url}/uploads/...
- Direct URLs from playlist:
Server Configuration (config/app_config.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.pywith SSLConfig class - Support for custom CA bundle path
Step 2: Modify PlayerAuth (10-15 min)
- Add
verify_sslparameter to__init__ - Update all 5
requestscalls to includeverify=self.verify_ssl - Improve SSL error handling/reporting
Step 3: Update Configuration (5 min)
- Extend
config/app_config.jsonto include optionalca_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