# 加解密流程与原理说明

---

## 一、整体密钥层次

本系统采用**三层密钥体系**，每一层保护下一层：

```
RSA 私钥 (vendor_private.pem)
    │  用于签名授权文件，防篡改
    ▼
vendor_secret (32字节随机数)
    │  用于派生 DEK 加密密钥，保护 DEK
    ▼
DEK — Data Encryption Key (32字节随机数)
    │  用于直接加密模型权重文件
    ▼
模型权重文件 (.safetensors / .gguf)
```

**为什么要三层？**

| 如果只有一层 | 问题 |
|------------|------|
| 直接用 RSA 私钥加密模型 | RSA 只能加密小数据（几百字节），无法处理 GB 级文件 |
| 直接把 DEK 明文放在授权文件里 | 任何人拿到授权文件就能解密模型，无法区分客户 |
| 直接硬编码密钥 | 一旦泄露所有客户的模型都被破解 |

三层的好处：**DEK 可以为每个模型独立生成，vendor_secret 可以为不同客户独立分发，RSA 私钥永远不离开厂商**。

---

## 二、加密流程（厂商端）

### 第一步：生成 DEK

```python
# crypto.py: generate_dek()
dek = AESGCM.generate_key(bit_length=256)
# dek = 32字节纯随机数，由 OS 的 CSPRNG 生成
```

- DEK 是 256 位（32 字节）纯随机数
- 每个模型独立生成一个 DEK
- 同一个模型的所有权重分片共用同一个 DEK

---

### 第二步：用 DEK 加密模型文件（AES-256-GCM 流式加密）

文件被切分为 64MB 的 chunk，逐块加密：

```
原始文件
├── [Chunk 1: 64MB]
├── [Chunk 2: 64MB]
├── [Chunk 3: 64MB]
└── [Chunk N: 剩余]
```

**加密后的文件格式：**

```
┌─────────────────────────────────────────┐
│              文件头 (32字节)              │
│  magic:     8B  "VLLMENC\x00"           │  ← 文件类型标识
│  version:   1B  0x01                    │  ← 格式版本
│  algorithm: 1B  0x01 (AES-256-GCM)     │  ← 加密算法
│  key_id:   16B  UUID                   │  ← 关联哪个 DEK
│  reserved:  6B  (留用)                  │
├─────────────────────────────────────────┤
│              Chunk 1                    │
│  size:   4B  (ciphertext 长度)          │  ← 小端序 uint32
│  nonce: 12B  随机数                     │  ← 每个 chunk 独立随机
│  data:   NB  ciphertext + 16B GCM tag  │  ← 密文 + 完整性标签
├─────────────────────────────────────────┤
│              Chunk 2                    │
│  ...                                    │
└─────────────────────────────────────────┘
```

**单个 chunk 的加密过程：**

```
plaintext (64MB)
    │
    ├── nonce = os.urandom(12)  ← 每次加密生成新的随机 nonce
    │
    ▼
AES-256-GCM.encrypt(key=DEK, nonce=nonce, data=plaintext)
    │
    ▼
ciphertext (64MB) + GCM_tag (16B)
```

**为什么每个 chunk 用不同的 nonce？**

GCM 模式的安全性要求：**同一个密钥下，nonce 绝对不能重复**。用随机 nonce（12字节 = 96位）重复概率极低（$2^{-96}$），同时支持并行加密多个文件。

**GCM tag 的作用？**

tag 是密文的**完整性认证码（MAC）**。解密时如果密文被篡改，tag 验证失败，立即报错。这保证了：
- 篡改检测：任何一个 bit 被修改都会被发现
- 认证加密：只有持有正确 DEK 的人才能通过验证

---

### 第三步：保护 DEK（HKDF + AES-256-GCM）

DEK 不能明文存储，需要用 `vendor_secret` 加密后存入授权文件：

