From 37610aeb2c49c07628c188292ac43a613bab6b42 Mon Sep 17 00:00:00 2001 From: wzj <244142824@qq.com> Date: Sat, 14 Jun 2025 08:47:23 +0800 Subject: [PATCH] first commit --- .env | 5 + .idea/.gitignore | 8 + .idea/certmgr.iml | 8 + .idea/deployment.xml | 28 + .idea/inspectionProfiles/Project_Default.xml | 16 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + __pycache__/config.cpython-38.pyc | Bin 0 -> 559 bytes app.py | 951 ++++++++++++++++++ config.py | 8 + requirements.txt | 4 + templates/base.html | 55 + templates/ca_detail.html | 124 +++ templates/ca_list.html | 47 + templates/certificate_detail.html | 148 +++ templates/certificate_list.html | 59 ++ templates/create_ca.html | 63 ++ templates/create_certificate.html | 91 ++ templates/export_certificate.html | 64 ++ templates/index.html | 40 + templates/login.html | 32 + templates/register.html | 43 + templates/renew_certificate.html | 29 + templates/revoke_certificate.html | 43 + 26 files changed, 1890 insertions(+) create mode 100644 .env create mode 100644 .idea/.gitignore create mode 100644 .idea/certmgr.iml create mode 100644 .idea/deployment.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 __pycache__/config.cpython-38.pyc create mode 100644 app.py create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 templates/base.html create mode 100644 templates/ca_detail.html create mode 100644 templates/ca_list.html create mode 100644 templates/certificate_detail.html create mode 100644 templates/certificate_list.html create mode 100644 templates/create_ca.html create mode 100644 templates/create_certificate.html create mode 100644 templates/export_certificate.html create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/register.html create mode 100644 templates/renew_certificate.html create mode 100644 templates/revoke_certificate.html diff --git a/.env b/.env new file mode 100644 index 0000000..206775f --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +FLASK_SECRET_KEY=your-secret-key +DB_HOST=192.168.31.11 +DB_NAME=cert_manager +DB_USER=root +DB_PASSWORD=Home123#$. \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/certmgr.iml b/.idea/certmgr.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/certmgr.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..b9c42ad --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..bc1a26a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d1e22ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4a68341 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/__pycache__/config.cpython-38.pyc b/__pycache__/config.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb9bc65206eb57b34830e04b14dc899a34b84331 GIT binary patch literal 559 zcmYjP%}(Pm5VoDP4S_BzEdsHJ71th0?`VZEsey`;E=fQls$_-OZByE$I4M%O!h_Hw zFTimh#MeFX3VT{RAZ^E*PxJF>W<2&)y>0^~zu&r}1_a=bc2>nu&XLmmy8;FnoPZFV z0*09J44CnRLdc*G8s@vo12@*VDi!4%Da{WRaG=;AMBEsgFBJxtaJJ1*SY<|7V`gZn z_}YtI;yT}0LRML#u+FOC7gkd}ELnB3bdU_C?Gwi%0X-)-k6VjuE}A)y1uvR+e9;_n z!5???`$vkl950|`r)(wzTAKddbk Rlh)6VeH^N90dh^V&_7!Gj?Dl7 literal 0 HcmV?d00001 diff --git a/app.py b/app.py new file mode 100644 index 0000000..61f1672 --- /dev/null +++ b/app.py @@ -0,0 +1,951 @@ +import os +import subprocess +from datetime import datetime, timedelta +from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from werkzeug.security import generate_password_hash, check_password_hash +import mysql.connector +from mysql.connector import Error +import random +import string +import uuid + +app = Flask(__name__) +app.secret_key = 'your-secret-key-here' + +# 数据库配置 +db_config = { + 'host': '192.168.31.11', + 'database': 'cert_manager', + 'user': 'root', + 'password': 'Home123#$.' +} + +# Flask-Login 配置 +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' + +# 确保证书存储目录存在 +CERT_STORE = os.path.join(os.path.dirname(__file__), 'cert_store') +os.makedirs(CERT_STORE, exist_ok=True) + + +class User(UserMixin): + pass + + +@login_manager.user_loader +def load_user(user_id): + try: + conn = mysql.connector.connect(**db_config) + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + user_data = cursor.fetchone() + + if user_data: + user = User() + user.id = user_data['id'] + user.username = user_data['username'] + user.is_admin = user_data['is_admin'] + return user + return None + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + + +# 辅助函数 +def get_db_connection(): + try: + conn = mysql.connector.connect(**db_config) + return conn + except Error as e: + print(f"Database connection error: {e}") + return None + + +def generate_captcha(): + # 生成6位随机验证码 + captcha_code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + # 清除旧的验证码 + cursor.execute("DELETE FROM captcha WHERE created_at < NOW() - INTERVAL 10 MINUTE") + # 插入新验证码 + cursor.execute("INSERT INTO captcha (code) VALUES (%s)", (captcha_code,)) + conn.commit() + return captcha_code + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return None + + +def verify_captcha(user_input): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute("SELECT code FROM captcha ORDER BY created_at DESC LIMIT 1") + result = cursor.fetchone() + if result and user_input.upper() == result[0]: + return True + return False + except Error as e: + print(f"Database error: {e}") + return False + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return False + + +def create_ca(ca_name, common_name, organization, organizational_unit, country, state, locality, key_size, days_valid, + created_by): + # 创建CA目录 + ca_dir = os.path.join(CERT_STORE, f"ca_{ca_name}") + os.makedirs(ca_dir, exist_ok=True) + + key_path = os.path.join(ca_dir, f"{common_name}.key") + cert_path = os.path.join(ca_dir, f"{common_name}.crt") + + # 生成CA私钥 + subprocess.run([ + 'openssl', 'genrsa', '-out', key_path, str(key_size) + ], check=True) + + # 生成CA自签名证书 + subprocess.run([ + 'openssl', 'req', '-new', '-x509', '-days', str(days_valid), + '-key', key_path, '-out', cert_path, + '-subj', f'/CN={common_name}/O={organization}/OU={organizational_unit}/C={country}/ST={state}/L={locality}' + ], check=True) + + # 保存到数据库 + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO certificate_authorities + (name, common_name, organization, organizational_unit, country, state, locality, + key_size, days_valid, cert_path, key_path, created_by) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (ca_name, common_name, organization, organizational_unit, country, state, locality, + key_size, days_valid, cert_path, key_path, created_by)) + conn.commit() + return cursor.lastrowid + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return None + + +def create_certificate(ca_id, common_name, san_dns, san_ip, organization, organizational_unit, + country, state, locality, key_size, days_valid, created_by): + # 获取CA信息 + ca = get_ca_by_id(ca_id) + if not ca: + return None + + # 创建证书目录 + cert_dir = os.path.join(CERT_STORE, f"certs_{common_name}") + os.makedirs(cert_dir, exist_ok=True) + + key_path = os.path.join(cert_dir, f"{common_name}.key") + csr_path = os.path.join(cert_dir, f"{common_name}.csr") + cert_path = os.path.join(cert_dir, f"{common_name}.crt") + + # 生成私钥 + subprocess.run([ + 'openssl', 'genrsa', '-out', key_path, str(key_size) + ], check=True) + + # 创建CSR配置文件 + csr_config = f""" + [req] + default_bits = {key_size} + prompt = no + default_md = sha256 + distinguished_name = dn + + [dn] + CN = {common_name} + O = {organization} + OU = {organizational_unit} + C = {country} + ST = {state} + L = {locality} + """ + + if san_dns or san_ip: + csr_config += "\nreq_extensions = req_ext\n[req_ext]\nsubjectAltName = @alt_names\n[alt_names]\n" + + if san_dns: + dns_entries = san_dns.split(',') + for i, dns in enumerate(dns_entries, 1): + csr_config += f"DNS.{i} = {dns.strip()}\n" + + if san_ip: + ip_entries = san_ip.split(',') + for i, ip in enumerate(ip_entries, 1): + csr_config += f"IP.{i} = {ip.strip()}\n" + + config_path = os.path.join(cert_dir, 'csr_config.cnf') + with open(config_path, 'w') as f: + f.write(csr_config) + + # 生成CSR + subprocess.run([ + 'openssl', 'req', '-new', '-key', key_path, '-out', csr_path, + '-config', config_path + ], check=True) + + # 使用CA签名证书 + subprocess.run([ + 'openssl', 'x509', '-req', '-in', csr_path, '-CA', ca['cert_path'], + '-CAkey', ca['key_path'], '-CAcreateserial', '-out', cert_path, + '-days', str(days_valid), '-sha256' + ], check=True) + + # 计算过期时间 + expires_at = datetime.now() + timedelta(days=days_valid) + + # 保存到数据库 + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO certificates + (common_name, san_dns, san_ip, organization, organizational_unit, country, state, locality, + key_size, days_valid, cert_path, key_path, csr_path, ca_id, created_by, expires_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (common_name, san_dns, san_ip, organization, organizational_unit, country, state, locality, + key_size, days_valid, cert_path, key_path, csr_path, ca_id, created_by, expires_at)) + conn.commit() + return cursor.lastrowid + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return None + + +def get_ca_by_id(ca_id): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM certificate_authorities WHERE id = %s", (ca_id,)) + return cursor.fetchone() + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return None + + +def get_certificate_by_id(cert_id): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM certificates WHERE id = %s", (cert_id,)) + return cursor.fetchone() + except Error as e: + print(f"Database error: {e}") + return None + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return None + + +def revoke_certificate(cert_id, reason): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute(""" + UPDATE certificates + SET status = 'revoked', revoked_at = NOW(), revocation_reason = %s + WHERE id = %s + """, (reason, cert_id)) + conn.commit() + return True + except Error as e: + print(f"Database error: {e}") + return False + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return False + + +def renew_certificate(cert_id, days_valid): + cert = get_certificate_by_id(cert_id) + if not cert: + return False + + ca = get_ca_by_id(cert['ca_id']) + if not ca: + return False + + # 使用现有CSR重新签名 + new_cert_path = os.path.join(os.path.dirname(cert['cert_path']), f"renewed_{cert['common_name']}.crt") + + subprocess.run([ + 'openssl', 'x509', '-req', '-in', cert['csr_path'], '-CA', ca['cert_path'], + '-CAkey', ca['key_path'], '-CAcreateserial', '-out', new_cert_path, + '-days', str(days_valid), '-sha256' + ], check=True) + + # 更新数据库 + new_expires_at = datetime.now() + timedelta(days=days_valid) + + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute(""" + UPDATE certificates + SET cert_path = %s, days_valid = %s, expires_at = %s, status = 'active', + revoked_at = NULL, revocation_reason = NULL + WHERE id = %s + """, (new_cert_path, days_valid, new_expires_at, cert_id)) + conn.commit() + return True + except Error as e: + print(f"Database error: {e}") + return False + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return False + + +def generate_crl(ca_id): + ca = get_ca_by_id(ca_id) + if not ca: + return False + + crl_path = os.path.join(os.path.dirname(ca['cert_path']), f"crl_{ca['name']}.pem") + + # 获取所有被吊销的证书 + revoked_certs = [] + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT cert_path FROM certificates + WHERE ca_id = %s AND status = 'revoked' + """, (ca_id,)) + revoked_certs = [row['cert_path'] for row in cursor.fetchall()] + except Error as e: + print(f"Database error: {e}") + return False + finally: + if conn.is_connected(): + cursor.close() + conn.close() + + # 创建索引文件 + index_file = os.path.join(os.path.dirname(ca['cert_path']), 'index.txt') + if not os.path.exists(index_file): + open(index_file, 'w').close() + + # 生成CRL + subprocess.run([ + 'openssl', 'ca', '-gencrl', '-out', crl_path, + '-keyfile', ca['key_path'], '-cert', ca['cert_path'], + '-crldays', '30' + ], check=True) + + # 更新数据库 + next_update = datetime.now() + timedelta(days=30) + + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + # 检查是否已有CRL记录 + cursor.execute("SELECT id FROM certificate_revocation_list WHERE ca_id = %s", (ca_id,)) + if cursor.fetchone(): + cursor.execute(""" + UPDATE certificate_revocation_list + SET crl_path = %s, last_updated = NOW(), next_update = %s + WHERE ca_id = %s + """, (crl_path, next_update, ca_id)) + else: + cursor.execute(""" + INSERT INTO certificate_revocation_list + (ca_id, crl_path, next_update) + VALUES (%s, %s, %s) + """, (ca_id, crl_path, next_update)) + conn.commit() + return True + except Error as e: + print(f"Database error: {e}") + return False + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return False + + +def export_pkcs12(cert_id, password): + cert = get_certificate_by_id(cert_id) + if not cert: + return None + + ca = get_ca_by_id(cert['ca_id']) + if not ca: + return None + + pkcs12_path = os.path.join(os.path.dirname(cert['cert_path']), f"{cert['common_name']}.p12") + + subprocess.run([ + 'openssl', 'pkcs12', '-export', '-out', pkcs12_path, + '-inkey', cert['key_path'], '-in', cert['cert_path'], + '-certfile', ca['cert_path'], '-passout', f'pass:{password}' + ], check=True) + + return pkcs12_path + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('index')) + + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + confirm_password = request.form['confirm_password'] + email = request.form.get('email', '') + captcha = request.form['captcha'] + + # 验证验证码 + if not verify_captcha(captcha): + flash('验证码错误', 'danger') + return redirect(url_for('register')) + + # 验证密码匹配 + if password != confirm_password: + flash('两次输入的密码不匹配', 'danger') + return redirect(url_for('register')) + + # 验证用户名是否已存在 + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor() + cursor.execute("SELECT id FROM users WHERE username = %s", (username,)) + if cursor.fetchone(): + flash('用户名已存在', 'danger') + return redirect(url_for('register')) + + # 创建新用户 + password_hash = generate_password_hash(password) + cursor.execute(""" + INSERT INTO users (username, password_hash, email, is_admin, is_active) + VALUES (%s, %s, %s, %s, %s) + """, (username, password_hash, email, False, True)) + conn.commit() + + flash('注册成功,请登录', 'success') + return redirect(url_for('login')) + except Error as e: + print(f"Database error: {e}") + flash('注册失败,请稍后再试', 'danger') + finally: + if conn.is_connected(): + cursor.close() + conn.close() + + captcha_code = generate_captcha() + return render_template('register.html', captcha_code=captcha_code) + +# 路由定义 +@app.route('/') +@login_required +def index(): + return render_template('index.html') + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('index')) + + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + captcha = request.form['captcha'] + + if not verify_captcha(captcha): + flash('验证码错误', 'danger') + return redirect(url_for('login')) + + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT * FROM users + WHERE username = %s AND is_active = TRUE + """, (username,)) + user_data = cursor.fetchone() + + if user_data and check_password_hash(user_data['password_hash'], password): + user = User() + user.id = user_data['id'] + user.username = user_data['username'] + user.is_admin = user_data['is_admin'] + login_user(user) + flash('登录成功', 'success') + return redirect(url_for('index')) + else: + flash('用户名或密码错误,或账户未激活', 'danger') + except Error as e: + print(f"Database error: {e}") + flash('登录失败,请稍后再试', 'danger') + finally: + if conn.is_connected(): + cursor.close() + conn.close() + + captcha_code = generate_captcha() + return render_template('login.html', captcha_code=captcha_code) + + +@app.route('/logout') +@login_required +def logout(): + logout_user() + flash('您已成功登出', 'success') + return redirect(url_for('login')) + + +@app.route('/cas') +@login_required +def ca_list(): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + if current_user.is_admin: + cursor.execute("SELECT * FROM certificate_authorities") + else: + cursor.execute("SELECT * FROM certificate_authorities WHERE created_by = %s", (current_user.id,)) + cas = cursor.fetchall() + return render_template('ca_list.html', cas=cas) + except Error as e: + print(f"Database error: {e}") + flash('获取CA列表失败', 'danger') + return redirect(url_for('index')) + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return redirect(url_for('index')) + + +@app.route('/cas/create', methods=['GET', 'POST']) +@login_required +def create_ca_view(): + if request.method == 'POST': + ca_name = request.form['ca_name'] + common_name = request.form['common_name'] + organization = request.form['organization'] + organizational_unit = request.form['organizational_unit'] + country = request.form['country'] + state = request.form['state'] + locality = request.form['locality'] + key_size = int(request.form['key_size']) + days_valid = int(request.form['days_valid']) + + ca_id = create_ca(ca_name, common_name, organization, organizational_unit, + country, state, locality, key_size, days_valid, current_user.id) + + if ca_id: + flash('CA创建成功', 'success') + return redirect(url_for('ca_list')) + else: + flash('CA创建失败', 'danger') + + return render_template('create_ca.html') + + +@app.route('/cas/') +@login_required +def ca_detail(ca_id): + ca = get_ca_by_id(ca_id) + if not ca: + flash('CA不存在', 'danger') + return redirect(url_for('ca_list')) + + # 检查权限 + if not current_user.is_admin and ca['created_by'] != current_user.id: + flash('无权访问此CA', 'danger') + return redirect(url_for('ca_list')) + + # 获取该CA颁发的证书 + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT * FROM certificates + WHERE ca_id = %s + ORDER BY created_at DESC + """, (ca_id,)) + certificates = cursor.fetchall() + + # 获取CRL信息 + cursor.execute(""" + SELECT * FROM certificate_revocation_list + WHERE ca_id = %s + """, (ca_id,)) + crl = cursor.fetchone() + + return render_template('ca_detail.html', ca=ca, certificates=certificates, crl=crl) + except Error as e: + print(f"Database error: {e}") + flash('获取CA详情失败', 'danger') + return redirect(url_for('ca_list')) + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return redirect(url_for('ca_list')) + + +@app.route('/cas//generate_crl') +@login_required +def generate_crl_view(ca_id): + ca = get_ca_by_id(ca_id) + if not ca: + flash('CA不存在', 'danger') + return redirect(url_for('ca_list')) + + # 检查权限 + if not current_user.is_admin and ca['created_by'] != current_user.id: + flash('无权操作此CA', 'danger') + return redirect(url_for('ca_list')) + + if generate_crl(ca_id): + flash('CRL生成成功', 'success') + else: + flash('CRL生成失败', 'danger') + + return redirect(url_for('ca_detail', ca_id=ca_id)) + + +@app.route('/cas//download_crl') +@login_required +def download_crl(ca_id): + ca = get_ca_by_id(ca_id) + if not ca: + flash('CA不存在', 'danger') + return redirect(url_for('ca_list')) + + # 检查权限 + if not current_user.is_admin and ca['created_by'] != current_user.id: + flash('无权操作此CA', 'danger') + return redirect(url_for('ca_list')) + + # 获取CRL路径 + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT crl_path FROM certificate_revocation_list + WHERE ca_id = %s + """, (ca_id,)) + crl = cursor.fetchone() + + if crl and os.path.exists(crl['crl_path']): + return send_from_directory( + os.path.dirname(crl['crl_path']), + os.path.basename(crl['crl_path']), + as_attachment=True + ) + else: + flash('CRL文件不存在', 'danger') + return redirect(url_for('ca_detail', ca_id=ca_id)) + except Error as e: + print(f"Database error: {e}") + flash('获取CRL失败', 'danger') + return redirect(url_for('ca_detail', ca_id=ca_id)) + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return redirect(url_for('ca_detail', ca_id=ca_id)) + + +@app.route('/certificates') +@login_required +def certificate_list(): + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + if current_user.is_admin: + cursor.execute(""" + SELECT c.*, ca.name as ca_name + FROM certificates c + JOIN certificate_authorities ca ON c.ca_id = ca.id + ORDER BY c.created_at DESC + """) + else: + cursor.execute(""" + SELECT c.*, ca.name as ca_name + FROM certificates c + JOIN certificate_authorities ca ON c.ca_id = ca.id + WHERE c.created_by = %s + ORDER BY c.created_at DESC + """, (current_user.id,)) + certificates = cursor.fetchall() + return render_template('certificate_list.html', certificates=certificates) + except Error as e: + print(f"Database error: {e}") + flash('获取证书列表失败', 'danger') + return redirect(url_for('index')) + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return redirect(url_for('index')) + + +@app.route('/certificates/create', methods=['GET', 'POST']) +@login_required +def create_certificate_view(): + if request.method == 'POST': + common_name = request.form['common_name'] + san_dns = request.form.get('san_dns', '') + san_ip = request.form.get('san_ip', '') + organization = request.form['organization'] + organizational_unit = request.form['organizational_unit'] + country = request.form['country'] + state = request.form['state'] + locality = request.form['locality'] + key_size = int(request.form['key_size']) + days_valid = int(request.form['days_valid']) + ca_id = int(request.form['ca_id']) + + cert_id = create_certificate(ca_id, common_name, san_dns, san_ip, organization, + organizational_unit, country, state, locality, + key_size, days_valid, current_user.id) + + if cert_id: + flash('证书创建成功', 'success') + return redirect(url_for('certificate_detail', cert_id=cert_id)) + else: + flash('证书创建失败', 'danger') + + # 获取可用的CA + conn = get_db_connection() + if conn: + try: + cursor = conn.cursor(dictionary=True) + if current_user.is_admin: + cursor.execute("SELECT id, name FROM certificate_authorities") + else: + cursor.execute(""" + SELECT id, name FROM certificate_authorities + WHERE created_by = %s + """, (current_user.id,)) + cas = cursor.fetchall() + return render_template('create_certificate.html', cas=cas) + except Error as e: + print(f"Database error: {e}") + flash('获取CA列表失败', 'danger') + return redirect(url_for('certificate_list')) + finally: + if conn.is_connected(): + cursor.close() + conn.close() + return redirect(url_for('certificate_list')) + + +@app.route('/certificates/') +@login_required +def certificate_detail(cert_id): + cert = get_certificate_by_id(cert_id) + if not cert: + flash('证书不存在', 'danger') + return redirect(url_for('certificate_list')) + + # 检查权限 + if not current_user.is_admin and cert['created_by'] != current_user.id: + flash('无权访问此证书', 'danger') + return redirect(url_for('certificate_list')) + + # 获取CA信息 + ca = get_ca_by_id(cert['ca_id']) + + return render_template('certificate_detail.html', cert=cert, ca=ca) + + +@app.route('/certificates//revoke', methods=['GET', 'POST']) +@login_required +def revoke_certificate_view(cert_id): + cert = get_certificate_by_id(cert_id) + if not cert: + flash('证书不存在', 'danger') + return redirect(url_for('certificate_list')) + + # 检查权限 + if not current_user.is_admin and cert['created_by'] != current_user.id: + flash('无权操作此证书', 'danger') + return redirect(url_for('certificate_list')) + + if cert['status'] == 'revoked': + flash('证书已被吊销', 'warning') + return redirect(url_for('certificate_detail', cert_id=cert_id)) + + if request.method == 'POST': + reason = request.form['reason'] + if revoke_certificate(cert_id, reason): + flash('证书吊销成功', 'success') + # 更新CRL + generate_crl(cert['ca_id']) + return redirect(url_for('certificate_detail', cert_id=cert_id)) + else: + flash('证书吊销失败', 'danger') + + return render_template('revoke_certificate.html', cert=cert) + + +@app.route('/certificates//renew', methods=['GET', 'POST']) +@login_required +def renew_certificate_view(cert_id): + cert = get_certificate_by_id(cert_id) + if not cert: + flash('证书不存在', 'danger') + return redirect(url_for('certificate_list')) + + # 检查权限 + if not current_user.is_admin and cert['created_by'] != current_user.id: + flash('无权操作此证书', 'danger') + return redirect(url_for('certificate_list')) + + if request.method == 'POST': + days_valid = int(request.form['days_valid']) + if renew_certificate(cert_id, days_valid): + flash('证书续期成功', 'success') + return redirect(url_for('certificate_detail', cert_id=cert_id)) + else: + flash('证书续期失败', 'danger') + + return render_template('renew_certificate.html', cert=cert) + + +@app.route('/certificates//export', methods=['GET', 'POST']) +@login_required +def export_certificate_view(cert_id): + cert = get_certificate_by_id(cert_id) + if not cert: + flash('证书不存在', 'danger') + return redirect(url_for('certificate_list')) + + # 检查权限 + if not current_user.is_admin and cert['created_by'] != current_user.id: + flash('无权操作此证书', 'danger') + return redirect(url_for('certificate_list')) + + if request.method == 'POST': + format_type = request.form['format'] + password = request.form.get('password', '') + + if format_type == 'pkcs12': + if not password: + flash('PKCS#12格式需要密码', 'danger') + return redirect(url_for('export_certificate_view', cert_id=cert_id)) + + pkcs12_path = export_pkcs12(cert_id, password) + if pkcs12_path: + return send_from_directory( + os.path.dirname(pkcs12_path), + os.path.basename(pkcs12_path), + as_attachment=True + ) + else: + flash('导出PKCS#12失败', 'danger') + elif format_type == 'pem': + # 合并证书和私钥为PEM + pem_content = "" + with open(cert['cert_path'], 'r') as f: + pem_content += f.read() + with open(cert['key_path'], 'r') as f: + pem_content += f.read() + + from io import StringIO + from flask import Response + return Response( + pem_content, + mimetype="application/x-pem-file", + headers={ + "Content-Disposition": f"attachment; filename={cert['common_name']}.pem" + } + ) + else: + flash('不支持的导出格式', 'danger') + + return render_template('export_certificate.html', cert=cert) + + +@app.route('/download/') +@login_required +def download_file(filename): + # 安全检查:确保文件路径在证书存储目录内 + file_path = os.path.join(CERT_STORE, filename) + if not os.path.exists(file_path): + flash('文件不存在', 'danger') + return redirect(url_for('index')) + + # 检查权限 + # 这里需要根据实际业务逻辑检查用户是否有权下载该文件 + # 简单示例:只允许下载自己的证书文件 + + return send_from_directory( + os.path.dirname(file_path), + os.path.basename(file_path), + as_attachment=True + ) + + +if __name__ == '__main__': + app.run(debug=True, ssl_context='adhoc', host='0.0.0.0', port='9875') \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..fc8fd60 --- /dev/null +++ b/config.py @@ -0,0 +1,8 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here' + SQLALCHEMY_DATABASE_URI = 'sqlite:///cert_manager.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + CERTS_ROOT = os.path.join(os.path.dirname(__file__), 'certs') + CAPTCHA_ENABLED = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3334465 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.0.1 +Flask-Login==0.5.0 +mysql-connector-python==8.0.26 +python-dotenv==0.19.0 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..bead1ac --- /dev/null +++ b/templates/base.html @@ -0,0 +1,55 @@ + + + + + + 证书管理系统 - {% block title %}{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/ca_detail.html b/templates/ca_detail.html new file mode 100644 index 0000000..be68916 --- /dev/null +++ b/templates/ca_detail.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} + +{% block title %}{{ ca.name }} - CA详情{% endblock %} + +{% block content %} +
+

