本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
TECHNICAL MONOGRAPH — APPLIED CRYPTOGRAPHY & TRANSPORT SECURITY RFC 8446 / RFC 6698 / RFC 8461 / RFC 8460 X25519 / HKDF-SHA256 / SPKI Pinning

TLS 1.3 (RFC 8446) 0-RTT再生攻撃耐性と DNSSEC / DANE / MTA-STS 暗号検証パイプライン

著者:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / ネットワークスペシャリスト / 登録情報処理安全確保支援士(第007617号))

1. 基本と全体像

この記事の要点と4つの章のご案内

要旨と脅威モデル:BGPハイジャック・StripTLS・0-RTT再生攻撃の構造的解剖に関するイメージ写真
要旨と脅威モデル:BGPハイジャック・StripTLS・0-RTT再生攻撃の構造的解剖
Curve25519 (X25519) 楕円曲線鍵共有と HKDF-Expand-Label 鍵スケジュール数理に関するイメージ写真
Curve25519 (X25519) 楕円曲線鍵共有と HKDF-Expand-Label 鍵スケジュール数理
TLS 1.3 (RFC 8446) 0-RTT再生攻撃耐性と DNSSEC / DANE / MTA-STS 暗号検証パイプライン の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 要旨と脅威モデル:BGPハイジャック・StripTLS・0-RTT再生攻撃の構造的解剖 WebブラウザやOSのルート証明書ストアには数百の商用・公的認証局(CA)が登録されており、原則としてどのCAでも任意のドメインに対する証明書を発行できる構造(Weakest-Link Model)になっています。万一、BGP経路ハイジャックによってドメイン制御検証(DV)通信が傍受された場合、攻撃者は正規CAから有効な証明書を即時取得できてしまいます。DNSSEC署名とDANE TLSA(RFC 6698)およびCAAレコード(RFC 8659)を組み合わせることで、信頼の起点(Trust Anchor)を特定ドメイン所有者の公開鍵へ暗号学的に束縛できます。
2. 現場での進め方と対策 Curve25519 (X25519) 楕円曲線鍵共有と HKDF-Expand-Label 鍵スケジュール数理 SMTPのSTARTTLS拡張(RFC 3207)は本来「日和見暗号化(Opportunistic TLS)」であるため、中間者(MitM)が平文のEHLO応答から「250-STARTTLS」の1行を削除するだけで、送信側メールサーバーは平文のままメール本文を送信してしまいます。これに対し、DNSSEC上で公開鍵ハッシュを宣言するDANE(RFC 7672)と、HTTPS経由で暗号化強制ポリシーを配布するMTA-STS(RFC 8461)を併用することで、TLS 1.3接続と証明書検証が成功しない限りメール配送をフェイルクローズで遮断します。
3. 関連する制度とルール DNSSEC署名連鎖と DANE TLSA (3 1 1) SPKIピンニング・MTA-STS の二重防壁 TLS 1.3の0-RTTモードは初回のClientHelloと同時にアプリケーションデータを送信できる反面、前方秘匿性(Forward Secrecy)が完全ではなく、ネットワーク上の盗聴者が同じ0-RTTパケットをそのまま再送する再生攻撃(Replay Attack: RFC 8446 Section 8)が成立します。当社の実装では、状態変更を伴うリクエストでの0-RTTを拒否するとともに、ClientHelloのobfuscated_ticket_age時計ずれ検査と二層スライディング・ブルームフィルタ(Strike Register)により同一Early Dataの二重実行を決定論的に排除しています。
4. 詳しい技術仕様としくみ なぜ従来のWeb PKI(商用認証局による証明書発行)だけでは、高度な経路ハイジャックや偽証明書発行を防ぎきれないのですか? 著者:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / ネットワークスペシャリスト / 登録情報処理安全確保支援士(第007617号))
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
SECTION 01 — ABSTRACT & CRYPTOGRAPHIC THREAT MODEL

1. 要旨と脅威モデル:BGPハイジャック・StripTLS・0-RTT再生攻撃の構造的解剖

