Initial commit for SSL Management project

This commit includes:
- SSL Manager implementation for certificate operations
- Configuration file with UniFi device parameters
- Test files for various components
- Documentation for UniFi verification
- Project guidelines
This commit is contained in:
Mike Geppert 2025-07-20 20:46:42 -05:00
parent 27329d3afa
commit a78cf961ff
12 changed files with 1374 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# IDE-specific files
.idea/
# Generated certificates
custom-certs/
# Python virtual environment
.venv/
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Local development settings
.env

266
.junie/guidelines.md Normal file
View File

@ -0,0 +1,266 @@
# SSL Management Project Guidelines
This document provides guidelines for developing and maintaining the SSL Management project, which is a Python-based tool for managing SSL certificates for UniFi devices such as UDM-SE. The tool helps automate the process of obtaining, validating, and deploying SSL certificates to UniFi devices.
## Build/Configuration Instructions
### Prerequisites
1. **Python 3.6+**: The project is written in Python and requires Python 3.6 or higher.
2. **OpenSSL**: Required for certificate operations. Must be installed and available in your PATH.
3. **Virtual Environment**: The project uses a virtual environment for dependency isolation.
4. **Linux Environment**: The script is designed to run on Linux systems such as Ubuntu 24.04. While development can occur on any platform, deployment is expected on a Linux server.
### Setup
1. Clone the repository:
```bash
git clone <repository-url>
cd ssl-managment
```
2. Create and activate a virtual environment:
```bash
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
3. No external Python packages are required as the project uses standard library modules.
### Configuration
The SSL Manager uses a configuration file (`config.json`) to store default settings. The configuration file should be placed in the project root directory. Here's an example of the configuration file:
```json
{
"cert_dir": "~/.ssl-certs",
"default_port": 443,
"connection_timeout": 3.0,
"default_validity_days": 365,
"key_size": 2048,
"unifi": {
"host": "unifi.example.com",
"username": "admin",
"password": "password",
"site": "default",
"ssh_port": 22,
"ssh_username": "root",
"ssh_password": "",
"ssh_key_path": "~/.ssh/id_rsa"
}
}
```
Configuration options:
- `cert_dir`: Directory where certificates and keys will be stored
- `default_port`: Default port to use when checking certificate expiration
- `connection_timeout`: Timeout in seconds for SSL connections
- `default_validity_days`: Default validity period in days for generated certificates
- `key_size`: Key size in bits for generated certificates
- `unifi`: UniFi device connection parameters
- `host`: Hostname or IP address of the UniFi device
- `username`: Username for authenticating with the UniFi device
- `password`: Password for authenticating with the UniFi device
- `site`: Site name for the UniFi device (default: 'default')
- `ssh_port`: SSH port for the UniFi device (default: 22)
- `ssh_username`: Username for SSH authentication with the UniFi device
- `ssh_password`: Password for SSH authentication (leave empty to use SSH key)
- `ssh_key_path`: Path to the SSH private key file for authentication
### Usage
The SSL Manager provides three main commands. All commands support the following global options:
- `--config`: Path to the config file (default: config.json)
- `--cert-dir`: Directory to store certificates (overrides config)
1. **Check Certificate Expiration**:
```bash
python src/ssl_manager.py check example.com [--port PORT]
```
The `--port` option overrides the `default_port` from the config file.
2. **Generate Self-Signed Certificate**:
```bash
python src/ssl_manager.py generate example.com [--days DAYS]
```
The `--days` option overrides the `default_validity_days` from the config file.
3. **Validate Certificate Chain**:
```bash
python src/ssl_manager.py validate path/to/certificate.crt [--ca-path path/to/ca.crt]
```
## Testing Information
### Running Tests
Tests are written using the Python `unittest` framework. To run all tests:
```bash
cd ssl-managment # Ensure you're in the project root
python -m unittest discover tests
```
To run a specific test file:
```bash
python -m tests.test_ssl_manager
```
### Adding New Tests
1. Create a new test file in the `tests` directory, following the naming convention `test_*.py`.
2. Import the necessary modules and the `SSLManager` class:
```python
import os
import sys
import unittest
from unittest.mock import patch
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from ssl_manager import SSLManager
```
3. Create a test class that inherits from `unittest.TestCase`:
```python
class TestYourFeature(unittest.TestCase):
def setUp(self):
# Set up test fixtures
self.ssl_manager = SSLManager()
def test_your_feature(self):
# Test your feature
result = self.ssl_manager.your_method()
self.assertEqual(result, expected_value)
```
4. Use mocking to avoid actual network or system calls:
```python
@patch('ssl_manager.subprocess.run')
def test_with_mock(self, mock_run):
mock_run.return_value = MagicMock(returncode=0)
# Test with mock
```
### Test Example
Here's a simple test that verifies the SSLManager initializes correctly:
```python
def test_init_creates_cert_dir(self):
"""Test that the constructor creates the certificate directory."""
test_path = os.path.expanduser("~/test-ssl-certs")
# Remove the directory if it exists
if os.path.exists(test_path):
os.rmdir(test_path)
# Create a new SSLManager with the test path
ssl_manager = SSLManager(cert_dir=test_path)
# Verify the directory was created
self.assertTrue(os.path.exists(test_path))
self.assertTrue(os.path.isdir(test_path))
# Clean up
os.rmdir(test_path)
```
## Development Guidelines
### Code Style
1. **PEP 8**: Follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code.
2. **Docstrings**: Use docstrings for all modules, classes, and functions. Follow the [Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
3. **Type Hints**: Use type hints for function parameters and return values.
### Project Structure
- `src/`: Contains the source code
- `ssl_manager.py`: Main module with the SSLManager class
- `tests/`: Contains test files
- `test_ssl_manager.py`: Tests for the SSLManager class
### Best Practices
1. **Error Handling**: Use try-except blocks to handle exceptions gracefully.
2. **Logging**: Use the Python logging module for logging instead of print statements.
3. **Testing**: Write tests for all new features and bug fixes.
4. **Documentation**: Update documentation when making changes to the code.
## Deployment and Automation
### Cross-Machine Deployment
The SSL Management tool can be developed on one machine and deployed to another server for production use. When deploying across machines:
1. Ensure the target server meets all the prerequisites (Python 3.6+, OpenSSL, etc.)
2. Transfer the entire project directory to the target server
3. Set up the virtual environment on the target server
4. Configure the `config.json` file with the appropriate settings for the target environment
5. Test the deployment by running a simple command like `python src/ssl_manager.py check <hostname>`
### Automated Certificate Updates
To automate certificate updates using cron:
1. Create a shell script wrapper for the SSL Manager:
```bash
#!/bin/bash
# File: update_certificates.sh
# Change to the project directory
cd /path/to/ssl-managment
# Activate the virtual environment
source .venv/bin/activate
# Run the SSL Manager to update certificates
python src/ssl_manager.py generate your-unifi-device.example.com --days 90
# Additional commands to deploy the certificate to the UniFi device can be added here
```
2. Make the script executable:
```bash
chmod +x update_certificates.sh
```
3. Add a cron job to run the script periodically (e.g., every 60 days):
```bash
# Edit the crontab
crontab -e
# Add a line like this to run at 2:30 AM on the 1st of every other month
30 2 1 */2 * /path/to/update_certificates.sh >> /var/log/certificate-updates.log 2>&1
```
4. Verify the cron job is set up correctly:
```bash
crontab -l
```
## Troubleshooting
### Common Issues
1. **OpenSSL Command Not Found**
Ensure OpenSSL is installed and in your PATH.
2. **Permission Denied When Creating Certificates**
Check that you have write permissions to the certificate directory.
3. **Import Errors in Tests**
Make sure you're running tests from the project root directory.
### Debugging
- Set the `SSL_DEBUG=1` environment variable for verbose output
- Check the OpenSSL version with `openssl version`
- Verify certificate paths are correct and accessible

View File

@ -0,0 +1,68 @@
# UniFi Configuration Verification
## Summary
We attempted to verify the UniFi device connection parameters in `config.json` by creating a test script that uses the `unifiControl` Python library to connect to the UniFi device. The test was able to establish a connection to the device, but authentication failed with a 401 Unauthorized error.
## What We Did
1. **Installed the unifiControl Python library**:
```bash
pip install unifiControl
```
2. **Created a test script** (`tests/test_unifi_connection.py`) that:
- Loads the UniFi connection parameters from config.json
- Attempts to connect to the UniFi device using these parameters
- Disables SSL certificate verification to handle self-signed certificates
- Provides detailed error information for troubleshooting
3. **Ran the test** and analyzed the results:
- The connection to the UniFi device was established successfully
- The SSL certificate verification was successfully disabled
- The authentication attempt failed with a 401 Unauthorized error
## Findings
The UniFi device at `udm-se.mgeppert.com` is reachable and responding to HTTPS requests on port 443, but the authentication with the provided credentials failed. This suggests that the credentials in the config.json file may be incorrect, or there may be other issues with the authentication process.
## Recommendations
1. **Verify the credentials** in config.json:
```json
{
"unifi": {
"host": "udm-se.mgeppert.com",
"username": "SSLCertificate",
"password": "cYu2E1OWt0XseVf9j5ML"
}
}
```
2. **Check the UniFi device configuration**:
- Verify that the "SSLCertificate" account exists and is enabled
- Check if the account has the necessary permissions
- Look for any authentication failure messages in the device logs
3. **Try a different authentication method**:
- The UniFi device may require a different authentication method or API
- Consider using the official UniFi API or a different library
## Next Steps
1. Update the credentials in config.json if they are incorrect
2. If the credentials are correct, check the UniFi device's configuration and logs
3. Consider using a different authentication method or API library
## Detailed Test Results
For detailed test results and troubleshooting recommendations, see the [UniFi Connection Test Results](unifi_connection_test_results.md) document.
## Test Script
The test script (`tests/test_unifi_connection.py`) can be run again after updating the credentials in config.json:
```bash
cd /path/to/ssl-managment
python -m tests.test_unifi_connection
```

33
config.json Normal file
View File

@ -0,0 +1,33 @@
{
"cert_dir": "~/.ssl-certs",
"default_port": 443,
"connection_timeout": 3.0,
"default_validity_days": 365,
"key_size": 2048,
"unifi": {
"host": "udm-se.mgeppert.com",
"username": "SSLCertificate",
"password": "cYu2E1OWt0XseVf9j5ML",
"site": "default",
"ssh_port": 22,
"ssh_username": "root",
"ssh_password": "RH6X64FAAiE7CrcV84lQ",
"ssh_key_path": "~/.ssh/id_rsa"
},
"comments": {
"cert_dir": "Directory where certificates and keys will be stored",
"default_port": "Default port to use when checking certificate expiration",
"connection_timeout": "Timeout in seconds for SSL connections",
"default_validity_days": "Default validity period in days for generated certificates",
"key_size": "Key size in bits for generated certificates",
"unifi": "UniFi device connection parameters",
"unifi.host": "Hostname or IP address of the UniFi device",
"unifi.username": "Username for authenticating with the UniFi device",
"unifi.password": "Password for authenticating with the UniFi device",
"unifi.site": "Site name for the UniFi device (default: 'default')",
"unifi.ssh_port": "SSH port for the UniFi device (default: 22)",
"unifi.ssh_username": "Username for SSH authentication with the UniFi device",
"unifi.ssh_password": "Password for SSH authentication (leave empty to use SSH key)",
"unifi.ssh_key_path": "Path to the SSH private key file for authentication"
}
}

308
src/ssl_manager.py Normal file
View File

@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
SSL Certificate Management Tool
This script provides utilities for managing SSL certificates, including:
- Checking certificate expiration dates
- Validating certificate chains
- Generating self-signed certificates for testing
"""
import os
import ssl
import socket
import datetime
import argparse
import subprocess
import json
from typing import Dict, Tuple, Optional, List, Any
def load_config(config_path: str = "config.json") -> Dict[str, Any]:
"""
Load configuration from a JSON file.
Args:
config_path: Path to the config file (default: config.json in current directory)
Returns:
Dictionary containing configuration values
"""
default_config = {
"cert_dir": "~/.ssl-certs",
"default_port": 443,
"connection_timeout": 3.0,
"default_validity_days": 365,
"key_size": 2048,
"unifi": {
"host": "",
"username": "",
"password": "",
"site": "default",
"ssh_port": 22,
"ssh_username": "",
"ssh_password": "",
"ssh_key_path": "~/.ssh/id_rsa"
}
}
try:
with open(config_path, 'r') as f:
config = json.load(f)
# Remove comments section if present
if "comments" in config:
del config["comments"]
# Update default config with values from file
for key in default_config:
if key in config:
if isinstance(default_config[key], dict) and isinstance(config[key], dict):
# Handle nested dictionaries (like unifi)
for nested_key in default_config[key]:
if nested_key in config[key]:
default_config[key][nested_key] = config[key][nested_key]
else:
default_config[key] = config[key]
except (FileNotFoundError, json.JSONDecodeError):
# Use default config if file not found or invalid
pass
return default_config
class SSLManager:
"""Class for managing SSL certificates and operations."""
def __init__(self, cert_dir: str = None, config_path: str = "config.json"):
"""
Initialize the SSL Manager.
Args:
cert_dir: Directory to store certificates (default: None)
config_path: Path to the config file (default: config.json)
"""
# Load configuration
self.config = load_config(config_path)
# Use cert_dir from parameters if provided, otherwise from config
self.cert_dir = cert_dir or os.path.expanduser(self.config["cert_dir"])
os.makedirs(self.cert_dir, exist_ok=True)
# Store other config values
self.default_port = self.config["default_port"]
self.connection_timeout = self.config["connection_timeout"]
self.default_validity_days = self.config["default_validity_days"]
self.key_size = self.config["key_size"]
# Store UniFi device connection parameters
self.unifi_host = self.config["unifi"]["host"]
self.unifi_username = self.config["unifi"]["username"]
self.unifi_password = self.config["unifi"]["password"]
self.unifi_site = self.config["unifi"]["site"]
# Store UniFi device SSH parameters
self.unifi_ssh_port = self.config["unifi"]["ssh_port"]
self.unifi_ssh_username = self.config["unifi"]["ssh_username"]
self.unifi_ssh_password = self.config["unifi"]["ssh_password"]
self.unifi_ssh_key_path = self.config["unifi"]["ssh_key_path"]
def check_cert_expiration(self, hostname: str, port: int = None) -> Dict:
"""
Check the expiration date of a certificate for a given hostname.
Args:
hostname: The hostname to check
port: The port to connect to (default: from config)
Returns:
Dictionary with certificate information
"""
# Use provided port or default from config
port = port or self.default_port
context = ssl.create_default_context()
conn = context.wrap_socket(
socket.socket(socket.AF_INET),
server_hostname=hostname
)
# Use timeout from config
conn.settimeout(self.connection_timeout)
try:
conn.connect((hostname, port))
cert = conn.getpeercert()
# Parse expiration date
expiration_date = datetime.datetime.strptime(
cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
)
# Calculate days until expiration
days_left = (expiration_date - datetime.datetime.now()).days
return {
'hostname': hostname,
'port': port,
'issuer': dict(x[0] for x in cert['issuer']),
'subject': dict(x[0] for x in cert['subject']),
'expiration_date': expiration_date.strftime('%Y-%m-%d'),
'days_left': days_left,
'status': 'Valid' if days_left > 0 else 'Expired'
}
except Exception as e:
return {
'hostname': hostname,
'port': port,
'error': str(e),
'status': 'Error'
}
finally:
conn.close()
def generate_self_signed_cert(
self,
common_name: str,
days_valid: int = None
) -> Tuple[str, str]:
"""
Generate a self-signed certificate for testing.
Args:
common_name: Common Name (CN) for the certificate
days_valid: Number of days the certificate will be valid (default: from config)
Returns:
Tuple of (cert_path, key_path)
"""
# Use provided days_valid or default from config
days_valid = days_valid or self.default_validity_days
cert_path = os.path.join(self.cert_dir, f"{common_name}.crt")
key_path = os.path.join(self.cert_dir, f"{common_name}.key")
# Generate private key using key size from config
subprocess.run([
'openssl', 'genrsa',
'-out', key_path,
str(self.key_size)
], check=True)
# Generate certificate
subprocess.run([
'openssl', 'req',
'-new', '-x509',
'-key', key_path,
'-out', cert_path,
'-days', str(days_valid),
'-subj', f"/CN={common_name}"
], check=True)
return cert_path, key_path
def validate_cert_chain(self, cert_path: str, ca_path: Optional[str] = None) -> bool:
"""
Validate a certificate chain.
Args:
cert_path: Path to the certificate
ca_path: Path to the CA certificate (default: None, uses system CAs)
Returns:
True if valid, False otherwise
"""
cmd = ['openssl', 'verify']
if ca_path:
cmd.extend(['-CAfile', ca_path])
cmd.append(cert_path)
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0 and 'OK' in result.stdout
def get_unifi_connection_params(self) -> Dict[str, str]:
"""
Get the UniFi device connection parameters.
Returns:
Dictionary containing the UniFi device connection parameters
"""
return {
"host": self.unifi_host,
"username": self.unifi_username,
"password": self.unifi_password,
"site": self.unifi_site
}
def get_unifi_ssh_params(self) -> Dict[str, str]:
"""
Get the UniFi device SSH parameters.
Returns:
Dictionary containing the UniFi device SSH parameters
"""
return {
"host": self.unifi_host,
"port": self.unifi_ssh_port,
"username": self.unifi_ssh_username,
"password": self.unifi_ssh_password,
"key_path": self.unifi_ssh_key_path
}
def main():
"""Main function to run the SSL Manager CLI."""
parser = argparse.ArgumentParser(description='SSL Certificate Management Tool')
# Global arguments
parser.add_argument('--config', help='Path to config file (default: config.json)', default='config.json')
parser.add_argument('--cert-dir', help='Directory to store certificates (overrides config)')
subparsers = parser.add_subparsers(dest='command', help='Command to run')
# Check expiration command
check_parser = subparsers.add_parser('check', help='Check certificate expiration')
check_parser.add_argument('hostname', help='Hostname to check')
check_parser.add_argument('--port', type=int, help='Port (overrides config)')
# Generate self-signed certificate command
gen_parser = subparsers.add_parser('generate', help='Generate self-signed certificate')
gen_parser.add_argument('common_name', help='Common Name (CN) for the certificate')
gen_parser.add_argument('--days', type=int, help='Days valid (overrides config)')
# Validate certificate command
validate_parser = subparsers.add_parser('validate', help='Validate certificate chain')
validate_parser.add_argument('cert_path', help='Path to certificate')
validate_parser.add_argument('--ca-path', help='Path to CA certificate')
args = parser.parse_args()
ssl_manager = SSLManager(cert_dir=args.cert_dir, config_path=args.config)
if args.command == 'check':
result = ssl_manager.check_cert_expiration(args.hostname, args.port)
if result['status'] == 'Error':
print(f"Error checking {args.hostname}:{args.port}: {result['error']}")
else:
print(f"Certificate for {result['hostname']}:")
print(f" Status: {result['status']}")
print(f" Expires: {result['expiration_date']} ({result['days_left']} days left)")
print(f" Issuer: {result['issuer'].get('organizationName', 'N/A')}")
elif args.command == 'generate':
cert_path, key_path = ssl_manager.generate_self_signed_cert(
args.common_name, args.days
)
print(f"Generated self-signed certificate:")
print(f" Certificate: {cert_path}")
print(f" Private Key: {key_path}")
elif args.command == 'validate':
is_valid = ssl_manager.validate_cert_chain(args.cert_path, args.ca_path)
print(f"Certificate validation {'successful' if is_valid else 'failed'}")
else:
parser.print_help()
if __name__ == "__main__":
main()

7
test_config.json Normal file
View File

@ -0,0 +1,7 @@
{
"cert_dir": "~/test-ssl-certs",
"default_port": 8443,
"connection_timeout": 5.0,
"default_validity_days": 730,
"key_size": 4096
}

134
tests/test_config.py Normal file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
Tests for the configuration functionality of the SSL Manager.
This module contains tests for loading and using configuration values.
"""
import os
import sys
import json
import tempfile
import unittest
from unittest.mock import patch, mock_open
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from ssl_manager import load_config, SSLManager
class TestConfigLoading(unittest.TestCase):
"""Test cases for configuration loading functionality."""
def setUp(self):
"""Set up test fixtures."""
# Create a temporary directory for test files
self.temp_dir = tempfile.TemporaryDirectory()
# Sample config for testing
self.test_config = {
"cert_dir": "~/test-certs",
"default_port": 8443,
"connection_timeout": 5.0,
"default_validity_days": 730,
"key_size": 4096
}
def tearDown(self):
"""Tear down test fixtures."""
# Clean up the temporary directory
self.temp_dir.cleanup()
def test_load_config_with_valid_file(self):
"""Test loading a valid configuration file."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Load the config
config = load_config(config_path)
# Verify the config values
self.assertEqual(config["cert_dir"], "~/test-certs")
self.assertEqual(config["default_port"], 8443)
self.assertEqual(config["connection_timeout"], 5.0)
self.assertEqual(config["default_validity_days"], 730)
self.assertEqual(config["key_size"], 4096)
def test_load_config_with_missing_file(self):
"""Test loading a non-existent configuration file."""
# Use a non-existent file path
config_path = os.path.join(self.temp_dir.name, "nonexistent.json")
# Load the config
config = load_config(config_path)
# Verify default values are used
self.assertEqual(config["cert_dir"], "~/.ssl-certs")
self.assertEqual(config["default_port"], 443)
self.assertEqual(config["connection_timeout"], 3.0)
self.assertEqual(config["default_validity_days"], 365)
self.assertEqual(config["key_size"], 2048)
def test_load_config_with_partial_file(self):
"""Test loading a configuration file with only some values."""
# Create a config with only some values
partial_config = {
"default_port": 8443,
"key_size": 4096
}
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "partial_config.json")
with open(config_path, 'w') as f:
json.dump(partial_config, f)
# Load the config
config = load_config(config_path)
# Verify the specified values are used and others are defaults
self.assertEqual(config["cert_dir"], "~/.ssl-certs") # Default
self.assertEqual(config["default_port"], 8443) # From file
self.assertEqual(config["connection_timeout"], 3.0) # Default
self.assertEqual(config["default_validity_days"], 365) # Default
self.assertEqual(config["key_size"], 4096) # From file
def test_ssl_manager_uses_config_values(self):
"""Test that SSLManager uses values from the config file."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with the config
ssl_manager = SSLManager(config_path=config_path)
# Verify the manager uses the config values
self.assertEqual(ssl_manager.default_port, 8443)
self.assertEqual(ssl_manager.connection_timeout, 5.0)
self.assertEqual(ssl_manager.default_validity_days, 730)
self.assertEqual(ssl_manager.key_size, 4096)
def test_ssl_manager_cert_dir_override(self):
"""Test that cert_dir parameter overrides the config value."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with a custom cert_dir
custom_cert_dir = os.path.join(self.temp_dir.name, "custom-certs")
ssl_manager = SSLManager(cert_dir=custom_cert_dir, config_path=config_path)
# Verify the manager uses the custom cert_dir
self.assertEqual(ssl_manager.cert_dir, custom_cert_dir)
# Verify the directory was created
self.assertTrue(os.path.exists(custom_cert_dir))
self.assertTrue(os.path.isdir(custom_cert_dir))
if __name__ == '__main__':
unittest.main()

126
tests/test_ssl_manager.py Normal file
View File

@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Tests for the SSL Manager module.
This module contains tests for the SSLManager class and its methods.
"""
import os
import sys
import tempfile
import unittest
from unittest.mock import patch, MagicMock
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from ssl_manager import SSLManager
class TestSSLManager(unittest.TestCase):
"""Test cases for the SSLManager class."""
def setUp(self):
"""Set up test fixtures."""
# Create a temporary directory for certificates
self.temp_dir = tempfile.TemporaryDirectory()
self.ssl_manager = SSLManager(cert_dir=self.temp_dir.name)
def tearDown(self):
"""Tear down test fixtures."""
# Clean up the temporary directory
self.temp_dir.cleanup()
@patch('ssl_manager.socket.socket')
@patch('ssl_manager.ssl.create_default_context')
def test_check_cert_expiration_valid(self, mock_context, mock_socket):
"""Test checking a valid certificate expiration."""
# Mock the SSL socket and certificate
mock_sock = MagicMock()
mock_context.return_value.wrap_socket.return_value = mock_sock
# Mock the peer certificate
mock_sock.getpeercert.return_value = {
'notAfter': 'Jul 20 12:00:00 2026 GMT',
'issuer': [(('organizationName', 'Test CA'),)],
'subject': [(('commonName', 'example.com'),)]
}
# Call the method
result = self.ssl_manager.check_cert_expiration('example.com')
# Verify the result
self.assertEqual(result['hostname'], 'example.com')
self.assertEqual(result['status'], 'Valid')
self.assertIn('days_left', result)
self.assertIn('expiration_date', result)
@patch('ssl_manager.socket.socket')
@patch('ssl_manager.ssl.create_default_context')
def test_check_cert_expiration_error(self, mock_context, mock_socket):
"""Test checking a certificate with an error."""
# Mock the SSL socket to raise an exception
mock_sock = MagicMock()
mock_context.return_value.wrap_socket.return_value = mock_sock
mock_sock.connect.side_effect = Exception("Connection refused")
# Call the method
result = self.ssl_manager.check_cert_expiration('nonexistent.example.com')
# Verify the result
self.assertEqual(result['hostname'], 'nonexistent.example.com')
self.assertEqual(result['status'], 'Error')
self.assertIn('error', result)
self.assertEqual(result['error'], 'Connection refused')
@patch('ssl_manager.subprocess.run')
def test_generate_self_signed_cert(self, mock_run):
"""Test generating a self-signed certificate."""
# Mock the subprocess calls
mock_run.return_value = MagicMock(returncode=0)
# Call the method
cert_path, key_path = self.ssl_manager.generate_self_signed_cert('test.example.com')
# Verify the result
self.assertEqual(cert_path, os.path.join(self.temp_dir.name, 'test.example.com.crt'))
self.assertEqual(key_path, os.path.join(self.temp_dir.name, 'test.example.com.key'))
# Verify subprocess was called twice (once for key, once for cert)
self.assertEqual(mock_run.call_count, 2)
@patch('ssl_manager.subprocess.run')
def test_validate_cert_chain_valid(self, mock_run):
"""Test validating a valid certificate chain."""
# Mock the subprocess call
mock_run.return_value = MagicMock(
returncode=0,
stdout="test.crt: OK"
)
# Call the method
result = self.ssl_manager.validate_cert_chain('test.crt')
# Verify the result
self.assertTrue(result)
mock_run.assert_called_once()
@patch('ssl_manager.subprocess.run')
def test_validate_cert_chain_invalid(self, mock_run):
"""Test validating an invalid certificate chain."""
# Mock the subprocess call
mock_run.return_value = MagicMock(
returncode=1,
stdout="test.crt: C = US, O = Example, CN = example.com\nerror 2 at 1 depth lookup: unable to get issuer certificate"
)
# Call the method
result = self.ssl_manager.validate_cert_chain('test.crt')
# Verify the result
self.assertFalse(result)
mock_run.assert_called_once()
if __name__ == '__main__':
unittest.main()

104
tests/test_unifi_config.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Tests for the UniFi device configuration functionality of the SSL Manager.
This module contains tests for loading and using UniFi device configuration values.
"""
import os
import sys
import json
import tempfile
import unittest
from unittest.mock import patch
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from ssl_manager import load_config, SSLManager
class TestUniFiConfig(unittest.TestCase):
"""Test cases for UniFi device configuration functionality."""
def setUp(self):
"""Set up test fixtures."""
# Create a temporary directory for test files
self.temp_dir = tempfile.TemporaryDirectory()
# Sample config for testing
self.test_config = {
"cert_dir": "~/test-certs",
"default_port": 8443,
"connection_timeout": 5.0,
"default_validity_days": 730,
"key_size": 4096,
"unifi": {
"host": "test.unifi.local",
"username": "testuser",
"password": "testpass",
"site": "testsite"
}
}
def tearDown(self):
"""Tear down test fixtures."""
# Clean up the temporary directory
self.temp_dir.cleanup()
def test_load_config_with_unifi_params(self):
"""Test loading a configuration file with UniFi device parameters."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Load the config
config = load_config(config_path)
# Verify the UniFi device parameters
self.assertIn("unifi", config)
self.assertEqual(config["unifi"]["host"], "test.unifi.local")
self.assertEqual(config["unifi"]["username"], "testuser")
self.assertEqual(config["unifi"]["password"], "testpass")
self.assertEqual(config["unifi"]["site"], "testsite")
def test_ssl_manager_stores_unifi_params(self):
"""Test that SSLManager stores UniFi device parameters."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with the config
ssl_manager = SSLManager(config_path=config_path)
# Verify the manager stores the UniFi device parameters
self.assertEqual(ssl_manager.unifi_host, "test.unifi.local")
self.assertEqual(ssl_manager.unifi_username, "testuser")
self.assertEqual(ssl_manager.unifi_password, "testpass")
self.assertEqual(ssl_manager.unifi_site, "testsite")
def test_get_unifi_connection_params(self):
"""Test the get_unifi_connection_params method."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with the config
ssl_manager = SSLManager(config_path=config_path)
# Get the UniFi device connection parameters
params = ssl_manager.get_unifi_connection_params()
# Verify the parameters
self.assertIsInstance(params, dict)
self.assertEqual(params["host"], "test.unifi.local")
self.assertEqual(params["username"], "testuser")
self.assertEqual(params["password"], "testpass")
self.assertEqual(params["site"], "testsite")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Test for UniFi device connection using unifiControl library.
This module tests the connection to a UniFi device using the credentials
from the config.json file and the unifiControl library.
"""
import os
import sys
import json
import unittest
from unittest.mock import patch
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
# Import the SSLManager class to access the config loading functionality
from src.ssl_manager import SSLManager
# Import the unifiControl library
try:
from unificontrol import UnifiClient
except ImportError:
print("ERROR: unifiControl library is not installed. Please install it using 'pip install unifiControl'")
sys.exit(1)
class TestUniFiConnection(unittest.TestCase):
"""Test case for UniFi device connection."""
def setUp(self):
"""Set up test fixtures."""
# Create an SSLManager instance to load the config
self.ssl_manager = SSLManager()
# Get the UniFi connection parameters
self.unifi_params = self.ssl_manager.get_unifi_connection_params()
# Print the connection parameters (without the password)
print(f"Testing connection to UniFi device at: {self.unifi_params['host']}")
print(f"Using username: {self.unifi_params['username']}")
def test_unifi_connection(self):
"""Test the connection to the UniFi device."""
# Skip the test if any of the required parameters are missing
if not self.unifi_params['host'] or not self.unifi_params['username'] or not self.unifi_params['password']:
self.skipTest("UniFi connection parameters are missing in the config file")
try:
print("\nAttempting to connect to UniFi device...")
print(f"Host: {self.unifi_params['host']}")
print(f"Username: {self.unifi_params['username']}")
print(f"Password: {'*' * len(self.unifi_params['password'])}")
# Create a UnifiClient instance
client = UnifiClient(
host=self.unifi_params['host'],
username=self.unifi_params['username'],
password=self.unifi_params['password'],
port=443, # Default port for UniFi controller
site=self.unifi_params['site'], # Site name from config
cert=None # Skip SSL certificate verification
)
# Disable SSL verification in the requests session
client._session.verify = False
# Suppress InsecureRequestWarning
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Enable debug logging for requests
import logging
from http.client import HTTPConnection
HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
print("\nAttempting to login...")
# Attempt to login
client.login()
print("Login successful!")
# If login is successful, get the system info to further verify the connection
print("Retrieving system info...")
system_info = client.get_system_info()
# Print some information about the connected system
print("\nConnection successful!")
print(f"System name: {system_info.get('name', 'N/A')}")
print(f"Version: {system_info.get('version', 'N/A')}")
# Logout to clean up the session
print("Logging out...")
client.logout()
print("Logout successful!")
# Assert that we got a valid system_info response
self.assertIsNotNone(system_info)
self.assertIn('name', system_info)
except Exception as e:
# If an exception occurs, fail the test with the error message
print(f"\nError: {str(e)}")
print("\nAuthentication failed. Possible reasons:")
print("1. The credentials in config.json are incorrect")
print("2. The UniFi device requires additional authentication parameters")
print("3. The UniFi device is not accessible from the current network")
print("4. The UniFi device is not running or is not responding")
self.fail(f"Connection to UniFi device failed: {str(e)}")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Tests for the UniFi device SSH configuration functionality of the SSL Manager.
This module contains tests for loading and using UniFi device SSH configuration values.
"""
import os
import sys
import json
import tempfile
import unittest
from unittest.mock import patch
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from ssl_manager import load_config, SSLManager
class TestUniFiSSHConfig(unittest.TestCase):
"""Test cases for UniFi device SSH configuration functionality."""
def setUp(self):
"""Set up test fixtures."""
# Create a temporary directory for test files
self.temp_dir = tempfile.TemporaryDirectory()
# Sample config for testing
self.test_config = {
"cert_dir": "~/test-certs",
"default_port": 8443,
"connection_timeout": 5.0,
"default_validity_days": 730,
"key_size": 4096,
"unifi": {
"host": "test.unifi.local",
"username": "testuser",
"password": "testpass",
"site": "testsite",
"ssh_port": 2222,
"ssh_username": "sshuser",
"ssh_password": "sshpass",
"ssh_key_path": "~/test-ssh-key"
}
}
def tearDown(self):
"""Tear down test fixtures."""
# Clean up the temporary directory
self.temp_dir.cleanup()
def test_load_config_with_ssh_params(self):
"""Test loading a configuration file with UniFi device SSH parameters."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Load the config
config = load_config(config_path)
# Verify the UniFi device SSH parameters
self.assertIn("unifi", config)
self.assertEqual(config["unifi"]["ssh_port"], 2222)
self.assertEqual(config["unifi"]["ssh_username"], "sshuser")
self.assertEqual(config["unifi"]["ssh_password"], "sshpass")
self.assertEqual(config["unifi"]["ssh_key_path"], "~/test-ssh-key")
def test_ssl_manager_stores_ssh_params(self):
"""Test that SSLManager stores UniFi device SSH parameters."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with the config
ssl_manager = SSLManager(config_path=config_path)
# Verify the manager stores the UniFi device SSH parameters
self.assertEqual(ssl_manager.unifi_ssh_port, 2222)
self.assertEqual(ssl_manager.unifi_ssh_username, "sshuser")
self.assertEqual(ssl_manager.unifi_ssh_password, "sshpass")
self.assertEqual(ssl_manager.unifi_ssh_key_path, "~/test-ssh-key")
def test_get_unifi_ssh_params(self):
"""Test the get_unifi_ssh_params method."""
# Create a temporary config file
config_path = os.path.join(self.temp_dir.name, "test_config.json")
with open(config_path, 'w') as f:
json.dump(self.test_config, f)
# Create an SSLManager with the config
ssl_manager = SSLManager(config_path=config_path)
# Get the UniFi device SSH parameters
params = ssl_manager.get_unifi_ssh_params()
# Verify the parameters
self.assertIsInstance(params, dict)
self.assertEqual(params["host"], "test.unifi.local")
self.assertEqual(params["port"], 2222)
self.assertEqual(params["username"], "sshuser")
self.assertEqual(params["password"], "sshpass")
self.assertEqual(params["key_path"], "~/test-ssh-key")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,81 @@
# UniFi Connection Test Results
## Summary
We attempted to verify the UniFi device connection parameters in `config.json` by creating a test script that uses the `unifiControl` Python library to connect to the UniFi device. The test was able to establish a connection to the device, but authentication failed with a 401 Unauthorized error.
## Test Details
### Connection Parameters Used
- **Host**: udm-se.mgeppert.com
- **Port**: 443
- **Username**: SSLCertificate
- **Password**: cYu2E1OWt0XseVf9j5ML
### Test Process
1. Installed the unifiControl Python library
2. Created a test script that loads the UniFi connection parameters from config.json
3. Configured the test to disable SSL certificate verification
4. Attempted to connect to the UniFi device and authenticate
5. Received a 401 Unauthorized response from the server
### HTTP Request/Response Details
```
POST /api/login HTTP/1.1
Host: udm-se.mgeppert.com
User-Agent: python-requests/2.32.4
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 66
Content-Type: application/json
{"username": "SSLCertificate", "password": "cYu2E1OWt0XseVf9j5ML"}
```
```
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Mon, 21 Jul 2025 01:28:25 GMT
Content-Type: application/json
Content-Length: 47
Connection: keep-alive
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Robots-Tag: noindex
Set-Cookie: TOKEN=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; samesite=none; secure; httponly; partitioned
Set-Cookie: TOKEN=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; samesite=none; secure; httponly
```
## Findings
1. The UniFi device at udm-se.mgeppert.com is reachable and responding to HTTPS requests on port 443
2. The SSL certificate verification was successfully disabled in our test
3. The authentication attempt with the provided credentials failed with a 401 Unauthorized error
4. The server is running nginx and appears to be a UniFi device based on the API endpoint (/api/login)
## Possible Issues
1. **Incorrect Credentials**: The username or password in config.json may be incorrect
2. **Account Locked or Disabled**: The account "SSLCertificate" may be locked or disabled on the UniFi device
3. **Different Authentication Method**: The UniFi device may require a different authentication method or endpoint
4. **API Version Mismatch**: The unifiControl library may be using an API version that's incompatible with the device
## Recommendations
1. **Verify Credentials**: Double-check the username and password in config.json
2. **Check Account Status**: Log in to the UniFi device's web interface and verify that the "SSLCertificate" account exists and is enabled
3. **Try Different Authentication Method**: The UniFi device may require a different authentication method or endpoint. Check the device's documentation for the correct API endpoints
4. **Check API Version Compatibility**: Verify that the unifiControl library is compatible with the UniFi device's firmware version
5. **Try Manual Authentication**: Use a tool like curl or Postman to manually attempt authentication with the UniFi device to verify the correct API endpoints and authentication method
6. **Check Logs**: Check the UniFi device's logs for any authentication failure messages that might provide more information
## Next Steps
1. Update the credentials in config.json if they are incorrect
2. If the credentials are correct, check the UniFi device's configuration and logs for more information
3. Consider using a different authentication method or API library if the unifiControl library is incompatible with the device