CA机构详情: {{ ca.name }}

+
+ 生成CRL + {% if crl %} + 下载CRL + {% endif %} +
+
+ +
+
+
+
+
基本信息
+
+
+
+
通用名
+
{{ ca.common_name }}
+ +
组织
+
{{ ca.organization }}
+ +
组织单位
+
{{ ca.organizational_unit or 'N/A' }}
+ +
国家
+
{{ ca.country }}
+ +
州/省
+
{{ ca.state or 'N/A' }}
+ +
城市
+
{{ ca.locality or 'N/A' }}
+
+
+
+
+ +
+
+
+
技术信息
+
+
+
+
密钥长度
+
{{ ca.key_size }}位
+ +
有效期
+
{{ ca.days_valid }}天
+ +
创建者
+
{{ ca.created_by }}
+ +
创建时间
+
{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}
+ +
证书路径
+
{{ ca.cert_path }}
+ +
私钥路径
+
{{ ca.key_path }}
+
+
+
+
+
+ +
+
+
颁发的证书
+ 创建证书 +
+
+
+ + + + + + + + + + + + + {% for cert in certificates %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
ID通用名状态有效期至创建时间操作
{{ cert.id }}{{ cert.common_name }} + {% if cert.status == 'active' %} + 有效 + {% elif cert.status == 'revoked' %} + 已吊销 + {% else %} + 已过期 + {% endif %} + {{ cert.expires_at.strftime('%Y-%m-%d') }}{{ cert.created_at.strftime('%Y-%m-%d') }} + 详情 +
该CA尚未颁发任何证书
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/ca_list.html b/templates/ca_list.html new file mode 100644 index 0000000..33603d1 --- /dev/null +++ b/templates/ca_list.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}CA机构列表{% endblock %} + +{% block content %} +
+

