1 điểm bởi GN⁺ 2025-10-25 | 1 bình luận | Chia sẻ qua WhatsApp
  • Một lập trình viên chia sẻ hành trình kỹ thuật lẫn tinh thần khi tự triển khai trình biên dịch ASN.1 (dasn1) bằng ngôn ngữ D
  • Dự án hướng tới triển khai x.509 certificate và TLS 1.3, đồng thời hỗ trợ xử lý mã hóa DER phức tạp của ASN.1
  • Bài viết đi sâu vào sự khó nhằn trong cấu trúc của ASN.1, độ khó khi hiện thực các đặc tả x.680~x.683, cũng như cách tận dụng metaprogramming của D
  • Bài viết giải thích cụ thể cách các tính năng của D như static import, mixin template, typeof(), alias this hữu ích cho việc sinh mã và thiết kế AST/IR
  • Tác giả thẳng thắn chia sẻ rằng “ASN.1 rất đau khổ nhưng cũng là một trải nghiệm học hỏi lớn”, đồng thời nói thật về những khó khăn thực tế và phần thưởng của việc làm trình biên dịch

Tổng quan dự án và động lực

  • Tác giả đang phát triển Juptune, một framework I/O bất đồng bộ dựa trên D, và để triển khai TLS thì cần tự xử lý mã hóa ASN.1 DER
    • Để parse cấu trúc x.509 certificate của TLS, cần hiểu cách biểu diễn dữ liệu phức tạp của ASN.1
  • Dự án này bắt đầu như một thử thách cá nhân vì học hỏi và niềm vui, và đã tiến tới giai đoạn parse thành công một số certificate thực tế
  • ASN.1 là một tiêu chuẩn cũ từ thập niên 1990 nhưng vẫn được dùng rộng rãi trong TLS, SNMP, LDAP và nhiều hệ thống hiện đại
  • Tác giả nhận xét rằng “ASN.1 được dùng khắp nơi trên thế giới nhưng phần lớn lập trình viên thậm chí không biết nó tồn tại”

ASN.1 là gì

  • ASN.1 (Abstract Syntax Notation One) là một ngôn ngữ để định nghĩa và mã hóa cấu trúc dữ liệu, có thể xem như “tổ tiên của protobuf”
  • Tiêu chuẩn gồm ký pháp (x.680~x.683)các quy tắc mã hóa (BER, CER, DER, PER, XER, JER, v.v.)
    • BER: định dạng TLV cơ bản, hỗ trợ độ dài vô hạn
    • CER: biến thể bị ràng buộc của BER, luôn dùng độ dài vô hạn
    • DER: tập con xác định của BER, được dùng làm chuẩn trong mật mã học
    • PER/OER: mã hóa nén ở mức bit
    • XER/JER: mã hóa dựa trên XML·JSON
  • Có nhiều kiểu mã hóa nên khá phức tạp, nhưng đổi lại tính linh hoạt và khả năng mở rộng rất cao

Độ phức tạp của ký pháp ASN.1

  • Tiêu chuẩn cơ bản của ASN.1 là x.680, còn các đặc tả mở rộng (x.681~x.683) được viết bằng văn phong học thuật cực kỳ khó nhằn
  • Chỉ với x.680 cũng đã có thể triển khai, nhưng vẫn rất khó do có nhiều quy tắc chuyển nghĩa và biến đổi cú pháp
  • x.681 định nghĩa hệ thống Information Object Class và hỗ trợ cú pháp khởi tạo riêng
    • Ví dụ: CALLED &name [WHO IS &age YEARS OLD]
  • x.682 định nghĩa Table Constraint, còn x.683 định nghĩa kiểu tham số hóa (Parameterized)
    • Đây là khái niệm gần giống generic của D, có thể nhận cả kiểu và giá trị làm tham số

Những tính năng thú vị của ASN.1

  • Hệ thống ràng buộc (Constraint): có thể chỉ định trực tiếp phạm vi giá trị hoặc kích thước ngay khi định nghĩa kiểu
    • Ví dụ: UInt8 ::= INTEGER (0..255)
    • Hỗ trợ các toán tử SIZE, UNION(|), INTERSECTION(^)
  • Hệ thống quản lý phiên bản: phân biệt rõ phiên bản module qua OBJECT IDENTIFIER
    • Ví dụ: id-pkix1-implicit(19) vs id-mod-pkix1-implicit-02(59)
    • Có thể định danh module rõ ràng mà không sợ xung đột tên

