1 ポイント 投稿者 GN⁺ 2025-10-25 | 1件のコメント | WhatsAppで共有
  • ある開発者が D言語でASN.1コンパイラ(dasn1) を自ら実装しながら経験した、技術的・精神的な道のりを共有
  • プロジェクトは x.509証明書とTLS 1.3実装を目標としており、ASN.1の複雑な DERエンコーディング処理 をサポート
  • 記事では、ASN.1の構造的な難解さ、x.680〜x.683仕様の実装難易度、D言語のメタプログラミング活用法などを詳しく扱う
  • Dの static import、mixin template、typeof()、alias this などの機能が、コード生成やAST/IR設計にどう役立ったかを具体的に説明
  • 記事は「ASN.1は苦しいが学びの多い経験」だとし、コンパイラ制作の現実的な難しさとやりがい を率直に伝えている

プロジェクト概要と動機

  • 著者は Juptune というDベースの非同期I/Oフレームワークを開発中で、TLS実装のためにASN.1 DERエンコーディングを自前で処理する必要があった
    • TLSのx.509証明書構造をパースするには、ASN.1の複雑なデータ表現方式を理解しなければならない
  • このプロジェクトは 学習と楽しみ のための個人的な挑戦として始まり、実際にいくつかの証明書を正常にパースする段階まで進んだ
  • ASN.1は1990年代の古い標準だが、今なお TLS、SNMP、LDAPなど現代システム全般で使われている
  • 著者は「ASN.1は世の中で広く使われているが、ほとんどの開発者はその存在すら知らない」と述べている

ASN.1とは何か

  • ASN.1(Abstract Syntax Notation One) は データ構造を定義してエンコードする言語 で、いわば「Protocol Buffersの祖先」
  • 標準は 表記法(x.680〜x.683)エンコーディング規則(BER, CER, DER, PER, XER, JERなど) で構成される
    • BER: 基本TLV形式、無限長をサポート
    • CER: BERの制限版で、常に無限長を使用
    • DER: BERの決定的サブセットで、暗号分野で標準的に使われる
    • PER/OER: ビット単位の圧縮エンコーディング
    • XER/JER: XML・JSONベースのエンコーディング
  • エンコーディングの種類が多く複雑だが、柔軟性と拡張性 は高い

ASN.1表記法の複雑さ

  • ASN.1の基本標準はx.680で、拡張仕様(x.681〜x.683)は 非常に難解な学術的文体 で書かれている
  • x.680だけでも実装は可能だが、意味変換規則や構文変形 が多く、実装難易度は高い
  • x.681は Information Object Classシステム を定義し、独自の初期化構文をサポートする
    • 例: CALLED &name [WHO IS &age YEARS OLD]
  • x.682は Table Constraint、x.683は テンプレート型(Parameterized)型 を定義する
    • D言語のジェネリクスに近い概念で、型と値の両方をパラメータに取れる

ASN.1の興味深い機能

  • 制約(Constraint)システム: 型定義時に値の範囲やサイズを直接指定できる
    • 例: UInt8 ::= INTEGER (0..255)
    • SIZEUNION(|)INTERSECTION(^) 演算子をサポート
  • バージョン管理システム: OBJECT IDENTIFIER によってモジュールのバージョンを明確に区別できる
    • 例: id-pkix1-implicit(19) vs id-mod-pkix1-implicit-02(59)
    • 名前衝突なしに明確なモジュール識別が可能

D言語がコード生成に有利な理由

  • Dの static import は名前衝突を防ぎ、ASN.1の型名をそのまま維持できる
  • モジュールローカル検索(.Type1) 機能でシンボル探索を明確に制限できる
  • typeof() によって型を自動推論でき、コード生成時に手動管理が不要
  • 末尾カンマ(trailing comma) を許容するため、コード生成を単純化できる
  • コンパイル時定数の結合 により、@nogc 関数でも文字列結合が可能

D言語機能を活用した実装例

Mixin templateベースのASTノード

  • Dの mixin template 機能を使ってASN.1構文木(AST)ノードを定義
    • 各ノード型(List, Container, OneOf)をテンプレートとして再利用
    • 複雑な継承の代わりに コンパイル時のコード複製 で単純化

テンプレートベースのAPIとコンパイル時検証

  • Container ノードは複数の下位ノードを含み、コンパイル時に型検証 を行う
    • node.getNode!Asn1TagDefaultNode 形式で安全にアクセス可能
  • OneOf ノードは複数型のうち1つを保持し、match 関数によるパターンマッチ をサポート
    • すべての型ハンドラを必ず定義する必要があるため、コンパイル時の安全性を確保

