第7章: モジュラー算術

難易度: 中級

Modular Arithmetic — Barrett, Montgomery, CRT, and Modular Exponentiation

モジュラー算術 $a \bmod m$ は、除算の特殊形でありながら、 独立の深い理論と最適化を持つ。 RSA 暗号の $m^e \bmod n$、楕円曲線暗号の点倍加、NTT の各ラウンド、 Miller-Rabin 素数判定 — これらはすべてモジュラー乗算を大量に行うため、 1 回のモジュラー乗算が $1\,\mu\text{s}$ から $0.1\,\mu\text{s}$ に縮むかどうかが 暗号システムの実用性を左右する。

本章では、Barrett 還元、Montgomery 乗算、Chinese Remainder Theorem、 高速冪剰余、そして暗号用の定時間実装を扱う。 sangi の Montgomery 実装 — 特に n=8 (512-bit) 特化のアセンブリ — を参照しながら、 現代 CPU でどこまで 1 サイクルを削り出せるかを見ていく。 512-bit 冪剰余を sangi は約 $32\,\mu\text{s}$ で完了する (Zen 3 / MSVC Release x64 での実測。測定条件は 7.5 節のベンチマーク表を参照)。

7.1 基本 — なぜ除算を避けるか

モジュラー剰余 $a \bmod m$ は、定義上は除算 $\lfloor a / m \rfloor$ で決まる:

$$a \bmod m = a - m \cdot \lfloor a / m \rfloor.$$

前章で見たように、多倍長除算は乗算の 2〜5 倍遅い。 ところが暗号や NTT では、同じ法 $m$数億回のモジュラー乗算を行うことが多い。 法 $m$ を固定して事前計算を行えば、実行時は乗算 1〜2 回で剰余が求まる。

目的: 除算を乗算に置き換える

Barrett と Montgomery はどちらも「$m$ 固定」の仮定のもと、 事前計算で $m$ の逆数的な値を作っておき、 実行時はそれを使った 2 回の乗算で剰余を得る戦略である。 使い分けの目安:

  • Barrett: 単発の剰余計算、あるいは乗算なしの剰余 ($a \bmod m$ の $a$ が積でない場合)。任意の法 $m$ で使える。
  • Montgomery: $(a \cdot b) \bmod m$ のように乗算と剰余がセットで連鎖する場合。ただし $m$ が奇数であることが必須 ($R = \beta^n$ と互いに素である必要)。変換コストを差し引いて有利になるのは乗算を数回以上連鎖させる場合で、目安は $k \ge 3$ 程度 (分岐点は実装とサイズに依存する)。

7.2 Barrett 還元

Barrett (1986) のアイデアは、$\lfloor 2^k / m \rfloor$ を事前計算した「偽の逆数」 $\mu$ を使って、除算を 2 回の乗算に置き換えることである。

アルゴリズム

法 $m$ が $n$ ワードなら、$k = 2n \cdot 64$ として:

$$\mu = \left\lfloor \dfrac{2^k}{m} \right\rfloor, \quad \hat{q} = \left\lfloor \dfrac{a \cdot \mu}{2^k} \right\rfloor, \quad r = a - m \cdot \hat{q}.$$

$\hat{q}$ は真の商 $\lfloor a/m \rfloor$ の近似で、誤差は高々 $1$。 よって $r < 2m$ であり、高々 1 回の減算で正規化できる。

導出の詳細:誤差 $\le 1$ の証明

真の商を $q = \lfloor a/m \rfloor$ とおく。$\mu = \lfloor 2^k/m \rfloor$ より $\mu = 2^k/m - \delta_1$($0 \le \delta_1 < 1$)。

Step 1:$\hat{q} = \lfloor a\mu/2^k \rfloor$ は $a\mu/2^k$ の床、つまり $\hat{q} = a\mu/2^k - \delta_2$($0 \le \delta_2 < 1$)。

Step 2:これを $\mu$ の式で展開すると

$$\displaystyle \hat{q} = \dfrac{a(2^k/m - \delta_1)}{2^k} - \delta_2 = \dfrac{a}{m} - \dfrac{a\delta_1}{2^k} - \delta_2.$$

Step 3:$a$ が $2n$ ワード以下なら $a < 2^{2n\cdot 64} = 2^k$。ここで必要なのはこの $a < 2^k$ だけで、$a < m^2$ は仮定しない($m$ が $n$ ワードでも $m^2$ は $2^k$ よりずっと小さくなり得るので、$a < m^2$ はワード数からは導けない)。$0 \le \delta_1 < 1$ とあわせて $0 \le a\delta_1/2^k < 1$、よって

$$\displaystyle 0 \le \frac{a}{m} - \hat{q} = \frac{a\delta_1}{2^k} + \delta_2 < 1 + 1 = 2.$$