CA机构列表

+ 创建CA机构 +
+ +
+ + + + + + + + + + + + + + + {% for ca in cas %} + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
ID名称通用名组织有效期(天)创建者创建时间操作
{{ ca.id }}{{ ca.name }}{{ ca.common_name }}{{ ca.organization }}{{ ca.days_valid }}{{ ca.created_by }}{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }} + 详情 +
暂无CA机构
+
+{% endblock %} \ No newline at end of file diff --git a/templates/certificate_detail.html b/templates/certificate_detail.html new file mode 100644 index 0000000..258983e --- /dev/null +++ b/templates/certificate_detail.html @@ -0,0 +1,148 @@ +{% extends "base.html" %} + +{% block title %}{{ cert.common_name }} - 证书详情{% endblock %} + +{% block content %} +
+

证书详情: {{ cert.common_name }}

+
+ {% if cert.status == 'active' %} + 吊销证书 + {% endif %} + 续期 + 导出 +
+
+ +
+
+
+
+
基本信息
+
+
+
+
通用名
+
{{ cert.common_name }}
+ +
颁发CA
+
{{ ca.name }}
+ +
组织
+
{{ cert.organization }}
+ +
组织单位
+
{{ cert.organizational_unit or 'N/A' }}
+ +
国家
+
{{ cert.country }}
+ +
州/省
+
{{ cert.state or 'N/A' }}
+ +
城市
+
{{ cert.locality or 'N/A' }}
+
+
+
+
+ +
+
+
+
技术信息
+
+
+
+
密钥长度
+
{{ cert.key_size }}位
+ +
有效期
+
{{ cert.days_valid }}天
+ +
状态
+
+ {% if cert.status == 'active' %} + 有效 + {% elif cert.status == 'revoked' %} + 已吊销 + {% else %} + 已过期 + {% endif %} +
+ + {% if cert.status == 'revoked' %} +
吊销原因
+
{{ cert.revocation_reason or '未指定' }}
+ +
吊销时间
+
{{ cert.revoked_at.strftime('%Y-%m-%d %H:%M') }}
+ {% endif %} + +
创建者
+
{{ cert.created_by }}
+ +
创建时间
+
{{ cert.created_at.strftime('%Y-%m-%d %H:%M') }}
+ +
过期时间
+
{{ cert.expires_at.strftime('%Y-%m-%d %H:%M') }}
+
+
+
+
+
+ +
+
+
+
+
SAN扩展
+
+
+ {% if cert.san_dns or cert.san_ip %} + {% if cert.san_dns %} +
DNS名称:
+
    + {% for dns in cert.san_dns.split(',') %} +
  • {{ dns }}
  • + {% endfor %} +