Dのメモリ管理実験パッケージ活用

  • std.experimental.allocator を使って @nogc環境でのオブジェクト生成・解放 を実装
    • RegionStatsCollector などの組み合わせでカスタムアロケータを構成
    • ただし、10年経っても実験的なまま維持されている

alias this機能

  • alias this を使い、ラッパー構造体が内部フィールドのように振る舞う よう実装
    • 例: cast(Asn1ValueReferenceIr)item のように簡潔なキャストが可能

version(unittest)

  • version(unittest) キーワードで テスト専用関数 を定義し、実際のビルドには含めない

template + with()を使ったテストハーネス

  • 共通テストロジックをテンプレート化し、with() 文で 簡潔なテストコード を記述
    • Harness.T() の代わりに T() として呼び出せる

実装中に直面した主な困難

値シーケンス構文(Value Sequence Syntax)

  • {} で始まるさまざまな値構文が 文脈によって曖昧 になる
    • パーサのコメントに「これは楽しくない」と書かれるほど複雑
  • 構文解析と意味解析を分離したため、処理難易度がさらに上がった

仕様書の曖昧さ

  • 特定条件でタグを EXPLICIT として扱うべき規則など、文書に明示されていない動作 が存在する
  • モジュールのバージョン管理方式も明確に定義されていない

制約条件の三重実装が必要

  1. 構文検証用
  2. 値の妥当性検証用
  3. ランタイムコード生成用
  • UNION、INTERSECTION処理時には エラーメッセージの構成も複雑

不変IRノードという幻想

  • ASTをIRに変換した後は修正不要だと考えていたが、
    AUTOMATIC TAGS などの意味変換過程でデータ変更が必要 だった

ASN.1の全面的な複雑さ

  • x.509は旧式の文法しか使わないため比較的単純だが、最新仕様では x.681〜x.683の実装が必須
    • このためASN.1は学術・商用分野以外ではほとんど使われていない

ANY DEFINED BY問題

  • ANY DEFINED BY は、別のフィールド値に応じて型が変わる構造
    • dasn1ではこれを実装せず、カスタム組み込みの Dasn1-Any で代替
    • 実際のデコード時には手動処理が必要

情報過多

  • ASN.1、x.68x、x.690、Juptuneなど複数のプロジェクトを並行して追っていたため、コードベースの文脈維持が難しい

コンパイラ制作の現実

  • 数千ものノードビジター、反復的なコード、微細な差異のある実装など、退屈で過酷な作業 が続く
  • しかし各段階に 大きな達成感と学習効果 がある
  • 「誰も使わないだろうが、本物のコンパイラ経験を得られた」と振り返る
  • 最後は「ASN.1はやるな、人生が変わる」という冗談で記事を締めくくる

結論

  • 1年間の作業にもかかわらずdasn1はまだ未完成だが、
    D言語の可能性とASN.1の複雑さ を深く理解するきっかけになった
  • いつか「ASN.1コンパイラ + TLS 1.3実装経験」を履歴書に書ける日を夢見つつ、
    開発者としての成長と業界の現実 をユーモラスに振り返る記事となっている

