Skip to content

HTTPS/SSL Support and Password Obfuscation Control#13

Open
gierwialo wants to merge 3 commits intopuddly:devfrom
gierwialo:feature/https-ssl-support
Open

HTTPS/SSL Support and Password Obfuscation Control#13
gierwialo wants to merge 3 commits intopuddly:devfrom
gierwialo:feature/https-ssl-support

Conversation

@gierwialo
Copy link

@gierwialo gierwialo commented Feb 13, 2026

I recently bought a NanoKVM Pro PCI-E and wanted to connect to it over HTTPS from Python. Turns out the library didn't support SSL/TLS at all, so I added it. While testing, I ran into a weird authentication issue that led me down a rabbit hole...

"The Authentication Mystery"

When I tried connecting to my NanoKVM Pro (running Application v1.2.12, Image v1.0.13) over HTTPS, authentication kept failing with "Invalid username or password" - even though I was using the correct credentials!

After SSH'ing into the device and monitoring logs, I discovered the reason: the library was sending AES-256-CBC encrypted passwords, but my device expected plain text over HTTPS (it does bcrypt hashing server-side). The web UI was working fine because it sends passwords in plain text.

Turns out newer NanoKVM versions (Pro and Standard ≥2.1.6) switched to accepting plain text passwords over HTTPS, while older versions still need the client-side obfuscation.

Note: NanoKVM Pro uses different version numbering (1.x.x) than standard NanoKVM (2.x.x), which initially confused me - I thought v1.2.12 was ancient!

What I Added

1. SSL/TLS Support

Three new optional parameters for NanoKVMClient.__init__():

  • verify_ssl: bool = True - Enable/disable SSL certificate verification

    • Secure by default
    • Set to False for self-signed certs (testing only)
  • ssl_ca_cert: str | None = None - Path to custom CA certificate

    • For self-signed certificates or private CAs
    • Better than disabling verification entirely

Implementation details:

  • Uses aiohttp.TCPConnector with SSL config
  • Works automatically for both HTTP requests and WebSocket connections
  • Validates certificate files before use
  • Clear error messages via new NanoKVMSSLError exception

2. Password Obfuscation Control

One new parameter to handle the authentication differences:

  • use_password_obfuscation: bool = True - Control password encryption
    • Default: True (backward compatible with older devices)
    • Set to False for modern NanoKVM with HTTPS
    • When True: Uses AES-256-CBC encryption (legacy behavior)
    • When False: Sends plain text (relies on HTTPS transport security)

Backward Compatibility

Everything is 100% backward compatible. Existing code works without changes:

# Old code still works perfectly
async with NanoKVMClient("http://kvm.local/api/") as client:
    await client.authenticate("user", "pass")

All new parameters have sensible defaults:

  • verify_ssl=True - Secure by default
  • use_password_obfuscation=True - Works with older devices

Testing

I tested everything with my actual NanoKVM Pro over HTTPS:

  • Hardware: NanoKVM Pro PCI-E
  • Software: Application v1.2.12, Image v1.0.13 (latest as of Feb 2026)
  • Certificate: Let's Encrypt (valid TLS 1.3)
  • HTTPS connection with certificate verification
  • Authentication with plain text password (use_password_obfuscation=False)
  • Device info retrieval (get_info, get_hardware, get_gpio)
  • WebSocket over SSL (mouse control via wss://)

Usage Examples

NanoKVM with HTTPS (Recommended)

async with NanoKVMClient(
    "https://kvm.local/api/",
    use_password_obfuscation=False
) as client:
    await client.authenticate("username", "password")

Legacy NanoKVM with HTTP

async with NanoKVMClient(
    "http://kvm.local/api/",
    use_password_obfuscation=True  # This is the default
) as client:
    await client.authenticate("username", "password")

Self-Signed Certificates

# Option 1: Disable verification (testing only - not recommended!)
async with NanoKVMClient(
    "https://kvm.local/api/",
    verify_ssl=False,
    use_password_obfuscation=False
) as client:
    await client.authenticate("username", "password")

# Option 2: Custom CA certificate (better approach)
async with NanoKVMClient(
    "https://kvm.local/api/",
    ssl_ca_cert="/path/to/ca.pem",
    use_password_obfuscation=False
) as client:
    await client.authenticate("username", "password")

Technical Details

SSL Implementation

  • Uses Python's ssl.create_default_context() for secure defaults
  • TLS 1.3 support (protocol auto-negotiated)
  • Validates certificate files before attempting connection
  • Clear error messages for SSL config issues

Password Obfuscation

  • When enabled: AES-256-CBC encryption using hardcoded EncryptSecretKey
  • When disabled: Plain text over HTTPS (transport layer handles security)
  • Server-side uses bcrypt for password storage (on modern versions)

Files Changed

  • nanokvm/client.py - Main implementation (~150 lines added/modified)
  • tests/test_ssl.py - New comprehensive SSL tests (~200 lines)
  • README.md - Updated docs with HTTPS examples

Version Compatibility Guide

Based on my testing and research:

Modern devices (use plain text password):

  • NanoKVM Pro (any version with HTTPS)
  • NanoKVM Standard ≥2.1.6 (with bcrypt hashing)
  • Any device with HTTPS enabled

→ Set use_password_obfuscation=False

Legacy devices (need obfuscation):

  • NanoKVM Standard <2.1.6 (HTTP only)
  • Any device using the old hardcoded EncryptSecretKey

→ Keep use_password_obfuscation=True (default)

Not sure which you have?

  • If your device supports HTTPS → try use_password_obfuscation=False
  • If authentication fails, toggle the parameter

My Test Setup

Device: NanoKVM Pro PCI-E
Software: Application v1.2.12, Image v1.0.13 (latest for Pro line)
Certificate: Let's Encrypt
Protocol: HTTPS with TLS 1.3
Authentication: Plain text password (use_password_obfuscation=False)
Result: Everything works perfectly for me ;-)

