なぜGoは「デフォルトが一番安全」を目指すのか? - Secure by DesignとGo の動向

はじめに

どうも、ANDPADテックリードの tomtwinkle です。

今回は

Go 1.26 リリースパーティで話す予定…でしたが、どうも尺に収まらないので泣く泣くカットした分

を事前に記事として公開しておく内容です。

と言いつつ、この記事も書いてたら分量多くなりすぎて Go 1.26 ネタが入りきらなかったので Go 1.26 ネタを聞きたい人はリリースパーティーで会いましょう。

発表後、Go 1.26ネタを含んだ第二弾を公開予定です。たぶん。

https://gocon.connpass.com/event/381405/gocon.connpass.com

みなさんは 「Secure by Design」 という言葉をご存知でしょうか?

元ネタ自体はAnn Cavoukian博士が1990年代の半ばにIPCの官庁出版物に書いた「Privacy by Design」をモジッたものだと思います。

2000年代初頭にマイクロソフトがビル・ゲイツの号令(Trustworthy Computingメモ)のもと、SDL(Security Development Lifecycle) を導入した時期から、ソフトウェア工学の文脈で頻繁に使われるようになり

2023年に米国のCISA(Cybersecurity and Infrastructure Security Agency) が「Secure by Design - Shifting the Balance of Cybersecurity Risk: Principles and Approaches for Secure by Design Software」というガイドラインを出したことで今、再度注目を浴びているキーワードです。

www.cisa.gov

英語が読める人はPDFを見てもらうのが一番手っ取り早いと思いますが、日本語で読みたい人向けに全文翻訳記事を以前書きました。結構同じこと書いてあって冗長だし長いのでAIに要約してもらったほうが良いかもしれません。

zenn.dev

この記事用の「Secure by Design」のざっくり概略

よし、みんな記事を読んだよね! ……だと流石に記事として不親切なのでざっくりと今回の記事に関連する部分だけ概要を書いておきます。

Secure by Design とは 「製品を作ってからセキュリティ対策をする(bolted on)のではなく、設計(デザイン)の段階からセキュリティを組み込んでおく(baked in)」 ということです。

CISAの「Secure by Designガイドライン」を読んでいくと 「Secure by Default」 というキーワードが出てきます。 要するに、何らかのシステムやプログラミング言語の関数などを利用する際に Defaultのまま利用するのが一番安全にしておくように設計しておきなさい という事ですね。

「Secure by Designガイドライン」の本質は「セキュリティの責任をユーザー(利用者)からベンダー(製造者)へ移す」点にあります。

Secure by Defaultではない設計例

  • C言語のgets関数 (C11以降は廃止されました)
#include <stdio.h>

int main() {
  char str[10];
  printf("文字列を入力してください: ");
  gets(str); // 10文字以上入力されるとバッファオーバーフローが発生する
  printf("入力された文字列: %s\n", str);
  return 0;
}
  • PHP の echo構文や 短縮構文<?= ?>
<div>
    <!-- 変数の文字列をエスケープせずにHTMLにそのまま書き出してしまうため、XSSの危険がある -->
    こんにちは、<?= $name ?> さん
</div>

特に昔から使われているプログラミング言語では歴史的経緯から利用者の選択と自由を最大限尊重する設計であるため、エスケープ機能などは実装者が意識的に行う必要があります。 ここに書いた特定言語を貶める意図はありません、念の為。 特に暗号化ライブラリを取り巻く状況などは過去と現在では大きく状況が異なっています。

Go言語でのSecure by Defaultな設計の例

// 標準パッケージが提供している関数の引数通りに利用すれば自動的にPreparedStatementが利用される
db.Query("SELECT * FROM users WHERE name = ?", userName)

// わざわざ実装者が明示的に文字列結合しないとSQLインジェクションが起きない
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userName)
db.Query(query)
// 悪意ある入力
input := "<script>alert('XSS')</script>"

// テンプレート定義
// {{.}} の部分に入力が入る
tpl := template.Must(template.New("page").Parse(`
    <div>{{.}}</div>
    <a href="/search?q={{.}}">Link</a>
`))