トランスポート層セキュリティ(TLS)および電子メール配送基盤(SMTP)の安全性は、しばしば「TLS 1.3が有効化されているか」という単一の指標のみで語られます。しかし、実世界のインターネット経路(BGP: Border Gateway Protocol)および名前解決基盤(DNS)が攻撃者の介入下に置かれた場合、単なるTLS有効化だけでは防ぎきれない3つの重大な暗号学的攻撃面が露呈します。

第1の脅威は、BGP Sub-prefix Hijacking を悪用した不正なドメイン検証(DV)証明書の発行です。攻撃者が対象のIPプレフィックスより長いサブネットマスク(例:/24)を不正広告してトラフィックを局所的に引き込み、ACME HTTP-01 / DNS-01 チャレンジに応答すると、公開CAから正規のX.509証明書が発行されてしまいます。第2の脅威は、SMTP通信における STARTTLS ダウングレード攻撃(StripTLS)と不正MX誘導です。平文のDNS MX応答が改ざんされるか、またはSMTPセッションの 250-STARTTLS 応答が中間者に削ぎ落とされた場合、従来のメールサーバーは暗号化を諦めて平文で機密メールを送出してしまいます。第3の脅威は、TLS 1.3 の 0-RTT(Early Data)における再生攻撃(Replay Attack)です。

本論文では、当社エッジ基盤(ap01.ok.st)および3重冗長メールクラスタ(mx1..mx3.ok.st)に実装されている、(1) Curve25519上のX25519一時鍵共有とHKDF鍵スケジュール、(2) スライディング・ブルームフィルタによる0-RTT再生防御、(3) DNSSEC RRSIG連鎖とDANE TLSA(3 1 1)SPKIピンニング、および (4) MTA-STS(RFC 8461)の統合検証アーキテクチャを定式化します。

ATTACK VECTOR 脆弱な標準構成(Web PKI単体 / Opportunistic STARTTLS) 当社暗号検証パイプライン(TLS 1.3 + DNSSEC/DANE + MTA-STS)
Rogue CA / BGP DV Hijack 任意のパブリックCAが発行した証明書を無条件に信頼 DNS CAA (RFC 8659) 制限 + DANE TLSA (3 1 1) SPKI SHA-256 厳密一致
SMTP StripTLS Downgrade 250-STARTTLS 削除時に平文配送へフォールバックし本文漏洩 DANE + MTA-STS (mode: enforce) により TLS 検証失敗時は即時配信拒否
TLS 1.3 0-RTT Replay Early Data パケットの再送によりAPI更新処理が複数回実行される 非べき等メソッドの HTTP 425 (Too Early) 拒否 + Strike Register 二重排除
Static RSA Key Compromise 過去の録画済みパケットが秘密鍵漏洩時に遡及復号される TLS 1.3 Ephemeral X25519 / P-256 のみ許可(完全前方秘匿性 PFS 保証)
SECTION 02 — TLS 1.3 (RFC 8446) KEY SCHEDULE & 0-RTT ANTI-REPLAY

2. Curve25519 (X25519) 楕円曲線鍵共有と HKDF-Expand-Label 鍵スケジュール数理

TLS 1.3(RFC 8446)では、静的RSA鍵交換が完全に廃止され、すべてのハンドシェイクで一時楕円曲線ディフィー・ヘルマン鍵共有(ECDHE)が強制されます。当社のエッジノード ap01.ok.st では、素数体 𝔽p(ただし p = 2255 − 19)上のモンゴメリ曲線 Curve25519(RFC 7748 X25519) を第一優先鍵共有グループ(NamedGroup 0x001d)として採用しています。定数時間モンゴメリ・ラダー演算により、キャッシュタイミング攻撃(サイドチャネル漏洩)をハードウェア命令レベルで排除します。

