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

46 lines
2.6 KiB
Python
Raw Normal View History

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