// 実行
tpl.Execute(os.Stdout, input)
// <div> の中ではHTMLエスケープされる
// <div>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</div>
// href のURLクエリの中では、URLエンコードされる
// <a href="/search?q=%3cscript%3ealert%28%27XSS%27%29%3c%2fscript%3e">Link</a>

「JCDC」「Secure by Design宣誓」と Go

CISAはこの「Secure by Design」ガイドラインを出した際に ガイドラインを守りますという「宣誓」を行った企業 を発表しています。

www.cisa.gov

そして、その企業の中に Goチームを抱えるGoogleのサインが存在している ことが分かります。

www.cisa.gov

なんとなく、2023年前後のGoのリリース(Go 1.20 - ) や Go Blogを見ていると徐々にセキュリティ関連のリリースが何となく増えたなという印象がありませんか? 気のせいかな?タイムラインでGoと周辺で起きた事件を調べてタイムラインにしてみましょう。

Go(と関連する周辺)のセキュリティ対応タイムライン

というわけでGo Blogでのセキュリティ対応に関する記事と米国でのサイバーセキュリティに関するニュースをピックアップしてタイムラインを作成しています。 元ネタは基本的にはGo BlogとCISAのリリースと「piyolog」と「セキュリティホール memo」。いつもお世話になっております。

go.dev

piyolog.hatenadiary.jp

www.st.ryukoku.ac.jp

Goに関係するものを 太字 にしています。

こうして見てみると CISA「Secure by Design」ガイドラインの発表前から、JCDC結成によりGoogleのセキュリティチームはCISAと連携して方針を策定してた立場 だったみたいですね。

ただ、GoチームとしてはJCDCが結成される前から「Secure by Default」という思想で動いているらしいことは「Cryptography Principles」に関するディスカッション会話から推測出来ます。

go.dev

Cryptography Principles には以下の一文が記されています。

golang.org

The default behavior should be safe in as many scenarios as possible, and unsafe functionality, if at all available, should require explicit acknowledgement in the API. デフォルトの動作は可能な限り多くのシナリオで安全であるべきであり、安全でない機能は、利用可能である場合でも、API内で明示的な承認を必要とするべきである。

「Secure by Default」 と Go

タイムラインではサラッと流してしまいましたが上記のGoのリリースの中には 「Secure by Designガイドライン」のテーマである 「Secure by Default」 に関するリリースが幾つか入っています。

それぞれ、具体的に詳細を追っていきましょう。

rand.Seed の廃止

Go 1.20 では math/rand の Seed function が deprecated になり

- src/math/rand/rand.go -
L318: // Deprecated: Programs that call Seed and then expect a specific sequence
L319: // of results from the global random source (using functions such as Int)
L320: // can be broken when a dependency changes how much it consumes
L321: // from the global random source. To avoid such breakages, programs
L322: // that need a specific result sequence should use NewRand(NewSource(seed))
L323: // to obtain a random generator that other packages cannot access.
L324: func Seed(seed int64) { globalRand.Seed(seed) }

https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/math/rand/rand.go;l=318-324;bpv=0

Go 1.24 で廃止(利用したい場合はGODEBUG=randseednop=0の指定が必須化)されました。

- src/math/rand/rand.go -
L398: // GODEBUG=randseednop=0.
L399: func Seed(seed int64) {
L400:     if randseednop.Value() != "0" {
L401:         return
L402:     }

https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/math/rand/rand.go;l=398-402;bpv=0

では何故Seed functionが廃止されたのか。

Go 1.20 未満では math/rand のSeedのDefault値は "1" で固定されており、安全な疑似乱数を生成するためには rand.Seed() を利用者が明示的に指定する必要がありました。 この実装は 「Secure by Default」 ではありませんね。

- src/math/rand/rand.go -
L293: var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})

https://cs.opensource.google/go/go/+/refs/tags/go1.19.9:src/math/rand/rand.go;l=293

Go 1.20 以降は runtime内の fastrand64 によりDefaultのSeedを生成します

