- ある開発者が 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)
SIZE、UNION(|)、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環境でのオブジェクト生成・解放 を実装
Region、StatsCollector などの組み合わせでカスタムアロケータを構成
- ただし、10年経っても実験的なまま維持されている
alias this機能
alias this を使い、ラッパー構造体が内部フィールドのように振る舞う よう実装
- 例:
cast(Asn1ValueReferenceIr)item のように簡潔なキャストが可能
version(unittest)
version(unittest) キーワードで テスト専用関数 を定義し、実際のビルドには含めない
template + with()を使ったテストハーネス
- 共通テストロジックをテンプレート化し、
with() 文で 簡潔なテストコード を記述
Harness.T() の代わりに T() として呼び出せる
実装中に直面した主な困難
値シーケンス構文(Value Sequence Syntax)
{} で始まるさまざまな値構文が 文脈によって曖昧 になる
- パーサのコメントに「これは楽しくない」と書かれるほど複雑
- 構文解析と意味解析を分離したため、処理難易度がさらに上がった
仕様書の曖昧さ
- 特定条件でタグを
EXPLICIT として扱うべき規則など、文書に明示されていない動作 が存在する
- モジュールのバージョン管理方式も明確に定義されていない
制約条件の三重実装が必要
- 構文検証用
- 値の妥当性検証用
- ランタイムコード生成用
- 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件のコメント
Hacker Newsのコメント
要するに、ASN.1、D言語、そしてコンパイラそのものについて話したかったということ
ただ、一貫した形式を見つけられなかったので、関連する考えをまとめてブログ記事にしたとのこと
完成度は高くないが、短く扱いにくいテーマなのでご容赦を、という話
数学的には
{0} ∪ ({2} ∩ {4,5,6,7,8}) = {0}なので、結果的に単一の値しか許されない個人的にはDが本当に好きだが、現実的にはGoやRustのほうがはるかに広く使われている
筆者の苦労に深く共感する
Dは好きだが、長いこと触っていなかった
以前にパーサやプロトコル実装をした経験があるので、なおさら興味深かった
「OMG ASN.1」とは、なんとも懐かしいテーマだ
インターネットが成長していた時代、IETFがプロトコルを発展させていた頃を思い出す
当時、企業はインターネットに関心がなく、学界とIETFが主導していた
だが企業が金になると気づくと、Protocol Wars が始まった
ASN.1はその戦争の産物であり、企業文化と学術文化の衝突を示す事例でもある
企業は「レシピ文化」、学界は「機能文化」とたとえられる
この考え方の違いは、今日のAI開発文化にも示唆を与える
あのとき、インターネットではなく「CN=wikipedia, OU=org, C=US」のようなアドレス体系に進んでいた可能性を思うとぞっとする
実際にはITUとISOが中心だった
その後90年代後半には別の「プロトコル戦争」があり、今度はIETFが負けた
ISOは完璧を求めて遅くなり、IETFは「後で直そう」という姿勢で素早く動いた
その結果、プロトコルが固定化してしまう問題を抱えた
また、1990年代のC向けASN.1実装がひどかったことも問題だった
トルコのことわざに「これは人が使うものではない!」という表現がある
この言葉をデザイン哲学のモットーにしたい
また、「裁きを下した者は自ら剣を振るわねばならない」という Game of Thrones の台詞のように、
仕様を書いた人は自分でパーサを実装すべきだ
実際に動くパーサとテストを一緒に提出しないと仕様が承認されないように変われば、品質はずっと良くなる気がする
D言語が本当に好きだ
Raylibだけに依存して、vimスタイルのテキストエディタを自作している
Dの長所は次の通り
version(unittest)ブロックでテスト専用コードの管理がしやすいドキュメントを調べたりChatGPTに聞いたりすれば、いつもエレガントな解決策を見つけられた
設計哲学としては完璧に近いが、ツールやエコシステムがRustやGo並みだったら、ずっと成功していただろう
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に変換してパースを単純化した
「JSONに変えてしまおう」というのは、結局傷ついた開発者の叫びのようだ 😄
なぜかASN.1の作業が楽しく感じられる
いつかRust向けのASN.1コンパイラを自分で作ってみたい
今のRust実装はたいていderiveマクロか手動チェイニング方式で、物足りない
一般に標準を実装するときは、80%の機能を20%の時間で完成させられるが、
ASN.1の残り20%には一生かかるかもしれない
昔、NetscapeコードベースのASN.1パーサを拡張してPKCS#12をサポートしたことがある
RSA標準とASN.1定義を深く知りすぎて後悔したが、
ブログ筆者の粘り強さと少しのマゾヒズムには敬意を表したい