每一個存取決定都悄悄假設了的那個問題
本階梯先前的幾篇導覽,交給你一套強大的機制來決定「誰可以做什麼」。檔案上的存取控制清單說「alice 可讀、bob 可寫」。最小權限原則說:給每個主體它所需的最小一組權限。但再讀一次那些規則,留意它們全都倚賴的那個詞:alice、bob、每個主體。它們每一條都把「系統已經知道是誰坐在鍵盤前」當成理所當然。而這個假設,正是本篇導覽要談的東西。
把三個各自獨立的概念分清楚會很有幫助,因為初學者老是把它們混為一談。識別是你的宣稱——你打出使用者名稱 alice,宣告你說自己是誰。身分驗證是系統檢驗這個宣稱——證明正在打字的人真的是 alice。唯有通過之後,授權才登場:既然系統已相信你就是 alice,它便查閱存取控制清單,決定 alice 可以做什麼。身分驗證是第二步,是那個讓第三步變得有意義的守門人。一個規則完美的存取矩陣,若任何人都能直接宣稱自己是 alice 並被採信,就一文不值。
為什麼作業系統本身要在意這件事?因為身分驗證正是一個人類被綁定到核心內某個主體的那一刻——一個使用者身分,此後保護機制都將信任它。這一步弄錯,後面每一道檢查都蓋在流沙上。這就是為什麼登入這段程式緊鄰系統中最受信任的程式碼,也是為什麼驗證你的那套機制被算進可信計算基礎裡:若登入路徑能被騙過,它之上的一切便都站不住腳。
三樣東西:你知道的、你持有的、你本身是的
其實任何「身分證明」都只能用三種原料來打造,值得替它們命名,因為你日後會遇到的每一種方法,不是其中之一,就是它們的混合。第一種是你知道的東西——一組密碼、一個 PIN、一道密語問題的答案。第二種是你持有的東西——一個實體權杖、一支手機、一把硬體金鑰、一張你必須擁有的智慧卡。第三種是你本身是的東西——一項生物特徵:你的指紋、你的臉、你虹膜的紋路。每一種倚靠不同種類的秘密,也各以不同方式失效。
要對每一種的弱點誠實,因為沒有任何單一因子是強的。你知道的東西,可能被猜中、被釣走、在各網站重複使用,或被人從便利貼上讀走。你持有的東西,可能被偷或遺失——而一旦如此,在機器看來,那個小偷就是你。你本身是的東西聽起來無懈可擊,卻是這三者中最悄無聲息地危險的:生物特徵並不是秘密(你在碰過的每個杯子上留下指紋,你的臉在每張照片裡),它一旦外洩便無法更換(你只有十根手指,用完就沒了),而且它是模糊的——它必須近似比對,這意味著它既可能錯誤地拒絕真正的你,也可能錯誤地接受一個冒名頂替者。
密碼難題:一個你必須驗證、卻絕不能保留的秘密
密碼是身分驗證的主力,所以值得看看作業系統處理它的真正巧妙之處。一個天真的系統會掉進這樣的陷阱:要在登入時核對你的密碼,它總得拿你打的字去比對檔案裡留存的密碼吧——於是它把每個人的密碼存進一張表。但這張表如今成了整台機器上最豐厚的戰利品。只要一次外洩——一顆被偷的硬碟、一份走失的備份、一個被攻擊者倒出的資料庫——所有帳號就同時門戶大開。那個用來證明身分的東西,反倒成了摧毀身分的東西。密碼儲存背後的深刻想法,正是要化解這個悖論:驗證一個你其實沒有保留的秘密。
讓這件事成為可能的工具,是密碼學雜湊函數。把它想成一台單向絞肉機:你可以餵任何輸入進去、得到一個固定大小的攪亂輸出(也就是「雜湊值」),但你無法把絞肉機倒著轉、從輸出還原回輸入。還有兩個性質讓它在這裡派上用場。同樣的輸入永遠絞出同樣的輸出,所以結果可重現;而且要找到兩個絞出相同輸出的不同輸入、或找到任何能產生某個指定輸出的輸入,在計算上都不可行。於是系統根本不存你的密碼——它只存雜湊值。登入時它把你打的字絞一遍,再檢查那個雜湊值是否與存著的相符。偷走整張表的小偷,得到的是一堆雜湊值,而絞肉機不會倒轉。
但光是雜湊還不夠,而這個缺口很有啟發性。如果每個選了密碼「dragon」的人最後都得到一模一樣的雜湊值,攻擊者根本不必把絞肉機倒轉。他們可以事先把數百萬個常見密碼絞過一遍,建一張從雜湊值回推密碼的巨大查表,然後直接讀出所有「存著的雜湊值出現在表中」的帳號。他們也能一眼看出兩個使用者用了同一組密碼。解法是加鹽(salt):替每個使用者產生一個獨一無二的隨機值,在雜湊之前混進去,並以明文存在雜湊值旁邊。如今 alice 的「dragon」和 bob 的「dragon」會絞出兩個截然不同的雜湊值,那張預先算好的查表也失效了,因為它得替每一個可能的鹽值各備一份;攻擊者只好退回去,對每個帳號一次一個痛苦的猜測逐一硬攻。
STORING a new password CHECKING a login attempt ---------------------- ------------------------ salt = random() look up stored (salt, H) H = hash(salt + password) h = hash(salt + typed) store (salt, H) <- no password accept if h == H the table holds only salt + hash; the password itself is never kept
拖慢攻擊者,以及作業系統對密碼學更廣的運用
加鹽阻擋了預先計算,但一個拿到失竊表、又下定決心的攻擊者仍能猜。他們取一個外洩的雜湊值,用那位使用者的鹽,一個接一個地絞候選密碼,等著比中——這是一種「離線」攻擊,在他們自己的硬體上以最快速度跑。一般用途的雜湊函數在這裡快得危險:現代機器一秒能試上數十億次。所以好的系統會刻意採用一個慢的密碼雜湊——像 bcrypt、scrypt 或 Argon2 這類函數,它們被刻意設計成昂貴的,往往把工作重複數千次、並要求大量記憶體。一次只花你十分之一秒的登入是無感的;但乘上數十億次猜測,它就把一個下午就能破解的事,變成了數百年。
退一步你會發現,同一套密碼學工具不只出現在登入提示處,而是遍布整個作業系統。驗證密碼的那種雜湊函數,正是同一類積木,讓作業系統能檢查一個下載來的更新沒被竄改(它的雜湊值與發行者簽署的值相符嗎?)、讓核心在載入一個驅動程式之前確認它由它信任的人所簽署,並支撐著把整顆磁碟加密、使一台被偷的筆電只交出毫無意義的位元組。密碼學是作業系統把保護延伸到執行中機器之外的方式——延伸到靜置的資料、以及從別處抵達的程式碼——延伸到它平常倚賴的硬體界線再也搆不到的地方。
走一遍登入:從按鍵到受信任的身分
讓我們從頭到尾追蹤一次密碼登入,好讓各個零件咬合起來。想像 alice 坐下來,打出她的使用者名稱與密碼。一路上請留意,這當中有多少是在設法不讓那個秘密在任何可能被偷的地方逗留,以及最終的成果不是一則訊息,而是核心對這個工作階段所「相信」的事的改變。
- Alice 打出 alice 與她的密碼。按鍵沿著輸入路徑往上傳到登入程式,而關鍵是:密碼絕不會回顯到螢幕上,只在記憶體中短暫地被持有。
- 系統查出使用者 alice 存著的紀錄,讀出兩樣東西:她獨一無二的鹽值,以及她上次設定密碼時算出的雜湊值。它不會——也無法——讀出她真正的密碼。
- 它把剛打進來的密碼與那個鹽值混合,對這個組合跑那個慢速的密碼雜湊函數,刻意在這份工作上花掉零點幾秒。
- 它把剛算出的雜湊值與存著的雜湊值相比。若兩者不同,這次嘗試被拒絕(真實系統還會加上一段小小的延遲與失敗計數,讓攻擊者無法像機關槍般狂猜)。若兩者相符,證明便成功了。
- 綁定此刻發生:核心把 alice 的使用者身分附加到這個工作階段上,從這一刻起,她啟動的每個行程都繼承那個身分。後續的存取檢查——檔案權限位元、存取控制清單——終於有了一個可信任的主體可供比對,授權也才能開始做它的工作。
用兩點反思作結。第一,看看身分驗證如何是人類與保護機制之間的那道樞紐:第五步之前只有一個宣稱,之後便有了一個受核心信任的身分,下游的一切都依賴它。第二,留意整支舞蹈有多小心,從不讓真正的秘密待在任何攻擊者能抓到的地方——打了字卻不回顯、立刻雜湊、絕不以明文存放。本階梯最後一篇導覽,正建立在同樣這種偏執的精神之上。一旦攻擊者已經在機器上跑起程式碼,現代防禦——堆疊金絲雀、位址隨機化、安全開機——又如何反擊?身分驗證把不該進來的人擋在外面;那些防禦則假設有人終究還是進來了。