目錄
    Add a header to begin generating the table of contents

    如何在 Python 監測網站 SSL 憑證?pandas 獲取 SSL 到期日

    如何在 Python 監測網站 SSL 憑證?pandas 獲取 SSL 到期日
    Share on facebook
    Share on twitter
    Share on linkedin
    Share on whatsapp
    目錄
      Add a header to begin generating the table of contents

      最近我其中一個個人網站的 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

      我是廣告 ^o^

      第 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 憑證仍未到期的例子。那麼如果憑證已到期,我們會見到什麼?

      我是廣告 ^o^

      這裡我們先介紹這個好用的 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 錯誤:

      我是廣告 ^o^
      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 代碼稍為更改一下,加入了以上 tryexcept 的數行。簡單而說,try 類似一個 if 的語法,分別在於如果 try 裡面的代碼出錯,Python 不會直接回報異常,改而執行 except 裡的代碼。這與 Excel 裡的 IFERROR() 函數差不多。

      在上面的代碼,我們加入了 2 個 except 的處理:

      我是廣告 ^o^
      • 第 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 個項目:urlstatusexpirationmessage

      根據我們的 try-except block,我們會更改 result 的內容,使其反映該網站的數據。比較重要的是:

      我是廣告 ^o^
      • 如果 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。

      我是廣告 ^o^

      有了 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 編程技能,可以按自己需求更改以上代碼。

      我是廣告 ^o^

      參考連結:

      1. https://stackoverflow.com/questions/44280747/how-to-check-a-ssl-certificate-expiration-date-with-aiohttp
      2. https://stackoverflow.com/questions/41620369/how-to-get-ssl-certificate-details-using-python
      3. https://serverlesscode.com/post/ssl-expiration-alerts-with-lambda/
      4. https://lucasroesler.com/2017/06/ssl-expiry-quick-and-simple/
      5. https://stackoverflow.com/questions/40923820/pandas-timedelta-in-months
      6. https://badssl.com/

      人氣文章

      快讓我學更多

      small_c_popup.png
      想學習 Python 嗎?
      快來訂閱我們的電子報!