+ {% endif %} + + {% if cert.san_ip %} +
IP地址:
+
    + {% for ip in cert.san_ip.split(',') %} +
  • {{ ip }}
  • + {% endfor %} +
+ {% endif %} + {% else %} +

未配置SAN扩展

+ {% endif %} +
+
+
+ +
+
+
+
文件路径
+
+
+
+
证书文件
+
{{ cert.cert_path }}
+ +
私钥文件
+
{{ cert.key_path }}
+ +
CSR文件
+
{{ cert.csr_path }}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/certificate_list.html b/templates/certificate_list.html new file mode 100644 index 0000000..0822511 --- /dev/null +++ b/templates/certificate_list.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block title %}证书列表{% endblock %} + +{% block content %} +
+

证书列表

+ 创建证书 +
+ +
+ + + + + + + + + + + + + + {% for cert in certificates %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
ID通用名CA机构状态有效期至创建时间操作
{{ cert.id }}{{ cert.common_name }}{{ cert.ca_name }} + {% if cert.status == 'active' %} + 有效 + {% elif cert.status == 'revoked' %} + 已吊销 + {% else %} + 已过期 + {% endif %} + {{ cert.expires_at.strftime('%Y-%m-%d') }}{{ cert.created_at.strftime('%Y-%m-%d') }} +
+ 详情 + {% if cert.status == 'active' %} + 吊销 + {% endif %} + 导出 +
+
暂无证书
+
+{% endblock %} \ No newline at end of file diff --git a/templates/create_ca.html b/templates/create_ca.html new file mode 100644 index 0000000..153df9b --- /dev/null +++ b/templates/create_ca.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block title %}创建CA机构{% endblock %} + +{% block content %} +
+
+

