카테고리 없음

DNSSEC Part 3 : Chain of Trust, 대조와 신뢰를 통한 검증

지기(ZIGI) 2026. 6. 26. 16:12

today keys : DNSSEC, chain, trust, 체인 오브 트러스트, 트러스트 앵커, DNSKEY, DS, RRSIG, 대조, 서명 검증


지난 편에서 DNSSEC의 레코드들을 개별적으로 정리했습니다. 이번 편에서는 이 레코드들이 어떻게 하나의 사슬로 연결되는지 다룹니다. 루트에서 출발한 신뢰가 TLD를 거쳐 최종 도메인까지 전달되는 구조, 즉 체인 오브 트러스트(Chain of Trust)입니다.

검증에는 성격이 다른 두 가지 연산이 번갈아 등장하며, 이것이 계층마다 3단계 패턴으로 반복됩니다. 한 계층만 정확히 이해하면 나머지는 같은 구조이므로 루트 계층을 기준으로 자세히 살펴보겠습니다. 지난 편에서 미뤄둔 'DS 자체를 먼저 검증해야 하는 순서'도 이 과정에서 함께 정리됩니다.


1. 대조와 서명 검증은 다른 연산이다

체인 오브 트러스트를 따라가다 보면 두 종류의 확인 작업이 번갈아 나옵니다. 이 둘을 혼동하면 전체 구조가 헷갈리므로 먼저 구분해두겠습니다.

대조 (해시 비교)

두 값이 같은지 단순히 비교하는 작업입니다. 암호 연산이나 키가 필요 없습니다.

해시는 입력이 1비트만 달라도 출력이 완전히 달라지므로, 두 값이 일치한다는 것은 곧 원본이 동일하다는 뜻입니다.

 

서명 검증 (공개키 연산)

RRSIG에 담긴 서명값을 공개키로 풀어보는 작업입니다. 반드시 공개키가 필요합니다.

두 값이 일치하면 해당 개인키를 가진 주체가 서명했고 데이터가 변조되지 않았음이 증명됩니다.

구분 대조 서명 검증
사용하는 키 없음 공개키
연산 해시 계산 해시 + 복호화
대상 KSK 공개키 vs DS RRset vs RRSIG
증명하는 것 같은 키인가 진짜 서명자인가, 변조되지 않았는가

 

 

2. 검증의 출발점 - 트러스트 앵커

 

사슬을 따라가려면 시작점이 필요합니다. 그런데 여기에 순환 문제가 있습니다.

  1. 어떤 키를 믿으려면  : 그 키를 보증하는 상위 정보가 필요
  2. 상위 정보를 믿으려면 → 다시 그 위의 정보가 필요
  3. 결국 어딘가에서는 "그냥 믿는" 지점이 있어야 함

DNSSEC은 이 지점을 루트 KSK 공개키로 정하고, 이를 트러스트 앵커로 리졸버에 미리 저장해둡니다.

지난 편에서 정리한 대로 DNS가 아닌 별도 경로로 배포되는 값입니다.

정리하면 리졸버가 검증 없이 신뢰하는 값은 이 하나뿐이고, 나머지 모든 키는 여기서 출발해 검증을 거칩니다.

 

3. 한 계층에서 일어나는 일 - 3단계 패턴

이제 실제 사슬을 따라가 보겠습니다. 루트 계층부터 시작합니다.

 

1단계: 트러스트 앵커와 루트 KSK 대조

리졸버가 루트 서버에 DNSKEY를 질의하면 아래와 같은 응답을 받습니다.

.  172800  IN  DNSKEY  257 3 8 AwEAAaz/tAm8y...    ← KSK
.  172800  IN  DNSKEY  256 3 8 AwEAAbXY2mNg...    ← ZSK
.  172800  IN  RRSIG   DNSKEY 8 0 172800 ... <KSK 키태그> . <서명값>

