最近我其中一個個人網站的 SSL 憑證出現了問題,細查之下才發現是 SSL 憑證已經到期,但伺服器未有自動更新。
趕緊搶修後,忽發奇想:我們能否在 Python 裡監測 SSL 憑證,以致我們可以在憑證到期前更新,避免網站訪客流失?
SSL 憑證的重要性
獲取一個網站的 SSL 憑證
事不宜遲,我們先學習如何獲取單一網站的 SSL 憑證。以下的教學使用 Google Colab Notebook,一個免費的免安裝 Python 平台。如您未曾使用過 Colab 可以參考這篇: 新手 1/3:5 分鐘免安裝學習 Python?Google Colab Notebook 幫緊您!
from urllib.request import ssl, socket from datetime import datetime import pandas as pd import json
#@title 獲取網站 SSL 憑證 url = 'pythonviz.com' #@param {type: "string"} context = ssl.create_default_context() with socket.create_connection((url,'443')) as sock: with context.wrap_socket(sock, server_hostname=url) as ssock: ver = ssock.version() data = ssock.getpeercert() print('TLS 的版本為:',ver,'\n') print('SSL 憑證的細節為:') data
我們在 Colab Notebook 新增 2 個 Code block,然後貼上以上的代碼。
留意我們第 2 個 Code block 有一個互動輸入(上圖紅框),可以讓我們直接輸入一個新的網站。這是由於我們在 url
這個定義後加入 #@param {type: "string"}
,令 Colab 製做一個可以輸入的空格 Text Field。詳細可以參考這裡:新手 2/3:如何用 Google Colab 製做互動表格和圖表?快來學 Import library/Jupyter
第 2 個 Code block 亦有許多艱辛難明的代碼,需要對網絡建設(internet infrastructure)有一定理解才會明白。我們以下略談每個物件的用處,如果您不太感興趣可以略過這點。
物件 | 用途 |
---|---|
context | 即 SSL Context,用於儲存加密連接(SSL Connection)的數據,如 SSL 憑證等。 |
socket | 即 SSL Socket,可以想像為一個加密連接(SSL Connection)。socket 會從 context 傳承(inherit)數據並開啟一個前往指定網站的連接。 |
回到正題:留意我們的回傳 data
是一個字典,載有這個網站 SSL 憑證的資料。
SSL 憑證的失效日期
我們最感興趣的是 data
的「notAfter
」數值。這是代表我們網站的 SSL 憑證到期日,假如我們沒有在該日前更新 SSL 憑證,網頁加密將會失效,使我們的用戶暴露在不安全的環境。
如上圖所示,pythonviz.com 的憑證在寫文的一刻是 "notAfter": "Oct 13 19:57:24 2021 GMT"
。換言之,我們需要在 10 月前更新 SSL 憑證,確保用戶能安全瀏覽我們的網站。
處理已失效的 SSL 憑證
以上我們探討的例子是 SSL 憑證仍未到期的例子。那麼如果憑證已到期,我們會見到什麼?
這裡我們先介紹這個好用的 badssl.com 網站。我們想要測試 Python 代碼如何處理網站的 SSL 憑證時,可以使用這個網站的不同版面測試。
我們的目的是監測 SSL 憑證是否到期。因此,這次我們使用 Certificate: expired 的選項,來測試我們在上一部份的 Python 編程會回傳什麼。
如果您在 Google Chrome 開啟 https://expired.badssl.com/ (在 badssl.com 選擇 expired certificate),會見到 Google Chrome 顯示一個網站不安全的警告,而原因是「NET::ERR_CERT_DATE_INVALID」,即 SSL 憑證已因到期(或錯誤時間)而失效。
我們使用同樣的 Python 代碼,只改變上圖紅色框裡的 URL 為「expired.badssl.com」。
按下播放鍵後,見到這次我們的出現了一個 Python 錯誤:
SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1091)
由於 Python 嘗試驗證 (handshake)網站時發現 expired.badssl.com 的 SSL 憑證早已過期,所以我們在提取 SSL 憑證時,便提醒我們這個網站的 SSL 憑證不安全。
既然如此,我們如何處理這個 SSLCertVerificationError
的同時,又不至於使我們的代碼出現 Python 錯誤,不能運行?
SSL 憑證的異常處理
#@title 獲取網站 SSL 憑證(包括異常處理) url = 'expired.badssl.com' #@param {type: "string"} context = ssl.create_default_context() try: with socket.create_connection((url,'443')) as sock: with context.wrap_socket(sock, server_hostname=url) as ssock: ver = ssock.version() data = ssock.getpeercert() print('TLS 的版本為:',ver,'\n') print('SSL 憑證的細節為:') data except ssl.SSLCertVerificationError as err: print(str(err)) print(str(err).split('certificate verify failed: ')[1]) except Exception as err: print(str(err))
要處理這個 SSLCertVerificationError
的問題有多種解法,但我們集中討論使用 try-except 的語法處理異常。
我們把 Python 代碼稍為更改一下,加入了以上 try
和 except
的數行。簡單而說,try
類似一個 if 的語法,分別在於如果 try
裡面的代碼出錯,Python 不會直接回報異常,改而執行 except
裡的代碼。這與 Excel 裡的 IFERROR() 函數差不多。
在上面的代碼,我們加入了 2 個 except
的處理:
- 第 1 個 except 只限於我們上文提及的
SSLCertVerificationError
。我們把 Python 異常變成文字(str),然後提取異常的實際原因,即「certificate has expired (_ssl.c:1091)
」 - 第 2 個 except 是一個截流器(catch-all),處理其他不是 SSL 憑證引起的異常。我習慣加入這個 catch-all 以確保我們處理異常時不會「多此一舉」
多個網站的 SSL 憑證報告
既然我們要做到監測 SSL 憑證的效果,自然要懂得輸出 SSL 憑證的報告。
如果我們要輸出一個統一標準(standardized),最直接的方法就是寫出一個自訂的功能(custom function)。透過將以上的編程稍作更改,以下是我們的自訂功能:
# 自訂功能 def verifySSLCertificate(url): result = {} result['url'] = url context = ssl.create_default_context() try: # 1: SSL 憑證有效,回傳 Success 及憑證到期日 with socket.create_connection((url,'443')) as sock: with context.wrap_socket(sock, server_hostname=url) as ssock: data = ssock.getpeercert() result['status'] = 'Success' result['expiration'] = datetime.strptime(data['notAfter'], r'%b %d %H:%M:%S %Y %Z') result['message'] = None except ssl.SSLCertVerificationError as err: # 2: SSL 憑證無效,回傳 SSL Error result['status'] = 'SSL Error' result['expiration'] = None result['message'] = str(err).split('certificate verify failed: ')[1] except Exception as err: # 3: 未知錯誤,回傳 Unknown Error result['status'] = 'Unknown Error' result['expiration'] = None result['message'] = str(err) return result # 測試自訂功能 verifySSLCertificate('pythonviz.com')
我們來分析一下上面的編程。
從編程的回傳(output)而言,我們回送的是 result
,一個 Python 字典(dictionary)。這個 result
的字典含有 4 個項目:url
、status
、expiration
、message
。
根據我們的 try-except block,我們會更改 result
的內容,使其反映該網站的數據。比較重要的是:
- 如果 SSL 憑證有效,回傳憑證到期日
- 如果 SSL 憑證無效,回傳 ‘SSL Error’
- 如果編程發現未知錯誤,回傳 ‘Unknown Error’
測試多個網站
如果我們想測試多個網站,那麼我們該如何處理?這時我們便可以使用 Python 大名鼎鼎的 list comprehension 獲取這個報告。如果您仍未了解 list comprehension,可以參考:List Comprehension: Python 的 For Loop 怎樣使用?
listOfDomain = ['pythonviz.com','expired.badssl.com','nonexistdomain123.com'] [verifySSLCertificate(x) for x in listOfDomain]
我們可以先定義一個列表 listOfDomain
,然後以上圖的語法同時測試 listOfDomain
的多個網站。
留意由於每一個 verifySSLCertificate()
都會回傳一個字典(dictionary),所以我們的輸出是一個字典的列表(list of dictionary)。
pandas dataframe 報告
最後,我們可以將 SSL 憑證的報告輸出成一個 pandas dataframe,方便我們輸出成其他報告、做一些篩選(filtering)等。
listOfDomain = ['pythonviz.com','apple.com','sha256.badssl.com','expired.badssl.com','thisdomaindoesnotexistright.com'] df = pd.DataFrame([verifySSLCertificate(x) for x in listOfDomain]) df['remaining'] = (df['expiration'] - datetime.today()).dt.days df.sort_values('remaining')
這個語法算是上面介紹過的 list comprehension 的一個延伸。我們同樣地使用 [verifySSLCertificate(x) for x in listOfDomain]
,唯獨是把這個字典的列表(list of dictionary)傳送到 pandas 裡,生成一個新的 pandas dataframe。
有了 pandas dataframe 便好辦事了。我們可以加入新的列(column)、作篩選(filter)去達至我們需要的效果。
譬如我們上面加入了一個叫做 remaining
的列,是距離每個網站 SSL 憑證的到期日還有多少天。我們再使用 df.sort_values('remaining')
便可以把先到期的網站放在上方,斷定我們需要先處理哪個網站。
教學完整代碼
最後為大家送上這篇教學等的完整代碼。還未使用過 Google Colab Notebook?快來看這篇教學吧:新手 1/3:5 分鐘免安裝學習 Python?Google Colab Notebook 幫緊您!
結語
以上我們簡單地介紹了如何用 Python 讀取 SSL 憑證的數據,並進行一些簡單的監測,匯出一個 pandas dataframe。
當然我們還有許多未能盡數的 SSL 憑證問題,例如已經 Revoked 的憑證等,但是希望您透過以上的教學能學會多一些 Python 編程技能,可以按自己需求更改以上代碼。
參考連結:
- https://stackoverflow.com/questions/44280747/how-to-check-a-ssl-certificate-expiration-date-with-aiohttp
- https://stackoverflow.com/questions/41620369/how-to-get-ssl-certificate-details-using-python
- https://serverlesscode.com/post/ssl-expiration-alerts-with-lambda/
- https://lucasroesler.com/2017/06/ssl-expiry-quick-and-simple/
- https://stackoverflow.com/questions/40923820/pandas-timedelta-in-months
- https://badssl.com/