feat(backend): self-managed TLS identity for the native app's encrypted LAN link

services/tls_identity.py creates an EC P-256 key + self-signed cert (10y,
SAN localhost/127.0.0.1/HOST_IP) under <data dir>/tls at startup, and
re-issues the cert with the SAME key when < 2 years remain. A cert that
doesn't belong to the key is replaced. Nothing to renew by hand; a backup of
the data directory keeps the identity. Runs in lifespan before the app is
healthy, so the proxy (which waits for healthy) always finds the files.

/api/system/identity and /api/system/status now include
tls: {port: TLS_PORT (default 8443), spki_sha256} - the base64 SHA-256 of the
public key that phones pin. Adds cryptography==46.0.4.

Tests: create / restart (no change) / renewal 8 years later keeps the key
and pin / foreign cert replaced; pin equals openssl's SPKI sha256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 17:41:24 +03:00
co-authored by Claude Opus 5.5
parent ab82598859
commit 8924a16747
4 changed files with 134 additions and 0 deletions
+4
View File
@@ -20,6 +20,7 @@ from models.table import Table, TableGroup
from services import printer_service
from services.cloud_sync import _sync_once, _push_menu_snapshot
from services.lan_ip import OVERRIDE_KEY, resolve_lan_ip, validate_lan_ip
from services.tls_identity import tls_info
from middleware.license_check import license_state
from config import settings
@@ -50,6 +51,8 @@ def identity(db: Session = Depends(get_db)):
"venue_name": (venue.value if venue and venue.value else None),
"version": settings.VERSION,
"api_version": API_VERSION,
# Encrypted LAN endpoint for the native app: https://<ip>:<port>, pin spki_sha256
"tls": tls_info(),
}
@@ -110,6 +113,7 @@ def system_status(db: Session = Depends(get_db), user: User = Depends(get_curren
"site_id": settings.SITE_ID or None,
"lan_ip": lan.get("effective"),
"lan_ip_info": lan,
"tls": tls_info(),
"printers": printer_statuses,
}