이 중 KSK(Flags 257) 줄을 꺼내 트러스트 앵커와 대조합니다. 그런데 여기서 한 가지 짚고 넘어갈 부분이 있습니다.

IANA가 배포하는 트러스트 앵커는 공개키 자체가 아니라 그 공개키를 해시한 값입니다.

IANA가 공개하는 root-anchors.xml 파일의 구성은 다음과 같습니다.

<KeyDigest id="..." validFrom="...">
  <KeyTag>20326</KeyTag>
  <Algorithm>8</Algorithm>
  <DigestType>2</DigestType>
  <Digest>E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D</Digest>
</KeyDigest>

Key Tag, Algorithm, Digest Type, Digest로 구성되어 있는데,

지난 편에서 정리한 DS 레코드와 필드 구성이 완전히 같습니다.

루트만 부모가 없어 DS를 등록할 곳이 없을 뿐, 값의 형식 자체는 다른 계층의 DS와 동일합니다.

해시 형태로 배포하면 값이 짧아 관리가 편하고(공개키 전체는 수백 바이트, SHA-256 해시는 32바이트),

다른 계층의 검증 방식과 구조가 통일된다는 이점이 있습니다.

따라서 대조는 다음과 같이 진행됩니다.

여기서 주의할 점은 이 단계가 대조일 뿐 서명 검증이 아니라는 것입니다. 아직 어떤 RRSIG도 풀어보지 않았습니다. 단지 "지금 받은 이 키가 내가 신뢰하기로 한 그 키가 맞는가"만 확인한 상태입니다.

참고로 리졸버가 앵커를 저장하는 형태는 구현에 따라 두 가지입니다. IANA 값을 그대로 DS 형태로 보관하는 경우에는 위와 같이 매번 해시를 계산해 비교합니다. 반면 최초에 한 번 해시로 확인한 뒤 루트 DNSKEY 공개키 자체를 저장해두는 방식도 있으며, 이 경우에는 값을 그대로 비교합니다. Windows DNS에서 Get-DnsServerTrustAnchor를 실행했을 때 TrustAnchorType 항목에 DS 또는 DNSKEY로 표시되는 것이 이 차이입니다. 어느 쪽이든 "받은 키가 신뢰하기로 한 키와 같은가"를 확인한다는 결과는 동일합니다.

2단계: KSK로 DNSKEY RRSIG 검증 → ZSK 확보

이제 1단계에서 확정한 KSK 공개키를 사용해 실제 서명 검증을 수행합니다. 검증 대상은 방금 받은 DNSKEY RRset과 그에 붙어 있는 RRSIG입니다.

서명한 쪽(루트 운영자)이 미리 해둔 일

 

리졸버가 검증하는 과정

즉 리졸버는 서로 다른 두 경로로 같은 지문을 구해내고, 마지막에 그 둘을 맞춰봅니다.

지문 A는 받은 데이터로부터 직접 계산한 값이고, 지문 B는 서명자가 만들어 넣어둔 값을 되돌린 것입니다.

핵심은 리졸버가 서명값을 만들어보는 것이 아니라, 이미 만들어진 서명값을 풀어서 자기가 계산한 값과 맞춰본다는 점입니다.

개인키는 루트 운영자만 가지고 있으므로 리졸버는 서명을 만들 수 없습니다.

대신 짝이 되는 공개키로 복호화하면 원래 지문이 복원되는 성질을 이용합니다.

A와 B가 일치한다는 것은 두 가지를 동시에 증명합니다.

  • 변조되지 않았다 — 데이터가 한 비트라도 바뀌었다면 A 트랙에서 계산한 지문이 달라졌을 것입니다.
  • 진짜 서명자다 — 다른 개인키로 서명했다면 B 트랙의 복호화 결과가 의미 없는 값이 되어 A와 일치할 수 없습니다.

그리고 여기서 검증 대상이 RRset 전체라는 점이 중요합니다.