- src/math/rand/rand.go -
L413: // source returns r.s, allocating and seeding it if needed.
L414: // The caller must have locked r.
L415: func (r *lockedSource) source() *rngSource {
L416:     if r.s == nil {
L417:         var seed int64
L418:         if randautoseed.Value() == "0" {
L419:             seed = 1
L420:         } else {
L421:             seed = int64(fastrand64())
L422:         }
L423:         r.s = newSource(seed)
L424:     }
L425:     return r.s
L426: }

https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/math/rand/rand.go;l=413-426

fastrand64 は プログラムの起動時に runtime.randinit() によりOSから提供される /dev/urandomgetrandom などから初期Seedを生成します開発者が何もしなくても、ランダムなシードが自動的に注入されることにより、「シード設定の失念」というヒューマンエラーによる脆弱性が構造的に発生しなくなりました。 これはまさに 「Secure by Default」 を体現した実装になっています。

crypto/ecdh の実装crypto/elliptic の実質廃止

従来の crypto/elliptic パッケージは、楕円曲線上の座標(x, y)を直接扱う低レイヤーなインターフェースです。

汎用的なCurve インターフェースであったため、特定の実装がタイミング攻撃に対して脆弱かどうかを呼び出し側が判断しにくく、 暗号学の専門知識がないエンジニアが安易に利用したり、うっかりミスで実装者がCurve.IsOnCurveによるチェックを忘れると、Small-Subgroup Attack や Invalid Curve Attack などにより秘密鍵が漏洩するリスクがあるなど脆弱性を埋め込みやすいインターフェースになっていました。 これは「Secure by Default」 ではありませんね。

そこで、専門知識がなくても安全に利用出来るようにより高レイヤーで抽象化したインターフェースとして crypto/ecdh が実装されました。

crypto/ecdh の内部実装は暗号専門家であるGoセキュリティチームにより最適化され、サイドチャネル攻撃への耐性がデフォルトで備わっています。 開発者は楕円曲線暗号の座標(x, y)を意識せず、PublicKey や PrivateKey というオブジェクトとして扱う事ができます。

NewPublicKey などの関数を呼ぶ際、ライブラリ内部で自動的に曲線上の点であるか、無限遠点でないか等のチェックが行われます。

さらに曲線ごとに専用の型が用意され、異なる曲線同士の計算をコンパイルレベルで防ぐことが出来ます。 設計により開発者が間違える自由を奪うことで、安全性を担保する事で、「Secure by Design」を体現しています。

math/rand/v2 の登場と乱数生成器の規定値としてChaCha8/PCGの採用

Go 1.22 以前の math/rand で利用していた Lagged Fibonacci Generatorは、現代の基準では統計的品質が低い問題がありました。

math/randは元々暗号目的で利用しない簡易的な暗号ライブラリとしての役割で、安全な乱数を生成する場合は開発者が意識して crypto/rand を利用する必要があります。

もし誤ってゲームのドロップ率や簡易的なトークン生成などに math/rand を利用してしまった際、安全ではない乱数生成器をうっかり利用してしまう可能性がありました。 これは「Secure by Default」ではありません。

Go 1.22 で登場した math/rand/v2 ですが、擬似乱数生成器の規定値として ChaCha8 が採用されています。

更に math/rand/v2 だけでなく math/rand の擬似乱数生成器の規定値も ChaCha8 に差し変わったというのがポイントです。

Go 1.21 では math/rand は runtime の fastrand64 を利用するように修正されています。