Feel free to comment and review what I've done :)


Review changes

Auto-Detection of Password Obfuscation Mode

Based on suggestion, the client now automatically detects whether the device needs obfuscated or plain text passwords — no manual configuration needed.

  • use_password_obfuscation: bool | None = None - Changed from bool = True to Optional[bool]
    • None (default): Auto-detect — tries obfuscated first, falls back to plain text
    • True: Force obfuscation (legacy behavior)
    • False: Force plain text

How auto-detect works:

  1. First attempt: obfuscated password (backward compatible with older devices)
  2. If NanoKVMAuthenticationFailure → retry with plain text
  3. If both fail → raise the exception from the second attempt

This means existing code works without changes and without the extra parameter:

# Just works — no need to know your NanoKVM version
async with NanoKVMClient("https://kvm.local/api/") as client:
    await client.authenticate("username", "password")

Other Review Fixes

  • Removed NanoKVMSSLError — SSL exceptions (FileNotFoundError, ssl.SSLError) now propagate naturally from ssl.create_default_context() instead of being wrapped
  • Async-safe SSL context creation_create_ssl_context() is offloaded via asyncio.to_thread() since it performs file IO
  • Flat test structure — all tests are plain async def test_* functions, no classes, consistent with the rest of the test suite

Copy link
Owner

@puddly puddly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've left a few comments about the code.

Is the password obfuscation setting something we can autodetect? Possibly from the API version (if that's exposed unauthenticated) or just by trying both approaches?

Set to False to disable verification for self-signed certificates.
ssl_ca_cert: Path to custom CA certificate bundle file for SSL verification.
Useful for self-signed certificates or private CAs.
use_password_obfuscation: Use password obfuscation/encryption (default: True).
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be autodetected if the URL is https://? Or can we automatically try both obfuscated and unobfuscated?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, good question! I looked into this but there's no unauthenticated endpoint that exposes the API/firmware
version - /application/version and /vm/info both require a token, so we can't detect the version before login.

Your suggestion: autodetect based on URL scheme is simple, but not 100% reliable. The use_password_obfuscation parameter would still be available as an explicit override.
Other approach would be try both fallback ie. try plain text first, if INVALID_USERNAME_OR_PASSWORD retry with obfuscated (or vice versa). It should work reliably regardless of configuration, but doubles the requests on one path and also doubles them when the user provides genuinely wrong credentials.

Which approach would you prefer? Or should we just keep it as an explicit parameter with a default value?

PS. I agree with your other review comments, I'll push the fixes by the end of the week ;-)

from nanokvm.client import NanoKVMClient, NanoKVMSSLError


class TestSSLConfiguration:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other unit tests, can you make these functional? We don't really use classes for test method organization.

try:
ca_path = Path(self._ssl_ca_cert)
if not ca_path.is_file():
raise NanoKVMSSLError(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead allow these normal exceptions to propagate without being re-raised as a NanoKVMSSLError. ssl.create_default_context raises a FileNotFoundError if the CA certificate is missing, ssl.SSLError otherwise.

"""Async context manager entry."""
self._session = ClientSession()

ssl_config = self._create_ssl_context()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a SSL context isn't async-safe (it performs file IO), this should be offloaded to the event loop executor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants