3. HTTPS安全下载
任务目标
在现有OTA客户端框架中,将固件下载通道从HTTP升级为HTTPS,实现服务器证书校验,确保固件传输过程的机密性与完整性。本案例聚焦于下载通道的安全改造,不涉及固件签名校验(属于另一独立环节)。
环境依赖
- 运行环境:Linux(Ubuntu 20.04+)或 macOS,需具备网络访问能力
- Python 3.8+,依赖库:
requests(HTTP客户端)、cryptography(生成测试证书) - 安装命令:
pip install requests cryptography
核心实现
以下代码实现一个完整的HTTPS固件下载客户端,包含:证书固定校验(使用预置CA证书)、下载进度回调、断点续传支持,以及针对证书验证失败、连接异常、校验和不匹配三类错误的明确处理。服务器地址通过环境变量注入,便于切换测试环境。
import os
import ssl
import hashlib
import tempfile
from pathlib import Path
import requests
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager
class FirmwareDownloader:
"""HTTPS固件下载客户端,支持证书固定与断点续传"""
def __init__(self, ca_cert_path: str, firmware_url: str, dest_path: str):
self.ca_cert = ca_cert_path
self.url = firmware_url
self.dest = Path(dest_path)
self.session = self._create_secure_session()
def _create_secure_session(self) -> requests.Session:
"""创建强制证书校验的会话,禁用证书警告"""
session = requests.Session()
session.verify = self.ca_cert # 固定CA证书,不使用系统默认
session.headers.update({"User-Agent": "OTA-Client/3.0"})
# 自定义适配器:仅允许TLS1.2+,禁用不安全重定向
class TlsAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = ssl.create_default_context(cafile=self.ca_cert)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
kwargs["ssl_context"] = ctx
return super().init_poolmanager(*args, **kwargs)
session.mount("https://", TlsAdapter())
return session
def download(self, expected_sha256: str = None) -> bool:
"""执行下载,返回是否成功。支持断点续传与完整性校验"""
headers = {}
if self.dest.exists():
headers["Range"] = f"bytes={self.dest.stat().st_size}-"
try:
with self.session.get(self.url, headers=headers, stream=True, timeout=(5, 30)) as resp:
resp.raise_for_status()
# 处理断点续传:服务器返回206或200
mode = "ab" if resp.status_code == 206 else "wb"
if resp.status_code == 200 and self.dest.exists():
self.dest.unlink() # 服务器不支持续传,重新下载
sha = hashlib.sha256()
with open(self.dest, mode) as f:
for chunk in resp.iter_content(chunk_size=64 * 1024):
if chunk:
f.write(chunk)
sha.update(chunk)
if expected_sha256:
actual = sha.hexdigest()
if actual != expected_sha256:
raise ValueError(f"SHA256 mismatch: expected {expected_sha256}, got {actual}")
return True
except requests.exceptions.SSLError as e:
print(f"[证书错误] 服务器证书验证失败: {e}")
except requests.exceptions.ConnectionError as e:
print(f"[连接错误] 无法建立安全连接: {e}")
except requests.exceptions.HTTPError as e:
print(f"[HTTP错误] 服务器返回异常状态: {e}")
except ValueError as e:
print(f"[完整性错误] {e}")
return False
# ---------- 最小验证入口 ----------
if __name__ == "__main__":
# 通过环境变量注入配置,避免硬编码
CA_PATH = os.environ.get("OTA_CA_CERT", "test_ca.pem")
FW_URL = os.environ.get("OTA_FW_URL", "https://localhost:8443/firmware.bin")
DEST_FILE = os.environ.get("OTA_DEST", "downloaded_fw.bin")
EXPECTED_SHA = os.environ.get("OTA_EXPECTED_SHA", "")
downloader = FirmwareDownloader(CA_PATH, FW_URL, DEST_FILE)
ok = downloader.download(expected_sha256=EXPECTED_SHA or None)
print(f"下载{'成功' if ok else '失败'}: {DEST_FILE}")
运行与验证
本案例需要本地HTTPS测试服务器。使用以下命令生成自签名CA与服务器证书,并启动一个简单的HTTPS文件服务(需另存为test_server.py):
# 1. 生成测试证书(仅用于本地验证)
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca_key.pem -out test_ca.pem -days 30 -subj "/CN=TestOTA_CA"
openssl req -newkey rsa:2048 -nodes -keyout server_key.pem -out server.csr -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA test_ca.pem -CAkey ca_key.pem -CAcreateserial -out server_cert.pem -days 7 -extfile <(echo "subjectAltName=DNS:localhost,IP:127.0.0.1")
# 2. 启动HTTPS文件服务器(Python 3.8+)
python3 -c "
import http.server, ssl, os
os.chdir('/tmp') # 放置firmware.bin的目录
with open('firmware.bin','wb') as f: f.write(b'OTA_TEST_FIRMWARE_'*1000) # 生成测试固件
srv = http.server.HTTPServer(('localhost', 8443), http.server.SimpleHTTPRequestHandler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain('server_cert.pem', 'server_key.pem')
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
print('HTTPS server on https://localhost:8443')
srv.serve_forever()
" &
sleep 1
# 3. 运行下载客户端(使用生成的CA证书)
export OTA_CA_CERT=test_ca.pem
export OTA_FW_URL=https://localhost:8443/firmware.bin
export OTA_DEST=/tmp/downloaded_fw.bin
export OTA_EXPECTED_SHA=$(sha256sum /tmp/firmware.bin | awk '{print $1}')
python3 firmware_downloader.py
通过条件:
- 终端输出
下载成功,且/tmp/downloaded_fw.bin与/tmp/firmware.bin的SHA256一致 - 将
OTA_CA_CERT指向错误CA文件(如server_cert.pem)重新运行,应输出[证书错误]且下载失败——证明证书校验生效 - 将
OTA_EXPECTED_SHA改为任意错误值,应输出[完整性错误]——证明校验和验证生效
常见问题
- 证书验证失败:确认客户端使用的CA文件与服务器证书由同一CA签发;服务器地址必须匹配证书中的
subjectAltName(使用localhost而非127.0.0.1可避免IP匹配问题) - 连接被拒绝:确认服务器已启动且端口未被占用;检查防火墙设置
- 断点续传不生效:部分简易服务器(如Python内置)不支持Range请求,此时客户端会自动回退为全量下载(代码已处理)