RRSIG는 KSK 줄만 서명한 것이 아니라 KSK와 ZSK가 함께 묶인 RRset에 대한 서명이므로, 이 검증이 통과하면 같은 RRset 안에 있던 ZSK도 자동으로 신뢰가 확보됩니다.

ZSK를 별도로 검증하는 절차는 없습니다. 이것이 KSK와 ZSK를 나눈 구조가 실제로 작동하는 방식입니다.

 

한 가지 더, 서명 검증에는 유효기간 확인이 함께 수반됩니다.

RRSIG의 Inception과 Expiration 값이 현재 시각을 포함하지 않으면 A와 B가 일치하더라도 검증은 실패로 처리됩니다.

리졸버의 시스템 시간이 어긋나 있으면 정상 서명도 만료된 것으로 판정되므로 NTP 동기화가 전제 조건입니다.

 

3단계: ZSK로 하위 DS의 RRSIG 검증

2단계에서 루트 ZSK를 확보했으므로, 이제 이 키로 하위 계층의 DS를 검증합니다. 대상은 루트가 응답해준 com의 DS 레코드입니다.

com.  86400  IN  DS     19718 13 2 8ACBB0CD28F41250A80A491389424D34...
com.  86400  IN  RRSIG  DS 8 1 86400 ... <루트 ZSK 키태그> . <서명값>

검증 방식은 2단계와 동일합니다. 대상 RRset과 사용하는 키만 바뀝니다.

지난 편에서 미뤄뒀던 부분이 바로 이 단계입니다. DS를 다음 계층의 대조 기준으로 쓰려면 그 DS 자체가 먼저 검증되어야 합니다. 공격자가 DS까지 위조해 넣었다면 아무리 대조가 성립해도 의미가 없기 때문입니다.

이 검증을 통과하면 com KSK 공개키의 해시값이 신뢰할 수 있는 값으로 확정되고, 이 값이 다음 계층 1단계의 대조 기준이 됩니다.

세 단계에서 사용된 키 정리

단계 연산 사용한 키 얻은 결과
1단계 대조 없음 (해시만) 루트 KSK 신뢰 확보
2단계 서명 검증 루트 KSK 공개키 루트 ZSK 신뢰 확보
3단계 서명 검증 루트 ZSK 공개키 com DS 값 확정

각 단계는 직전 단계에서 신뢰가 확정된 키만 사용합니다.

아직 검증되지 않은 키로 무언가를 풀어본다면 그 결과 역시 신뢰할 수 없기 때문입니다.

이 순서가 강제되는 이유가 여기에 있습니다.

 

📌 한 줄 요약: 한 계층의 검증은 대조 → KSK로 서명 검증 → ZSK로 서명 검증 3단계로 이루어지며, 마지막 단계의 산출물이 다음 계층의 대조 기준이 됩니다.

 

4. 같은 패턴이 계층마다 반복된다

com 계층으로 내려가면 정확히 같은 3단계가 반복됩니다. 달라지는 것은 1단계의 대조 기준뿐입니다.

ROOT 계층 COM(TLD) 계층 cloudflare.com  계층

 

com 계층

대조 기준: com DS (루트 계층에서 확정된 값)
     ↓ 대조
com KSK 확정
     ↓ KSK로 RRSIG DNSKEY 검증
com ZSK 확보
     ↓ ZSK로 RRSIG DS 검증
cloudflare.com DS 확정

cloudflare.com 계층

대조 기준: cloudflare.com DS (com 계층에서 확정된 값)
     ↓ 대조
cloudflare.com KSK 확정
     ↓ KSK로 RRSIG DNSKEY 검증
cloudflare.com ZSK 확보
     ↓ ZSK로 A 레코드의 RRSIG 검증
A 레코드 확정 ★

세 계층의 구조가 완전히 동일합니다. 각 단계의 내부 동작도 앞서 본 것과 같습니다.

