---
id: df2ea4cfe0d5ec72a92e1b3cfd4156f2
author: snowmerak
title: Go言語におけるMLDSAおよびMLKEMの利用記
description: Go言語における耐量子暗号MLDSAおよびMLKEMの利用方法を解説し、各アルゴリズムの特徴とコード例を提供します。
language: ja
date: 2024-12-22T08:17:58.797764292Z
path: /blog/posts/usage-of-mldsa-and-mlkem-in-the-go-language-zb0442110
category: security
---

## 概要

### 背景

かなり以前から、量子コンピュータの高速な演算は既存の暗号方式にとって脅威として認識されてきました。既存のRSAやECCなどは、量子コンピュータのこのような演算能力によって解読される可能性があるためです。しかし数年前から量子コンピュータという概念が現実味を帯び始めると、それに対する代替案の研究および開発が始まり、NISTではPQC(耐量子暗号)の標準化を進めてきました。

### MLDSAとMLKEM

ついにNISTは2024年8月に、CRYSTALS-KyberおよびCRYSTALS-Dilithiumを基盤とするMLKEMとMLDSAを標準として採択しました。両アルゴリズムは、MLWE(Module Learning with Errors)という問題に基づいて動作します。このような形式を、私たちは格子基盤暗号と呼んでいます。

格子基盤暗号は、その名のとおり格子上の数学的問題の困難性に基づく暗号システムです。私自身もこれについて深い数学的知識はありませんが、一行で要約すると`モジュール格子においてノイズのある線形方程式を解く問題`だといいます。どれほど困難なのかは実感が湧きませんが、このような問題は量子コンピュータでも解けないほど難しいとされています。

## MLDSA

それでは、まずMLDSAについて見ていきましょう。

### 構成

MLDSAは名前からも分かるように公開鍵(非対称)署名アルゴリズムであり、全体として以下の2段階を経ます。

1. 署名生成: 秘密鍵を使用してメッセージに対する署名を生成
2. 署名検証: 公開鍵を用いて、生成された署名の有効性を確認

また、MLDSAには次の4つの特性があります。

1. strong existential unforgeability: 一つの署名と公開鍵から、別の有効な署名を生成することはできません。
2. chosen message attack: 任意のメッセージに対する署名を用いても、公開鍵を使って新たな有効な署名を生成することはできません。
3. side-channel attack: 署名時に毎回新しいランダム値と、メッセージから導出された擬似ランダム値を用いるため、安全性が高くなります。
4. domain separation: 異なるパラメータに対して異なるseedを使用するようにし、反復による安全性の問題を予防します。

### コード

それでは、簡単なGo言語のサンプルコードをご紹介します。  
この例では[cloudflare/circl](github.com/cloudflare/circl)のmldsaを使用しました。

```go
package main

import (
	"crypto"
	"crypto/rand"
	"encoding/base64"
	"fmt"

	"github.com/cloudflare/circl/sign/mldsa/mldsa44"
)

func main() {
    // mldsa44スペックで鍵を生成します。
	pub, priv, err := mldsa44.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}

	message := []byte("Hello, World!")

    // 署名を生成します。
    // 一つ注意すべき点は、24年12月22日現在のバージョンでは、crypto.Hash(0)でない場合にエラーが発生するということです。
	signature, err := priv.Sign(rand.Reader, message, crypto.Hash(0))
	if err != nil {
		panic(err)
	}

	encodedSignature := base64.URLEncoding.EncodeToString(signature)
	fmt.Println(len(encodedSignature), encodedSignature)

    // 公開鍵のschemeを呼び出して検証を行います。
	ok := pub.Scheme().Verify(pub, message, signature, nil)
	fmt.Println(ok)
}
```

```
3228 oaSaOA-...
true
```

