借用 SSH 金鑰實現網站無密碼登入
发布时间:2026-08-20 | 浏览:1
用慣 SSH 金鑰免密碼登入 SSH/SFTP/SCP 後,就很難回去每次登入要敲密碼的日子~ (註:若覺得沒密碼少一道關卡不安心,還可加碼用 實體金鑰 ) 若登入網站或呼叫 WebAPI 時也能比照辦理,那該有多好?
SSH 的身分認證原理是在 ~/.ssh 目錄產生一組 Ed25519 公私鑰, id_ed25519 是私鑰檔,有密碼保護且不需外流;而 id_ed25519.pub 則是可公開散佈的公鑰,可預先傳給連線對象。登入時,用私鑰對特定內容進行簽章,伺服器端則用用公鑰驗證簽章真偽,確認登入者身分。
最近要寫個 WebAPI,覺得用 API Key 存在竊聽及伺服器端洩漏風險,想升級仿效 SSH 等級的公私鑰驗證,另外建立金鑰太麻煩,何不直接共用 SSH/SCP 在用的現成 Ed25519 金鑰就好? 註:這種設計適合客戶端數量少且固定的情境,類似 SSH 客戶端情境,無法適用所有應用場景,請自行評估。
原本的天真計劃是用 BouncyCastle.Cryptography 的 OpenSshPrivateKeyUtilities.ParsePrivateKeyBlob(...) 讀取 id_ed25519 私鑰檔,簽章後用 OpenSshPublicKeyUtilities.ParsePublicKey(...) 讀取 id_ed25519.pub 公鑰檔就能輕鬆搞定,忘了 OpenSSH 的層層安全防護!
OpenSSH 的 id_ed25519 私鑰檔處於加密狀態,需先輸入保護密碼解密才能使用,而這段由私鑰管理員 ssh-agent 負責,呼叫端不需要也無法存取私鑰本體。延伸閱讀: OpenSSH for Windows 之 ssh-agent 安裝與私鑰保存探密
Windows 的 ssh-agent 採用 Named Pipe 傳輸,訊息格式則依循標準 SSH Agent Protocol ,實作不難但有些背景知識,如果不是全部甩給 AI 寫,可以了解一下。
SSH Public Key Line Format id_ed25519.pub 公鑰檔案的內容只有一行,格式如 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF5i1vQ9gXm0Fp3XnZy8Jb4PZ9qK9v7v3GZ7vN3mZn9k [email protected] (以下簡稱公鑰字串),共分為三段,第一段是金鑰型別,第二段是 Base64 編碼的 SSH Public Key Blob,第三段是 Comment 方便識別。 SSH Public Key Blob 為一段 byte[],由兩個 SSH 字串組成,每個 SSH 字串最前面 4 個 Byte 為 uint32 宣告資料長度,接著該長度的 byte[] 資料本體。 +---------------------------+ | uint32 = 11 | +---------------------------+ | "ssh-ed25519" | +---------------------------+ | uint32 = 32 | +---------------------------+ | 32-byte public key | +---------------------------+ ssh-agent 可能管理多支金鑰,故簽章時需要提供公鑰字串以指定用哪一支金鑰進行簽章;驗證簽章時當然也必須提供金鑰字串,以便從 SSH Public Key Blob 取出公鑰本體進行驗證。
SSH Public Key Line Format id_ed25519.pub 公鑰檔案的內容只有一行,格式如 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF5i1vQ9gXm0Fp3XnZy8Jb4PZ9qK9v7v3GZ7vN3mZn9k [email protected] (以下簡稱公鑰字串),共分為三段,第一段是金鑰型別,第二段是 Base64 編碼的 SSH Public Key Blob,第三段是 Comment 方便識別。 SSH Public Key Blob 為一段 byte[],由兩個 SSH 字串組成,每個 SSH 字串最前面 4 個 Byte 為 uint32 宣告資料長度,接著該長度的 byte[] 資料本體。
ssh-agent 可能管理多支金鑰,故簽章時需要提供公鑰字串以指定用哪一支金鑰進行簽章;驗證簽章時當然也必須提供金鑰字串,以便從 SSH Public Key Blob 取出公鑰本體進行驗證。
SSH Agent Protocol Windows SSH Agent 可透過 NamedPipe 地址 \\.\pipe\openssh-ssh-agent 存取,對其寫入及讀取 byte[] 資料,而傳輸資料的格式則依循 SSH Agent Protocol ,我們只會用到四種請求或回應: RequestIdentities (11) - 請求公鑰清單客戶端發送此訊息,向 Agent 詢問目前記憶體中載入的所有金鑰 請求內容:總長度 (uint32):整個封包扣除長度欄位後的位元組數(此處固定為 1)/ 訊息代碼 (byte):固定為 11 (SSH2_AGENTC_REQUEST_IDENTITIES) IdentitiesAnswer (12) - 回傳公鑰清單Agent 收到代碼 11 後的回應,包含金鑰數量與各金鑰的詳細資訊 回應內容:總長度 (uint32) / 訊息代碼 (byte):固定為 12 (SSH2_AGENT_IDENTITIES_ANSWER) / 金鑰筆數 (uint32):目前 Agent 內持有的金鑰總數(假設為 N)/ 金鑰陣列(重複 N 次): [ Key Blob (string):公開金鑰的二進位編碼 / Comment (string):金鑰註解 ] SignRequest (13) - 要求簽章客戶端發送欲簽署的資料,要求 Agent 使用特定私鑰進行數位簽章。 請求內容:總長度 (uint32) / 訊息代碼 (byte):固定為 13 (SSH2_AGENTC_SIGN_REQUEST) / Key Blob (string):目標公鑰的 SSH Public Key Blob (告知 Agent 要用哪一把私鑰) / Data (string):要簽署的原始資料或雜湊值 / Flags (uint32):簽章控制旗標(例如:指示 RSA 金鑰必須使用 SHA-256 或 SHA-512 演算法) SignResponse (14) - 回傳簽章Agent 完成數位簽章後的回應(若失敗則會回傳 SSH_AGENT_FAILURE 代碼 5) 回應內容:總長度 (uint32) / 訊息代碼 (byte):固定為 14 (SSH2_AGENT_SIGN_RESPONSE) / Signature Blob (string):包含完整簽章資料的字串,結構為 [ Format (string):簽章格式名稱(例如 ssh-rsa、rsa-sha2-256) + Signature (string):數位簽章二進位內容 ]
SSH Agent Protocol Windows SSH Agent 可透過 NamedPipe 地址 \\.\pipe\openssh-ssh-agent 存取,對其寫入及讀取 byte[] 資料,而傳輸資料的格式則依循 SSH Agent Protocol ,我們只會用到四種請求或回應:
RequestIdentities (11) - 請求公鑰清單客戶端發送此訊息,向 Agent 詢問目前記憶體中載入的所有金鑰 請求內容:總長度 (uint32):整個封包扣除長度欄位後的位元組數(此處固定為 1)/ 訊息代碼 (byte):固定為 11 (SSH2_AGENTC_REQUEST_IDENTITIES)
IdentitiesAnswer (12) - 回傳公鑰清單Agent 收到代碼 11 後的回應,包含金鑰數量與各金鑰的詳細資訊 回應內容:總長度 (uint32) / 訊息代碼 (byte):固定為 12 (SSH2_AGENT_IDENTITIES_ANSWER) / 金鑰筆數 (uint32):目前 Agent 內持有的金鑰總數(假設為 N)/ 金鑰陣列(重複 N 次): [ Key Blob (string):公開金鑰的二進位編碼 / Comment (string):金鑰註解 ]
SignRequest (13) - 要求簽章客戶端發送欲簽署的資料,要求 Agent 使用特定私鑰進行數位簽章。 請求內容:總長度 (uint32) / 訊息代碼 (byte):固定為 13 (SSH2_AGENTC_SIGN_REQUEST) / Key Blob (string):目標公鑰的 SSH Public Key Blob (告知 Agent 要用哪一把私鑰) / Data (string):要簽署的原始資料或雜湊值 / Flags (uint32):簽章控制旗標(例如:指示 RSA 金鑰必須使用 SHA-256 或 SHA-512 演算法)
SignResponse (14) - 回傳簽章Agent 完成數位簽章後的回應(若失敗則會回傳 SSH_AGENT_FAILURE 代碼 5) 回應內容:總長度 (uint32) / 訊息代碼 (byte):固定為 14 (SSH2_AGENT_SIGN_RESPONSE) / Signature Blob (string):包含完整簽章資料的字串,結構為 [ Format (string):簽章格式名稱(例如 ssh-rsa、rsa-sha2-256) + Signature (string):數位簽章二進位內容 ]
AI 時代,觀念搞懂了,寫完程式只在談笑之間,實作用 Named Pipe 與 Windows SSH Agent 的程式範例如下,是這個認證機制的重點:
關於驗證身分的做法,我設計了附加電子簽名的認證字串,格式為 <帳號名稱> <時間戳> <電子簽章> 包含三部分:帳號名稱讓伺服器找到該帳號對應的公開金鑰,時間戳證明此簽名為即時產生(避免 重送攻擊 Replay Attack ),而電子簽章為使用者使用私鑰對 <帳號名稱> <時間戳> 產生的電子簽章。
伺服器端接收到認證字串後,由帳號名稱找出該帳號的公開金鑰,驗證簽名正確且時間戳為剛剛產生(例如:不早於 5 秒之前),不是重送攻擊,則認定使用者擁有私鑰是本人無誤,宣告登入成功。
註:時間戳檢查無法阻擋太快的重送攻擊,若想避免可再加上 Nonce 拋棄式隨機數,用一次就作廢。如此需在伺服器保存一段時間內用過的 Nonce 進行比對。
我先寫了一支超簡單的 ASP.NET WebForm 驗證可以用 OpenSSH 預設的個人金鑰 (~/.ssh/ed25519) 完成身分驗證:
可執行的客戶端程式範例我已放上 Github ,若想測試記得 先用 ssh-keygen -t ed25519 建立 SSH 個人金鑰 並準備 ASPX 或其他程式語言寫的登入驗證網頁。
Use existing OpenSSH Ed25519 keys for WebAPI authentication via Windows ssh-agent, Named Pipe, SSH Agent Protocol signing, and server-side signature verification to replace API keys.
Be the first to post a comment