1P by GN⁺ 9일전 | ★ favorite | 댓글 1개
  • 한 개발자가 D 언어로 ASN.1 컴파일러(dasn1) 를 직접 구현하며 겪은 기술적·정신적 여정을 공유
  • 프로젝트는 x.509 인증서와 TLS 1.3 구현을 목표로 하며, ASN.1의 복잡한 DER 인코딩 처리를 지원
  • 글은 ASN.1의 구조적 난해함, x.680~x.683 규격의 구현 난이도, D 언어의 메타프로그래밍 활용법 등을 상세히 다룸
  • D의 정적 import, mixin 템플릿, 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)은 데이터 구조를 정의하고 인코딩하는 언어, 일종의 “프로토버프의 조상”
  • 표준은 표기법(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의 정적 import(static import) 는 이름 충돌을 방지하며, ASN.1 타입 이름을 그대로 유지 가능
  • 모듈 로컬 조회(.Type1) 기능으로 심볼 탐색을 명확히 제한
  • typeof() 로 타입을 자동 추론해 코드 생성 시 수동 관리 불필요
  • 후행 쉼표(trailing comma) 허용으로 코드 생성 단순화
  • 컴파일 타임 상수 결합 덕분에 @nogc 함수에서도 문자열 조합 가능

D 언어 기능을 활용한 구현 사례

Mixin 템플릿 기반 AST 노드

  • D의 mixin template 기능을 이용해 ASN.1 구문 트리(AST) 노드를 정의
    • 각 노드 타입(List, Container, OneOf)을 템플릿으로 재사용
    • 복잡한 상속 대신 컴파일 타임 코드 복사로 단순화

템플릿 기반 API와 컴파일 타임 검증

  • Container 노드는 여러 하위 노드를 포함하며, 컴파일 타임에 타입 검증 수행
    • node.getNode!Asn1TagDefaultNode 형태로 안전한 접근 가능
  • OneOf 노드는 여러 타입 중 하나를 저장하며, match 함수로 패턴 매칭 지원
    • 모든 타입 핸들러를 반드시 정의해야 하므로 컴파일 타임 안전성 확보

D의 메모리 관리 실험 패키지 활용

  • std.experimental.allocator를 사용해 @nogc 환경에서 객체 생성/해제 구현
    • Region, StatsCollector 등 조합으로 커스텀 할당자 구성
    • 단, 10년째 실험적 상태로 유지 중

alias this 기능

  • alias this를 이용해 래퍼 구조체가 내부 필드처럼 동작하도록 구현
    • 예: cast(Asn1ValueReferenceIr)item 형태로 간결한 캐스팅 가능

version(unittest)

  • version(unittest) 키워드로 테스트 전용 함수를 정의, 실제 빌드에는 포함되지 않음

템플릿 + with()를 이용한 테스트 하네스

  • 공통 테스트 로직을 템플릿화하고, with() 문으로 간결한 테스트 코드 작성
    • Harness.T() 대신 T()로 호출 가능

구현 중 겪은 주요 어려움

값 시퀀스 구문(Value Sequence Syntax)

  • {}로 시작하는 여러 형태의 값 구문이 문맥에 따라 모호
    • 파서 주석에 “이건 즐겁지 않다”는 표현이 있을 정도로 복잡
  • 구문 분석과 의미 분석을 분리했기 때문에 처리 난이도 상승

명세서의 불명확함

  • 특정 조건에서 태그가 EXPLICIT로 처리되어야 하는 규칙 등, 문서에 명시되지 않은 동작 존재
  • 모듈 버전 관리 방식도 명확히 정의되어 있지 않음

제약 조건의 3중 구현 필요

  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은 이를 구현하지 않고, 커스텀 intrinsic Dasn1-Any 로 대체
    • 실제 디코딩 시 수동 처리 필요

정보 과부하

  • ASN.1, x.68x, x.690, Juptune 등 여러 프로젝트 병행으로 코드베이스 맥락 유지 어려움

컴파일러 제작의 현실

  • 수천 개의 노드 방문자, 반복적 코드, 미세한 차이의 구현 등 지루하고 고된 작업
  • 그러나 각 단계마다 큰 성취감과 학습 효과 존재
  • “아무도 쓰지 않겠지만, 진짜 컴파일러 경험을 얻었다”고 회고
  • 마지막으로 “ASN.1은 하지 말라, 인생이 바뀐다”는 농담으로 글을 마무리

결론

  • 1년간의 작업에도 불구하고 dasn1은 아직 미완성이지만,
    D 언어의 잠재력과 ASN.1의 복잡성을 깊이 이해하게 된 계기
  • 언젠가 “ASN.1 컴파일러 + TLS 1.3 구현 경험”을 이력서에 쓸 날을 꿈꾸며,
    개발자의 성장과 업계 현실을 유머러스하게 되짚는 글로 마무리됨
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의 일부 기능만 구현했지만, 한 번의 코덱 호출로 전체 인증서를 재귀적으로 디코딩할 수 있게 했음
    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 정의를 너무 깊이 알게 되어 후회했지만,
    블로그 작성자의 끈기와 약간의 마조히즘에는 존경을 보냄

    • 그 경험이라면 정말 전쟁 같은 개발 일화가 많을 것 같음