1. Curve25519 (RFC 7748) Montgomery Ladder Ephemeral Key Agreement:
E(𝔽p) : B · v2 = u3 + 486662 · u2 + u   (mod p = 2255 − 19)
Clamp(k) : k[0] &= 248,   k[31] &= 127,   k[31] |= 64
Zecdhe = X25519(kserver, X25519(kclient, 9))   ∈ {0, 1}256

共有秘密 Zecdhe および事前共有鍵(PSK / セッション再開チケット)から、RFC 5869 の HMAC-based Extract-and-Expand Key Derivation Function(HKDF)を用いて3段階のシークレット(Early Secret, Handshake Secret, Master Secret)を順次導出します。各段階では、ハンドシェイクメッセージ全体のトランスクリプトハッシュ Transcript-Hash(M1 ∥ … ∥ Mn) が HKDF-Expand-Label に入力されるため、ハンドシェイク途中のビット改ざんやダウングレード試行は Finished メッセージの検証時点で100%検知されます。

2. TLS 1.3 (RFC 8446 Section 7.1) HKDF Key Schedule Derivation:
EarlySecret = HKDF-Extract(salt = 0Hash.len, IKM = PSK)
client_early_traffic_secret = Derive-Secret(EarlySecret, "c e traffic", ClientHello)
HandshakeSecret = HKDF-Extract(salt = Derive-Secret(EarlySecret, "derived", ""), IKM = Zecdhe)
MasterSecret = HKDF-Extract(salt = Derive-Secret(HandshakeSecret, "derived", ""), IKM = 0Hash.len)
HKDF-Expand-Label(Secret, Label, Context, L) = HKDF-Expand(Secret, uint16(L) ∥ "tls13 " ∥ Label ∥ Context, L)

0-RTT Early Data 再生攻撃の数理と二層ブルームフィルタ防御

上式が示す通り、client_early_traffic_secret は一時鍵共有値 Zecdhe(ServerHelloで確定)に依存せず、PSKと ClientHello のみから決定されます。したがって、攻撃者がネットワーク経路上で 0-RTT パケットをキャプチャして再送した場合、サーバーが状態を記憶していない限り同一の復号結果が得られてしまいます。これを防ぐため、RFC 8446 Section 8 に基づき以下の3重防御を適用します。

3. Freshness Window & Strike Register Anti-Replay Invariant:
Δtage = |(obfuscated_ticket_age − ticket_age_add) mod 232 − (tserver_now − tticket_issued)| ≤ Wfresh (e.g., 6,000 ms)
Accept0RTT(CH) ⇔ (Method ∈ {GET, HEAD}) ∧ (Δtage ≤ Wfresh) ∧ (H1..k(ClientRandom ∥ binder) ∉ StrikeBloomactive)
FIGURE 1: TLS 1.3 HKDF KEY SCHEDULE & DNSSEC / DANE TLSA (3 1 1) / MTA-STS CRYPTOGRAPHIC VERIFICATION TREE
01 DNSSEC & DANE TLSA CHAIN Root KSK → .jp DS → ok.st DNSKEY RFC 4034/4035 RRSIG ECDSA P-256 _25._tcp.mx1.ok.st. IN TLSA Usage=3 (DANE-EE) Selector=1 (SPKI) MatchingType=1 (SHA-256 Digest) MTA-STS Policy (RFC 8461) mode: enforce / max_age: 604800 TLS-RPT (RFC 8460) Failure Telemetry 02 TLS 1.3 HKDF SCHEDULE (RFC 8446) Early Secret (PSK + 0-RTT Guard) Strike Register Bloom Filter Check Reject Replay / Non-Idempotent HTTP 425 Handshake Secret (X25519 ECDHE) Curve25519: p = 2^255 - 19 EncryptedExtensions + CertificateVerify Master Secret (AEAD Traffic Keys) TLS_AES_256_GCM_SHA384 / ChaCha20 Perfect Forward Secrecy (PFS) Locked 03 PINNING GATE SPKI Digest Comparison SHA-256(Cert.SPKI_DER) == TLSA.CertAssocData 定数時間XOR比較(タイミング漏洩防止) Verified Channel Output ・BGP/CA偽装を100%拒絶 ・StripTLS平文降格を遮断 ・0-RTT再送パケットを破棄 ap01 / mx1..mx3 Enforced
SECTION 03 — DNSSEC, DANE TLSA (RFC 6698 / 7672) & MTA-STS (RFC 8461)