Vì sao ngôn ngữ D có lợi cho việc sinh mã

  • static import của D giúp tránh xung đột tên và cho phép giữ nguyên tên kiểu ASN.1
  • Tính năng module-local lookup (.Type1) giúp giới hạn rõ ràng việc tra cứu symbol
  • typeof() cho phép suy luận kiểu tự động nên không cần quản lý thủ công khi sinh mã
  • Cho phép dấu phẩy cuối (trailing comma) nên việc sinh mã đơn giản hơn
  • Nhờ kết hợp hằng số tại compile time, có thể ghép chuỗi cả trong hàm @nogc

Các ví dụ triển khai tận dụng tính năng của D

Node AST dựa trên mixin template

  • Tác giả dùng tính năng mixin template của D để định nghĩa các node AST của cú pháp ASN.1
    • Tái sử dụng từng loại node (List, Container, OneOf) dưới dạng template
    • Thay vì kế thừa phức tạp, dùng sao chép mã ở compile time để đơn giản hóa

API dựa trên template và kiểm tra tại compile time

  • Node Container chứa nhiều node con và thực hiện kiểm tra kiểu tại compile time
    • Có thể truy cập an toàn theo dạng node.getNode!Asn1TagDefaultNode
  • Node OneOf lưu một trong nhiều kiểu, đồng thời hỗ trợ pattern matching bằng hàm match
    • Vì bắt buộc phải định nghĩa handler cho mọi kiểu nên đảm bảo an toàn tại compile time

Tận dụng package thử nghiệm quản lý bộ nhớ của D

  • Dùng std.experimental.allocator để triển khai khởi tạo/giải phóng đối tượng trong môi trường @nogc
    • Kết hợp Region, StatsCollector và các thành phần khác để tạo allocator tùy biến
    • Tuy nhiên package này vẫn ở trạng thái thử nghiệm suốt 10 năm

Tính năng alias this

  • Dùng alias this để struct bọc hoạt động như trường dữ liệu bên trong
    • Ví dụ có thể cast ngắn gọn như cast(Asn1ValueReferenceIr)item

version(unittest)

  • Từ khóa version(unittest) được dùng để định nghĩa các hàm chỉ dành cho test, không được đưa vào bản build thực tế

Test harness với template + with()

  • Logic test dùng chung được đưa vào template, và dùng câu lệnh with() để viết mã test ngắn gọn
    • Có thể gọi T() thay vì Harness.T()

Những khó khăn chính trong quá trình triển khai

Cú pháp value sequence

  • Nhiều dạng cú pháp giá trị bắt đầu bằng {} trở nên mơ hồ tùy theo ngữ cảnh
    • Trong comment của parser thậm chí có câu “cái này chẳng vui vẻ gì” vì quá phức tạp
  • Vì tách riêng phân tích cú pháp và phân tích ngữ nghĩa nên độ khó xử lý càng tăng

Sự mơ hồ của đặc tả

  • Có những hành vi không được ghi rõ trong tài liệu, chẳng hạn quy tắc một tag phải được xử lý là EXPLICIT trong một số điều kiện nhất định
  • Cách quản lý phiên bản module cũng không được định nghĩa rõ ràng

Cần triển khai ràng buộc ba lần

  1. Để kiểm tra cú pháp
  2. Để kiểm tra tính hợp lệ của giá trị
  3. Để sinh mã runtime
  • Khi xử lý UNION và INTERSECTION, việc tạo thông báo lỗi cũng rất phức tạp

Ảo tưởng về IR node bất biến

  • Tác giả từng nghĩ rằng sau khi chuyển AST sang IR thì sẽ không cần sửa nữa, nhưng
    các quá trình chuyển nghĩa như AUTOMATIC TAGS vẫn buộc phải thay đổi dữ liệu

Độ phức tạp toàn diện của ASN.1

  • x.509 chỉ dùng cú pháp cũ nên còn tương đối đơn giản, nhưng các đặc tả hiện đại thì bắt buộc phải triển khai x.681~x.683
    • Vì thế ASN.1 gần như chỉ được dùng trong giới học thuật hoặc thương mại chuyên biệt

Vấn đề ANY DEFINED BY

  • ANY DEFINED BY là cấu trúc mà kiểu thay đổi theo giá trị của trường khác
    • dasn1 không triển khai phần này mà thay bằng intrinsic tùy biến Dasn1-Any
    • Khi decode thực tế vẫn cần xử lý thủ công

Quá tải thông tin

  • Vừa làm ASN.1, x.68x, x.690, Juptune và nhiều dự án khác song song khiến rất khó giữ được ngữ cảnh của codebase