```
vendor_secret (32字节随机数)
    │
    ▼
HKDF-SHA256(
    ikm   = vendor_secret,      ← 输入密钥材料
    salt  = license_id (UUID),  ← 每个授权独立的盐值
    info  = b"vllm-model-security-dek-key",
    length = 32
)
    │
    ▼
dek_key (32字节)                ← 每个授权派生出不同的 dek_key
    │
    ▼
AES-256-GCM.encrypt(key=dek_key, nonce=随机12B, data=DEK)
    │
    ▼
dek_encrypted (base64)          ← 存入 .lic 授权文件
```

**为什么用 HKDF 而不是直接用 vendor_secret 加密 DEK？**

因为同一个 vendor_secret 可能被用来签发多个客户的授权文件。如果直接用 vendor_secret 加密，所有客户的 dek_encrypted 用同一个密钥加密，一旦某个客户通过分析多个授权文件找到规律，可能还原 DEK。

用 `license_id`（每个授权的唯一 UUID）作为 HKDF 的 salt，确保每个授权派生出**不同的** dek_key，即使使用相同的 vendor_secret。

**vendor_secret.bin 是什么？**

`vendor_secret.bin` 是一个 32 字节的随机数文件，在第一次为某个模型签发授权时自动生成：

```python
vendor_secret = os.urandom(32)          # OS 的 CSPRNG 生成
open("vendor_secret.bin", "wb").write(vendor_secret)
```

它是整个 DEK 保护链的"根密钥"，安全级别等同于 `vendor_private.pem`：

| 文件 | 泄露后果 |
|------|---------|
| `vendor_private.pem` 泄露 | 攻击者可伪造任意授权文件 |
| `vendor_secret.bin` 泄露 | 攻击者可从任意授权文件中还原 DEK，进而解密模型 |

**vendor_secret 与模型的对应关系：**

- 同一个模型的**所有客户授权**共用同一个 `vendor_secret`
- 不同模型应使用**不同的** `vendor_secret`（各自独立生成）
- 续签授权时必须复用原来的 `vendor_secret`，否则现有客户无法解密

```bash
# 第一次签发（自动生成 vendor_secret.bin）
vllm-gen-license --model-dir ./model-encrypted --vendor-key vendor_private.pem ...

# 后续续签（必须复用）
vllm-gen-license --model-dir ./model-encrypted --vendor-key vendor_private.pem \
    --vendor-secret @vendor_secret.bin ...
```

**运行时传递方式（客户端）：**

```bash
# 推荐：环境变量（不出现在命令行历史）
export VLLM_VENDOR_SECRET="$(xxd -p vendor_secret.bin | tr -d '\n')"

# 或通过 --model-loader-extra-config 传入 hex 字符串
```

### 第四步：签名授权文件（RSA-2048 PSS）

授权文件包含所有元数据（客户名、有效期、机器指纹等），用私钥签名：

```
license_payload = JSON({
    license_id, customer, model_key_id,
    dek_encrypted, dek_hash,
    issued_at, expire_at,
    fingerprint_policy, allowed_fingerprints,
    max_nodes
})
    │
    ▼
RSA-PSS.sign(
    key      = vendor_private_key,
    data     = SHA256(license_payload),
    padding  = PSS(MGF1-SHA256, salt=MAX),
)
    │
    ▼
signature (base64)              ← 追加到 .lic 文件
```

**为什么用 RSA-PSS 而不是 PKCS#1 v1.5？**

PSS（Probabilistic Signature Scheme）是现代标准，安全性更强，能抵抗已知的对 PKCS#1 v1.5 的攻击。

**dek_hash 的作用？**

授权文件中除了 dek_encrypted，还存了 DEK 的 SHA-256 哈希值。解密 DEK 之后会比对哈希，确保 DEK 解密正确、未损坏。

---

## 三、解密流程（客户端运行时）

### 第一步：验证授权签名

```
.lic 文件中的 license_payload
    │
    ▼
RSA-PSS.verify(
    key       = vendor_public_key,
    signature = .lic 中的 signature,
    data      = SHA256(license_payload),
)
    │
    ├── 验证失败 → 抛出异常，拒绝加载
    └── 验证通过 → 继续
```