Step 4:$\mu \le 2^k/m$ より $\hat{q} \le \lfloor a/m \rfloor = q$、つまり $q - \hat{q} \ge 0$。また $q \le a/m$ だから Step 3 より $q - \hat{q} < 2$。$q - \hat{q}$ は整数なので $q - \hat{q} \in \{0, 1\}$、すなわち $r = a - m\hat{q} < 2m$ で補正の減算は高々 1 回。$\square$

この上界はタイトで、$q - \hat{q} = 1$ は実際に起こる。なお古典的な Barrett 還元 (HAC 14.42) は $a\mu$ の全桁ではなく上位ワードだけを使う近似を追加で入れるため、そちらの誤差上界は $2$(減算 2 回)になる。上の評価は本節で定義した「$a\mu$ をそのまま使う」式に対するものである。

なぜ速いか

$m$ が $n$ ワードなら $2^k = \beta^{2n}$ (ここで $\beta = 2^{64}$) であり、 $\mu = \lfloor \beta^{2n} / m \rfloor \ge \beta^{n}$ となるので、$\mu$ は一般に $n+1$ ワードである ($n$ ワードではない)。したがって $a \cdot \mu$ は $2n$ ワード $\times$ $(n{+}1)$ ワードの乗算になる。

ただし商の近似に必要なのは $\lfloor a\mu / \beta^{2n} \rfloor$、すなわち積のうち $\beta^{2n}$ より上の部分だけである。 全積を作らずに高位部分積 (high product) だけを求める短縮乗算を使えばその分安くなるが、 実際にどこまで削れるかは採用する短縮乗算のアルゴリズムと実装に依存する。 いずれにせよ $O(n^2)$ の除算より軽い。

Barrett の数値例 ($m$ = 小さい法)

$m = 7$, $k = 8$: $\mu = \lfloor 256 / 7 \rfloor = 36$。 $a = 97$ を法 7 で還元:

$$\hat{q} = \lfloor 97 \cdot 36 / 256 \rfloor = \lfloor 3492 / 256 \rfloor = 13,$$ $$r = 97 - 7 \cdot 13 = 97 - 91 = 6.$$

真の答え $97 \bmod 7 = 6$ と一致。補正ステップが不要だった幸運な例。

Barrett と $\mu$-division の関係

前章の $\mu$-div は、Barrett 還元のロジックを 不均衡除算に適用したものである。 Barrett 単独では剰余のみ、$\mu$-div は商 + 剰余の両方を返す。

7.3 Montgomery 乗算

Montgomery (1985) の発想は、$a \bmod m$ の代わりに $\bar{a} = a R \bmod m$ ($R = 2^{64n}$) という Montgomery 形式で データを保持し、この形式上で乗算 + 還元を「除算なし」で行うことである。

Montgomery 形式と REDC

Montgomery 形式

整数 $a$ の Montgomery 表現を $\bar{a} = a R \bmod m$ とする。 通常の乗算と対比:

$$\bar{a} \cdot \bar{b} = a b R^2 \bmod m \neq (ab) R.$$

つまり、単純に乗算すると余分な $R$ が付く。この余分を取り除く操作が REDC:

$$\text{REDC}(T) = T R^{-1} \bmod m.$$

すると $\text{REDC}(\bar{a} \bar{b}) = (abR^2) R^{-1} = abR = \overline{ab}$ となり、 Montgomery 形式のまま乗算結果が得られる。

REDC のアルゴリズム

ここが Montgomery の魔法である。$0 \le T < Rm$ を満たす $T$ (最大 $2n$ ワード) が与えられたとき:

  1. $u = T \cdot (-m^{-1}) \bmod R$ を計算 (下位 $n$ ワードのみ)。
  2. $T' = T + u \cdot m$ を計算。すると $T' \equiv T \pmod m$ かつ $T' \equiv 0 \pmod R$。
  3. $T'' = T' / R$ は単なる右シフト (下位 $n$ ワード切り捨て)。
  4. 必要なら $T''$ から $m$ を 1 回減算して正規化。

前提 $T < Rm$ について

Step 4 の「減算は高々 1 回」を保証するのは $T < Rm$ という上界であって、$T$ のワード数ではない。 $m$ が $R$ よりずっと小さければ、$2n$ ワードに収まる値でも容易に $Rm$ を超える。

Montgomery 乗算の用途ではこの前提は自動的に満たされる: $0 \le a, b < m$ の積 $T = ab$ を渡すので、$m < R$ より $T < m^2 < Rm$ である。

導出の詳細:なぜ $T'$ が $R$ で割り切れるか

Step 1($T \pmod m$ の不変性):$T' = T + u m \equiv T \pmod m$ は自明($u m$ は $m$ の倍数)。

