Vì sao tôi აღარ dùng chứng chỉ kiểu cũ cho trang HTTPS của mình
(rachelbythebay.com)- Tác giả đã có ác cảm trong nhiều năm với giao thức ACME vì độ phức tạp và rủi ro khi triển khai
- Các ACME client hiện có thường chứa mã nguồn tiềm ẩn rủi ro bảo mật hoặc khó hiểu, nên tác giả ngại tự chạy chúng
- Nhưng do chất lượng của nhà đăng ký tên miền Gandi đi xuống và giá tăng, tác giả đã tự triển khai công cụ gia hạn chứng chỉ
- Sau rất nhiều lần thử và sai, tác giả cuối cùng đã hoàn thành thành công một công cụ tự cấp phát chứng chỉ thông qua Let's Encrypt
- Ở nửa sau bài viết, tác giả giải thích chi tiết cách ACME thực sự hoạt động cùng các chi tiết triển khai mức thấp như JSON, base64, chữ ký số
Why I no longer have an old-school cert on my https site
Bối cảnh và động lực
- Đầu năm 2023, tác giả từng giải thích lý do tiếp tục duy trì chứng chỉ kiểu cũ, nhưng đến năm 2025 thì chia sẻ lý do nay đã từ bỏ cách đó
- Sự e ngại với giao thức ACME đã có từ năm 2018, khi các công nghệ web phức tạp và những kiểu mã hóa khó hiểu trở thành rào cản lớn
- Phần lớn ACME client là mã mà tác giả khó lòng tin tưởng, và cho rằng quá rủi ro nếu để chúng chạy với quyền root
- Sau khi Gandi bị một quỹ đầu tư tư nhân mua lại, chất lượng suy giảm và giá tăng lên, nên không còn lý do gì để tiếp tục giữ chứng chỉ cũ nữa
Bắt đầu tự triển khai
- Thay vì dùng công cụ có sẵn, tác giả trực tiếp tự viết từng hàm tiện ích nhỏ một
- Bắt đầu từ việc bọc thư viện JSON cho C là jansson để có thể dùng trong C++
- Tác giả đã xem xét nhiều thư viện để tạo JWK (cấu trúc khóa), nhưng đa số không giúp được nhiều nên quyết định tự triển khai
- Trong quá trình đó, tác giả đã nhiều lần dừng lại rồi bắt đầu lại, dần dần kết nối các thành phần nhỏ lại với nhau
Môi trường thử nghiệm và áp dụng thực tế
-
Để tránh tác động trực tiếp đến máy chủ thật của Let's Encrypt, tác giả dùng máy chủ ACME thử nghiệm tên là "pebble" trong một môi trường riêng biệt
-
Sau vô số lần thất bại, tác giả đã hoàn thành công cụ nguyên mẫu nhận CSR làm đầu vào và cấp phát chứng chỉ, và
- thử nghiệm thành công trên máy chủ staging của Let's Encrypt
- thành công cả trong môi trường production
- đã áp dụng xong cho website thực tế
Giải thích chi tiết giao thức ACME
- Tạo khóa RSA và tạo CSR (Certificate Signing Request) có bao gồm CN và SAN
- Phân tích JSON từ URL directory của ACME để lấy ra các endpoint như newNonce, newAccount, newOrder
- Trích xuất modulus và public exponent từ private key, rồi chuyển chúng sang mã hóa base64url phù hợp cho web
- Sau khi tạo JWK, thực hiện ký RSA SHA256 cùng với JSON payload
- Lấy Nonce bằng một yêu cầu HTTP HEAD, sau đó gửi POST cho yêu cầu đã được ký để tạo tài khoản
- Header phản hồi
Locationkhông phải chuyển hướng thực sự mà được dùng như URL định danh tài khoản
Độ phức tạp của giao thức ACME
- Dù chỉ là cấp phát chứng chỉ đơn thuần, nó vẫn đòi hỏi
- băm SHA256, base64web, JSON lồng trong JSON, chữ ký RSA
- yêu cầu HEAD, định danh tài khoản qua header Location, cần Nonce dùng một lần, v.v.
- Tác giả cho biết ngay cả phần đặt hàng chứng chỉ, chứng minh quyền sở hữu tên miền (như bản ghi TXT), và hoàn tất xác thực vẫn còn chưa được đề cập đến
- Một số client vẫn hoạt động dù triển khai sai phần mã hóa
publicExponent, qua đó tác giả cũng chỉ ra sự lỏng lẻo của tiêu chuẩn
Kết luận
- ACME quá phức tạp, và là một hệ thống đòi hỏi vô số lần thử sai cùng rất nhiều công sức nếu tự triển khai
- Dù vậy, tác giả đã chia sẻ rằng mình đã bỏ được chứng chỉ kiểu cũ và chuyển đổi thành công sang phương thức tự động hóa hoàn toàn
- Tác giả cũng đùa rằng không biết sự phức tạp này có phải là một cấu trúc để đảm bảo việc làm cho ai đó hay không
1 bình luận
Ý kiến trên Hacker News
Tôi là tech lead của nhóm SRE/infra tại Let’s Encrypt, nên là người đã suy nghĩ rất nhiều về các vấn đề kiểu này
JSON Web Signature thực sự là một định dạng khá rắc rối, và ACME API cũng rất nghiêm túc theo đuổi tính RESTful
Nếu tự tay thiết kế thì có lẽ tôi đã không làm như vậy
Tôi nghĩ bối cảnh tạo ra cấu trúc này có phần đến từ ý định của IETF muốn tận dụng nhiều tiêu chuẩn IETF, cùng với kiểu thiết kế theo ủy ban
Chỉ cần có vài thư viện cho JSON, JWS và HTTP thì mọi thứ đã đỡ hơn nhiều, nhưng vấn đề là đặc biệt trong C, ngay cả các thư viện đó cũng không dễ dùng
Bản thân ngôn ngữ RFC cũng phức tạp và thường tham chiếu sang nhiều tài liệu khác, nên chúng tôi đang làm thêm một client tương tác và tài liệu để hỗ trợ việc này
Tôi không thật sự hiểu ý nói JWS là một định dạng rắc rối
Tôi đã xử lý nhiều thứ phức tạp như ASN.1, Kerberos, PKI, nên không thấy JWS là định dạng quá khó
Ngay cả khi tự viết bằng mã nguồn thì tôi vẫn thấy nó dễ hơn nhiều so với S/MIME, CMS, Kerberos, v.v.
Cần giải thích thêm JWS “rắc rối” ở chỗ nào
Nếu nói về vấn đề của JWT, thì tôi nghĩ cốt lõi hơn là chưa có quy chuẩn rõ ràng về cách HTTP user agent nên nhận hay yêu cầu JWT
Có người nói rằng “muốn cấp hơn 3 chứng chỉ thì phải trả tiền”, nhưng tôi đã dùng 5 năm nay mà chưa từng nhận hóa đơn nào, nên có vẻ đây là hiểu nhầm hoặc thông tin sai
Khi nói về phần xử lý
e=AQABthay vìe=65537, có giải thích rằng nguyên nhân nằm ở đặc tính JSON không xử lý số cho đúngNếu đưa một giá trị cực lớn như 4723476276172647362476274672164762476438 vào JSON parser, phần lớn parser sẽ âm thầm cắt nó về số nguyên 64-bit hoặc float, hoặc may mắn thì báo lỗi
Ngôn ngữ như Common Lisp có thể xử lý tốt, nhưng trên thực tế không nhiều người phát triển trong môi trường như vậy
Vì thế, nếu muốn truyền số lớn trong JSON một cách ổn định thì chuyển nó thành mảng byte qua base64 có lẽ còn tốt hơn
Dù bề ngoài có vẻ chạy ổn, đây lại là nguồn gốc của nhiều vấn đề bảo mật, nên việc xử lý mọi con số trong giao thức theo cách này cũng là hợp lý
Tuy vậy, nhược điểm là JSON sẽ mất tính dễ đọc thân thiện với con người, và cá nhân tôi thấy S-Expression được chuẩn hóa sẽ là lựa chọn tốt hơn nhiều
Nhưng thế giới đã chọn JSON
Nếu không hiểu vì sao thế giới chọn JSON thì tôi nghĩ là đang cố tình phớt lờ
JSON cho phép con người tự viết/chỉnh sửa/đọc rất dễ với phần lớn dữ liệu
Trong khi đó Canonical S-Expression yêu cầu thêm thông tin độ dài trước mỗi phần tử, nên thao tác thủ công cực kỳ phiền
Việc viết S-Expression đòi hỏi phải đếm ký tự và sửa tiền tố liên tục, rất bất tiện
Trái với dự đoán, chính khả năng viết tay và chỉnh sửa dễ dàng này là lý do JSON sống sót
Nhân tiện, Ruby JSON parser xử lý số lớn cũng khá tốt
Tôi từng vật lộn với một lỗi trong ứng dụng C# khi JSON serializer xuất
BigIntdưới dạng số, rồi JS nhận vào và âm thầm diễn giải saiVẫn thấy khó tin khi overflow thay vì báo lỗi lại là hành vi tiêu chuẩn
Từ đó về sau, tôi có thói quen luôn xử lý mọi số lớn hơn 32-bit dưới dạng chuỗi
So sánh giữa
{"e":"AQAB"}và{"e":65537}cũng có lý, nhưng nếu so với{"e":"65537"}thì kết quả xử lý của mọi JSON parser cũng giống nhauDù là số hay chuỗi thì việc chuyển đổi đều rõ ràng
Tất nhiên nếu số lớn đến mức không thể đưa vào
doublethì đó là vấn đề của ngôn ngữ hay parser, nhưng tôi nghĩ nó tách biệt với cách biểu diễnTôi nghĩ vấn đề của JSON không nằm ở bản thân định dạng, mà ở chỗ parser vốn được tạo ra để ánh xạ sang kiểu của JS
Một số parser có thể xử lý tốt hơn, nhưng làm vậy thì JSON sẽ mất tính di động
Dù chuyển sang Base64 thì vấn đề tương tự vẫn xảy ra (vì không theo chuẩn)
Có thể parse tùy biến bằng
replacervàreviver, nhưng không phải môi trường nào cũng đảm bảo có các tính năng nàyCuối cùng, giả định rằng có thể giải thích JSON bằng parser tiêu chuẩn mới chính là nguồn gốc của lỗi
Nếu gọi nó là một định dạng khác thay vì JSON thì có lẽ vấn đề sẽ ít hơn, nhưng hễ trông giống JSON thì mọi người vẫn sẽ cố ném thẳng vào parser
Go có thể giải mã số thành chuỗi mà không mất dữ liệu thông qua kiểu
json.NumberTôi cũng giới thiệu một trong những kiểu decimal chính xác tùy ý gần như là “yêu thích nhất” của mình https://github.com/ncruces/decimal?tab=readme-ov-file#decimal-arithmetic
Nói nửa đùa nửa thật thì tôi không thấy rõ vì sao S-Expression lại tốt hơn trong trường hợp này
Ngay cả trong họ LISP cũng có những ngôn ngữ không hỗ trợ số học độ chính xác tùy ý
Tôi thấy lạ vì sao tác giả lại có thái độ chỉ trích ACME và nhiều client như vậy
Có vẻ đây không đơn thuần là vấn đề kỹ năng sử dụng, nên tôi đoán là họ có ác cảm với chính khái niệm ACME hoặc toàn bộ hệ công cụ xung quanh
Từ năm 2019, chúng tôi cũng đã triển khai LE cho một vài site và đã thử nhiều ACME client khác nhau
Chẳng hạn Crypt-LE khá phù hợp với nhu cầu của chúng tôi, và khi tích hợp với Sectigo ACME thì le64 không đủ nên chúng tôi đã dùng thử certbot, lego, posh-acme và nhiều thứ khác
Cuối cùng, chúng tôi sửa vấn đề môi trường GHA trong certbot để dùng, và posh-acme cũng khá tốt
Đọc lại thì có vẻ giọng điệu sắc bén của tác giả không nhắm vào ACME hay client, mà nhắm vào chính đặc tả
Kết luận là ý tưởng ACME thì tốt, nhưng cách triển khai và áp dụng thực tế lại gây thất vọng
Tôi nghĩ mình có góc nhìn tương tự tác giả
Tôi trích câu của tác giả rằng “nhiều client hiện có là mã nguy hiểm, và tôi không thể tin tưởng để chạy chúng với quyền root trên server của mình”
Với các công việc nhạy cảm về bảo mật, tôi nghĩ thái độ thận trọng như vậy là hoàn toàn hợp lý
Với những ai khó hiểu giọng văn của bài gốc, đây là liên kết tới các bài viết cũ giúp cung cấp thêm bối cảnh
Cũng có nhiều người ghét việc phải chạy thứ gì đó khó hiểu trên server của mình, và tôi cũng đồng cảm với suy nghĩ ấy
Nhưng lĩnh vực bảo mật vốn là trò mèo vờn chuột nên bản chất là luôn phải thay đổi, cuối cùng vẫn phải theo kịp
May mắn là với ACME, bạn có tự do viết client theo ý mình
Không nhất thiết phải dùng certbot, và nó cũng không phải kiểu cơ chế khóa tài nguyên của bạn như TPM
Nếu định tự triển khai ACME client từ đầu, thì kinh nghiệm chia sẻ là việc đọc trực tiếp các RFC (và tài liệu liên quan như JOSE) thật ra dễ hơn tưởng tượng
Người này cũng đã tự triển khai và viết một bài tổng hợp để giải thích luồng ACME v2 https://www.arnavion.dev/blog/2019-06-01-how-does-acme-v2-work/
Nó không thay thế RFC chính thức, nhưng rất hữu ích nếu dùng như sơ đồ luồng và chỉ mục theo từng kiểu xử lý
Từng có người trực tiếp triển khai ACME client làm bài tập cuối kỳ của lớp bảo mật MIT https://css.csail.mit.edu/6.858/2023/labs/lab5.html
Châm biếm rằng thay vì ngồi đọc manual, cứ đăng một bài trên Hacker News giải thích toàn bộ quy trình bằng tiếng Anh thì lại kiếm được nhiều “điểm Internet” hơn
Có lời cảm ơn tác giả vì đã chỉ ra rằng độ phức tạp của các giao thức hạ tầng web đang tiếp tục tăng
Những tiêu chuẩn như vậy không chỉ là gánh nặng với lập trình viên phải dùng tool hay client, mà còn hoạt động như một “rào cản pháp quy”, khiến cuối cùng chỉ các công ty lớn sẵn có mới đáp ứng nổi điều kiện để vận hành Internet
Chỉ riêng ACME chưa phải là rào cản không thể vượt qua, nhưng mọi thứ cộng dồn lại sẽ thành một bức tường
Trên OpenBSD có một ACME client rất đơn giản và nhẹ, được tích hợp sẵn trong base OS
Nghe nói nó được tạo ra vì các lựa chọn trước đó quá nặng nề và đi ngược triết lý Unix
Hơi tiếc là tác giả dường như không cân nhắc hướng này
Có lẽ chỉ cần chút công sức là cũng có thể port sang các OS khác
Tôi lại nghĩ ACME client của OpenBSD là ví dụ cho việc triết lý OpenBSD chưa hiểu vì sao bảo mật lại phức tạp đến vậy
Client này được cài và dùng ngay trên chính máy đó, với cấu trúc tách biệt để các thành phần không ảnh hưởng lẫn nhau
Nhưng bản thân giao thức ACME hoàn toàn cho phép phân tách triệt để (
air-gapping), nghĩa là web server, bộ phận yêu cầu chứng chỉ và DNS server có thể nằm ở các môi trường khác nhauNếu không dùng client tích hợp của OpenBSD thì có thể phức tạp hơn, nhưng xét theo nguyên tắc thiết kế bảo mật thì cách đó vượt trội hơn
“Cứ cài OpenBSD là xong” chỉ đơn thuần là con đường dễ hơn mà thôi
Cũng giới thiệu
uacme(https://github.com/ndilieto/uacme)Đây là mã C gọn nhẹ, và sau khi liên tục gặp rắc rối với Python client của LE trên hệ thống chạy pin, tôi đã chuyển sang dùng nó rất ổn định
Tôi đang trực tiếp dùng ACME client của OpenBSD, và nó hoạt động rất tốt
Khuyến nghị “hãy tạo khóa riêng RSA 4096-bit” thực ra chỉ làm giảm tốc độ cho người truy cập, còn bảo mật thực tế thì vẫn ở mức 2048-bit
Nhấn mạnh rằng dùng chứng chỉ leaf RSA 2048-bit sẽ tốt hơn
Có người hỏi liệu 4096-bit có chống lại các kiểu thu thập thụ động/giải mã trong tương lai tốt hơn không
Cũng thắc mắc liệu bảo mật của chứng chỉ trung gian có ảnh hưởng tới các cuộc tấn công bất đối xứng hay không
Có người cố tình dùng RSA 4096-bit vì web host chỉ hỗ trợ khóa RSA, để thúc họ sớm hỗ trợ khóa EC hơn
Tự tay làm mấy việc này đúng là có thể giúp nâng trình, nhưng giọng điệu bài viết khiến người ta cảm giác tác giả đang bực bội với giao thức hoặc quá trình triển khai Let’s Encrypt
Với các thư viện ACME lightweight như https://github.com/jmccl/acme-lw cũng có thể tự động hóa khá ổn, nên tôi hơi thắc mắc vì sao lại phải khổ sở đến mức này
Các vấn đề về field/bitfield đều là di sản lịch sử của ASN.1/X.509, độ phức tạp toán học thì rất nặng, và mọi thư viện cũng như phần mềm đều bị trói vào giới hạn công nghệ của thập niên 80
Khi LetsEncrypt xuất hiện hoặc khi HTTP/2 ra đời từng có cơ hội cuối cùng để dọn dẹp mớ hỗn loạn này, nhưng trên thực tế ACME CA vẫn có thể được ghép từ shell script, OpenSSL và rượu, lại còn phải tương thích với phần mềm cũ nên không thể tạo cú nhảy vọt
Có người chia sẻ trải nghiệm rằng áp lực chuyển sang HTTPS ngày càng lớn
Ví dụ như trong WhatsApp, liên kết HTTP giờ đã không còn mở được nữa
Có đề xuất rằng dùng proxy và caching có thể giảm tải lưu lượng, là cách hay cho các server nhỏ
Nhấn mạnh rằng dù ACME có phức tạp đến đâu thì vẫn tốt hơn rất nhiều so với việc không hỗ trợ TLS
“Khóa RSA, digest SHA256, chữ ký RSA, thứ thực tế không phải base64 mà là base64, nối chuỗi, JSON bên trong JSON, dùng header Location như một định danh thay vì redirect 301, dùng request HEAD chỉ để lấy một giá trị header, cần request riêng cho nonce cho mọi request, v.v.”
“Và đó là chưa kể các bước còn phức tạp hơn như tạo order chứng chỉ, xử lý authorization/challenge, key thumbprint, cấu hình TXT record...”
Đây là mức độ phức tạp khó tin, và người bình luận gửi lời động viên cảm ơn vì đã chia sẻ phần tổng hợp này