Hiện thực làm trình biên dịch

  • Công việc có hàng nghìn visitor cho node, mã lặp đi lặp lại, và các triển khai chỉ khác nhau rất nhỏ, nên vừa tẻ nhạt vừa nặng nhọc
  • Nhưng ở mỗi giai đoạn đều có cảm giác thành tựu lớn và hiệu quả học hỏi rõ rệt
  • Tác giả hồi tưởng rằng “chắc chẳng ai dùng đâu, nhưng mình đã có được trải nghiệm làm trình biên dịch thật sự”
  • Cuối bài, tác giả đùa rằng “đừng đụng vào ASN.1, cuộc đời bạn sẽ thay đổi”

Kết luận

  • Dù đã làm suốt một năm, dasn1 vẫn chưa hoàn thiện, nhưng đây là cơ hội để tác giả hiểu sâu hơn về tiềm năng của ngôn ngữ D và độ phức tạp của ASN.1
  • Tác giả khép lại bài viết bằng ước mơ một ngày nào đó có thể ghi “trình biên dịch ASN.1 + kinh nghiệm triển khai TLS 1.3” vào CV, đồng thời nhìn lại sự trưởng thành của một lập trình viên và hiện thực của ngành bằng giọng điệu hài hước

1 bình luận

 
GN⁺ 2025-10-25
Ý kiến trên Hacker News
  • Tóm lại là tác giả muốn nói về ASN.1, ngôn ngữ D, và bản thân trình biên dịch
    Nhưng vì không tìm ra được một khuôn dạng nhất quán nên đã gom các suy nghĩ liên quan lại thành một bài blog
    Mức độ hoàn thiện chưa cao, nhưng đây là chủ đề khó mà nói ngắn gọn nên mong mọi người thông cảm

    • Có vẻ ví dụ giao nhau (intersection example) không hoạt động như dự định
      Xét về mặt toán học thì {0} ∪ ({2} ∩ {4,5,6,7,8}) = {0}, nên kết quả cuối cùng chỉ cho phép một giá trị duy nhất
    • Cứ nhắc đến ngôn ngữ D là như đang triệu hồi Walter Bright
      Cá nhân tôi thật sự rất thích D, nhưng thực tế thì Go và Rust đang được dùng rộng rãi hơn nhiều
    • Tôi cũng từng xử lý dữ liệu ASN.1, và đặc biệt phần liên quan đến chứng chỉ rất đau đầu
      Tôi rất đồng cảm với những vất vả của tác giả
    • Tôi đã đọc bài này rất thích thú
      Tôi yêu D nhưng đã bỏ không đụng đến nó trong một thời gian dài
      Trước đây tôi từng triển khai parser và protocol, nên càng thấy thú vị hơn
    • Blog suy cho cùng là không gian của chính mình, nên mong tác giả cứ tiếp tục theo cách của mình
  • “OMG ASN.1”, đúng là một chủ đề thật đáng mừng
    Tôi còn nhớ thời Internet đang lớn mạnh, khi IETF phát triển các giao thức
    Khi đó doanh nghiệp chưa quan tâm đến Internet, mà giới học thuật và IETF là bên dẫn dắt
    Nhưng khi các công ty nhận ra có thể kiếm tiền từ đó thì Protocol Wars bắt đầu
    ASN.1 là sản phẩm của cuộc chiến đó, đồng thời là ví dụ cho sự va chạm giữa văn hóa doanh nghiệp và văn hóa học thuật
    Có thể ví doanh nghiệp là “văn hóa công thức”, còn học thuật là “văn hóa chức năng”
    Sự khác biệt trong lối tư duy này cũng gợi ra nhiều điều cho văn hóa phát triển AI ngày nay

    • Có lần tôi xem phim Father of the Bride và giật mình khi thấy nhắc đến mạng X.25
      Nghĩ đến chuyện ngày đó mọi thứ có thể đã đi theo hệ địa chỉ kiểu “CN=wikipedia, OU=org, C=US” thay vì Internet như bây giờ mà thấy rùng mình
    • Tôi chợt nghĩ “OMG ASN.1” nên là tên ban nhạc tiếp theo của mình
    • Một phần câu chuyện là đúng, nhưng gọi tác nhân chính là “doanh nghiệp” thì hơi thiếu chính xác
      Trên thực tế, ITU và ISO mới là trung tâm
      Sau đó vào cuối thập niên 90 còn có một “cuộc chiến giao thức” khác, và lần này IETF đã thua
    • Cuộc chiến này cũng là một phần của quá trình thương mại hóa ban đầu (en-shittification) của Internet
      ISO theo đuổi sự hoàn hảo nên chậm chạp, còn IETF thì chạy nhanh với tinh thần “để sửa sau”
      Kết quả là họ gặp vấn đề các giao thức bị đóng cứng
      Ngoài ra, các triển khai ASN.1 cho C trong thập niên 1990 cũng tệ khủng khiếp
    • Điểm cốt lõi là cái gọi là góc nhìn doanh nghiệp thực chất chính là góc nhìn mainframe
  • Có một câu tục ngữ Thổ Nhĩ Kỳ kiểu như: “Cái này không phải thứ để con người dùng!”
    Tôi muốn lấy câu đó làm khẩu hiệu cho triết lý thiết kế
    Và cũng như câu thoại trong Game of Thrones rằng “Người đưa ra phán quyết phải tự tay vung kiếm”,
    người viết spec phải tự mình triển khai parser
    Nếu việc phê duyệt spec được đổi thành kiểu phải nộp kèm parser chạy được và bộ test, thì có lẽ chất lượng sẽ tốt hơn rất nhiều

  • Tôi thật sự rất thích ngôn ngữ D
    Tôi đang tự viết một trình soạn thảo văn bản kiểu vim chỉ phụ thuộc vào Raylib
    Ưu điểm của D gồm có

    • Có thể viết unit test ở bất cứ đâu
    • Dễ quản lý code chỉ dành cho test bằng khối version(unittest)
    • Hỗ trợ ngôn ngữ cho enum, union, assert, lập trình theo hợp đồng rất tuyệt vời
      Mỗi khi tra tài liệu hoặc hỏi ChatGPT, tôi luôn tìm được lời giải thanh lịch
    • Với tôi, D là một ngôn ngữ ngọt ngào mà dang dở
      Về mặt triết lý thiết kế, nó gần như hoàn hảo, nhưng nếu công cụ và hệ sinh thái đạt mức như Rust hay Go thì chắc đã thành công hơn nhiều
    • Tính năng của D thì tốt, nhưng nó ngày càng có xu hướng ồn ào (noisy) hơn
      Thư viện chuẩn Phobos có quá nhiều bất tiện nhỏ nên cuối cùng tôi đã bỏ cuộc
      Phiên bản mới là Phobos V3 đang được phát triển, nhưng nhân lực ít nên tôi vừa kỳ vọng vừa lo lắng
  • “Tôi đã từng nói ASN.1 phức tạp à?”
    Cả schema lẫn định dạng dữ liệu đều phức tạp, nhưng phần lớn là kiểu phức tạp có thể bỏ qua
    Tôi không dùng ký pháp schema ASN.1 mà tự viết triển khai DER bằng C
    DER là chuẩn mã hóa duy nhất mà tôi thấy thực sự dùng được
    Tôi cũng đã tạo các định dạng mã hóa riêng như DSER, SDSER, TER
    Những cấu trúc như ANY DEFINED BY vẫn còn được tôi dùng rất hữu ích,
    và để mã hóa hiệu quả hơn tôi còn thêm cả tính năng phi chuẩn OBJECT IDENTIFIER RELATIVE TO

  • Tôi cũng từng làm một trình biên dịch ASN.1
    Dù chỉ triển khai một phần tính năng của X.681~X.683, tôi vẫn làm được để toàn bộ chứng chỉ có thể được giải mã đệ quy chỉ với một lần gọi codec
    ASN.1 không chỉ là một cú pháp đơn giản mà là một hệ thống kiểu rất mạnh
    Nó bị đánh giá thấp, nhưng thật sự là công nghệ rất tuyệt

  • Trước đây tôi từng làm một trình biên dịch ASN.1 cho Swift
    Dự án ASN1Codable, tận dụng libasn1 của Heimdal
    để chuyển ASN.1 thành JSON AST và đơn giản hóa việc phân tích

    • Trong README của libasn1 có thể cảm nhận được một sự chán ghét âm ỉ với ASN.1
      Câu “hãy chuyển nó sang JSON” nghe như tiếng kêu của một lập trình viên đã bị tổn thương 😄
  • Kỳ lạ là tôi lại thấy làm việc với ASN.1 khá vui
    Một ngày nào đó tôi muốn tự viết một trình biên dịch ASN.1 cho Rust
    Các triển khai Rust hiện nay đa số vẫn là kiểu derive macro hoặc chain thủ công nên khá đáng tiếc

  • Thông thường khi triển khai một tiêu chuẩn, người ta hoàn thành 80% tính năng trong 20% thời gian,
    nhưng 20% còn lại của ASN.1 thì có thể mất cả đời

  • Ngày trước tôi từng mở rộng parser ASN.1 trong codebase của Netscape để hỗ trợ PKCS#12
    Tôi đã hiểu quá sâu các tiêu chuẩn RSA và định nghĩa ASN.1 đến mức thấy hối hận,
    nhưng vẫn dành sự kính trọng cho sự bền bỉ và một chút khổ dâm của tác giả blog

    • Với trải nghiệm đó thì hẳn là có rất nhiều giai thoại phát triển như chiến trường