대조는 받은 KSK를 해시해서 기준값과 비교하는 것이고, 서명 검증은 RRset을 직렬화해 구한 지문 A와 RRSIG를 공개키로 복호화한 지문 B를 비교하는 것입니다. 대상 RRset과 사용하는 공개키만 계층에 따라 바뀔 뿐입니다.

차이는 양 끝에만 있습니다.

  • 시작점: 루트만 트러스트 앵커를 쓰고, 나머지는 부모에게서 받아 검증을 마친 DS를 씁니다.
  • 끝점: 중간 계층은 자식의 DS를 산출하고, 마지막 계층에서만 실제 레코드(A, MX 등)가 나옵니다.

이 반복 구조가 도메인 계층이 아무리 깊어져도 동일하게 적용됩니다.

a.b.c.example.com처럼 여러 단계로 위임되어 있어도 각 위임 지점마다 같은 3단계를 밟을 뿐입니다.

 

5. 직접 확인해보기

체인 전체를 한 번에 보기

dig cloudflare.com A +trace +dnssec

+trace는 리졸버가 하는 재귀 조회를 단계별로 재현해 보여줍니다.

출력을 위에서부터 보면 루트 → com  → cloudflare 순으로 내려가면서,

각 위임 지점마다 DS와 RRSIG가 함께 나오는 것을 확인할 수 있습니다.

앞서 설명한 계층 반복 구조가 그대로 드러납니다.

 

검증 결과를 사람이 읽기 쉽게 보기

zigi@ZIGI:~$ delv @8.8.8.8 cloudflare.com A
; fully validated
cloudflare.com.         300     IN      A       104.16.132.229
cloudflare.com.         300     IN      A       104.16.133.229
cloudflare.com.         300     IN      RRSIG   A 13 2 300 20260914054655 20260912034655 34505 cloudflare.com. NEH6E427j64O5xdMqLNioylA8VmAk7CqfN/MbF5DwqOgKmEX1owReu0o HLYCRcbLT/NYQjJkevmyEUNe/5er5Q==

delv는 BIND에 포함된 검증 전용 도구입니다. 정상이라면 ; fully validated 가 출력됩니다.

체인의 각 단계를 모두 보고 싶다면 아래처럼 실행합니다.

zigi@ZIGI:~$ delv @8.8.8.8 +vtrace cloudflare.com A
;; fetch: cloudflare.com/A
;; validating cloudflare.com/A: starting
;; validating cloudflare.com/A: attempting positive response validation
;; fetch: cloudflare.com/DNSKEY
;; validating cloudflare.com/DNSKEY: starting
;; validating cloudflare.com/DNSKEY: attempting positive response validation
;; fetch: cloudflare.com/DS
;; validating cloudflare.com/DS: starting
;; validating cloudflare.com/DS: attempting positive response validation
;; fetch: com/DNSKEY
;; validating com/DNSKEY: starting
;; validating com/DNSKEY: attempting positive response validation
;; fetch: com/DS
;; validating com/DS: starting
;; validating com/DS: attempting positive response validation
;; fetch: ./DNSKEY
;; validating ./DNSKEY: starting
;; validating ./DNSKEY: attempting positive response validation
;; validating ./DNSKEY: verify rdataset (keyid=20326): success
;; validating ./DNSKEY: marking as secure (DS)
;; validating com/DS: in fetch_callback_dnskey
;; validating com/DS: keyset with trust secure
;; validating com/DS: resuming validate
;; validating com/DS: verify rdataset (keyid=57780): success
;; validating com/DS: marking as secure, noqname proof not needed
;; validating com/DNSKEY: in fetch_callback_ds
;; validating com/DNSKEY: dsset with trust secure
;; validating com/DNSKEY: verify rdataset (keyid=19718): success
;; validating com/DNSKEY: marking as secure (DS)
;; validating cloudflare.com/DS: in fetch_callback_dnskey
;; validating cloudflare.com/DS: keyset with trust secure
;; validating cloudflare.com/DS: resuming validate
;; validating cloudflare.com/DS: verify rdataset (keyid=41446): success
;; validating cloudflare.com/DS: marking as secure, noqname proof not needed
;; validating cloudflare.com/DNSKEY: in fetch_callback_ds
;; validating cloudflare.com/DNSKEY: dsset with trust secure
;; validating cloudflare.com/DNSKEY: verify rdataset (keyid=2371): success
;; validating cloudflare.com/DNSKEY: marking as secure (DS)
;; validating cloudflare.com/A: in fetch_callback_dnskey
;; validating cloudflare.com/A: keyset with trust secure
;; validating cloudflare.com/A: resuming validate
;; validating cloudflare.com/A: verify rdataset (keyid=34505): success
;; validating cloudflare.com/A: marking as secure, noqname proof not needed
; fully validated
cloudflare.com.         300     IN      A       104.16.132.229
cloudflare.com.         300     IN      A       104.16.133.229
cloudflare.com.         300     IN      RRSIG   A 13 2 300 20260914054757 20260912034757 34505 cloudflare.com. hvRDZxrvegz110PAYfsGOgk5xbP8GKYqibd45+ms19nsEMQVBlbFi251 ZxqdJBd72gcbDn1Sv9iQlDy8HL+V4w==