此步确保授权文件没有被客户或第三方篡改。

---

### 第二步：校验有效期和机器指纹

```
expire_at ≥ today?
    ├── 否 → 检查是否在 3 天宽限期内
    │         ├── 是 → 打印 WARNING，继续
    │         └── 否 → 拒绝加载（或运行中触发 SIGTERM）
    └── 是 → 继续

fingerprint_policy == SINGLE?
    ├── get_machine_fingerprint()
    │   = SHA256(CPU_ID + MAC + hostname)
    ├── 不在 allowed_fingerprints → 拒绝加载
    └── 匹配 → 继续
```

---

### 第三步：还原 DEK

```
vendor_secret（来自环境变量 VLLM_VENDOR_SECRET）
    │
    ▼
HKDF-SHA256(ikm=vendor_secret, salt=license_id, ...)
    │
    ▼
dek_key
    │
    ▼
AES-256-GCM.decrypt(key=dek_key, data=dek_encrypted)
    │
    ▼
DEK (明文，32字节)
    │
    ▼
SHA256(DEK) == dek_hash ?
    ├── 不一致 → 抛出异常
    └── 一致 → DEK 可用
```

---

### 第四步：流式解密模型权重

```python
# 每个 .enc 文件的解密过程：

读取文件头 (32B)
  → 校验 magic bytes "VLLMENC\x00"
  → 校验 version, algorithm
  → 提取 key_id

循环读取每个 chunk：
  → 读 4B → chunk_size
  → 读 12B → nonce
  → 读 chunk_size B → ciphertext+tag

  → AES-256-GCM.decrypt(key=DEK, nonce=nonce, data=ciphertext+tag)
      ├── tag 验证失败 → 抛出异常（数据损坏或密钥错误）
      └── 成功 → yield plaintext (64MB)

所有 chunk 解密完 → 完整明文数据流交给 safetensors/GGUF 解析器
```

解密是**全流式**的，不需要先把整个文件解密到磁盘，内存中同时只有一个 64MB 的 chunk，适合处理 70B 以上的大模型。

---

## 四、机器指纹生成

```
CPU 型号字符串
    +
MAC 地址列表（排序后）
    +
主机名
    │
    ▼
SHA256(拼接字符串)
    │
    ▼
fingerprint (64位十六进制字符串)
```

指纹不包含 GPU UUID（默认），因为 GPU 可能被替换。指纹变化的常见原因：
- 更换网卡（MAC 地址变化）
- 修改主机名
- 虚拟机迁移（MAC/CPU 信息变化）

---

## 五、各算法选型说明

| 算法 | 用途 | 选型理由 |
|------|------|---------|
| AES-256-GCM | 模型文件加密、DEK 加密 | 认证加密（AEAD），同时保证机密性和完整性，硬件加速支持好 |
| HKDF-SHA256 | 从 vendor_secret 派生 dek_key | 标准密钥派生函数，将一个主密钥安全扩展为多个独立子密钥 |
| RSA-2048 PSS | 授权文件签名 | 非对称签名，公钥可公开分发，私钥留厂商，PSS 是现代安全标准 |
| SHA-256 | 机器指纹、DEK 完整性校验 | 标准哈希，单向不可逆 |

---

## 六、攻击面分析

| 攻击方式 | 防御机制 |
|---------|---------|
| 直接读取 .enc 文件 | AES-256-GCM 加密，无 DEK 无法解密 |
| 伪造/篡改授权文件 | RSA 签名验证，无私钥无法伪造 |
| 复制授权文件到其他机器 | 机器指纹绑定 |
| 截获 vendor_secret | 传输通道安全（加密邮件等），运行时只存环境变量 |
| 修改模型权重数据 | GCM tag 完整性验证，篡改立即被检测 |
| 授权到期继续使用 | 后台线程每 24 小时检查，宽限期后 SIGTERM |
| 重放旧授权文件 | expire_at 日期检查 |
