Understanding Verified Boot (Part 1): Signatures, Hashes, and the Root of Trust
A walkthrough of verified boot — how RSA signatures and SHA-256 hashes combine, why the public key has to be anchored outside the package, and the full chain of trust from Boot ROM to Linux kernel, with AM335x as the reference case study.
Verified boot là cơ chế buộc mỗi tầng trong chuỗi khởi động phải xác thực tầng kế tiếp bằng mật mã học trước khi trao quyền — không hợp lệ thì dừng luôn thay vì chạy tiếp. Bài này giải thích cơ chế đứng sau nó: vì sao phải có chữ ký, vì sao chữ ký phải đi kèm hàm băm, và chuỗi đó neo vào đâu. Dùng BeagleBone Black (TI Sitara AM335x) làm reference case study — kiến trúc đủ đơn giản để nhìn rõ từng khái niệm, và cũng chính là board sẽ được đem ra thực hành ở phần sau.
Cơ chế này bịt một lỗ hổng cụ thể. Thiết bị nhúng khởi động qua nhiều tầng phần mềm nối tiếp nhau, tầng trước nạp tầng sau rồi trao quyền thực thi — và mặc định, không tầng nào kiểm tra xem tầng kế tiếp có đúng là bản gốc hay không. Với thiết bị đặt ở nơi ai cũng chạm tới được, kẻ tấn công thay được kernel hay bootloader sẽ đứng bên dưới mọi lớp bảo vệ dựng bên trong hệ điều hành: phân quyền, SELinux, firewall, chữ ký gói phần mềm — tất cả đều ngầm dựa trên một giả định duy nhất, rằng hệ điều hành đang chạy đúng là hệ điều hành đã cài vào. Backdoor loại này sống sót qua reboot, qua cả factory reset, và không công cụ nào chạy trong hệ thống phát hiện ra được, vì chính hệ thống đi kiểm tra đã bị kiểm soát từ trước khi nó kịp khởi động.
Lưu ý trước: các khái niệm nền (chuỗi tin cậy, root of trust, ký/verify) mang tính chung cho mọi nền tảng, còn cách hiện thực hoá thì mỗi hãng mỗi khác — tên thành phần, định dạng ảnh, nơi cất key đều có thể lệch nhau đáng kể. Những chỗ nào là đặc thù của BeagleBone Black sẽ được đánh dấu rõ khi gặp; phần cụ thể của board để dành cho phần sau.
Tài liệu gốc: Verified Boot — U-Boot documentation. Bản đầy đủ về FIT signing: FIT Signature Verification.
1. What Verified Boot Guarantees
1.1. The Core Idea
Trước khi đi vào định nghĩa hay thuật ngữ, đây là toàn bộ ý tưởng cốt lõi của verified boot, minh hoạ trên mô hình tham chiếu chung của một SoC ARMv8 đầy đủ — board thực tế có thể thiếu bớt một vài mắt xích hoặc có đủ, tuỳ kiến trúc và biến thể chip:
flowchart LR
ROM["Boot ROM"] -->|verify OK| ATF["ARM Trusted Firmware"]
ATF -->|verify OK| UBOOT["U-Boot"]
UBOOT -->|verify OK| KERNEL["Linux Kernel"]
ROM -.->|verify FAIL| HALT(["Boot halted"])
ATF -.->|verify FAIL| HALT
UBOOT -.->|verify FAIL| HALT
Hàng trên là luồng boot bình thường — không có gì lạ, vẫn là chuỗi ROM → ATF → U-Boot → Kernel quen thuộc. Thứ mà verified boot chèn thêm vào chỉ là một cổng verify gắn trên mỗi mũi tên chuyển giao: bước hiện tại verify chữ ký của bước kế tiếp trước khi trao quyền, đi tiếp nếu khớp, còn sai thì rẽ sang nhánh dưới — dừng lại, báo lỗi ngay tại chỗ, không đoán, không “cứ chạy thử”. Đây chính là ý tưởng cốt lõi của verified boot: không thêm thành phần mới vào luồng boot, chỉ thêm điều kiện vào mỗi lần chuyển giao đã có sẵn.
Mỗi cổng “verify OK” đó trả lời đúng hai câu hỏi đặt ra ở phần mở đầu — vừa xác nhận authenticity (đúng người tạo ra), vừa xác nhận integrity (không bị sửa dọc đường). Cơ chế cụ thể đứng sau chữ “verify” đó là gì sẽ được mổ xẻ ở mục 2.
Cũng cần nói rõ một điểm hay bị hiểu lầm: verified boot không giấu gì cả. Kernel và device tree trong sơ đồ trên vẫn là dữ liệu đọc được bình thường, ai copy ra cũng xem được nội dung — chữ ký chỉ đính kèm bên cạnh, không mã hoá dữ liệu gốc. Câu hỏi được trả lời là “cái này có đúng bản gốc không”, chứ không phải “cái này có bí mật không”.
1.2. Root of Trust — Why the Chain Needs an Anchor
Sơ đồ trên đứng vững được là nhờ một điều kiện: mỗi bước tin được bước trước vì bước trước đã bị verify. Nhưng bước đầu tiên — Boot ROM verify ATF — thì không có ai đứng trước nó để verify giúp. Vậy điều gì khiến bước đó đáng tin?
Thử suy ngược sẽ thấy đây là câu hỏi bắt buộc phải có lời giải, không thể lảng tránh: nếu nói “có một phần mềm X verify Boot ROM trước khi nó chạy”, thì X lại cần một thứ khác verify chính nó, lặp vô hạn không có điểm dừng. Chuỗi chỉ đứng vững khi neo được vào một điểm không cần verify nữa, đơn giản vì không ai thay được nó. Với khối “Boot ROM” trong sơ đồ trên, điểm neo đó gồm hai phần: public key/hash của OEM, ghi một chiều vào eFuse/OTP lúc sản xuất chip, cùng chính đoạn code Boot ROM khắc cứng trong silicon. Cả hai đều nằm ngoài tầm với của bất kỳ phần mềm nào chạy sau đó — kể cả phần mềm độc hại.
Điểm neo bất khả xâm phạm đó gọi là Root of Trust (RoT). Không có nó, không có chỗ nào để bắt đầu chuỗi — sơ đồ ở mục 2.4 sẽ vẽ cái neo đó nằm ở đâu trong luồng.
2. Signature Verification Mechanics
Các mục trên dùng chữ “verify chữ ký” như một hộp đen. Mục này mở hộp đó ra — trước hết ở mức ý tưởng, sau đó mới tới hai công cụ toán học cụ thể dựng nên nó.
2.1. The Core Idea — Fingerprint, Lock, Compare
Verify trả lời câu hỏi: file này có đúng là bản gốc không — và phải trả lời mà không có sẵn bản gốc để đối chiếu. Mang bản gốc theo cũng không giải quyết gì: nó chỉ là một file nữa, thay được như mọi file khác. Cơ chế gồm ba bước:
- Rút gọn dữ liệu về một vân tay ngắn, cố định.
- Khoá vân tay đó bằng một thứ chỉ tác giả tạo được.
- Trên thiết bị: tính lại vân tay từ dữ liệu nhận được, mở chỗ đã khoá để lấy lại vân tay lúc ký, rồi so hai vân tay — khớp thì đi tiếp, lệch thì dừng.
Toàn bộ quy trình, trông như sau:
flowchart LR
subgraph SIGN["Signing (offline — private key holder)"]
A1["Original data<br/>(kernel/dtb)"] --> A2["SHA-256<br/>→ fingerprint"]
A2 --> A3["Sign fingerprint<br/>with private key"]
A3 --> A4["signature"]
end
A1 -.-> DATA["Data + signature<br/>(packed into FIT)"]
A4 -.-> DATA
subgraph VERIFY["Verification (on device — public key only)"]
DATA --> B1["Hash received data<br/>→ new fingerprint"]
DATA --> B2["Open signature with<br/>public key → old fingerprint"]
B1 --> C{"Compare<br/>fingerprints"}
B2 --> C
C -->|Match| OK["Valid — continue boot"]
C -->|Mismatch| NG["Reject — halt boot"]
end
Bước 1 và bước 2 cần hai công cụ khác hẳn nhau về bản chất, và đó là hai mục kế tiếp. Bước 1 cần một hàm rút gọn mà đổi một bit là ra kết quả khác hoàn toàn — SHA-256. Bước 2 cần một phép khoá mà ai cũng mở được nhưng chỉ một người khoá được — RSA.
2.2. SHA-256 — The Fingerprint Function
Chữ ký không đặt thẳng lên toàn bộ dữ liệu gốc — kernel vài MB thì bất tiện. Thay vào đó, dữ liệu được rút gọn thành một “dấu vân tay” ngắn bằng SHA-256, rồi mới đem vân tay đó đi ký.
SHA là viết tắt của Secure Hash Algorithm, còn 256 là số bit của kết quả trả về — dù đầu vào to nhỏ thế nào. Nó thuộc họ SHA-2, do NSA thiết kế và NIST chuẩn hoá năm 2001.
Băm thực chất là gì: hàm băm giữ sẵn một khối trạng thái cố định 256 bit, rồi cắt dữ liệu đầu vào thành từng khối nhỏ và lần lượt “trộn” từng khối vào trạng thái đó. Mỗi vòng trộn chỉ gồm những phép biến đổi bit rất cơ bản — dịch bit, xoay bit, XOR, cộng — nhưng được lặp đi lặp lại đủ nhiều vòng. Trộn hết dữ liệu thì trạng thái còn lại chính là vân tay.
flowchart LR
IV["Initial state<br/>256 bit"] --> R1["Mix"]
B1["Block 1"] --> R1
R1 --> R2["Mix"]
B2["Block 2"] --> R2
R2 --> R3["Mix"]
B3["Block n"] --> R3
R3 --> OUT["Final state<br/>= 256-bit fingerprint"]
Nhìn sơ đồ cũng thấy luôn vì sao đầu ra lúc nào cũng đúng 256 bit bất kể đầu vào dài bao nhiêu: dữ liệu càng dài thì càng nhiều khối được trộn vào, nhưng khối trạng thái thì vẫn giữ nguyên kích thước từ đầu tới cuối.
Cách trộn đó giải thích luôn hai tính chất cần dùng:
- Không lần ngược được. Dữ liệu dài bao nhiêu cũng bị ép về đúng 256 bit, nghĩa là thông tin bị vứt bớt dọc đường. Không có phép “giải nén” nào lấy lại được thứ đã bị vứt — đây không phải chuyện giấu kín cho khéo, mà là thông tin thật sự không còn nằm trong vân tay nữa.
- Đổi 1 bit thì vân tay đổi hoàn toàn. Mỗi bit đầu vào được trộn qua nhiều vòng, mỗi vòng lại lan ảnh hưởng của nó rộng thêm trong khối trạng thái. Sau đủ số vòng, lật đúng một bit ở đầu vào sẽ kéo theo khoảng một nửa số bit đầu ra đổi giá trị — hiệu ứng này gọi là avalanche.
Avalanche dễ thấy nhất khi tự chạy thử. Hai chuỗi dưới đây chỉ khác nhau đúng ký tự cuối:
1
2
SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256("hellp") = fdd7585e08c4e2afd71dcabdb4636c89d557a3f42db9e2040c8bbd1708aa4ce7
Hai kết quả không còn một chút liên hệ nào với nhau — và đây chính là thứ khiến việc dò dần trở nên vô nghĩa: không có kiểu “sửa gần đúng thì hash gần giống” để lần từng bước tới đích.
Nhờ tính chất thứ hai, so hai vân tay là đủ để biết dữ liệu có còn nguyên vẹn hay không, khỏi phải đối chiếu từng byte. Còn muốn cố tình nặn ra một file khác cho ra đúng cùng một vân tay thì không có đường tắt nào ngoài dò — mà không gian phải dò là 2^256.
2.3. RSA — The Asymmetric Lock
RSA — tên ghép từ ba tác giả Rivest, Shamir và Adleman, công bố năm 1977 — là thuật toán mã hoá khoá công khai được dùng rộng rãi đầu tiên, và đến nay vẫn là lựa chọn phổ biến nhất cho việc ký firmware.
Nó chính là bước 2 ở mục 2.1 — phép khoá mà ai cũng mở được nhưng chỉ một người khoá được. Toàn bộ sức mạnh của nó đến từ một kiểu phép toán đặc biệt: làm theo chiều xuôi thì dễ, làm ngược lại thì gần như bất khả thi.
Phép toán đó chính là nhân số nguyên tố. Cho hai số nguyên tố rất lớn, nhân chúng với nhau thì máy tính làm xong trong tích tắc. Nhưng đưa ngược lại đúng kết quả phép nhân đó và hỏi “hai số nguyên tố ban đầu là gì”, thì không ai biết cách nào nhanh hơn việc dò gần như toàn bộ — với số cỡ 2048 bit, thời gian dò vượt xa tuổi của vũ trụ. Chiều xuôi và chiều ngược lệch nhau khủng khiếp như vậy, dù cả hai đều chỉ là phép nhân/chia thông thường.
Cặp khoá RSA dựng thẳng trên sự lệch đó:
- Public key mang theo tích của hai số nguyên tố kia. Ai cũng đọc được tích, nhưng đọc được tích không có nghĩa là biết được hai thừa số.
- Private key chỉ tính ra được nếu biết hai thừa số gốc — tức là thứ mà người sinh ra cặp khoá có sẵn, còn người khác thì phải đi giải bài toán ngược ở trên.
Ký và verify là hai phép biến đổi ngược nhau trên cùng bộ số đó: ký dùng tham số phía private, verify dùng tham số phía public. Chúng khớp nhau về mặt số học nên ai cầm public key cũng verify được — nhưng đi ngược lại để tạo ra một chữ ký mới thì bắt buộc phải có private key.
Đây cũng là lý do public key nhúng công khai vào thiết bị vẫn an toàn: độ an toàn của RSA không đến từ việc giấu thứ gì cả — thuật toán được công bố từ 1977, public key thì ai cũng đọc được — mà đến từ chỗ bài toán đi ngược quá đắt để giải.
2.4. Putting It Together — Why It Has to Be This Way
Đã có RSA rồi thì sao không ký thẳng lên cả file kernel cho xong, còn băm làm gì?
Thay vì trả lời thẳng, ta dựng lại luồng xử lý bằng cách nâng cấp dần nó lên. Mỗi bản vá đúng một chỗ hổng. Cả ba bản đều sập ở cùng một kiểu lỗi, và thấy được cái lỗi lặp lại đó là hiểu được vì sao luồng cuối cùng phải có hình dạng như vậy.
Lấy một vật thể cụ thể để nói cho dễ: file kernel zImage, thứ mà U-Boot nạp lúc boot.
Bản 1 — chỉ băm.
Máy build băm zImage ra vân tay. Nó để chung cả hai trong một file trên thẻ nhớ. Lúc boot, thiết bị băm lại zImage rồi so với vân tay trong file đó.
- Điểm mạnh: lệch nhau là biết ngay file không phải bản gốc.
- Sập ở: kẻ tấn công ghi đè cả hai. Nó thay
zImagebằng kernel của nó, băm ra một vân tay mới, rồi ghi đè lên vân tay cũ. Thiết bị băm kernel của nó, so với vân tay nó vừa ghi, thấy khớp. Kernel của nó chạy.
Bản 2 — thêm chữ ký.
Máy build ký vân tay bằng private key. Thiết bị cần public key để mở chữ ký, và ở bản này public key để chung trong gói.
- Điểm mạnh: nguồn gốc. Không có private key thì không ai tạo được chữ ký.
- Sập ở: kẻ tấn công tự tạo một cặp khoá. Nó ký kernel của nó bằng private key của nó, rồi thay public key trong gói bằng public key của nó. Thiết bị lấy public key từ gói, mở chữ ký, thấy khớp. Kernel của nó chạy.
- Lỗi lộ ra: vẫn là lỗi cũ, chỉ đổi tên. Vế thứ hai giờ do kẻ tấn công tự ký. Chữ ký không yếu, chỗ sai nằm ở chỗ khác.
Bản 3 — neo public key lại.
Public key không để chung trong gói nữa. Nó nằm sẵn trong thiết bị, ở chỗ được tin từ đầu. Chỗ đó gọi là root of trust, đã nói ở mục 1.2. Gói chỉ còn chở zImage và chữ ký.
- Điểm mạnh: chữ ký chỉ mở được bằng public key, mà public key thì nằm sẵn trong thiết bị chứ không đi theo gói. Cần mở chữ ký thì thiết bị lấy nó ra dùng. Kẻ tấn công thay
zImagehay thay chữ ký đều không được, vì không có private key thì không tạo ra nổi chữ ký mà public key đó chấp nhận. - Sập ở: không còn. Muốn qua được cổng này, kẻ tấn công phải có private key thật, hoặc phải thay được public key đã neo trong thiết bị.
Đây là bản đứng được, và cũng chính là luồng thật của secure boot.
Gom toàn bộ mạch trên lại thành sơ đồ, và mở rộng cho hết chuỗi boot chứ không chỉ một tầng.
Vẫn nguyên tắc cũ: SHA-256 tạo ra vân tay, còn RSA mới là thứ tạo ra chữ ký — từ hai đầu vào là vân tay đó cộng với private key. Ba tầng MLO, u-boot.img, kernel + dtb đều đi qua đúng một khuôn đó.
Chuỗi vẽ ở đây là của một chip có root of trust thật.
BBB: AM335x là Cortex-A8, kiến trúc ARMv7 — không có ATF như mô hình tham chiếu chung ở mục 1.1, nên chuỗi thật ngắn hơn một mắt xích: Boot ROM nạp thẳng MLO.
flowchart TB
subgraph BUILD["Build time — on the dev machine"]
KEYGEN["RSA key<br/>generation"] --> PRIV["private key"]
KEYGEN --> PUB["public key"]
MLO["MLO"] --> H1["SHA-256"] --> R1["RSA sign"] --> S1["MLO signature"]
UB["u-boot.img"] --> H2["SHA-256"] --> R2["RSA sign"] --> S2["u-boot.img<br/>signature"]
KD["kernel + dtb"] --> H3["SHA-256"] --> R3["RSA sign"] --> S3["kernel + dtb<br/>signature"]
PRIV --> R1
PRIV --> R2
PRIV --> R3
end
PKG["Shipped to device:<br/>each stage carries<br/>its own signature"]
S1 --> PKG
S2 --> PKG
S3 --> PKG
subgraph BOARD["Boot time — on the device"]
PUB ==> EFUSE["eFuse<br/>public key<br/>write-once"]
EFUSE ==> ROM["Boot ROM<br/>fixed in silicon"]
PKG --> ROM
ROM -->|verify MLO| SPL["MLO (SPL)"]
SPL -->|verify u-boot.img| UBOOT["U-Boot"]
UBOOT -->|verify kernel + dtb| LINUX["Linux Kernel"]
ROM -.->|FAIL| HALT(["Boot halted"])
SPL -.->|FAIL| HALT
UBOOT -.->|FAIL| HALT
end
Ba tầng, ba cổng verify, và cả ba đều mở chữ ký bằng đúng cái public key đã neo trong eFuse. Chữ ký đi kèm từng tầng, còn khoá thì không đi theo gói — nó nằm sẵn trong thiết bị từ lúc sản xuất.
Một chỗ đã vẽ gọn: sơ đồ dùng một cặp khoá cho cả ba tầng. Thực tế trên AM335x thì hai chặng đầu được ký bằng khoá của nhà sản xuất chip, và chỉ khoá đó nằm trong eFuse. Chặng cuối được ký bằng khoá riêng của người triển khai, nhúng trong control DTB của U-Boot. Hai cặp khác nhau, neo ở hai chỗ khác nhau.
Ba thứ cần phân biệt cho khỏi lẫn:
- Vân tay — sản phẩm của SHA-256, tính từ dữ liệu, ai cũng tính lại được, không cần key.
- Signature — sản phẩm của RSA, tính từ vân tay + private key, chỉ người giữ private key mới tạo ra được.
- Public key — thứ duy nhất thiết bị cần để mở signature lấy lại vân tay lúc ký.
Quick Summary Table
Mạch chứng minh ở mục 2.4, tóm lại:
| Bản | Có gì | Sập ở đâu |
|---|---|---|
| 1 | hash đi kèm dữ liệu | kẻ tấn công thay dữ liệu rồi băm lại, ghi đè luôn cả hash |
| 2 | thêm chữ ký, public key để chung trong gói | kẻ tấn công tự tạo cặp khoá mới, thay public key trong gói |
| 3 | public key neo ngoài gói, trong eFuse | không còn |
Nguyên tắc chung: mọi cơ chế verified boot đều quy về đúng một câu hỏi — thiết bị có sẵn đầu vào nào mà kẻ tấn công không cung cấp được hay không? Hash thì không, vì ai cũng tính lại được. Signature thì có, nhưng chỉ khi public key dùng để mở nó không đi chung với gói. Toàn bộ chuỗi ở mục 2.4 chỉ là câu hỏi đó lặp lại ở từng tầng.