简介:这是一份面向计算机相关专业本科生的毕业设计级电子投票系统实现方案,聚焦密码学前沿应用——门限环签名技术,解决匿名性、可验证性与容错性兼顾的投票安全需求,适用于毕设、课程设计或密码学实践学习。资源包共104个文件,含24个核心Python源码(涵盖密钥生成、签名验证、投票流程等模块)、37个编译后pyc文件、17张界面与流程图(如login.jpeg、vote.jpg、keyGen.png),以及SQL数据库脚本、README.md说明文档和XML配置文件,整体仅331KB,轻量易部署。已有173人下载学习,项目经实际运行测试与答辩评审,平均分达96分,代码稳定可靠。读者可直接运行调试,快速掌握门限环签名在分布式场景下的工程落地逻辑,并基于现有结构扩展功能,如添加审计模块或适配区块链存证。
1. 门限环签名不是“匿名投票”的代名词,而是让投票者身份可追溯但不可单点识别的关键机制
电子投票系统常被误认为只要“加密+匿名”就万事大吉,但真实场景中,既要防止选民身份被泄露,又要确保出现争议时能协同验证、追责到具体子集——这正是门限环签名(Threshold Ring Signature)的核心价值。它不像普通环签名那样完全隐藏签名者所属群组,也不像门限签名那样要求固定成员共同参与;它把两者融合:任意 t 个成员(t < n)即可联合生成一个无法定位单个签名者的环签名,而一旦发生舞弊或争议,授权方可通过阈值机制协作解出实际参与签名的成员子集。本项目用 Python 实现了这一密码学原语,并完整嵌入投票生命周期:从密钥生成、选民注册、匿名投票、计票验证到结果审计,所有环节均基于同态加密与门限环签名组合设计,数据库 SQL 脚本已预置角色表、选票表、公钥环表及审计日志表。适合计算机类专业学生做毕设、课程设计或安全方向入门实践,尤其适合作为「密码学应用落地」的具象案例——不堆砌理论,每行代码都对应一个可验证的安全目标。
2. 门限环签名的 Python 实现:从数学原理到ring_sig.py的关键函数拆解
门限环签名并非标准库函数,必须基于椭圆曲线离散对数问题(ECDLP)手工构造。本项目选用 secp256k1 曲线(与 Bitcoin 同源),通过ecdsa和pycryptodome库完成底层运算,核心逻辑封装在ring_sig.py中。理解其实现前,需明确三个不可绕过的数学前提:一是环签名依赖的“随机环链”结构(即 Schnorr-type 链式挑战响应),二是门限机制引入的拉格朗日插值重构私钥片段,三是同态加法支持的票数聚合——三者缺一不可。
2.1 环签名基础:为什么必须用环链而非单点哈希?
普通数字签名验证依赖(m, sig, pk)三元组,而环签名要使验证者确信签名来自某未知子集R = {pk₁, pk₂, ..., pkₙ}中至少一人,且无法指出具体是谁。本项目采用改进型的 Liu–Wu–Wang 环签名方案,其核心是构造一个闭合环形挑战链:
# ring_sig.py 片段:环链初始化(简化版) def init_ring_chain(msg, pubkeys, k): """msg: 投票明文;pubkeys: 公钥列表;k: 随机掩码种子""" n = len(pubkeys) # 步骤1:生成n个随机数v_i,仅v_0公开,其余由签名者本地持有 v = [os.urandom(32) for _ in range(n)] v[0] = hashlib.sha256(msg.encode() + v[-1]).digest() # 闭合环 # 步骤2:逐轮计算挑战c_i = H(v_i || m || pk_i),形成环状依赖 c = [None] * n c[0] = hashlib.sha256(v[0] + msg.encode() + pubkeys[0].encode()).digest() for i in range(1, n): c[i] = hashlib.sha256(v[i] + msg.encode() + pubkeys[i].encode()).digest() return v, c提示:此处
v[0]的生成必须依赖v[-1],否则环链断裂,攻击者可逆向推导出签名者位置。原始论文中该步骤易被忽略,本项目在README.md第7行特别标注了环闭合校验逻辑。
2.2 门限机制注入:如何让 t 个成员协作生成单个环签名?
纯环签名允许任意一人签名,但无法满足“需多人共识才可投票”的治理需求。本项目通过门限秘密共享(Shamir's Secret Sharing)将签名私钥x拆分为n个份额,任取t个即可重构x,但少于t个则信息论安全。关键修改在签名阶段:
# ring_sig.py 片段:门限签名生成(含拉格朗日插值) def threshold_sign(msg, priv_shares, pubkeys, t, n): """ priv_shares: [(i, share_i)] 列表,长度 >= t pubkeys: 对应公钥列表,长度 == n t: 门限值(如 t=3, n=5 表示需3人同意) """ # 步骤1:从 priv_shares 中随机选 t 个份额 selected = random.sample(priv_shares, t) # 步骤2:用拉格朗日插值重构私钥 x x_recon = 0 for i, share_i in selected: # l_i(x) = Π_{j≠i} (x - j) / (i - j),此处 x=0 numerator = 1 denominator = 1 for j, _ in selected: if j != i: numerator = (numerator * (0 - j)) % CURVE_ORDER denominator = (denominator * (i - j)) % CURVE_ORDER l_i = (numerator * pow(denominator, -1, CURVE_ORDER)) % CURVE_ORDER x_recon = (x_recon + l_i * share_i) % CURVE_ORDER # 步骤3:用重构私钥 x_recon 执行标准环签名 return standard_ring_sign(msg, x_recon, pubkeys) # CURVE_ORDER 来自 secp256k1:0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141参数说明:
priv_shares:每个选民注册时由可信中心分发的私钥份额,格式为(index, value),index为整数编号(1~n),value为模CURVE_ORDER下的整数;t和n在config.py中硬编码,默认t=3, n=5,表示5人环中至少3人参与才视为有效投票;standard_ring_sign()是前述环链构造函数,此处复用但输入私钥已变为重构值。
2.3 同态聚合:如何在不解密前提下统计票数?
选票本身是加密的(使用 Paillier 同态加密),但门限环签名需对明文msg签名。本项目采用“签名-加密分离”架构:先对明文投票项(如"candidate_A")生成环签名,再将该签名连同加密后的选票一起提交。服务端验证签名有效性后,直接对密文执行同态加法:
# vote_handler.py 片段:服务端计票逻辑 def tally_encrypted_votes(encrypted_ballots): """ encrypted_ballots: [Enc(0), Enc(1), Enc(0), Enc(1), ...] 返回 Enc(total_count),无需解密单张票 """ total_enc = encrypted_ballots[0] for enc in encrypted_ballots[1:]: total_enc = paillier.add(total_enc, enc) # 同态加法 return total_enc # 解密仅在最终公示阶段由阈值私钥持有者协作完成 def decrypt_tally(encrypted_tally, priv_shares, t, n): shares = random.sample(priv_shares, t) sk_recon = lagrange_interpolate(shares) # 同2.2节插值逻辑 return paillier.decrypt(encrypted_tally, sk_recon)注意:Paillier 密钥长度设为 1024bit(见
paillier_keygen.py),虽低于生产级 2048bit,但满足毕设性能与安全性平衡;若需提升,修改KEY_SIZE = 1024即可,但生成时间将增加约4倍。
3. 数据库建模与 SQL 脚本解析:从voting_system.sql到可审计的投票流水
本项目数据库采用 MySQL 8.0 兼容语法,voting_system.sql文件包含 5 张核心表:users(选民身份)、candidates(候选人)、public_keys(环公钥池)、votes(加密选票+签名)、audit_log(操作留痕)。设计原则是“写时约束、读时验证”,所有关键字段均设NOT NULL与CHECK,避免运行时逻辑补丁。
3.1public_keys表:环签名的物理载体
该表存储每个投票环的公钥集合,是环签名验证的输入源。结构如下:
CREATE TABLE public_keys ( id INT PRIMARY KEY AUTO_INCREMENT, ring_id VARCHAR(32) NOT NULL COMMENT '环ID,如 RING_2024_001', user_id INT NOT NULL COMMENT '所属用户ID,外键关联 users.id', public_key TEXT NOT NULL COMMENT 'PEM 格式公钥,含-----BEGIN PUBLIC KEY-----', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_ring_user (ring_id, user_id), KEY idx_ring_id (ring_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键设计点:
UNIQUE KEY uk_ring_user强制同一环内用户公钥唯一,防止重复注册;idx_ring_id加速按环查询——验证签名时需一次性加载整个环的公钥列表,索引缺失会导致全表扫描。
3.2votes表:加密选票与签名的绑定存储
此表是系统核心,字段设计直指安全目标:
CREATE TABLE votes ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vote_hash CHAR(64) NOT NULL COMMENT '选票明文 SHA256,用于防重放', encrypted_vote TEXT NOT NULL COMMENT 'Paillier 加密后的选票(Base64)', ring_signature TEXT NOT NULL COMMENT '门限环签名的 JSON 序列化,含 v,c 数组', ring_id VARCHAR(32) NOT NULL COMMENT '对应 public_keys.ring_id', voter_ids JSON NOT NULL COMMENT '参与签名的用户ID数组,如 [101,102,105]', status ENUM('pending','verified','invalid') DEFAULT 'pending' COMMENT '状态机', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_ring_status (ring_id, status), UNIQUE KEY uk_vote_hash (vote_hash) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段安全逻辑说明:
vote_hash:客户端提交前计算SHA256(candidate_name + timestamp),服务端拦截重复哈希值,杜绝刷票;voter_ids:JSON 存储实际参与签名的用户 ID 列表(非全部环成员),由签名模块生成并写入,是争议审计的直接依据;status:三态机控制流程,pending→verified需调用verify_ring_signature()函数,失败则置为invalid并记录日志;UNIQUE KEY uk_vote_hash:强制哈希唯一,比单纯INSERT IGNORE更可靠,避免并发插入冲突。
3.3audit_log表:满足“可验证、可追溯、不可抵赖”审计要求
所有敏感操作均落库留痕,字段设计拒绝模糊描述:
CREATE TABLE audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, action_type ENUM('key_gen','vote_submit','tally_start','tally_end','admin_login') NOT NULL, actor_id INT COMMENT '操作者ID,users.id 或 NULL(系统自动)', target_id VARCHAR(64) COMMENT '目标ID,如 ring_id/vote_id', details JSON COMMENT '结构化详情,如 {"candidate":"A","t":3,"n":5}', ip_address VARCHAR(45) COMMENT 'IPv4/IPv6 地址', user_agent TEXT COMMENT '浏览器/客户端标识', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_action_time (action_type, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;实战技巧:答辩时演示审计能力,可执行以下 SQL 快速定位异常:
-- 查询某环内所有投票及签名者 SELECT v.id, v.voter_ids, u.username FROM votes v JOIN audit_log a ON v.id = CAST(a.target_id AS UNSIGNED) AND a.action_type='vote_submit' JOIN users u ON JSON_CONTAINS(v.voter_ids, CAST(u.id AS JSON)) WHERE v.ring_id = 'RING_2024_001';
4. 运行环境搭建与README.md关键步骤实操指南
项目依赖明确,但 Python 版本与库版本存在隐性冲突点。经实测,仅支持 Python 3.8–3.10(因pycryptodome3.18.0+ 不兼容 3.11 的typing模块),且必须关闭pip的依赖冲突检查,否则ecdsa与pycryptodome会相互覆盖。
4.1 环境初始化:四步完成零错误部署
创建隔离环境(推荐 conda,避免污染全局):
conda create -n voting-env python=3.9 conda activate voting-env安装指定版本依赖(
requirements.txt已锁定关键版本):pip install --no-deps -r requirements.txt # 单独安装 pycryptodome(避免依赖树冲突) pip install pycryptodome==3.18.0初始化数据库(假设 MySQL 服务已运行,root 密码为
123456):mysql -uroot -p123456 < voting_system.sql # 修改 config.py 中 DATABASE_URL 为:mysql+pymysql://root:123456@localhost:3306/voting_db生成密钥环并启动服务:
python key_gen.py --ring-id RING_2024_001 --n 5 --t 3 # 该命令在 keys/ 目录下生成 5 对密钥,并写入 public_keys 表 python app.py # 访问 http://127.0.0.1:5000 即可登录
注意:
key_gen.py的--n 5 --t 3参数必须与config.py中THRESHOLD_N和THRESHOLD_T一致,否则签名验证失败。项目默认值已统一,但二次开发时务必同步修改。
4.2 登录与投票流程:前端交互背后的三次关键校验
系统前端基于 Flask + Bootstrap 构建,无 JS 框架依赖,所有校验分三层:
| 校验层级 | 触发时机 | 校验内容 | 失败响应 |
|---|---|---|---|
| 前端 JS | 表单提交前 | 用户名/密码非空、邮箱格式、选票选项存在 | alert()提示,阻止提交 |
| Flask Route | 接收 POST 请求后 | Session 是否有效、用户是否在当前环内、vote_hash是否已存在 | 返回400 Bad Request,JSON 错误码 |
| 数据库事务 | 写入votes表前 | INSERT ... ON DUPLICATE KEY UPDATE捕获uk_vote_hash冲突 | 记录audit_log,状态置为invalid |
验证方法:打开浏览器开发者工具 → Network 标签 → 提交投票 → 查看/vote请求的 Response,成功返回{"status":"success","vote_id":123},失败则返回{"error":"vote_hash_exists"}。
5. 安全边界与典型故障排查:当verify_ring_signature()返回 False 时该查什么
门限环签名验证失败是毕设调试最高频问题,根源往往不在密码学逻辑,而在数据序列化与类型转换。本节聚焦三个真实发生过的故障点,给出可立即执行的诊断命令。
5.1 公钥格式不匹配:PEM 解析失败导致KeyError: 'pointQ'
现象:app.py启动后,任何投票请求均返回500 Internal Server Error,日志显示ValueError: No PEM header found。
根因:public_keys.public_key字段存入的是 OpenSSL 命令生成的-----BEGIN EC PRIVATE KEY-----格式,但验证函数期望-----BEGIN PUBLIC KEY-----。key_gen.py默认生成私钥 PEM,需手动提取公钥:
# 修正命令(在 keys/ 目录下执行) openssl ec -in user_101.key -pubout -out user_101.pub # 然后将 user_101.pub 内容(含 BEGIN/END 行)粘贴至数据库 public_keys.public_key 字段验证命令:连接 MySQL,执行
SELECT LENGTH(public_key) FROM public_keys WHERE id=1;,正常值应 > 300(含换行符),若 < 100 则大概率格式错误。
5.2 环签名 JSON 解析失败:json.decoder.JSONDecodeError
现象:votes.ring_signature字段存入后为乱码,验证时抛出JSONDecodeError: Expecting value。
根因:Pythonjson.dumps()默认不处理 bytes 类型,而环签名中的v和c数组是bytes,直接序列化会转成 base64 字符串但未声明编码。修复方式在ring_sig.py的签名输出处:
# 错误写法(导致 JSON 无法解析) return json.dumps({'v': v, 'c': c}) # 正确写法(显式编码 bytes) def serialize_signature(v, c): return json.dumps({ 'v': [b64encode(x).decode() for x in v], 'c': [b64encode(x).decode() for x in c] })快速检测:在 MySQL 中执行
SELECT ring_signature FROM votes LIMIT 1;,若返回结果含\x00\x01等二进制字符,则未正确 base64 编码。
5.3 门限阈值不一致:lagrange_interpolate()计算结果为 0
现象:签名生成成功,但验证始终失败,verify_ring_signature()中c_final与c[0]不等。
根因:拉格朗日插值时模数CURVE_ORDER使用错误。secp256k1 的阶n与p(素域模数)不同,插值必须用n,而非p。项目中CURVE_ORDER已正确定义,但若修改曲线,必须同步更新:
# 正确值(secp256k1) CURVE_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 # 错误值(常见误用 p 值) # CURVE_P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f验证脚本:运行
python -c "from ring_sig import CURVE_ORDER; print(hex(CURVE_ORDER))",输出必须与上述十六进制完全一致。
运行python app.py后访问http://127.0.0.1:5000,使用admin/admin123登录,进入「密钥管理」页面点击「生成新环」,系统将自动创建 5 个测试用户并分配密钥,此时即可开始第一轮真实投票流程。
本文还有配套的精品资源,点击获取