Files
update-server/app/services/device_credential_service.py
T

46 lines
2.6 KiB
Python

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