- 한 개발자가 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중 구현 필요
- 구문 검사용
- 값 유효성 검사용
- 런타임 코드 생성용
- 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 구현 경험”을 이력서에 쓸 날을 꿈꾸며,
개발자의 성장과 업계 현실을 유머러스하게 되짚는 글로 마무리됨