3. DNSSEC署名連鎖と DANE TLSA (3 1 1) SPKIピンニング・MTA-STS の二重防壁

DANE(DNS-Based Authentication of Named Entities: RFC 6698 / RFC 7672)は、TLSサーバーのX.509証明書またはSubjectPublicKeyInfo(SPKI)のハッシュ値を、DNSSEC署名された TLSA リソースレコード(RRType = 52)として権威DNSゾーンに直接束縛する規格です。

DNSSEC + DANE TLSA (3 1 1) Cryptographic Verification Predicate:
VerifyDNSSEC(Zone) ⇔ ∀ i ∈ {root, tld, apex} : VerifySig(KSKi, RRSIG(DNSKEYi)) ∧ Hash(KSKi+1) == DSi
TLSArecord(_443._tcp.ok.st) = (Usage = 3 [DANE-EE], Selector = 1 [SPKI], MatchingType = 1 [SHA-256], Dexpected)
VerifyDANE(CertTLS, TLSArecord) ⇔ VerifyDNSSEC(TLSArecord) ∧ (SHA-256(DER(CertTLS.SPKI)) == Dexpected)

当社では、証明書の自動更新(Let's Encrypt等の90日ローテーション)時に毎回DNSゾーンのTLSAレコードを書き換える運用負荷と伝播遅延(TTL問題)を排除するため、Selector = 1(SubjectPublicKeyInfoのみを対象)かつ Usage = 3(DANE-EE:Domain-Issued Certificate)である 3 1 1 構成と、現用鍵(Current Key)+次期予備鍵(Rollover Backup Key)のデュアルTLSAレコード公開方式を採用しています。これにより、X.509証明書のシリアル番号や有効期限が更新されても、SPKI公開鍵または予約済み次期鍵のSHA-256ダイジェストが一致する限り、無停止でゼロダウンタイム鍵ローテーションが完了します。

FIELD / RFC PRODUCTION VALUE 暗号学的意味と選択理由(Engineering Rationale)
TLSA Certificate Usage 3 (DANE-EE) / 2 (DANE-TA) 外部CAの誤発行に影響されず、ドメイン管理者が指定したエンドエンティティ鍵(3)または専用CA鍵(2)のみを信頼
TLSA Selector 1 (SubjectPublicKeyInfo) 証明書全体(0)ではなく公開鍵構造体 DER(SPKI) のみを対象とすることで、ACME自動更新時のTLSA不一致障害を防止
TLSA Matching Type 1 (SHA-256 / 32 Bytes) DNS UDP/EDNS0 パケットサイズ肥大化を抑えつつ、2128 衝突耐性と 2256 第二原像計算困難性を確保
MTA-STS Policy (RFC 8461) mode: enforce, max_age: 604800 DNSSEC未対応のリゾルバ経由でも、HTTPS Web PKI経由でMXホスト名パターンとTLS必須ポリシーを強制キャッシュ
SMTP TLS-RPT (RFC 8460) _smtp._tls.ok.st TXT rua=... 配送先MTAとのTLSネゴシエーション失敗・DANE不一致・STSポリシー違反のJSON統計テレメトリを日次自動収集
SECTION 04 — PRODUCTION RUST 2024 IMPLEMENTATION

DANE TLSA (3 1 1) SPKI ピン検証器・MTA-STS ポリシー評価器・0-RTT Strike Register の Rust 2024 実装

Rust Edition 2024 / Constant-Time Verifier
Key Exchange X25519 (RFC 7748)
DANE Profile TLSA 3 1 1 (SPKI SHA-256)
0-RTT Replay Guard Dual-Epoch Bloom Filter
Digest Equality Constant-Time O(1) XOR
src/crypto/dane_mtasts_tls13_verifier.rs
Rust Edition 2024 — DANE TLSA + MTA-STS + 0-RTT Strike Register
#![forbid(unsafe_code)]

/// Constant-time 32-byte SHA-256 digest comparison to prevent timing side-channels.
#[inline]
pub fn ct_eq_sha256(a: &[u8; 32], b: &[u8; 32]) -> bool {
    let mut diff: u8 = 0;
    let mut i = 0;
    while i < 32 {
        diff |= a[i] ^ b[i];
        i += 1;
    }
    diff == 0
}

/// RFC 6698 DANE TLSA Record parameters.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TlsaRecord {
    /// 3 = DANE-EE (Domain-Issued Certificate), 2 = DANE-TA (Trust Anchor)
    pub cert_usage: u8,
    /// 1 = SubjectPublicKeyInfo (SPKI DER), 0 = Full Certificate
    pub selector: u8,
    /// 1 = SHA-256 (32 bytes)
    pub matching_type: u8,
    pub spki_sha256: [u8; 32],
    pub dnssec_ad_flag: bool,
}