– src/math/rand/rand.go –
L318: func globalRand() *Rand {
L330:       r = &Rand{
L331:           src: &fastSource{},
L332:           s64: &fastSource{},
L333:       }

L349: //go:linkname fastrand64
L350: func fastrand64() uint64
L351: 
L352: // fastSource is an implementation of Source64 that uses the runtime
L353: // fastrand functions.
L354: type fastSource struct {
L355:     // The mutex is used to avoid race conditions in Read.
L356:     mu sync.Mutex
L357: }

L367: func (*fastSource) Uint64() uint64 {
L368:     return fastrand64()
L369:}

L431: func Uint64() uint64 { return globalRand().Uint64() }

https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/math/rand/rand.go;l=350;bpv=0

そして、Go 1.22 で fastrand64 のデフォルトアルゴリズムは ChaCha8 に変更されました。

- src/runtime/rand.go -
L126: //go:linkname rand
L127: func rand() uint64 {
L128:     // Note: We avoid acquirem here so that in the fast path
L129:     // there is just a getg, an inlined c.Next, and a return.
L130:     // The performance difference on a 16-core AMD is
L131:     // 3.7ns/call this way versus 4.3ns/call with acquirem (+16%).
L132:     mp := getg().m
L133:     c := &mp.chacha8
        //省略
L147: }

L244: //go:linkname legacy_fastrand64 runtime.fastrand64
L245: func legacy_fastrand64() uint64 {
L246:     return rand()
L247: }

https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/runtime/rand.go;l=127;bpv=0

ちなみに何故Goではnumpy.randomのPCG64やRustで採用されているChaCha12ではなくChaCha8を採用したかという話は公式ブログに記載があります。

go.dev

ChaChaNはストリーム暗号であり、RSAのような計算量的困難性により安全性を担保するアルゴリズムとは根本的に考え方が違い統計的ランダム性と不可知性によって安全性を担保しています。これは非常に強力な暗号方式です。

https://cr.yp.to/chacha/chacha-20080128.pdf

ChaChaNのNは暗号化アルゴリズムを計算する回数であるラウンド数です。ラウンド数が多ければ多いほど計算量が増えるため遅くなります。

なんでChaCha8になったのかは、Jean-Philippe Aumasson氏の論文に記載されている「ChaCha20はToo Muchな暗号でChaCha8くらいで十分安全でありChaCha20に比べて2.5倍早い」という主張によるものです。

https://eprint.iacr.org/2019/1492.pdf

より安全な乱数生成を利用したい場合は今まで通り crypto/rand を利用するべきですが math/randmath/rand/v2 という "標準の乱数生成関数の乱数生成器" が ChaCha8 にさし変わったことにより、もし開発者が誤って math/rand を使ってしまっても、従来より遥かに予測が困難になるように実装が修正されました。 これは「Secure by Default」な設計になったと言えるでしょう。

ポスト量子暗号 crypto/mlkem(X25519MLKEM768) の実装

Go 1.23 で ポスト量子暗号 X25519Kyber768Draft00

Go 1.24 で ポスト量子暗号 crypto/mlkem (X25519MLKEM768) が実装されました。

X25519Kyber768Draft00NIST PQC コンペティションの Kyber-768 Round 3 をベースにした ドラフト段階のものであり

X25519MLKEM768ML-KEM-768 (FIPS 203) により正式に標準化された仕様です。

兄弟のようなものですが内部ロジックが異なるため互換性はありません。

量子暗号とは今後実用的な量子コンピュータが登場しても暗号解読され得ない、次世代の暗号技術の事を指します。

現在主流のRSA暗号は、巨大な数の因数分解などの計算を利用していますが、量子コンピュータはこの計算を従来の手法よりも高速で解いてしまいます。 量子コンピュータによる暗号解析が可能になる日「Q-Day」と呼ばれるその日のために、世界中で新しい暗号への移行が進められています。

ちなみに、楕円曲線上の離散対数問題を利用した楕円曲線暗号(ECC)は十分に長い桁を確保すればRSAよりは量子コンピュータによる演算による解読に強い暗号方式だと言われていますが短い桁数では安全ではないというのは同じです。

もちろんQ-Dayはすぐやってくる訳ではありません、一般的には後10年くらいはかかるだろうと言われています。しかし、では何故今ポスト量子暗号の実装が推し進められているのでしょうか?

それは、暗号化済みのデータを今盗んでおけばQ-Dayのタイミングで解読を行う事ができるからです。

これは 「Harvest Now, Decrypt Later(今盗んで、後で解読する)」 と呼ばれています。

2013年にエドワードスノーデン氏が告発したNSAによるUpstream収集(PRISM)などにより、HNDLがもう既に現実で行われていることは知られています。

Go 1.25までの Goでのポスト量子暗号の実装は「Secure by Design」とは直接的には関係はありません。 crypto/mlkem は他のcrypto packageと同様ユーザーが選択可能な暗号方式の1つです。

しかし、Go 1.26 では指定通りに利用すれば安全にポスト量子暗号が利用できるパッケージとして crypto/hpke が実装されます。

この記事でGo 1.26の内容も含めてしまうと長くなってしまうので別の記事で解説します。

crypto/rsa 1024bit key の廃止

Go 1.24 では crypto/rsa で 1024bit未満の鍵を利用しようとするとエラーを返すようになりました。

1024bitのRSA鍵はAESで80bit相当の強度しかない事が知られています。 現在、2030年まで安全とされる暗号強度の最低ラインは「112bit相当(RSA 2048bit)」です。

NIST SP 800-131Aガイドラインなどにより米国の主要機関では112bit未満の鍵の利用は禁止されています。

既に安全ではない暗号鍵を利用不可にすることで誤って利用される事を防ぐ設計は「Secure by Design」な設計と呼べますね。

crypto.SignMessage の実装

従来の crypto.Signer インターフェースでは、署名関数に渡すのは「ハッシュ化済みのデータ(Digest)」です。つまり、 crypto.Signer#Sign を利用する場合、ハッシュアルゴリズムの選択は呼び出し側に委ねられています。

例えば、 ECDSA署名に対して脆弱なハッシュ関数を組み合わせてしまうといった、組み合わせのミスが発生し得ます。 これは「Secure by Design」な設計とは呼べません。

さらにハッシュ化済みのデータしか渡されないということは開発者が「何をハッシュ化したか」を正確に把握せずに署名を行ってしまうBlind Signing(ブラインド署名)のリスクがありました。

それに対して crypto.SignMessage は、生のメッセージを直接受け取ります。

署名者が自らメッセージをハッシュ化するため、署名プロセスにおいて「今、何を、どのアルゴリズムでハッシュ化しているか」を完全にコントロールでき、その署名鍵に最適なハッシュ処理と署名処理を選択できます。

そして開発者は「どのハッシュを使うべきか」を都度判断する必要がなくなり、鍵の種類に応じたデフォルトで安全な処理が強制されるため、設定ミスによる脆弱性を防げます。 これは「Secure by Default」な設計ですね。

なぜGoは「デフォルトが一番安全」を目指すのか?

ここまで見てきた通り、近年のGoのアップデートは単なる機能追加ではなく、 「開発者にセキュリティの判断を委ねない」 という強い意思に基づいています。

かつてのソフトウェア開発では、セキュリティに対する問題はレビュー時に「ちょっと詳しい人が気をつけるもの」でした。 しかし、世間のサイバーセキュリティをめぐる状況はどんどん高度化し最早一般の開発者が全てをキャッチアップすることが不可能になってきています。

「Secure by Design」、「Secure by Default」への順守は Goチームが所属するGoogleが「Secure by Design宣誓」をしているから対応する……のではなく、 そうせざるを得ないほどサプライチェーンのリスクが無視できなくなってきた という事でしょう。

Goが目指しているのは、 「普通に、気持ちよくコードを書いているだけで、気づけば世界基準の安全性を手に入れている」 という体験です。

標準ライブラリが「Secure by Default」になれば、それを利用する世界中のサードパーティ製ライブラリも、再コンパイルするだけで自動的に安全性が向上します。Go言語を選択するだけで、私たちは巨大な安全のネットワークに参加していることになります。

量子コンピュータの脅威(Q-Day)はまだ先の話かもしれません。しかし、Goがいち早くポスト量子暗号を実装したのは、「今あるデータを守る」という製造者としての責任感の表れです。

「Secure by Design」の波は、言語やフレームワークの層から着実に広がっています。

単に「動くコード」を書くだけでなく、その裏にある「なぜこの関数は非推奨になったのか?」「なぜ新しいパッケージが登場したのか?」という導入者の思想に触れてみてはいかがでしょうか?

この記事をきっかけとして世の中のシステムが少しでも「Secure by Design」「Secure by Default」を目指してくれることを祈っています。

「安全」をデフォルトに。 Goと共に、より良い設計の未来へ踏み出しましょう。


アンドパッドではセキュリティネタが大好きな Gopher も募集しています!

hrmos.co