发送至Pyodide时数据验证失败,SQLite存储签名后验证异常
ECDSA签名存入SQLite后验证失败问题排查与解决方案
你的代码本地运行时签名验证正常,但将签名经过Base64编码存入SQLite再取回解码后验证失败,核心问题大概率出在签名的编码/解码环节或者数据库存储的字符串处理上,以下是具体的排查和解决方法,同时适配Flask+Pyodide的认证场景。
核心问题分析
- Base64编码/解码不匹配:如果存储时用了普通Base64编码,取回时误用了URL安全的Base64解码,或者编码后没有正确转为UTF-8字符串存储,会导致解码后的签名二进制数据和原签名不一致。
- SQLite字符串存储的隐性修改:SQLite存储字符串时如果没有正确处理特殊字符(比如Base64中的
+、/),可能会被自动转义或截断,导致取回的Base64字符串失真。 - 手动哈希的一致性问题:你当前手动计算消息摘要的方式,若在存储消息或取回消息时编码不一致(比如字符串转bytes用了不同编码),会导致摘要不匹配,进而验证失败。
修正后的实现方案
1. 规范签名的编码/解码流程
签名是二进制bytes类型,不能直接存入SQLite,必须先转为Base64字符串;取回时再解码回bytes。使用标准的Base64编码(避免URL安全变体,除非你明确需要):
import base64 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key import sqlite3 import os # 用os.path.join处理路径,适配跨平台 private_key_path = os.path.join("CA_test", "private_key.pem") public_key_path = os.path.join("CA_test", "public_key.pem") with open(private_key_path, 'rb') as f: private_key = load_pem_private_key(f.read(), password=None) with open(public_key_path, 'rb') as f: public_key = load_pem_public_key(f.read()) # 待签名的消息(统一用utf-8转bytes) message = b'14789' # 生成签名(推荐直接传入消息,让库自动计算哈希,避免手动哈希出错) signature = private_key.sign(message, ec.ECDSA(hashes.SHA256())) # 将签名转为Base64字符串,用于存入SQLite signature_b64 = base64.b64encode(signature).decode('utf-8') # 存入SQLite示例 conn = sqlite3.connect('auth.db') cursor = conn.cursor() # 先创建表(首次运行) cursor.execute("CREATE TABLE IF NOT EXISTS user_auth (message TEXT PRIMARY KEY, signature TEXT)") cursor.execute("INSERT OR REPLACE INTO user_auth (message, signature) VALUES (?, ?)", (message.decode('utf-8'), signature_b64)) conn.commit() conn.close() # 从SQLite取回并验证示例 conn = sqlite3.connect('auth.db') cursor = conn.cursor() cursor.execute("SELECT signature FROM user_auth WHERE message = ?", (message.decode('utf-8'),)) row = cursor.fetchone() stored_signature_b64 = row[0] conn.close() # 解码Base64字符串回签名bytes stored_signature = base64.b64decode(stored_signature_b64) # 验证签名(同样直接传入消息,库自动计算哈希) try: public_key.verify(stored_signature, message, ec.ECDSA(hashes.SHA256())) print('验证通过,用户认证成功') except Exception as e: print(f'验证失败,认证失败: {str(e)}')
2. Flask后端适配要点
在Flask接口中,要确保传递的签名Base64字符串不被篡改,比如:
- 接收前端(Pyodide)传来的消息和签名时,用
request.get_json()获取原始JSON数据,避免表单编码导致的字符转义 - 返回签名给前端时,直接返回Base64字符串,不要额外添加换行或空格
示例Flask接口:
from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/auth/sign', methods=['POST']) def generate_signature(): data = request.get_json() message = data['message'].encode('utf-8') # 加载密钥(建议缓存密钥,不要每次请求都读取文件) with open(private_key_path, 'rb') as f: private_key = load_pem_private_key(f.read(), password=None) signature = private_key.sign(message, ec.ECDSA(hashes.SHA256())) signature_b64 = base64.b64encode(signature).decode('utf-8') # 存入数据库 conn = sqlite3.connect('auth.db') cursor = conn.cursor() cursor.execute("INSERT OR REPLACE INTO user_auth (message, signature) VALUES (?, ?)", (data['message'], signature_b64)) conn.commit() conn.close() return jsonify({'signature': signature_b64}) @app.route('/auth/verify', methods=['POST']) def verify_signature(): data = request.get_json() message = data['message'].encode('utf-8') stored_signature_b64 = data['signature'] try: stored_signature = base64.b64decode(stored_signature_b64) with open(public_key_path, 'rb') as f: public_key = load_pem_public_key(f.read()) public_key.verify(stored_signature, message, ec.ECDSA(hashes.SHA256())) return jsonify({'status': 'success', 'message': '认证通过'}) except Exception as e: return jsonify({'status': 'fail', 'message': f'认证失败: {str(e)}'}), 401 if __name__ == '__main__': app.run(debug=True)
3. Pyodide前端适配要点
在Pyodide中,确保消息编码和签名的Base64处理和后端一致:
- 将用户输入的消息转为UTF-8编码的字符串
- 后端返回的签名直接用于验证,不要额外处理
示例Pyodide代码片段:
async function verifyAuth() { const message = "14789"; // 先请求签名(或从本地存储获取) const signResponse = await fetch('/auth/sign', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({message: message}) }); const signData = await signResponse.json(); const signature = signData.signature; // 请求验证 const verifyResponse = await fetch('/auth/verify', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({message: message, signature: signature}) }); const verifyData = await verifyResponse.json(); console.log(verifyData.message); }
关键注意事项
- 避免手动计算哈希:cryptography的
sign和verify方法可以直接处理原始消息,自动计算正确的SHA-256哈希,手动计算容易因为编码、字节顺序等问题出错。 - 统一字符编码:所有字符串转bytes的操作都用
utf-8编码,避免因编码不一致导致的消息哈希差异。 - 检查SQLite存储的字符串:可以直接查询数据库查看存储的Base64字符串是否和生成的完全一致,排除数据库自动修改的可能。
内容的提问来源于stack exchange,提问作者Mr.Deadpool
相关产品推荐
相关产品推荐

