"""„Провери сървъра“ (етап 2, 08.11; DEPLOY.md §6а): отчетът от процесора AMD (SEV-SNP attestation report). Какво доказва: отчетът е подписан от ключ (VCEK), който AMD издава само за този физически чип и тази версия на фърмуера; веригата VCEK → ASK → ARK стига до коренния ключ на AMD (отпечатъкът му е закован тук). В отчета са измерването на стартираната машина (firmware + запечатаната система — `sealed/`) и 64 байта по наш избор (`report_data`) = SHA-512 на „твърденията“ (claims) — в тях е числото (nonce) на проверяващия, затова отчетът е пресен, а не записан отпреди. Модулът е самостоятелен (stdlib + `cryptography`): същият файл е и офлайн проверката за агенцията — `python attest.py verify отчет.bin claims.json --nonce <числото> [--product Genoa]` (сваля VCEK и веригата от AMD). Нищо не се приема мълчаливо: всяка проверка е ред в `checks` с ok = True / False / None (None = НЕ е проверено и защо — напр. още няма публикувано очаквано измерване); общото „ok“ е True само ако всички са True. Байтовете — по AMD SEV-SNP ABI (56860), таблицата ATTESTATION_REPORT; сверено и с google/go-sev-guest `abi/abi.go`. """ import base64 import ctypes import hashlib import json import os import struct import sys import urllib.request from pathlib import Path from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, padding from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature from cryptography.hazmat.primitives.serialization import Encoding REPORT_SIZE = 0x4A0 SIGNED_END = 0x2A0 # подписва се report[0:0x2A0] KDS = "https://kdsintf.amd.com/vcek/v1" DEVICE = "/dev/sev-guest" # Коренните ключове на AMD (ARK): SHA-256 на DER. ИЗМЕРЕНО 08.11 от kdsintf.amd.com/vcek/v1/<продукт>/cert_chain # и сверено с вградените в google/go-sev-guest (verify/trust) — два независими източника. ARK_SHA256 = {"Milan": "69d063b45344d26a2e94e1f4210de49ef555308287d4c174445c95639a540bcd", "Genoa": "4c6598d19c18719c5dfd4a7d335f674e5bfe1d8f800cea2cf270c10d103db2f1", "Turin": "1f084161a44bb6d93778a904877d4819cafa5d05ef4193b2ded9dd9c73dd3f6a"} PSS = padding.PSS(mgf=padding.MGF1(hashes.SHA384()), salt_length=48) # ARK/ASK: RSASSA-PSS, SHA-384, сол 48 # VCEK разширенията (AMD KDS спецификация 57230; go-sev-guest kds.go) OID_SPL = {"bl": "1.3.6.1.4.1.3704.1.3.1", "tee": "1.3.6.1.4.1.3704.1.3.2", "snp": "1.3.6.1.4.1.3704.1.3.3", "ucode": "1.3.6.1.4.1.3704.1.3.8"} OID_HWID = "1.3.6.1.4.1.3704.1.4" POLICY_DEBUG, POLICY_MIGRATE_MA = 1 << 19, 1 << 18 def parse(report: bytes) -> dict: if len(report) < REPORT_SIZE: raise ValueError(f"Отчетът е {len(report)} байта, очаквани {REPORT_SIZE}") r = report[:REPORT_SIZE] u32 = lambda o: struct.unpack_from("> 2) & 7, # 0 = VCEK, 1 = VLEK, 7 = без подпис "report_data": r[0x50:0x90], "measurement": r[0x90:0xC0], "host_data": r[0xC0:0xE0], "id_key_digest": r[0xE0:0x110], "author_key_digest": r[0x110:0x140], "report_id": r[0x140:0x160], "reported_tcb": u64(0x180), "cpuid": (r[0x188], r[0x189], r[0x18A]), "chip_id": r[0x1A0:0x1E0], "committed_tcb": u64(0x1E0), "launch_tcb": u64(0x1F0), "sig_r": int.from_bytes(r[0x2A0:0x2E8], "little"), "sig_s": int.from_bytes(r[0x2E8:0x330], "little")} def product(rep: dict) -> str: """Milan / Genoa / Turin. От v3 нататък — по CPUID в отчета; във v2 го няма → само Turin се познава (chip_id).""" fam, model, _ = rep["cpuid"] if rep["version"] >= 3 and fam: if fam == 0x19: return "Milan" if model < 0x10 else "Genoa" if fam == 0x1A: return "Turin" raise ValueError(f"Непознат процесор: family {fam:#x}, model {model:#x}") if not any(rep["chip_id"][8:]): return "Turin" raise ValueError("Отчет версия 2: процесорът не личи от него — дай го изрично (--product Milan|Genoa)") def tcb(raw: int, prod: str) -> dict: b = raw.to_bytes(8, "little") if prod == "Turin": # Turin: FMC най-отдолу, друг ред (ABI 1.57+) return {"fmc": b[0], "bl": b[1], "tee": b[2], "snp": b[3], "ucode": b[7]} return {"bl": b[0], "tee": b[1], "snp": b[6], "ucode": b[7]} def vcek_url(rep: dict, prod: str) -> str: t = tcb(rep["reported_tcb"], prod) hwid = rep["chip_id"][:8] if prod == "Turin" else rep["chip_id"] q = (f"fmcSPL={t['fmc']:02d}&" if prod == "Turin" else "") + \ f"blSPL={t['bl']:02d}&teeSPL={t['tee']:02d}&snpSPL={t['snp']:02d}&ucodeSPL={t['ucode']:02d}" return f"{KDS}/{prod}/{hwid.hex()}?{q}" def _get(url: str, cache: Path = None) -> bytes: f = Path(cache) / hashlib.sha256(url.encode()).hexdigest() if cache else None if f and f.exists(): return f.read_bytes() with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "imonavi-attest/1"}), timeout=20) as r: data = r.read() if f: f.parent.mkdir(parents=True, exist_ok=True) f.write_bytes(data) return data def fetch_certs(rep: dict, prod: str, cache: Path = None) -> tuple: """(VCEK като DER, веригата ASK+ARK като PEM) от AMD KDS. Кешира се: AMD ограничава броя заявки.""" return _get(vcek_url(rep, prod), cache), _get(f"{KDS}/{prod}/cert_chain", cache) def _ext_int(cert, oid: str): for e in cert.extensions: if e.oid.dotted_string == oid: v = e.value.value return int.from_bytes(v[2:2 + v[1]], "big") if v[:1] == b"\x02" else None return None def _ext_raw(cert, oid: str): for e in cert.extensions: if e.oid.dotted_string == oid: return e.value.value return None def claims_digest(claims: bytes) -> bytes: return hashlib.sha512(claims).digest() def verify(report: bytes, vcek_der: bytes, chain_pem: bytes, prod: str = None, claims: bytes = None, nonce: str = None, expected: list = None) -> dict: """Всички проверки; → {"ok", "product", "measurement", "checks": [{name, ok, detail}], "report": {...}}.""" rep = parse(report) prod = prod or product(rep) checks = [] def add(name, ok, detail=""): checks.append({"name": name, "ok": ok, "detail": detail}) ask, ark = x509.load_pem_x509_certificates(chain_pem)[:2] ark_hash = hashlib.sha256(ark.public_bytes(Encoding.DER)).hexdigest() add("Коренният ключ е на AMD (ARK)", ark_hash == ARK_SHA256.get(prod), f"{prod} ARK SHA-256 {ark_hash}") def signed_by(cert, issuer) -> bool: try: issuer.public_key().verify(cert.signature, cert.tbs_certificate_bytes, PSS, hashes.SHA384()) return True except InvalidSignature: return False add("ARK е подписан от себе си", signed_by(ark, ark)) add("ASK е подписан от ARK", signed_by(ask, ark)) vcek = x509.load_der_x509_certificate(vcek_der) add("VCEK (ключът на този чип) е подписан от ASK", signed_by(vcek, ask)) hwid = _ext_raw(vcek, OID_HWID) want = rep["chip_id"][:8] if prod == "Turin" else rep["chip_id"] add("VCEK е за чипа от отчета", hwid == want, f"chip_id {rep['chip_id'].hex()[:16]}…") t = tcb(rep["reported_tcb"], prod) got = {k: _ext_int(vcek, oid) for k, oid in OID_SPL.items()} add("VCEK е за версията на фърмуера от отчета", all(got[k] == t[k] for k in OID_SPL), f"отчет {t} · VCEK {got}") if rep["sig_algo"] != 1 or rep["signing_key"] != 0: add("Подписът на отчета (ECDSA P-384)", False, f"алгоритъм {rep['sig_algo']}, ключ {rep['signing_key']} — поддържаме само VCEK (0) с ECDSA P-384 (1)") else: try: vcek.public_key().verify(encode_dss_signature(rep["sig_r"], rep["sig_s"]), report[:SIGNED_END], ec.ECDSA(hashes.SHA384())) add("Подписът на отчета (ECDSA P-384)", True) except InvalidSignature: add("Подписът на отчета (ECDSA P-384)", False, "отчетът е променян или не е от този чип") add("Машината не позволява дебъгване (четене на паметта ѝ отвън)", not rep["policy"] & POLICY_DEBUG, f"policy {rep['policy']:#x}") add("Машината не може да се изнесе с агент за миграция", not rep["policy"] & POLICY_MIGRATE_MA, f"policy {rep['policy']:#x}") meas = rep["measurement"].hex() if not expected: add("Измерването е на публикувана запечатана система", None, "НЕ е проверено: очакваното измерване още не е публикувано (sealed/measurements.json)") else: hit = [e for e in expected if e.get("measurement", "").lower() == meas] add("Измерването е на публикувана запечатана система", bool(hit), (f"{hit[0].get('label', '')}" if hit else f"{meas} не е в публикуваните ({len(expected)})")) if claims is None: add("Отчетът е пресен (с твоето число)", None, "НЕ е проверено: няма твърдения (claims)") else: bound = claims_digest(claims) == rep["report_data"] try: c = json.loads(claims) except ValueError: c = {} fresh = nonce is None or c.get("nonce") == nonce add("Отчетът е пресен (с твоето число)", bound and fresh if nonce is not None else (None if bound else False), ("SHA-512(claims) = report_data" if bound else "SHA-512(claims) ≠ report_data") + ("" if nonce is not None else " · числото не е дадено — свежестта НЕ е проверена")) return {"ok": all(x["ok"] is True for x in checks), "product": prod, "measurement": meas, "checks": checks, "report": {"version": rep["version"], "policy": hex(rep["policy"]), "vmpl": rep["vmpl"], "reported_tcb": tcb(rep["reported_tcb"], prod), "chip_id": rep["chip_id"].hex(), "report_data": rep["report_data"].hex(), "host_data": rep["host_data"].hex()}} # ─── Взимане на отчета в запечатаната машина (/dev/sev-guest; linux uapi sev-guest.h) ───────────────────────── def _iowr(nr: int, size: int) -> int: return (3 << 30) | (size << 16) | (ord("S") << 8) | nr SNP_GET_REPORT = _iowr(0x0, 32) # = 0xC0205300 (struct snp_guest_request_ioctl е 32 байта) SNP_GET_DERIVED_KEY = _iowr(0x1, 32) # = 0xC0205301 def _guest_request(nr: int, req: bytes, device: str = DEVICE) -> bytes: import fcntl # само Linux; на Windows модулът се ползва само за проверка req_buf = ctypes.create_string_buffer(req, len(req)) resp_buf = ctypes.create_string_buffer(4000) ioc = bytearray(struct.pack(" bytes: """Пресен отчет с нашите 64 байта. MSG_REPORT_RSP: status u32, report_size u32, 24 празни, отчетът от 0x20.""" if len(report_data) != 64: raise ValueError("report_data е точно 64 байта") resp = _guest_request(SNP_GET_REPORT, report_data + struct.pack(" bytes: """32 байта, които само ТАЗИ машина (този чип + избраните полета) може да получи: SNP_GET_DERIVED_KEY. root_key_select: бит 0 = 0 (VCEK, не VMRK), битове 2:1 = 1 (изрично VCEK — не VLEK на доставчика).""" req = struct.pack(" list: return json.loads(MEASUREMENTS.read_text(encoding="utf-8"))["measurements"] if MEASUREMENTS.exists() else [] def server_check(nonce: str, cache: Path, now: str) -> dict: """За страницата „Провери сървъра“: пресен отчет с числото на посетителя + всички проверки. Не е запечатана машина (етап 1 — няма /dev/sev-guest) → казва го (`sealed: False`), не показва „наред“.""" if not nonce or len(nonce) > 128 or not all(c.isalnum() or c in "-_" for c in nonce): raise ValueError("Числото: 1–128 букви/цифри") if not os.path.exists(DEVICE): return {"sealed": False, "detail": "Този сървър още не е запечатан (няма отчет от процесора) — етап 1."} rf = os.environ.get("CRM_RELEASE_FILE") release = json.loads(Path(rf).read_text(encoding="utf-8")) if rf and Path(rf).exists() else None claims = json.dumps({"v": 1, "purpose": "server", "nonce": nonce, "time": now, "domain": os.environ.get("CRM_PUBLIC_URL"), "release": release}, ensure_ascii=False, sort_keys=True).encode() report = get_report(claims_digest(claims)) rep = parse(report) prod = product(rep) vcek, chain = fetch_certs(rep, prod, cache) out = verify(report, vcek, chain, prod, claims, nonce, expected_measurements()) idx = (release or {}).get("rekor_log_index") out["checks"].append({"name": "Работи подписано издание, записано в публичния регистър", "ok": True if idx else None, "detail": f"{release['version']} · Rekor #{idx}" if idx else "НЕ е проверено: няма запис за подписано издание"}) out["ok"] = out["ok"] and bool(idx) return {"sealed": True, "claims": claims.decode(), "report": b64(report), "vcek": b64(vcek), "chain": chain.decode(), "result": out, "rekor": f"https://search.sigstore.dev/?logIndex={idx}" if idx else None} def b64(data: bytes) -> str: return base64.b64encode(data).decode() def main(argv=None) -> int: import argparse p = argparse.ArgumentParser(description="Проверка на отчета на сървъра на ImoNavi (AMD SEV-SNP)") sub = p.add_subparsers(dest="cmd", required=True) v = sub.add_parser("verify") v.add_argument("report", help="отчетът (report.bin)") v.add_argument("claims", help="твърденията (claims.json) — точно байтовете от страницата") v.add_argument("--nonce", help="числото, което ТИ даде на страницата") v.add_argument("--product", choices=sorted(ARK_SHA256)) v.add_argument("--expected", help="measurements.json (публикуваните измервания)") a = p.parse_args(argv) report = Path(a.report).read_bytes() rep = parse(report) prod = a.product or product(rep) vcek, chain = fetch_certs(rep, prod) expected = json.loads(Path(a.expected).read_text(encoding="utf-8"))["measurements"] if a.expected else None out = verify(report, vcek, chain, prod, Path(a.claims).read_bytes(), a.nonce, expected) for c in out["checks"]: mark = {True: "✓", False: "✗", None: "?"}[c["ok"]] print(f"{mark} {c['name']}" + (f" — {c['detail']}" if c["detail"] else "")) print(f"\nИзмерване: {out['measurement']}\n{'ВСИЧКО Е ПРОВЕРЕНО' if out['ok'] else 'НЕ всичко е проверено/вярно'}") return 0 if out["ok"] else 1 if __name__ == "__main__": sys.exit(main())