"Đừng dùng /dev/random, nó sẽ chặn" là một trong những lời khuyên được lặp lại nhiều nhất về Linux. Bài này đo lại nó.
Cái bẫy entropy không còn tồn tại
Đọc 100 MB từ mỗi thiết bị, nhân 6.12:
| Nguồn | Thời gian | Thông lượng |
|---|---|---|
/dev/random |
138 ms | 782 MB/s |
/dev/urandom |
137 ms | 771 MB/s |
entropy_avail trước: 256
entropy_avail sau : 256
Không chặn, không giảm entropy, và hai thiết bị cho cùng một tốc độ.
Trên nhân trước 5.6, cùng lệnh đó có thể treo hàng giờ trên một máy chủ không có bàn phím và không có chuột. Đó là hoàn cảnh sinh ra haveged, rng-tools, và hàng loạt bài viết khuyên theo dõi entropy_avail.
Từ nhân 5.6, /dev/random chỉ chặn đúng một lần — lúc khởi động, trước khi bộ sinh số được gieo mầm. Sau thời điểm đó nó không bao giờ chặn nữa, vì mô hình "entropy bị tiêu hao khi đọc" đã bị bỏ.
Lý do bỏ nó: một bộ sinh số giả ngẫu nhiên an toàn về mật mã, đã được gieo mầm bằng 256 bit thật, không yếu đi khi bạn đọc thêm. Việc "trừ entropy" mỗi lần đọc là mô hình sai từ đầu, và nó gây ra nhiều sự cố hơn là bảo vệ được gì.
Ba hệ quả cho hôm nay:
entropy_availlà 256 và luôn là 256. Cảnh báo dựa trên nó không bao giờ kêu, và nếu nó kêu thì đó là nhân rất cũ.havegedvàrng-toolskhông còn cần thiết trên nhân hiện đại. Chúng vẫn có ích ở đúng một chỗ: rút ngắn thời gian gieo mầm lúc khởi động trên máy ảo mới tạo.- Chọn
/dev/randomhay/dev/urandomkhông còn là quyết định về hiệu năng.
Cách kiểm tra nhân của bạn có ở phía nào của mốc này:
uname -r
cat /proc/sys/kernel/random/poolsize # 256 = nhan moi; 4096 = nhan cu
Chi phí lấy 16 byte
Kích thước điển hình cho UUID, token phiên, muối băm:
| Nguồn | µs mỗi lần | An toàn mật mã |
|---|---|---|
rand() của libc |
0,02 | không |
getrandom(0) |
0,20 | có |
/dev/random |
0,21 | có |
/dev/urandom, giữ fd mở |
0,23 | có |
/dev/urandom, mở lại mỗi lần |
0,63 | có |
Ba nguồn an toàn chênh nhau chưa tới 15%. Chọn cái nào không quan trọng về tốc độ.
Điều quan trọng là dòng cuối: mở lại tệp mỗi lần tốn 2,7 lần, và toàn bộ phần chênh là open cộng close. Với một dịch vụ sinh một token mỗi yêu cầu, đó là 0,4 µs vứt đi trên mỗi yêu cầu.
getrandom() giải quyết chuyện này gọn nhất: nó là lời gọi hệ thống, không cần mô tả tệp nào:
#include <sys/random.h>
char buf[16];
getrandom(buf, sizeof buf, 0);
Nó cũng miễn nhiễm với hai vấn đề khác của /dev/urandom: hết mô tả tệp (phần 33) và chroot không có /dev.
rand() nhanh gấp mười lần và không dùng được
0,02 µs so với 0,20 µs. Cám dỗ rõ ràng, và câu trả lời cũng rõ ràng: không bao giờ dùng rand() cho bất cứ thứ gì liên quan tới bảo mật.
rand() của glibc là một bộ sinh số tuyến tính. Biết vài giá trị đầu ra là suy được toàn bộ dãy — cả quá khứ lẫn tương lai. Token phiên, mã đặt lại mật khẩu, khoá API sinh bằng rand() đều đoán được.
Nó dùng được cho: mô phỏng, sinh dữ liệu thử, chọn ngẫu nhiên trong thuật toán, jitter cho việc thử lại. Những chỗ đó thì 0,02 µs là món hời.
Một lưu ý: rand() không an toàn với đa luồng ở nhiều cài đặt. Dùng rand_r() hoặc random_r() khi có nhiều luồng, nếu không bạn sẽ có hai luồng nhận cùng một dãy.
Với khối lớn thì mọi thứ bằng nhau
Lấy 4.096 byte mỗi lần:
| Nguồn | µs mỗi lần | MB/s |
|---|---|---|
getrandom(0) |
4,96 | 787,5 |
/dev/random |
5,01 | 779,8 |
/dev/urandom |
5,02 | 778,2 |
rand() |
5,27 | 741,2 |
Cả năm hội tụ về khoảng 780 MB/s, và rand() chậm nhất.
Kết quả này ngược hẳn với bảng 16 byte, và lý do đơn giản: ở 16 byte, chi phí là mỗi lời gọi; ở 4 KB, chi phí là mỗi byte. Bộ sinh số của nhân dùng ChaCha20 và sinh cả khối một lượt, trong khi rand() phải chạy vòng lặp bốn byte một.
Nghĩa là bộ sinh số an toàn của nhân không còn chậm hơn bộ sinh số thường khi lấy lượng lớn. Lập luận "dùng bộ sinh số yếu cho nhanh" không còn cơ sở đo lường nào.
Trong các ngôn ngữ
| Ngôn ngữ | An toàn | Không an toàn |
|---|---|---|
| C | getrandom() |
rand() |
| Python | secrets, os.urandom |
random |
| Go | crypto/rand |
math/rand |
| Java | SecureRandom |
Random |
| Node | crypto.randomBytes |
Math.random |
| Rust | rand::rngs::OsRng |
rand::thread_rng (tuỳ mục đích) |
Một bẫy riêng của Java: SecureRandom.getInstance("NativeRandom") trên nhân cũ đọc /dev/random và chặn. Đây là nguồn của rất nhiều báo cáo "ứng dụng Java treo khi khởi động trong container". Cách chữa cũ là:
-Djava.security.egd=file:/dev/./urandom
Trên nhân từ 5.6 thì vấn đề tự biến mất, nhưng tham số đó vẫn còn trong vô số tệp cấu hình.
Thử ba mươi giây
Kiểm tra nhân của bạn có còn cái bẫy cũ không:
echo "nhan : $(uname -r)"
echo "poolsize : $(cat /proc/sys/kernel/random/poolsize) (256 = nhan moi, 4096 = cu)"
echo "avail : $(cat /proc/sys/kernel/random/entropy_avail)"
echo "--- doc 10 MB tu /dev/random ---"
time dd if=/dev/random of=/dev/null bs=1M count=10 2>&1 | tail -1
echo "avail sau: $(cat /proc/sys/kernel/random/entropy_avail)"
Nếu lệnh dd xong trong vài chục mili giây và entropy_avail không đổi, mọi lời khuyên về entropy trong tài liệu vận hành của bạn đều có thể xoá đi.
Nếu nó treo, bạn đang chạy nhân trước 5.6 — và lúc đó vấn đề thật không phải entropy mà là phiên bản nhân.
Phần sau: tín hiệu và dừng êm — đo cách tiến trình nên phản ứng.