46 lines
2.6 KiB
Python
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
|