تاریخ گواهی فقط یکی از نشانه‌های سلامت است

تمام‌شدن اعتبار گواهی می‌تواند دسترسی کاربران به سایت و اتصال سرویس‌های داخلی را متوقف کند. بررسی دستی گاه‌به‌گاه کافی نیست، به‌ویژه اگر چند دامنه و محیط آزمایش دارید. پیش از نوشتن ابزار، فهرست میزبان‌ها، مالک تمدید و فاصلهٔ هشدار را مشخص کنید. این مقاله تاریخ گواهی را از یک اتصال معتبر می‌خواند؛ موفقیت چنین اتصالی به معنی بررسی کامل امنیت یا سلامت تمام مسیرهای برنامه نیست.

ابزار از تنظیم پیش‌فرض امن پایتون برای بررسی زنجیرهٔ اعتماد و نام میزبان استفاده می‌کند. اگر اعتبارسنجی شکست بخورد، برای گرفتن یک تاریخ ظاهراً قابل قبول آن را خاموش نمی‌کند. نام میزبان با درگاه جدا دریافت می‌شود و نشانی کامل وب پذیرفته نیست. این محدودیت رابط را روشن می‌کند، ولی ابزار را به یک سرویس عمومی بررسی نشانی‌های دلخواه کاربران تبدیل نمی‌کند؛ فهرست هدف باید مدیریتی و مجاز باشد.

اعتبارسنجی نام میزبان و زمان جهانی

محاسبهٔ زمان با ساعت آگاه از منطقهٔ زمانی و مبنای جهانی انجام می‌شود. اختلاف روز می‌تواند اعشاری یا منفی باشد؛ گردکردن زودهنگام نباید گواهیِ نزدیک پایان را چند روز سالم‌تر نشان دهد. درگاه، نام میزبان و آستانهٔ هشدار برای هر هدف ثبت می‌شود. زمان اتصال نیز محدود است تا یک میزبان خاموش، بررسی همهٔ هدف‌های دیگر را برای مدت نامشخص متوقف نکند.

چهار فایل را کنار هم ذخیره و python -m unittest test_cert_check.py را اجرا کنید. این آزمون بدون اینترنت، مرز روز جهانی، تاریخ گذشته و ورودی نامعتبر را بررسی می‌کند. سپس فهرست hosts.json را با دامنه‌های تحت مدیریت خود جایگزین کنید و python run_checks.py hosts.json را اجرا کنید. این مرحله به شبکه نیاز دارد و ممکن است به دلیل دیوارهٔ آتش یا اعتمادنامهٔ محیط شما خطا بدهد.

خروجی هر میزبان یک رکورد قابل پردازش است و هشدار نزدیک‌شدن انقضا یا خطای اتصال، کد خروج ناموفق می‌دهد. پیام خطای اعتبارسنجی را با خطای قطع شبکه یکسان تفسیر نکنید؛ هر دو نیازمند رسیدگی‌اند، اما علت و مسئول متفاوت دارند. اگر سامانه پشت شبکهٔ توزیع محتواست، گواهی قابل مشاهدهٔ کاربر و گواهی اتصال آن شبکه به مبدأ می‌توانند متفاوت باشند و باید جدا بررسی شوند.

فایل‌های کامل و قابل اجرا

چهار فایل زیر کامل‌اند؛ پیش‌نیاز، ترتیب اجرا و روش بررسی در متن آمده است. کدها خلاصه یا پنهان نشده‌اند.

1. فایل کامل cert_check.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

import socket
import ssl
from datetime import datetime, timezone


def days_remaining(not_after, now=None):
    now = now or datetime.now(timezone.utc)
    if now.tzinfo is None:
        raise ValueError('Use a timezone-aware clock')
    expires = datetime.fromtimestamp(ssl.cert_time_to_seconds(not_after), timezone.utc)
    return (expires - now).total_seconds() / 86400


def check(host, port=443):
    if not host or '/' in host or ':' in host or not 1 <= port <= 65535:
        raise ValueError('Supply a DNS hostname and separate TCP port')
    context = ssl.create_default_context()
    with socket.create_connection((host, port), timeout=5) as connection:
        with context.wrap_socket(connection, server_hostname=host) as secured:
            certificate = secured.getpeercert()
            return {'host': host, 'port': port, 'daysRemaining': round(days_remaining(certificate['notAfter']), 2),
                    'expires': certificate['notAfter'], 'protocol': secured.version()}

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

2. فایل کامل hosts.json

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

[
  {"host": "example.com", "port": 443, "warnDays": 21}
]

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

3. فایل کامل run_checks.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

import json
from pathlib import Path
import sys
from cert_check import check

if __name__ == '__main__':
    hosts = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
    failed = False
    for target in hosts:
        try:
            result = check(target['host'], target.get('port', 443))
            result['warning'] = result['daysRemaining'] <= target.get('warnDays', 21)
            failed |= result['warning']
        except (OSError, ValueError) as error:
            result = {'host': target['host'], 'error': type(error).__name__}
            failed = True
        print(json.dumps(result))
    raise SystemExit(1 if failed else 0)

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

4. فایل کامل test_cert_check.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

import unittest
from datetime import datetime, timezone
from cert_check import check, days_remaining


class CertificateTests(unittest.TestCase):
    def test_utc_boundary(self):
        now = datetime(2026, 10, 27, tzinfo=timezone.utc)
        self.assertEqual(days_remaining('Oct 28 00:00:00 2026 GMT', now), 1)
        self.assertEqual(days_remaining('Oct 26 12:00:00 2026 GMT', now), -0.5)

    def test_reject_ambiguous_clock_and_url(self):
        with self.assertRaises(ValueError):
            days_remaining('Oct 28 00:00:00 2026 GMT', datetime(2026, 10, 27))
        with self.assertRaises(ValueError):
            check('https://example.com/')


if __name__ == '__main__':
    unittest.main()

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

اجرای فهرست میزبان‌ها و رسیدگی به هشدار

برای نگهداری واقعی، هشدار را به مالک مشخص و روند تمدید آزموده‌شده متصل کنید. ساعت میزبان، مجوز دسترسی به کلید و بارگذاری گواهی تازه روی سرویس را هم کنترل کنید. تاریخ معتبر به‌تنهایی ثابت نمی‌کند تمدید خودکار در نوبت بعد کار می‌کند. موفقیت عملی زمانی است که هشدار پیش از حادثه دیده شود و بعد از تمدید، همان مسیر مورد استفادهٔ کاربر دوباره با اعتبارسنجی کامل بررسی شود.

چک‌لیست اجرای عملی

  • اعتبارسنجی زنجیره و نام میزبان روشن بماند.
  • محاسبه با ساعت آگاه از منطقهٔ زمانی انجام شود.
  • مبدأ و شبکهٔ توزیع محتوا جدا بررسی شوند.
  • مالک هشدار و روند تمدید مشخص باشند.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.