트러스트 앵커 로드부터 각 계층의 DS 대조, RRSIG 검증까지 전 과정이 순서대로 출력됩니다.

이번 편에서 설명한 3단계 패턴이 실제로 어떻게 반복되는지 눈으로 확인할 수 있습니다.

 

검증 완료 표시 확인하기

zigi@ZIGI:~$ dig  cloudflare.com A +dnssec @8.8.8.8

; <<>> DiG 9.18.39-0ubuntu0.24.04.5-Ubuntu <<>> cloudflare.com A +dnssec @8.8.8.8
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10965
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 512
;; QUESTION SECTION:
;cloudflare.com.                        IN      A

;; ANSWER SECTION:
cloudflare.com.         300     IN      A       104.16.133.229
cloudflare.com.         300     IN      A       104.16.132.229
cloudflare.com.         300     IN      RRSIG   A 13 2 300 20260914055646 20260912035646 34505 cloudflare.com. 7oCRrAdtL4fcnbMhyrM123n9ZdmU/j4xqz2z/OSs+0Ff/DI3AaK27pG/ OR+Fz37kVv51fxyTjxQ/CbUhxp/T2A==

flags 줄에 ad가 포함되어 있으면 리졸버가 사슬 전체를 검증했다는 의미입니다.

ad는 Authenticated Data의 약자로, "이 응답은 내가 검증했고 진짜다"를 클라이언트에게 알리는 표시입니다.

참고로 이 플래그는 검증에 성공했을 때만 붙습니다. 붙지 않는 경우가 두 가지 있는데, 그 구분은 다음 편에서 다루겠습니다.

계층 구조를 그림으로 보기

https://dnsviz.net/d/cloudflare.com/dnssec/

DNSViz는 체인 오브 트러스트를 시각화해주는 웹 도구입니다. 루트부터 해당 도메인까지의 키와 DS 연결 관계가 그래프로 표시되므로, 텍스트 출력보다 구조를 파악하기 쉽습니다.

 

6. 참고 자료 (Reference Links)

※ 이전 포스팅
[DNSSEC ① - DNSSEC은 왜 필요한가]
[DNSSEC ② - DNSSEC 용어 정리]

이번 편에서는 사슬이 정상적으로 이어지는 경우를 다뤘습니다. 그런데 모든 도메인이 DNSSEC을 적용한 것은 아닙니다. 사슬이 중간에 끊기는 도메인은 어떻게 처리될까요. 다음 편에서는 "없다"는 사실을 증명하는 방식과, 리졸버가 내리는 세 가지 판정 결과를 다루겠습니다.