> 署名の値は長すぎるため省略しました。全文を確認したい場合は、[playground](https://go.dev/play/p/nqbUnKpFW3j)で実行してみてください。

base64でエンコードしたとはいえ、3228バイトになるのは少し負担に感じられるでしょう。  
近いうちに、私たちは量子コンピュータに対抗する署名としてこのサイズをやり取りしなければならないかもしれないと考えると、少し負担に感じられますね……

## MLKEM

### 構成

MLKEMは鍵カプセル化メカニズム(Key Encapsulation Mechanism)です。KEMは公開鍵暗号方式を用いて、二者間の共有鍵を生成できるようにするアルゴリズムです。MLKEMの鍵交換メカニズムは次の過程を経ます。

1. 鍵カプセル化: 送信者は受信者の公開鍵を用いて、暗号化されたメッセージ(cipher text)と共有鍵(shared key)を生成します。この暗号化されたメッセージを受信者に送り、利用してもらいます。
2. 鍵カプセル解除: 受信者は自身の秘密鍵を用いて、暗号化されたメッセージから共有鍵を抽出します。

MLKEMには全部で3つのパラメータが存在します。MLKEM-512、MLKEM-768、MLKEM-1024があり、数字が小さいほど小さな鍵と暗号文が生成され、大きいほど長い鍵と暗号文が生成されて、セキュリティレベルがより高くなります。

### コード

MLKEMはGo 1.24に追加される予定のため、現時点で利用可能なGo 1.24rc1を使用しました。

```go
package main

import (
	"crypto/mlkem"
	"encoding/base64"
	"fmt"
)

func main() {
    // 受信者のPrivateKeyを生成します。
	receiverKey, err := mlkem.GenerateKey1024()
	if err != nil {
		panic(err)
	}

    // MLKEMではPublicKeyではなくEncapsulationKeyという用語を使用します。
	receiverPubKey := receiverKey.EncapsulationKey()

    // 簡単のため、EncapsulationKeyのBytes()とNewEncapsulationKeyXでキーを抽出して再利用できることを示すために複製しました。
    // 実際の運用では、この過程は、テキストとして公開されていた受信者のEncapsulationKeyを送信者がオブジェクトとして生成する過程だと考えてください。
	clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
	if err != nil {
		panic(err)
	}

    // Encapsulateにより、送信者が暗号文と共有鍵を生成します。
	cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()

    // 受信者の秘密鍵を保存して取り出す過程を示すため、あえて複製しました。
	clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
	if err != nil {
		panic(err)
	}

    // 受信者は秘密鍵を用いて暗号文をDecapsulateし、もう一つの共有鍵を生成します。
	sharedKeyReceiver, err := clonedReceiverKey.Decapsulate(cipherText)
	if err != nil {
		panic(err)
	}

	fmt.Println(base64.StdEncoding.EncodeToString(SenderSharedKey))
	fmt.Println(base64.StdEncoding.EncodeToString(sharedKeyReceiver))
}
```

```sh
Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=
Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=
```

その結果、同じサイズの共有鍵が生成されることを確認できます!

このコードは[プレイグラウンド](https://go.dev/play/p/n_cxNp435Qn?v=gotip)でも確認できます。

## 結論

各アルゴリズムのスペック、セキュリティレベル、秘密鍵、公開鍵、署名や暗号文のサイズは次のように整理できます。いずれもPQCという名前に恥じない大きなサイズを誇ります。

| アルゴリズム | NISTセキュリティレベル | 秘密鍵サイズ | 公開鍵サイズ | 署名/暗号文サイズ |
| ------------ | ---------------------- | ------------ | ------------ | ----------------- |
| ML-DSA-44    | 2                      | 2,560        | 1,312        | 2,420             |
| ML-DSA-65    | 3                      | 4,032        | 1,952        | 3,309             |
| ML-DSA-87    | 5                      | 4,896        | 2,592        | 4,627             |
| ML-KEM-512   | 1                      | 1,632        | 800          | 768               |
| ML-KEM-768   | 3                      | 2,400        | 1,184        | 1,088             |
| ML-KEM-1024  | 5                      | 3,168        | 1,568        | 1,568             |

これらのアルゴリズムにより、量子コンピュータ上においても十分に安全なインターネットを利用できることを期待していますが、相対的に大きくなった鍵や署名/暗号文のサイズにより、より多くの演算が必要になることは避けられないように思われます。

それでも、Go言語では各アルゴリズムが効果的に実装されているため、適切な場面でみなさんのセキュリティを守るために積極的に活用されることを期待しています!