Step 2($T'$ が $R$ の倍数):$u \equiv T \cdot (-m^{-1}) \pmod R$ なので $u m \equiv -T \pmod R$、よって

$$\displaystyle T' = T + um \equiv T + (-T) = 0 \pmod R.$$

つまり $T'$ の下位 $n$ ワードはすべて 0、$T'/R$ は単なる右シフトで実装できる。

Step 3(最終結果):$T'' = T'/R$ なので $T'' \equiv T R^{-1} \pmod m$、これが REDC の定義 $\text{REDC}(T) = T R^{-1} \bmod m$ と一致。サイズは $T'' < (T + Rm)/R = T/R + m \le 2m$ なので 1 回の減算で正規化可能。$\square$

$$T'' = \text{REDC}(T) = \dfrac{T + ((T \cdot (-m^{-1})) \bmod R) \cdot m}{R}.$$
Montgomery REDC の 4 ステップ: u 計算 → T+um → /R → 正規化 Montgomery REDC のフロー 入力 T 2n ワード (T < Rm) Step 1: u = T·(−m⁻¹) mod R 下位 n ワード乗算のみ Step 2: T′ = T + u·m T′ ≡ T (mod m)、T′ ≡ 0 (mod R) Step 3: T″ = T′ / R 下位 n ワード切捨て (右シフト) Step 4: T″ ≥ m なら T″ ← T″ − m (高々 1 回) 出力 REDC(T) = T R⁻¹ mod m
図1: REDC アルゴリズムの 4 ステップ。Step 1 で $u$ を求め下位 $n$ ワードのみ計算、Step 2 で $T + um$ が $R$ の倍数になるよう構成、Step 3 の 除算 $/R$ は右シフトに退化、Step 4 で最大 1 回の正規化減算。除算命令を一切使わない点が Montgomery の核心。

計算は乗算 2 回 ($u$ と $u \cdot m$) と加算、右シフト、高々 1 回の減算のみ。 除算は事前計算の $m^{-1} \bmod R$ に隠されている。 $m$ が固定なら $m^{-1}$ は 1 回だけ計算すればよい。

入出力のコスト

あらかじめ $R^2 \bmod m$ を用意しておけば、通常の整数 $a$ の Montgomery 形式は $\text{REDC}(a \cdot (R^2 \bmod m)) = aR \bmod m$ として 1 回の Montgomery 乗算で得られる。 逆変換には $\bar{a} \cdot 1$ を REDC すればよい。 $k$ 回の乗算を Montgomery 空間で行うなら、変換コストは $2$ 回、内部乗算が $k$ 回。 通常剰余より有利になるのは $k \ge 3$ ほどからというのが目安だが、実際の分岐点は実装と法のサイズに依存する。

7.4 CIOS アルゴリズムと sangi の実装

Montgomery の REDC を実装する際、$T = a \cdot b$ を一度に計算してから還元するSOS (Separated Operand Scanning) と、乗算と還元を交互に進めるCIOS (Coarsely Integrated Operand Scanning) がある。CIOS は中間結果の保存を節約でき、 暗号パラメータ (n=4〜64) では最も速い部類とされる (Koç らの比較による。実際の順位は実装と CPU に依存する)。

CIOS の構造

内側ループで $b$ の各ワード $b_i$ について:

  1. 乗算段: $t \leftarrow t + a \cdot b_i$ (これで 1 ワード分の $T$ が確定)。
  2. 還元段: $q \leftarrow t_0 \cdot m_{\text{inv}} \bmod \beta$ を求め、 $t \leftarrow (t + q \cdot m) / \beta$ (下位 1 ワードが 0 に)。

$n$ 回のループ終了時に $t$ が REDC 済みの結果になる。

sangi の汎用 REDC 実装

次に挙げるのは sangi の汎用の還元ルーチンである。名前のとおり REDC そのもので、 上で述べた CIOS の「乗算段」は含んでいない — 積 $T = a \cdot b$ は呼び出し側が先に作り、 この関数はその $T$ を還元するだけである (構成としては SOS 側にあたる)。 CIOS の形にするには、$a b_i$ の累積と $q_i m$ の累積を同じ外側ループの中で交互に行う必要がある。

// IntModular.cpp: mont_redc (汎用 REDC。積 T は呼び出し側が用意する)
// 前提: T は 2n+1 ワード分の領域を持ち、0 ≤ T < Rm を満たす。
// 前提: mpn::addmul_1 は「n ワード分の addmul 結果の最上位から溢れた
//       単一ワードのキャリー」を返す。tn > i+n のケースでは、このキャリー
//       を上位ワードへ伝搬させる必要がある (内側の for ループ)。
inline void mont_redc(uint64_t* r, uint64_t* t, size_t tn,
                      const uint64_t* m, size_t n, uint64_t m_inv) {
    for (size_t i = 0; i < n; ++i) {
        uint64_t q = t[i] * m_inv;                     // q = T[i] · (-m^{-1}) mod β
        uint64_t carry = mpn::addmul_1(t + i, m, n, q); // T += q·m·β^i の局所結果
        // addmul_1 から返るキャリーを上位ワードへ順次伝搬 (非ゼロの間のみ)
        for (size_t j = i + n; carry && j < tn; ++j) {
            uint64_t sum = t[j] + carry;
            carry = (sum < t[j]) ? 1 : 0;
            t[j] = sum;
        }
    }
    // 結果は t[n..2n-1] だけでなく、その上の桁上げワード t[2n] との組である。
    // t[2n] != 0 なら値は β^n > m なので減算は必須。
    const uint64_t top = (tn > 2 * n) ? t[2 * n] : 0;
    std::memcpy(r, t + n, n * sizeof(uint64_t));
    if (top != 0 || mpn::cmp(r, n, m, n) >= 0) {
        mpn::sub(r, r, n, m, n);                        // 正規化 (r < m に)
    }
}

最上位ワードを見落とすと、ちょうど $m$ だけ大きい値が返る

最後の条件付き減算で t[2n] を見ずに t[n..2n-1] だけを $m$ と比べると、 還元後の値が $n+1$ ワードになったケースを取りこぼす。返る値は誤りだが $n$ ワードには収まるので、 呼び出し側からは正常に見える。sangi の C++ フォールバック経路には実際にこの欠陥があり (BMI2/ADX を持たない x64 CPU で使われる経路)、$n=2$ の乱数平方 64 回中 1 回が誤答だった。

汎用版は $n$ によらない。特定サイズ (n=4, 8, 16, 32) では個別のアセンブリカーネルに ディスパッチする。

7.5 Montgomery n=8 特化 — sangi のアセンブリ

512 bit (8 ワード) は多倍長モジュラー演算のベンチマークとして代表的なサイズである。 (実際の暗号方式の法はこれとは異なる: RSA-4096 の法 $n$ は 4096 bit = 64 ワード、CRT 内部の $p, q$ でも 2048 bit = 32 ワード。ECDSA P-521 は 521 bit なので 64-bit ワードでは $\lceil 521/64 \rceil = 9$ ワード必要である。) この 1 サイズに特化したアセンブリカーネルを書くと、 汎用の C++ 実装より 30-50% 速くなる (sangi の実測値。効き幅は CPU と比較対象の実装に依存する)。sangi の mpn_mont_mul_8 は 3 段構成で実装されている。

3 フェーズ構成

mpn_mont_mul_8 の構造

  1. Phase 1: 8×8 完全乗算 (SOS 相当、スタック上 17 ワードの product buffer)。
  2. Phase 2: REDC 8 反復 ($q_i = t_i \cdot m_{\text{inv}}$ を計算し、$T$ を 1 ワード縮める)。
  3. Phase 3: 条件付き減算 ($T \ge m$ なら $m$ を引く)。

累算器のレジスタ常駐

8 ワードの累算器 $r_8, r_9, \ldots, r_{15}$ を GPR (r8-r15) に常駐させる。 これでメモリアクセスを大幅に減らせる:

; mpn_x64_mont.asm: Phase 1 の骨格
xor r8d, r8d          ; 8 個の累算器を 0 クリア
xor r9d, r9d
...                   ; r15 までゼロ
mov rdx, [rcx]        ; b[0] を rdx へ (MULX の暗黙オペランド)
ADDMUL_FIRST_8        ; 種乗算: r8 = a[0]·b[0]_lo, r9 に桁上げ
mov QWORD PTR [rdi], r8  ; r8 は確定したので product buffer へ格納
ADDMUL_REST_8         ; r9-r15 に残りの乗算を累積
; b[1]..b[7] で繰り返し

ADDMUL マクロ

; ADDMUL_FIRST_8: b[j] との乗算の最初の 1 ワード (rbp:rbx に a[0]·rdx)
ADDMUL_FIRST_8 MACRO
    xor eax, eax
    mulx rbx, rbp, [rsi]    ; rbp:rbx = a[0] · rdx (b[j])
    adox r8, rbp            ; r8 += rbp (OF を残す)
ENDM

; ADDMUL_REST_8: 残り a[1]..a[7] の積を r9..r15 に累積 (2 本のキャリーチェーン)
ADDMUL_REST_8 MACRO
    mulx rbp, rax, [rsi+8]   ; a[1]·rdx の低半
    adcx r8, rbx             ; 前回の rbx を CF 鎖で r8 に
    adox r9, rax             ; 低半を OF 鎖で r9 に
    mulx rbx, rax, [rsi+16]  ; a[2]·rdx
    adcx r9, rbp
    adox r10, rax
    ; ... 同じパターンで a[3]..a[7] を r11..r15 まで展開
    ; 最後に adcx r15, rbx; adox r15, 0 で両キャリーを合流
ENDM

; REDC_ITER: 還元 1 ラウンド (iter_idx = 0..7)
REDC_ITER MACRO iter_idx
    mov rax, r8
    imul rax, QWORD PTR [rsp+168]   ; rax = r8 · m_inv
    mov rdx, rax                     ; q = rax (MULX の暗黙オペランド)
    ADDMUL_FIRST_8                   ; T += q · m (先頭ワード)
    ADDMUL_REST_8                    ; T += q · m (残り 7 ワード)
    add r15, QWORD PTR [rdi + (8 + iter_idx)*8]  ; 上位 product buffer の合流
    jnc ri_nc
ENDM

MULX / ADCX / ADOX の 2 並列キャリーチェーン

BMI2 の MULX は乗算で CF/OF を変えない。 ADX 拡張の ADCX / ADOXCFOF を別々のキャリーフラグとして扱う。これにより 2 本のキャリーチェーンを同時に走らせ、 依存性を半分にできる。Intel/AMD の現代 CPU で Montgomery 乗算の典型的な実装技法である。

scratch 不要

Phase 1 の product buffer 17 ワード (8 ワード + REDC 用 8 ワード + 最上位) と、 入力・出力ポインタ 48 バイト = 計 184 バイトがスタックに収まる。 ヒープ割当を避けられるため、暗号用途の 低遅延特性 (マイクロ秒以下) を維持できる。

ディスパッチ

// IntModular.cpp: n==8 の分岐
if (n == 8 && mpn::detail::has_bmi2_adx()) {
    mpn_mont_mul_8(r, a, b, m, m_inv);
    return;
}

ベンチマーク (Zen 3, MSVC Release x64)

$x \mapsto x^e \bmod m$ を法 $m$ のビット数と同じビット数の乱数指数 $e$ で測定 (Montgomery 空間でのスライディングウィンドウ、各ビットあたり平方 + 必要時に乗算)。

サイズ 演算 sangi (μs)
512 bit powerMod 31.9
1024 bit powerMod 261.8

n=8 特化が効く 512 bit が単一乗算あたり最速。1024 bit は n=16 特化の効きが相対的に弱い。

7.6 Chinese Remainder Theorem

互いに素な法 $m_1, m_2, \ldots, m_k$ が与えられたとき、中国剰余定理 (CRT) により

$$(a \bmod m_1, \ldots, a \bmod m_k) \leftrightarrow a \bmod M, \quad M = \prod m_i.$$

が 1 対 1 対応する。この対応は 環同型 $\mathbb{Z}/M\mathbb{Z} \cong \prod_i \mathbb{Z}/m_i\mathbb{Z}$ であり、 加算・減算・乗算のいずれも法ごとに独立に行えるので、 大きな法 $M$ での計算を $k$ 個の小さな法での計算に分割できる。 sangi では NTT の素数合成にも使うが、 整数 CRT 単独でも RSA 鍵生成や多項式補間で利用する。

Garner のアルゴリズム

残差 $r_i = a \bmod m_i$ から $a$ を復元する効率的な方法:

$$\begin{aligned} v_1 &= r_1, \\ v_2 &= (r_2 - v_1) \cdot m_1^{-1} \bmod m_2, \\ v_3 &= (r_3 - v_1 - v_2 m_1) \cdot (m_1 m_2)^{-1} \bmod m_3, \\ &\vdots \end{aligned}$$

最後に $a = v_1 + v_2 m_1 + v_3 m_1 m_2 + \cdots$ を計算する。 Garner の利点は $M = \prod m_i$ を明示的に構築せず、各ステップで $m_1 m_2 \cdots m_{i-1}$ を累積しながら逐次合成する点にある (小さな $m_i$ で モジュラ逆元を計算すれば済む)。

導出の詳細:$v_2$ の式はどこから来るか

復元したい $a$ を混合基数表示 $a = v_1 + v_2 m_1 + v_3 m_1 m_2 + \cdots$ で表す($0 \le v_i < m_i$)。

Step 1($v_1$ の決定):両辺を $m_1$ で還元すると $a \equiv v_1 \pmod{m_1}$、よって $v_1 = r_1$。

Step 2($v_2$ の決定):両辺を $m_2$ で還元すると

$$\displaystyle r_2 = a \bmod m_2 \equiv v_1 + v_2 m_1 \pmod{m_2}.$$

これを $v_2$ について解くと($\gcd(m_1, m_2) = 1$ より $m_1^{-1} \bmod m_2$ が存在):

$$\displaystyle v_2 = (r_2 - v_1) \cdot m_1^{-1} \bmod m_2.$$

Step 3(一般化):$v_3, v_4, \ldots$ も同様に「既知の下位項を引いてから累積積の逆元を掛ける」で逐次決定できる。各ステップは小さな $m_i$ 内のモジュラ演算で完結し、$M$ 全体での剰余演算が不要。$\square$

// Garner のアルゴリズム (逐次構築版, 擬似コード)
// std::integral は C++20 concept。C++17 以前では template <typename T> に置換。
// 注: 混合基数係数 v[i] は m_i 未満なので T に収まるが、最後の再構成
//     a = v[0] + v[1]·m_0 + ... は M = Π m_i の大きさになるため、組込み整数型では
//     容易にオーバーフローする。実装では戻り値と再構成部を多倍長整数型にすること。
template <std::integral T>
T garner_crt(const std::vector<T>& remainders,
             const std::vector<T>& moduli) {
    std::vector<T> v(moduli.size());
    v[0] = remainders[0];
    for (size_t i = 1; i < moduli.size(); ++i) {
        T t = remainders[i];
        T prod = 1;  // m_0 * m_1 * ... * m_{i-1} mod moduli[i]
        for (size_t j = 0; j < i; ++j) {
            t = ((t - v[j]) * mod_inverse(moduli[j], moduli[i])) % moduli[i];
            if (t < 0) t += moduli[i];
        }
        v[i] = t;
    }
    // 最後に a = v[0] + v[1]·m_0 + v[2]·m_0·m_1 + ... を多倍長で合成
    T a = v.back();
    for (size_t i = moduli.size() - 1; i-- > 0;) a = a * moduli[i] + v[i];
    return a;
}

なお、$M$ を先に計算する単純な Lagrange 型 CRT ($a = \sum r_i \cdot M_i \cdot (M_i^{-1} \bmod m_i) \bmod M$, ここで $M_i = M / m_i$) も 実装上は有効で、小さな $k$ では差が小さい。両方の長所短所を理解して用途に応じて選ぶ。

RSA-CRT の応用

RSA 復号 $c^d \bmod n$ ($n = pq$) を直接計算すると重い。 代わりに $m_p = c^{d \bmod (p-1)} \bmod p$ と $m_q$ を別々に計算し、 CRT で合成する。法のサイズが半分になるので 1 回の Montgomery 乗算が軽くなり、 冪剰余を 2 本走らせても全体では数倍速い (古典的な目安は約 4 倍だが、 実際の比率は乗算アルゴリズムと実装・CPU に依存する)。 RSA 実装の標準技法である。

RSA-CRT の数値例 (教科書的小サイズ)

$n = pq = 61 \times 53 = 3233$、公開指数 $e = 17$、秘密指数 $d = 2753$、暗号文 $c = 855$ を復号する ($d$ は $e \cdot d \equiv 1 \pmod{(p-1)(q-1)}$ より計算済)。

事前計算 (秘密鍵側に保存):

  • $d_p = d \bmod (p - 1) = 2753 \bmod 60 = 53$
  • $d_q = d \bmod (q - 1) = 2753 \bmod 52 = 49$
  • $q^{-1} \bmod p = 53^{-1} \bmod 61 = 38$

復号:

  • $m_p = c^{d_p} \bmod p = 855^{53} \bmod 61$。$855 \bmod 61 = 1$ なので $1^{53} \bmod 61 = 1$
  • $m_q = c^{d_q} \bmod q = 855^{49} \bmod 53$。$855 \bmod 53 = 7$ なので $7^{49} \bmod 53 = 17$
  • CRT 合成 (Garner 形式): $h = (m_p - m_q) \cdot q^{-1} \bmod p = (1 - 17) \cdot 38 \bmod 61 = -608 \bmod 61 = 2$
  • $m = m_q + h \cdot q = 17 + 2 \cdot 53 = 17 + 106 = 123$

検算: $123^{17} \bmod 3233 = 855$ ✓。直接 $855^{2753} \bmod 3233$ を計算しても同じ $123$ が得られる。法のビット長が半分になると、学校式乗算なら 1 回のモジュラー乗算のコストはおよそ 1/4 になる。ただし冪剰余を 2 本走らせる分と、実際の乗算が Karatsuba・Toom・FFT といった劣二次のアルゴリズムであることを考えると、全体の比率は「約 4 倍」という古典的な目安のとおりになるとは限らず、乗算アルゴリズム・実装・CPU に依存する。

7.7 高速冪剰余 — スライディングウィンドウ

$m^e \bmod n$ を計算する最も簡単な方法は、 $e$ を 2 進展開して $e$-ビットを左から右 (あるいは右から左) に処理することである (バイナリ法)。これだけで $O(\log e)$ 回の乗算で済む。

$k$ ビットウィンドウ法

$e$ の $k$ 連続ビットをまとめて処理すると、加算連鎖がさらに短くなる。 前処理で $m^{1}, m^{3}, m^{5}, \ldots, m^{2^k - 1}$ (奇数冪) を計算し、 実行時はウィンドウ $w$ について:

$$\text{result} \leftarrow \text{result}^{2^k} \cdot m^w.$$

sangi のウィンドウ幅選択は次の通り:

// IntModular.cpp: choose_window_width (sangi 2026-05 時点)
inline int choose_window_width(size_t expBits) {
    if (expBits <= 24)   return 1;
    if (expBits <= 64)   return 3;
    if (expBits <= 256)  return 4;
    if (expBits <= 1024) return 5;
    if (expBits <= 4096) return 6;
    return 7;
}

スライディングウィンドウ

通常の固定ウィンドウでは、ウィンドウが $0$ で始まる場合も乗算を行う必要がある。 スライディングウィンドウでは、ウィンドウを常に $1$ で始まるようにずらし、 連続する $0$ はスキップして平方のみ行う。平均的な乗算削減は $20\text{-}30\%$ 程度が目安で、 実際の削減率は指数のビット列とウィンドウ幅に依存する。

// IntModular.cpp: powerMod (スライディングウィンドウ, 概略)
int oddTableSize = 1 << (w - 1);   // 奇数冪のみプレコンピュート
uint64_t g[oddTableSize];
g[0] = baseR;                       // m^1 (Montgomery 形式)
if (oddTableSize > 1) {
    uint64_t base2R = mont_mul(baseR, baseR);    // m^2
    for (int i = 1; i < oddTableSize; ++i)
        g[i] = mont_mul(g[i-1], base2R);         // m^3, m^5, m^7, ...
}
// 本体: 指数のビット列を左から走査、ウィンドウ検出で乗算

Montgomery 空間で全計算を行うので、各 mont_mul は除算なし。 最後に結果を通常形式に戻す。

7.8 定時間化を意識した実装 — powerModSec

暗号用途では、計算時間が秘密鍵の値に依存すると タイミング攻撃で秘密鍵を推定されうる。 1996 年の Kocher の攻撃以来、このリスクは定期的に実用攻撃として報告されている (CVE-2018-0737 など)。

何がタイミングに影響するか

  • 分岐: if 文の分岐予測ミスで数サイクル変わる。
  • キャッシュアクセスパターン: ルックアップテーブルの index が秘密なら L1 ヒット/ミスで 10 倍変わる。
  • データ依存の命令: 除算や乗算が一部 CPU ではオペランドでサイクル数が変わる。

定時間冪剰余 (sangi の powerModSec)

sangi の powerModSec は次の 3 点を通常の powerMod と変えている:

  1. 固定ウィンドウ (スライディングなし): 常に同じ回数の乗算を行う。 ウィンドウが 0 でも $m^0 = 1$ との乗算を実行。
  2. マスク選択のテーブル参照: すべてのテーブルエントリを読み、 マスク演算で所望のものを選ぶ。キャッシュアクセスパターンを秘密依存にしない。
  3. 条件分岐の除去: if の代わりに算術マスクを使う。

どこまでが保証されているか

上の 3 点が取り除くのは、powerModSec 自身が持っていた 「秘密に依存する分岐」と「秘密に依存するテーブル添字」である。 一方、powerModSec が呼び出す Montgomery 乗算の側には、 最後の条件付き減算 (if (cy != 0 || cmp(r, m) >= 0) r -= m;) や、 キャリーが尽きるまで回る伝搬ループのように、データに依存する処理がまだ残っている。

したがって現状で言えるのは「冪剰余の骨格を定時間化した」ところまでで、 powerModSec 全体の定時間性は保証していない。 保証するには、呼び出す Montgomery 乗算・比較・減算を含む全経路について、 生成された機械語のレベルで検証する必要がある。 本章末で述べるとおり sangi は暗号ライブラリではなく、そこまでの検証は行っていない。

// IntModular.cpp: powerModSec の constant-time table select
int tableSize = 1 << w;  // 奇数のみでなく 0..2^w-1 すべて
std::vector<uint64_t> g_buf(tableSize * n);
// 注: スライディングなし、全エントリ常時準備

auto ct_select = [&](uint64_t* dst, int idx) {
    std::memset(dst, 0, n * sizeof(uint64_t));
    for (int i = 0; i < tableSize; ++i) {
        // i==idx なら 1, それ以外なら 0 → int64_t キャスト後の単項マイナスで
        // 0xFFFF...FFFF (all-ones) または 0x0000...0000 に (2 の補数表現の利用)
        uint64_t mask = -(static_cast<int64_t>(i == idx));
        for (size_t j = 0; j < n; ++j)
            dst[j] |= g_buf[i * n + j] & mask;              // マスク OR で分岐なし選択
    }
};

マスク生成のトリック

-(static_cast<int64_t>(i == idx)) は、整数昇格と 2 の補数表現を組み合わせて以下のように動作する:

  • i == idx が真 → bool trueint64_t(1) → 単項マイナスで $-1$ → 2 の補数表現で全ビット 1 (0xFFFFFFFFFFFFFFFF)
  • i == idx が偽 → bool falseint64_t(0) → 単項マイナスで $0$ → 全ビット 0 (0x0000000000000000)

このマスクを各エントリと AND して OR 累積すれば選択対象だけが残り、条件分岐なしに値を取得できる。このイディオムは、秘密に依存する分岐と、秘密に依存するテーブル添字の両方を C++ ソースから取り除く。ただしソースに分岐が見えないことだけでは定時間性は保証されない。コンパイラが比較やマスクを分岐に戻していないか、呼び出している多倍長演算自体にデータ依存の処理が残っていないか、生成された機械語まで確認する必要がある。

性能トレードオフ

powerModSecpowerMod より 20〜40% 遅い。 理由はスライディングウィンドウの省略と、全テーブルエントリの読み出し。 RSA/ECDSA 署名ではこの遅さを受け入れるのが標準的である — 秘密鍵漏洩のリスクは 署名時間の増大よりはるかに重大。

さらなる対策

完全な定時間実装には、以下も考慮する必要がある:

  • ブラインディング: RSA 復号 $c^d \bmod n$ を例にとると、乱数 $r$ で $c^{\prime} = c\,r^e \bmod n$ と目隠ししてから $m^{\prime} = (c^{\prime})^d \bmod n$ を計算し、$m = m^{\prime} r^{-1} \bmod n$ として戻す。秘密指数 $d$ が毎回異なる入力に作用するので、実行時間から $d$ を推定しにくくなる(本章では $m$ は一般に法を表すが、この項に限り $m$ は平文、法は $n$ である)。
  • メモリアクセスパターンの固定: テーブルを規則的に配置し、秘密値に依存せず同じキャッシュライン群を同じ順序で走査する。512 bit なら 1 エントリがちょうど 64 バイト = 1 キャッシュラインに収まるが、それより大きい法では 1 ラインには収まらない。本質は「1 ラインに収める」ことではなく「アクセス列を秘密に依存させない」ことである。
  • 電力解析 (DPA) 対策: 物理攻撃まで視野に入れるなら、マスク化計算 (secret sharing) などの対策が必要になる場合がある。

sangi は暗号ライブラリではないため、ブラインディングや DPA 対策は実装していない。 用途に応じて OpenSSL や libsodium 等の確立された暗号ライブラリを使うのが原則である。

7.9 まとめ

  • Barrett: 事前計算 $\mu = \lfloor 2^k / m \rfloor$ で除算を 2 回の乗算に置換。単発の剰余計算に最適。
  • Montgomery: Montgomery 形式 $\bar{a} = a R \bmod m$ で乗算連鎖を除算なしに。REDC が核。暗号・NTT のホットループで支配的。
  • CIOS: 乗算と還元を交互に進める実装パターン。sangi の汎用経路は積を先に作ってから還元する形 (REDC 単独) で、n=4, 8, 16, 32 では特化アセンブリにディスパッチする。
  • mpn_mont_mul_8: 512 bit 特化。8 累算器を r8-r15 レジスタ常駐。MULX/ADCX/ADOX の 2 キャリーチェーンで並列化。
  • CRT: 互いに素な法での並列計算を統合。Garner で $O(n^2)$ 合成。RSA-CRT は標準技法。
  • スライディングウィンドウ冪剰余: 奇数冪のプレコンピュート + ウィンドウ検出で乗算を 20-30% 程度削減。
  • 定時間化: powerModSec は固定ウィンドウ + マスク選択で骨格を定時間化する。ただし下位の Montgomery 乗算にはデータ依存の処理が残っており、全体の定時間性は機械語レベルの検証を経て初めて主張できる。

次章 (第8章 数論関数の高速計算) では、Euler の $\phi$、 Möbius の $\mu$、約数和 $\sigma_k$ といった乗法的関数と、素数計数関数 $\pi(x)$ の高速計算を扱う。

参考文献

  • Barrett, P. "Implementing the Rivest Shamir and Adleman Public Key Encryption Algorithm on a Standard Digital Signal Processor", CRYPTO '86, LNCS 263, pp. 311-323, 1987.
  • Montgomery, P.L. "Modular Multiplication without Trial Division", Mathematics of Computation, 44(170), pp. 519-521, 1985.
  • Menezes, A.J., van Oorschot, P.C., Vanstone, S.A. Handbook of Applied Cryptography, CRC Press, 1996. 第14章。
  • Brent, R.P., Zimmermann, P. Modern Computer Arithmetic, Cambridge University Press, 2010. 第2章。
  • Koç, Ç.K., Acar, T., Kaliski, B.S. "Analyzing and Comparing Montgomery Multiplication Algorithms", IEEE Micro, 16(3), pp. 26-33, 1996. CIOS / SOS / FIOS の比較。
  • Kocher, P.C. "Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and Other Systems", CRYPTO '96, LNCS 1109, pp. 104-113.
  • Bernstein, D.J. "Curve25519: New Diffie-Hellman Speed Records", PKC '06, LNCS 3958, pp. 207-228. 定時間実装の実例。

関連章

よくある質問

モジュラ演算とは何か?

整数を法 $m$ で割った余りの演算体系で、$a \equiv b \pmod{m}$ は $m \mid (a-b)$ を意味する。暗号・ハッシュ・FFT など多くの計算の基礎となる。

中国剰余定理(CRT)はどのような場面で使われるか?

互いに素な複数の法 $m_1,\ldots,m_k$ でのモジュラ演算の結果から、$M=\prod m_i$ を法とする唯一の解を復元する。並列計算・多変数多項式演算・RSA 高速化などに応用される。

Montgomery乗算の原理は?

法 $m$(奇数)と互いに素な $R = 2^k > m$ を選び、数を Montgomery 表現 $aR \bmod m$ に変換する。$R$ が 2 の冪でワード境界に一致するため、$m$ による除算をシフトと乗算・加減算に置き換えられる。繰り返しモジュラ乗算が多い冪剰余計算で特に高速化が顕著である。