/// RFC 8461 MTA-STS Policy representation.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MtaStsMode {
    Enforce,
    Testing,
    None,
}

#[derive(Clone, Debug)]
pub struct MtaStsPolicy {
    pub mode: MtaStsMode,
    pub max_age_seconds: u64,
    pub allowed_mx_patterns: Vec<&'static str>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransportVerifyError {
    DnssecBogusOrUnsigned,
    UnsupportedTlsaProfile,
    TlsaSpkiPinMismatch,
    MtaStsMxHostForbidden,
    MtaStsWeakTlsVersion,
    ZeroRttClockSkewExceeded,
    ZeroRttReplayDetected,
    ZeroRttUnsafeMethodRejected,
}

impl TlsaRecord {
    /// Verifies peer certificate SPKI SHA-256 digest against DNSSEC-authenticated TLSA (3 1 1).
    pub fn verify_peer_spki(
        &self,
        peer_spki_sha256: &[u8; 32],
    ) -> Result<(), TransportVerifyError> {
        if !self.dnssec_ad_flag {
            return Err(TransportVerifyError::DnssecBogusOrUnsigned);
        }
        if (self.cert_usage != 3 && self.cert_usage != 2)
            || self.selector != 1
            || self.matching_type != 1
        {
            return Err(TransportVerifyError::UnsupportedTlsaProfile);
        }
        if !ct_eq_sha256(&self.spki_sha256, peer_spki_sha256) {
            return Err(TransportVerifyError::TlsaSpkiPinMismatch);
        }
        Ok(())
    }
}

impl MtaStsPolicy {
    /// Validates MX hostname against RFC 8461 wildcard/exact policy and enforces TLS 1.3.
    pub fn verify_mx_session(
        &self,
        candidate_mx: &str,
        negotiated_tls_version: u16,
    ) -> Result<(), TransportVerifyError> {
        // Require TLS 1.2 (0x0303) minimum; enforce TLS 1.3 (0x0304) on strict profile
        if negotiated_tls_version < 0x0304 {
            return Err(TransportVerifyError::MtaStsWeakTlsVersion);
        }
        let matched = self.allowed_mx_patterns.iter().any(|pattern| {
            if let Some(suffix) = pattern.strip_prefix("*.") {
                candidate_mx.ends_with(suffix)
                    && candidate_mx.len() > suffix.len()
                    && candidate_mx.as_bytes()[candidate_mx.len() - suffix.len() - 1] == b'.'
            } else {
                candidate_mx.eq_ignore_ascii_case(pattern)
            }
        });

        if !matched && self.mode == MtaStsMode::Enforce {
            return Err(TransportVerifyError::MtaStsMxHostForbidden);
        }
        Ok(())
    }
}

