import base64 import json from datetime import datetime, timezone, timedelta from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from fastapi import HTTPException from app.repositories import client_update_repository from app.services.signing_service import load_manifest_private_key def verify_device_credential(encoded: str) -> dict: try: wrapper = json.loads(base64.b64decode(encoded, validate=True).decode("utf-8")) identity_text = wrapper["identity_text"] signature = base64.b64decode(wrapper["signature"], validate=True) load_manifest_private_key().public_key().verify(signature, identity_text.encode(), padding.PKCS1v15(), hashes.SHA256()) identity = json.loads(identity_text) if any(not identity.get(k) for k in ("device_id", "license_id", "app_id", "channel", "installation_id", "credential_seq", "valid_until")): raise ValueError("missing field") except Exception: raise HTTPException(status_code=401, detail={"error": "invalid_device_credential", "msg": "设备凭证格式或签名无效"}) row = client_update_repository.get_device(identity["device_id"]) if not row: raise HTTPException(status_code=401, detail={"error": "device_not_registered", "msg": "设备未登记"}) if row["disabled"]: reason = row["disabled_reason"] or "设备已被管理员禁用" raise HTTPException(status_code=403, detail={"error": "device_disabled", "msg": reason}) if row["app_id"] != identity["app_id"] or row["installation_id"] != identity["installation_id"] or int(row["credential_seq"]) != int(identity["credential_seq"]): raise HTTPException(status_code=401, detail={"error": "device_credential_revoked", "msg": "设备凭证已失效"}) license_row = client_update_repository.get_license(identity.get("license_id", "")) now = datetime.now(timezone.utc) try: license_expiry = datetime.fromisoformat((license_row["valid_until"] if license_row else "").replace("Z", "+00:00")) except ValueError: license_expiry = now - timedelta(seconds=1) if not license_row or license_row["status"] != "active" or license_expiry <= now: raise HTTPException(status_code=403, detail={"error": "license_invalid", "msg": "授权不存在、已禁用或已过期"}) if license_row["app_id"] != identity["app_id"] or license_row["channel_code"] != identity.get("channel"): raise HTTPException(status_code=403, detail={"error": "license_scope_mismatch", "msg": "授权应用或渠道不匹配"}) client_update_repository.touch_device(identity["device_id"]) return identity