创建新的CA机构

+
+
+
+
+
+ + +
CA机构的显示名称
+
+
+ + +
证书的Common Name字段
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
2字母国家代码,如CN
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+

创建新证书

+
+
+ +
+
+ + +
证书的Common Name字段,通常是域名
+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
2字母国家代码,如CN
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
多个DNS用逗号分隔,如: example.com,www.example.com
+
+
+ + +
多个IP用逗号分隔,如: 192.168.1.1,10.0.0.1
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ 取消 + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/export_certificate.html b/templates/export_certificate.html new file mode 100644 index 0000000..3a3cd6e --- /dev/null +++ b/templates/export_certificate.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block title %}导出证书 - {{ cert.common_name }}{% endblock %} + +{% block content %} +
+
+

导出证书: {{ cert.common_name }}

+
+
+
+
+ +
+ + +
包含证书和私钥的加密存档,适用于大多数应用
+
+
+ + +
Base64编码的证书和私钥,适用于Nginx/Apache等
+
+
+ +
+ + +
用于保护PKCS#12文件的密码
+
+ +
+ 取消 + +
+
+
+
+ +{% block scripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e4eb802 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %}首页{% endblock %} + +{% block content %} +
+
+
+
+
系统概览
+
+
+

欢迎使用证书管理系统!