/// RFC 8446 Section 8: Dual-Epoch Sliding Bloom Filter (Strike Register) for 0-RTT Anti-Replay.
pub struct ZeroRttStrikeRegister {
    active_bits: [u64; 64],
    previous_bits: [u64; 64],
    epoch_start_ms: u64,
    freshness_window_ms: u64,
}

impl ZeroRttStrikeRegister {
    pub const fn new(epoch_start_ms: u64, freshness_window_ms: u64) -> Self {
        Self {
            active_bits: [0u64; 64],
            previous_bits: [0u64; 64],
            epoch_start_ms,
            freshness_window_ms,
        }
    }

    #[inline]
    fn bloom_indices(client_random: &[u8; 32], psk_binder: &[u8; 32]) -> [usize; 4] {
        let mut h0: u64 = 0x243F_6A88_85A3_08D3;
        let mut h1: u64 = 0x1319_8A2E_0370_7344;
        let mut i = 0;
        while i < 32 {
            h0 = (h0 ^ (client_random[i] as u64)).wrapping_mul(0x1000_0000_01B3);
            h1 = (h1 ^ (psk_binder[i] as u64)).wrapping_mul(0x1000_0000_01B3);
            i += 1;
        }
        [
            (h0 as usize) & 4095,
            ((h0 >> 16) as usize) & 4095,
            (h1 as usize) & 4095,
            ((h1 >> 16) as usize) & 4095,
        ]
    }

    /// Validates TLS 1.3 0-RTT Early Data against method safety, ticket age skew, and replay cache.
    pub fn validate_and_record_0rtt(
        &mut self,
        is_safe_http_method: bool,
        obfuscated_ticket_age_ms: u32,
        ticket_age_add_ms: u32,
        ticket_issued_at_ms: u64,
        server_now_ms: u64,
        client_random: &[u8; 32],
        psk_binder: &[u8; 32],
    ) -> Result<(), TransportVerifyError> {
        if !is_safe_http_method {
            return Err(TransportVerifyError::ZeroRttUnsafeMethodRejected);
        }

        // Rotate bloom filter epoch when freshness window elapses
        if server_now_ms.saturating_sub(self.epoch_start_ms) >= self.freshness_window_ms {
            self.previous_bits = self.active_bits;
            self.active_bits = [0u64; 64];
            self.epoch_start_ms = server_now_ms;
        }

        let claimed_age = obfuscated_ticket_age_ms.wrapping_sub(ticket_age_add_ms) as u64;
        let actual_age = server_now_ms.saturating_sub(ticket_issued_at_ms);
        let skew = claimed_age.abs_diff(actual_age);
        if skew > self.freshness_window_ms {
            return Err(TransportVerifyError::ZeroRttClockSkewExceeded);
        }

        let indices = Self::bloom_indices(client_random, psk_binder);
        let mut seen_in_active = true;
        let mut seen_in_prev = true;

        for &idx in &indices {
            let word = idx >> 6;
            let mask = 1u64 << (idx & 63);
            if (self.active_bits[word] & mask) == 0 {
                seen_in_active = false;
            }
            if (self.previous_bits[word] & mask) == 0 {
                seen_in_prev = false;
            }
        }

        if seen_in_active || seen_in_prev {
            return Err(TransportVerifyError::ZeroRttReplayDetected);
        }

        for &idx in &indices {
            let word = idx >> 6;
            let mask = 1u64 << (idx & 63);
            self.active_bits[word] |= mask;
        }
        Ok(())
    }
}
ご質問・公的制度の活用に関するお問い合わせ窓口

掲載資料に関するご質問・公的制度活用のご案内

本ページの図解や解説内容に関するご質問、経済産業省「SCS評価制度」やIPA「SECURITY ACTION・サイバーセキュリティお助け隊サービス」等の公的制度の確認方法について、落ち着いた参考窓口としてご案内しております。売り込みや営業連絡は一切行いませんので、社内勉強会やご家庭での確認資料としてご自由にご活用ください。

図解・ビジュアルの拡大表示

図表内のテキストもそのまま選択・検索可能です(ベクターSVG描画) 枠外クリックまたは ESC キーで閉じます