1件のコメント

 
GN⁺ 2025-10-25
Hacker Newsのコメント
  • 要するに、ASN.1、D言語、そしてコンパイラそのものについて話したかったということ
    ただ、一貫した形式を見つけられなかったので、関連する考えをまとめてブログ記事にしたとのこと
    完成度は高くないが、短く扱いにくいテーマなのでご容赦を、という話

    • 交差の例(intersection example)が意図した通りに動いていないように見える
      数学的には {0} ∪ ({2} ∩ {4,5,6,7,8}) = {0} なので、結果的に単一の値しか許されない
    • D言語の話を持ち出すと、Walter Brightを召喚するようなものだ
      個人的にはDが本当に好きだが、現実的にはGoやRustのほうがはるかに広く使われている
    • 私もASN.1データを扱ったことがあるが、特に証明書関連の作業はつらかった
      筆者の苦労に深く共感する
    • 本当に楽しく読んだ
      Dは好きだが、長いこと触っていなかった
      以前にパーサやプロトコル実装をした経験があるので、なおさら興味深かった
    • ブログは結局自分の場所なのだから、自分のやり方で続けていけばいい
  • 「OMG ASN.1」とは、なんとも懐かしいテーマだ
    インターネットが成長していた時代、IETFがプロトコルを発展させていた頃を思い出す
    当時、企業はインターネットに関心がなく、学界とIETFが主導していた
    だが企業が金になると気づくと、Protocol Wars が始まった
    ASN.1はその戦争の産物であり、企業文化と学術文化の衝突を示す事例でもある
    企業は「レシピ文化」、学界は「機能文化」とたとえられる
    この考え方の違いは、今日のAI開発文化にも示唆を与える

    • 昔、映画 Father of the Bride を見ていたらX.25ネットワーキングの話が出てきて驚いた
      あのとき、インターネットではなく「CN=wikipedia, OU=org, C=US」のようなアドレス体系に進んでいた可能性を思うとぞっとする
    • 「OMG ASN.1」を次のバンド名にしようかと思った
    • 話の一部は正しいが、主な当事者を「企業」と表現するのはやや不正確だ
      実際にはITUとISOが中心だった
      その後90年代後半には別の「プロトコル戦争」があり、今度はIETFが負けた
    • この戦争は、インターネットの初期商業化(en-shittification) の過程でもあった
      ISOは完璧を求めて遅くなり、IETFは「後で直そう」という姿勢で素早く動いた
      その結果、プロトコルが固定化してしまう問題を抱えた
      また、1990年代のC向けASN.1実装がひどかったことも問題だった
    • 企業の視点というのは、結局のところメインフレームの視点だったというのが核心だ
  • トルコのことわざに「これは人が使うものではない!」という表現がある
    この言葉をデザイン哲学のモットーにしたい
    また、「裁きを下した者は自ら剣を振るわねばならない」という Game of Thrones の台詞のように、
    仕様を書いた人は自分でパーサを実装すべきだ
    実際に動くパーサとテストを一緒に提出しないと仕様が承認されないように変われば、品質はずっと良くなる気がする

  • D言語が本当に好きだ
    Raylibだけに依存して、vimスタイルのテキストエディタを自作している
    Dの長所は次の通り

    • どこにでもunit testを書ける
    • version(unittest) ブロックでテスト専用コードの管理がしやすい
    • enum、union、assert、契約プログラミングなど、言語としての支援がすばらしい
      ドキュメントを調べたりChatGPTに聞いたりすれば、いつもエレガントな解決策を見つけられた
    • Dは私にとってほろ苦い言語
      設計哲学としては完璧に近いが、ツールやエコシステムがRustやGo並みだったら、ずっと成功していただろう
    • Dの機能は良いが、だんだん言語がうるさくなる(noisy) 傾向がある
      Phobos標準ライブラリには小さな不便が多すぎて、結局あきらめた
      新版のPhobos V3が進行中だが、人手が少ないので期待半分、不安半分だ
  • 「ASN.1が複雑だなんて言ったことがあったか?」
    スキーマもデータ形式も複雑だが、その大半は無視できる複雑さだ
    私はASN.1のスキーマ記法を使わず、直接DER実装をCで書いた
    DERは標準エンコーディングの中で唯一まともに使えるものだと思う
    また、DSER、SDSER、TERのような独自エンコーディング形式も作った
    ANY DEFINED BY のような構造も今なお有用に使っており、
    効率的なエンコーディングのために OBJECT IDENTIFIER RELATIVE TO という非標準機能も追加した

  • 私もASN.1コンパイラを作ったことがある
    X.681〜X.683の一部機能しか実装していないが、1回のコーデック呼び出しで証明書全体を再帰的にデコードできるようにした
    ASN.1は単なる文法ではなく、強力な型システム
    過小評価されているが、本当にすばらしい技術だ

  • 以前、Swift向けのASN.1コンパイラを作ったことがある
    ASN1Codable プロジェクトで、Heimdalの libasn1 を活用し、
    ASN.1をJSON ASTに変換してパースを単純化した

    • libasn1のREADMEからは、ASN.1に対するほのかな嫌悪感が感じられる
      「JSONに変えてしまおう」というのは、結局傷ついた開発者の叫びのようだ 😄
  • なぜかASN.1の作業が楽しく感じられる
    いつかRust向けのASN.1コンパイラを自分で作ってみたい
    今のRust実装はたいていderiveマクロか手動チェイニング方式で、物足りない

  • 一般に標準を実装するときは、80%の機能を20%の時間で完成させられるが、
    ASN.1の残り20%には一生かかるかもしれない

  • 昔、NetscapeコードベースのASN.1パーサを拡張してPKCS#12をサポートしたことがある
    RSA標準とASN.1定義を深く知りすぎて後悔したが、
    ブログ筆者の粘り強さと少しのマゾヒズムには敬意を表したい

    • その経験なら、まさに戦場のような開発エピソードがたくさんありそうだ