+
+
+
+
+
CA机构
+

+ 查看所有CA机构 +

+
+
+
+
+
+
+
证书
+

+ 查看所有证书 +

+
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..d2b5ef5 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}登录{% endblock %} + +{% block content %} +
+
+
+
+

用户登录

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..94d759a --- /dev/null +++ b/templates/register.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}用户注册{% endblock %} + +{% block content %} +
+
+
+
+

用户注册

+
+
+
+
+ + +
请输入4-20位的字母、数字或下划线
+
+
+ + +
至少8位字符,包含字母和数字
+
+
+ + +
+
+ + +
+
+ + +
+ + 已有账号?去登录 +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/renew_certificate.html b/templates/renew_certificate.html new file mode 100644 index 0000000..d02255f --- /dev/null +++ b/templates/renew_certificate.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}续期证书 - {{ cert.common_name }}{% endblock %} + +{% block content %} +
+
+

续期证书: {{ cert.common_name }}

+
+
+
+

当前有效期: {{ cert.expires_at.strftime('%Y-%m-%d') }}

+

续期将使用相同的CSR和密钥对生成新证书。

+
+ +
+
+ + +
+ +
+ 取消 + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/revoke_certificate.html b/templates/revoke_certificate.html new file mode 100644 index 0000000..b1cbb17 --- /dev/null +++ b/templates/revoke_certificate.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}吊销证书 - {{ cert.common_name }}{% endblock %} + +{% block content %} +
+
+

吊销证书: {{ cert.common_name }}

+
+
+
+ 警告! 此操作不可逆。吊销后证书将立即失效。 +
+ +
+
+ + +
+ +
+ + +
+ +
+ 取消 + +
+
+
+
+{% endblock %} \ No newline at end of file