Ký hoá cấu trúc dữ liệu sai cách
Signing data structures the wrong way
Việc serialize dữ liệu sai cách trước các hoạt động mã hóa như ký số có thể dẫn đến lỗ hổng bảo mật nghiêm trọng. Kẻ tấn công có thể lợi dụng việc các loại dữ liệu khác nhau có cùng một biểu diễn byte để thực hiện "domain confusion", từ đó làm giả các thông điệp. Các ví dụ điển hình trong lịch sử bao gồm các cuộc tấn công trên hệ thống Bitcoin và Ethereum. Để khắc phục vấn đề này, các developer cần đảm bảo phương pháp serialize của mình áp dụng "domain separation". Điều này có nghĩa là các hoạt động mã hóa sẽ gắn chặt với một loại thông điệp cụ thể, chứ không chỉ dựa trên nội dung byte đã serialize. Giải pháp được đề xuất là tích hợp các "domain separator" độc đáo trực tiếp vào IDL. Các compiler và runtime libraries sẽ tự động áp dụng các separator này, giúp liên kết mật mã các hoạt động với các cấu trúc dữ liệu riêng biệt, từ đó ngăn chặn hiệu quả các cuộc tấn công dạng này.
Làm cách nào để bạn đóng gói dữ liệu trước khi đưa dữ liệu vào thuật toán mã hóa, như Ký hiệu, Mã hóa, MAC hoặc Hash? Câu hỏi này đã kéo dài hàng chục năm mà không có lời giải thỏa đáng. Có ít nhất hai...
Làm cách nào để đóng gói dữ liệu trước khi đưa dữ liệu vào thuật toán mã hóa, chẳng hạn như Ký, mã hóa, MAC hoặc băm? Câu hỏi này đã tồn tại trong nhiều thập kỷ không có giải pháp thỏa đáng. Có ít nhất hai vấn đề quan trọng cần giải quyết. Đầu tiên, quá trình mã hóa phải tạo ra kết quả đầu ra chuẩn, vì các hệ thống như Bitcoin đã gặp khó khăn khi hai mã hóa khác nhau giải mã thành cùng một dữ liệu trong bộ nhớ. Nhưng quan trọng hơn, hệ thống mã hóa phải cân nhắc vấn đề quan trọng của miền sự chia ly.
Để hiểu vấn đề này, hãy xem một ví dụ đơn giản, sử dụng
một IDL nổi tiếng như protobufs. Hãy tưởng tượng một hệ thống phân tán có hai
các loại thông báo (trong số những loại khác): TreeRoot đóng gói thư mục gốc
của cây trong suốt và KeyRevoke biểu thị khóa đang bị thu hồi:
tin nhắn TreeRoot {
dấu thời gian int64 = 1;
hàm băm byte = 2;
}
tin nhắn KeyRevoke {
dấu thời gian int64 = 1;
publicKeyFingerprint băm = 2;
}
Do một sự xui xẻo, hai cấu trúc dữ liệu này xếp hàng từng trường,
mặc dù xét về mặt chương trình và lập trình viên, chúng có nghĩa là
những điều hoàn toàn khác nhau. Nếu một nút trong hệ thống này ký TreeRoot và
đưa chữ ký vào mạng, kẻ tấn công có thể cố gắng giả mạo
Thông báo KeyRevoke tuần tự hóa từng byte thành một thông báo giống như thông báo
đã ký gốc cây, sau đó ghim chữ ký TreeRoot vào dữ liệu KeyRevoke
cấu trúc. Bây giờ có vẻ như người ký đã ký KeyRevoke khi nó
chưa bao giờ làm vậy, nó chỉ ký một TreeRoot. Người xác minh có thể bị lừa
“xác minh” một tuyên bố mà người ký không bao giờ có ý định.
Đây không phải là một cuộc tấn công lý thuyết. Nó có một lịch sử lâu dài thành công, trong bối cảnh Bitcoin, DEX trong Ethereum, TLS, JWT , và AWS, trong số những người khác.
Và mặc dù ví dụ nhỏ của chúng tôi liên quan đến việc ký tên, nhưng ý tưởng tương tự cũng đang diễn ra cho MAC'ing (thông qua HMAC hoặc SHA-3), băm hoặc thậm chí mã hóa, như hầu hết các mã hóa những ngày này được xác thực. Nói chung, mật mã nên đảm bảo rằng người gửi và người nhận không chỉ đồng ý về nội dung của tải trọng mà còn là “loại” dữ liệu.
Các hệ thống gặp khó khăn trong việc phân tách miền sử dụng các kỹ thuật đặc biệt, chẳng hạn như băm tên địa phương của các phương pháp chương trình xung quanh trong Solana , các phương pháp hay nhất về Ethereum hoặc “chuỗi ngữ cảnh” trong TLS v1.3. Với vô số lỗi nghiêm trọng có thể xảy ra ở đây, một cách tiếp cận có hệ thống hơn được đảm bảo. Khi xây dựng FOKS, chúng tôi đã phát minh ra một thứ.
Ý tưởng: Bộ tách tên miền trong IDL
Ý tưởng chính đằng sau kế hoạch tuần tự hóa mật mã của FOKS dữ liệu (được gọi là Snowpack) là đặt các dấu phân cách miền ngẫu nhiên, không thể thay đổi trực tiếp vào IDL:
struct TreeRoot @0x92880d38b74de9fb {
dấu thời gian @0 : Uint;
hàm băm @1 : Blob;
}
Một trình biên dịch đơn giản sẽ dịch IDL sang ngôn ngữ đích. Trong mục tiêu
ngôn ngữ, một thư viện thời gian chạy cung cấp một phương thức để ký một đối tượng như vậy: nó tạo một phép nối của dấu phân cách miền
(@0x92880d38b74de9fb) và tuần tự hóa đối tượng, sau đó cung cấp dữ liệu
luồng byte vào nguyên thủy ký. Tương tự, việc xác minh một đối tượng
xác minh kết nối được xây dựng lại này dựa trên chữ ký được cung cấp.
Lưu ý rằng dấu phân cách tên miền không xuất hiện trong quá trình tuần tự hóa cuối cùng
(điều này sẽ gây lãng phí byte), vì cả người ký và người nhận đều đồng ý về điều đó thông qua điều này
đặc tả giao thức chia sẻ. Mã hóa, HMAC và hàm băm hoạt động theo cách tương tự.
Trong Go (cũng như TypeScript và các ngôn ngữ khác), hệ thống kiểu thực thi các đảm bảo an ninh. Trình biên dịch xuất ra một phương thức:
func (t TreeRoot) GetUniqueTypeID() uint64 { return 0x92880d38b74de9fb }
Và các phương thức Sign và Verify trông như sau:
func Ký(khóa Khóa, obj VerificableObjecter) ([]byte, lỗi )
func Xác minh(key Key, sig []byte, obj VerificableObjecter) lỗi
VeribilityObjecter là một giao diện yêu cầu phương thức GetUniqueTypeID(),
ngoài các phương thức khác như EncodeToBytes.
Các bộ phân tách miền 64 bit này không bắt buộc đối với tất cả các cấu trúc và nhiều cấu trúc
không cần chúng. Tuy nhiên, các cấu trúc không được gắn thẻ này không nhận được các phương thức GetUniqueTypeID() và do đó không thể
được đưa vào Ký hoặc Xác minh mà không có lỗi loại. giống nhau
dùng để mã hóa, MAC, băm tiền tố, v.v.
Miễn là các dấu phân cách miền ngẫu nhiên là duy nhất (chúng sẽ là như vậy, trên toàn cầu, với xác suất cao), không có cơ hội cho người ký và người xác minh sắp xếp sai loại dữ liệu mà họ đang xử lý. Bất kỳ sự thay thế nào như một cái mà chúng ta đã thảo luận trước đó sẽ không xác minh được. Các nhà phát triển nên sử dụng đơn giản công cụ, trong IDE hoặc CLI, để tạo ra các bộ phân tách miền ngẫu nhiên này và chèn chúng vào thông số kỹ thuật giao thức của chúng.
Logic đằng sau việc tạo ngẫu nhiên các trình phân tách tên miền gợi nhớ đến tạo p(x) ngẫu nhiên trong Rabin Lấy dấu vân tay . Trong căn cứ trường hợp, nếu Bob ngồi viết một dự án mới ngày hôm nay và tạo ra tất cả tên miền các dấu phân cách ngẫu nhiên, với xác suất rất cao, anh ấy biết các trình xác minh trong dự án sẽ không bao giờ xác minh chữ ký được tạo bởi một dự án hiện có khác. Ngẫu nhiên Thế hệ này giúp anh ta đỡ phải suy nghĩ về những va chạm nhầm lẫn. Như một bước quy nạp, hãy tưởng tượng Mallory xây dựng một dự án mới sau khi Bob xuất bản đặc tả giao thức. Cô ấy có thể cố tình sử dụng lại bộ phân tách tên miền của anh ấy. Nếu Bob cấp cho dự án của cô ấy quyền truy cập vào các khóa riêng của anh ấy, cô ấy có thể nhầm lẫn giữa những người xác minh trong khóa riêng của anh ấy dự án xác minh chữ ký do cô ấy tạo ra. Chúng tôi khẳng định không có gì phải được thực hiện ở đây. Cuộc tấn công của Mallory chống lại các bộ phân tách tên miền có thể xảy ra ở bất kỳ hệ thống, và vì dự án của cô ấy là độc hại nên thật sai lầm khi tin tưởng nó vào tay anh ấy. khóa riêng ngay từ đầu. Mặt khác, nếu Mallory tạo ra tên miền phân tách ngẫu nhiên, cô ấy và Bob nhận được sự đảm bảo mong muốn giống như trong trường hợp cơ bản.
Một rủi ro khác là các tác nhân mã hóa hoặc tự động hoàn thành AI có thể sao chép-dán các bộ phân tách tên miền hiện có hoặc tạo chúng một cách tuần tự. Trình biên dịch gói tuyết và thời gian chạy đảm bảo rằng tất cả các dấu phân cách tên miền là duy nhất trong cùng một dự án và lỗi hoặc hoảng loạn (tương ứng) nếu không.
Mặc dù các nhà phát triển có thể tự do thay đổi tên cấu trúc TreeRoot theo cách họ muốn, nhưng họ
nên giữ cố định dấu tách miền trong suốt thời gian tồn tại của giao thức,
ngay cả khi họ thêm hoặc xóa các trường. Như trong protobuf
và Cap'n Proto, hệ thống hỗ trợ xóa, thêm trường nên rất lâu
vì vị trí của các trường còn lại (như được đưa ra bởi @0 và @1 ở trên) không bao giờ thay đổi,
và miễn là các trường đã ngừng hoạt động không bao giờ được sử dụng lại.
Snowpack IDL: Tách tên miền + Mã hóa chuẩn + hơn thế nữa!
Tính năng tách miền tích hợp là ý tưởng mới lạ trong Snowpack. Nhưng nhìn chung, nó đã được chứng minh là một hệ thống tương thích xuôi và ngược đơn giản và hiệu quả cho cả RPC và tuần tự hóa các đầu vào thành các chức năng mật mã. Chúng tôi nhấn mạnh rằng cùng một hệ thống sẽ phục vụ cả hai mục đích tốt. Ví dụ: Protobuf không đảm bảo về canonical mã hóa . Mã hóa JSON, mặc dù thường được sử dụng trong cài đặt mật mã, vẫn còn thiếu sót ở chỗ chúng thiếu bộ đệm nhị phân (là đầu ra của hầu hết các mã hóa nguyên thủy!), và do đó gây ra sự nhầm lẫn giữa các chuỗi và mã hóa base64 dữ liệu nhị phân.
Tuy nhiên, Snowpack đã kiểm tra tất cả các hộp cho chúng tôi. Ý tưởng đơn giản là mã hóa
cấu trúc có dạng TreeRoot ở trên dưới dạng mảng vị trí giống JSON:
[ 1234567890, \xdeadbeef ]
@1 trong đặc tả giao thức ở trên hướng dẫn bộ mã hóa và bộ giải mã
để tìm trường hash : Blob ở vị trí đầu tiên
của mảng. Các trường bị bỏ qua và gỡ bỏ
được mã hóa dưới dạng nils. Nếu TreeRoot
tin nhắn nâng cấp lên thứ gì đó trông giống hơn:
struct TreeRoot @0x92880d38b74de9fb {
hàm băm @1 : Blob;
dấu thời gianMsec @2 : Uint;
}
mã hóa trung gian trở thành:
[ nil, \xdeadbeef, 1234567890123 ]
Bộ giải mã cũ vẫn có thể giải mã mã hóa mới, nhưng nhìn thấy giá trị 0 đối với mã hóa mới
dấu thời gian mà họ mong đợi. Bộ giải mã mới có thể giải mã các bảng mã cũ, nhưng
hãy xem các giá trị 0 cho trường timestampMsec mà họ mong đợi. Đó là của
tất nhiên tùy thuộc vào nhà phát triển ứng dụng để quyết định xem các điều kiện này có bị phá vỡ hay không
chương trình hay không và do đó liệu sự phát triển giao thức này có hợp lý hay không,
nhưng họ có thể yên tâm rằng việc giải mã sẽ không bị lỗi ở cấp độ giao thức.
Từ mã hóa trung gian này, Snowpack đạt đến luồng byte phẳng thông qua Mã hóa Msgpack nhưng có những hạn chế quan trọng. Đầu tiên, tất cả các mã hóa số nguyên phải sử dụng mã hóa kích thước tối thiểu có thể. Và thứ hai, từ điển có nhiều hơn một cặp khóa-giá trị không bao giờ được gửi vào bộ mã hóa, vì vậy chúng ta có thể tránh được toàn bộ vấn đề hóc búa về thứ tự khóa chuẩn. Như một kết quả là chúng tôi luôn kết thúc bằng cách mã hóa chuẩn.
Do đó, luồng tổng thể là:
Không giống như các chuyển đổi bên ngoài, các chuyển đổi bên trong (sang và từ byte) là tự mô tả và không cần định nghĩa giao thức Snowpack để hoàn thành. Lựa chọn thiết kế này cho phép khả năng tương thích về phía trước: bộ giải mã cũ có thể giải mã thông điệp từ tương lai. Nó cũng cho phép gỡ lỗi và kiểm tra thuận tiện của luồng byte.
Chúng ta đã thấy cách mã hóa và giải mã cấu trúc. Ngoài ra, Snowpack cung cấp vừa đủ
phức tạp để giải quyết mọi tình huống chúng tôi đã thấy trong FOKS.
Các tính năng quan trọng khác là: Danh sách, Tùy chọn và biến thể. Hai cái đầu tiên
tìm biểu thức đơn giản dưới dạng mã hóa dựa trên mảng. Các biến thể hoặc được gắn thẻ
hợp nhất, mã hóa dưới dạng từ điển cặp khóa-giá trị duy nhất, cho phép hiện có
Thư viện Msgpack để giải mã chúng với kiểu an toàn.
Tóm tắt
Lỗi phân tách tên miền đã liên tục tấn công các hệ thống thực. hiện tại các biện pháp giảm thiểu mang tính đặc biệt: chuỗi ngữ cảnh, băm tên phương thức và cuộn thủ công tiền tố dễ quên và khó kiểm tra.
Snowpack có cách tiếp cận khác: miền 64-bit ngẫu nhiên, không thể thay đổi các dấu phân cách tồn tại trong chính IDL và hệ thống loại đảm bảo bạn không thể ký, mã hóa hoặc MAC một đối tượng thiếu một đối tượng. Chúng tôi cho rằng ý tưởng cốt lõi này lớn hơn bất kỳ hệ thống nào và chúng tôi muốn thấy các chương trình tuần tự hóa khác áp dụng nó. Trong khi đó, tải nó trong Snowpack, có nguồn mở trên GitHub, hiện đang nhắm mục tiêu Go và TypeScript với nhiều ngôn ngữ sắp tới.
Tín dụng
Cảm ơn Jack O'Connor vì phản hồi của anh ấy trên bản nháp của bài đăng này và để xây dựng các hệ thống liên quan có ảnh hưởng đến Snowpack.
Tác giả: malgorithms