前回の続きからです。
Common Lispで楕円曲線DSAを実装する4(確認) - nptclのブログ
7. DSAとは何か
あまりにも今さら過ぎますが、DSAの説明をします。
DSAとは、ディジタル署名アルゴリズム(Digital Signature Algorithm)だそうです。
ディジタル署名とは、「そのメッセージは、確かにあの人が書きましたよ」というのを確認するためのものです。
例として「私」が、みんなに向けてHello
というメッセージを
送信することを考えます。
Hello
は大変重要なメッセージなので、
悪意がある人たちによって「私」のなりすましがあったり、
あるいはHello
が改ざんされてHallo
になったりすることが懸念されます。
事前に私は、秘密鍵と公開鍵を作成します。
秘密鍵は、単なる乱数です。
秘密なので隠しておきましょう。
公開鍵は、起点を秘密鍵やらhashやらを用いてスカラー倍したものです。
公開なので自分のページやメールで、
みんながわかる所に公開しておきましょう。
それでは署名を行います。
署名には下記のものが必要です。
- 秘密鍵
- メッセージ
Hello
署名によって次のものが得られます。
- 署名
とは、それぞれ32byteくらいのデータです。
とを連結したものをと表現し、それを署名データとします。
ではみんなにメッセージを送りましょう。
すでにみんなに周知されているものは下記になります。
- 公開鍵
今回新たにみんなに送信するものは下記になります。
- メッセージ
Hello
- 署名
これで私の作業は終わりです。
場面が変わり、メッセージHello
を受け取ったみなさんの立場になります。
みなさんは、本当に私がHello
というメッセージを送信したのか疑っているわけです。
そこで次のデータをそろえます。
- 公開鍵
- メッセージ
Hello
- 署名
これらのデータを使い、
署名が正しいかどうかを「はい」か「いいえ」で答えるというのが検証です。
「はい」ならHello
は「私」が書いたものです。
「いいえ」ならHello
は「私」が書いたものではありません。
なりすましか改ざんが行われたということです。
7.1 アリスとボブについて
何なのあいつら
7.2 曲線とエンディアン
ここからはDSAそのものを作成していくことになります。
自分が思ったことなのですが、
曲線ごとに使うエンディアンが違うようです。
作った人が違うからなのでしょう。
次のような感じを受けました。
未確認だの確定だのあいまいですが、
secp256k1, secp256r1は自分が見てそう思った感想レベルです。
ただed25519とed448はRFCに出力例が載っており、
Little Endianとして扱わないとその値になりませんでした。
何にせよ、Endianを扱うための関数を用意する必要があります。
まずはLittle Endianから。
(defun integer-little-vector (v size) (let ((a (make-array size :element-type '(unsigned-byte 8)))) (dotimes (i size) (setf (aref a i) (ldb (byte 8 (* i 8)) v))) a)) (defun vector-little-integer (v &key (start 0) end) (unless end (setq end (length v))) (let ((r 0) (k 0)) (loop for i from start below end do (setq r (logior r (ash (aref v i) (* k 8)))) (incf k 1)) r))
上記の2つの関数は、次のような機能を持ちます。
integer-little-vector
- 整数をLittle Endianと見なして、8byteの配列を返却
vector-little-integer
- 8byteの配列をLittle Endianの列とみなして、整数を返却。
難しくはないと思います。
* (integer-little-vector #x12345678 4) -> #(#x78 #x56 #x34 #x12) * (vector-little-integer #(#x78 #x56 #x34 #x12)) -> #x12345678
Big Endianの方は次の通り。
(defun integer-big-vector (v size) (let ((a (make-array size :element-type '(unsigned-byte 8)))) (dotimes (i size) (let ((k (- size i 1))) (setf (aref a i) (ldb (byte 8 (* k 8)) v)))) a)) (defun vector-big-integer (v &key (start 0) end) (unless end (setq end (length v))) (let ((r 0) (k (- end start 1))) (loop for i from start below end do (setq r (logior r (ash (aref v i) (* k 8)))) (decf k 1)) r))
8. 鍵を生成
秘密鍵と公開鍵の生成を説明します。
それぞれの形式を覚えておいてください。
- 秘密鍵は、整数です。
- 公開鍵は、座標です。
たぶん一般的に鍵というのは、
16進数で記載されたテキストなんじゃないかなと思います。
でもここでは整数か座標として扱います。
8.1. 秘密鍵
最初に秘密鍵を作ります。
秘密鍵はただの乱数です。
しかし細かいところをちゃんと見ていきましょう。
8.1.1. 乱数を使うときの注意
暗号のための乱数は、非常に気を使った方がいいようです。
残念ながら今の私には/dev/urandom
ではなく
/dev/random
を使って下さいくらいしか知識がありません。
しかもこれって確か乱数を得るためのものではなく、
乱数の初期値を設定するためのものだったと思う。
もし暗号に使う乱数を生成する場合、 疑似乱数生成で最良と考えられるMersenne Twisterですら 予測可能な乱数列なのでそのまま使うべきではないと言っています。
Mersenne Twister Home Page
よく聞かれる質問
http://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/faq.html
SHAと組み合わせて下さいだそうです。
まじめにやるならよく考えましょう。
自分もとりあえずSHA-256あたりを絡めてみようと思います。
最近SHAを作ったので、使ってみたくなりました。
ということで、SHAを使った例を示します。
(defun make-private-256bit (&optional (n 4)) (let ((hash (make-sha256encode))) (dotimes (i n) (little-endian-sha256encode hash (random (ash 1 256)) 32)) (vector-little-integer (calc-sha256encode hash))))
256bitの乱数を、4回だけSHA-256に突っ込んでいます。
めんどうなら次の命令で代用できます。
(random (ash 1 256))
少しだけでもよくなっているのを期待します。
ed448の場合はSHA-512でも使ってください。
言い忘れていましたが、このためにSHAを自作しました。
好きに使って下さい。
https://github.com/nptcl/fixed/blob/main/sha.lisp
8.1.2. secp256k1, secp256r1
公式サイトのPDFを参考にします。
3.2.1 Elliptic Curve Key Pair Generation Primitive
https://www.secg.org/sec1-v2.pdf
Randomly or pseudorandomly select an integer d in the interval [1,n − 1].
つまり、1~n-1の乱数です。
256bitの乱数を出したら、nで割ってあまりを取りましょう。
0の場合はやり直しましょう。
あとあとでエラーになるっぽい。
★注意
乱数の範囲は、曲線パラメーターの未満です。
素数未満ではないようです。
私は間違えました!
実装例を示します。
(defun modn (x) (mod x *elliptic-n*)) (defun make-private-secp256k1 () (let ((x (modn (make-private-256bit)))) (if (zerop x) (make-private-secp256k1) x))) (defun make-private-secp256r1 () (make-private-secp256k1))
(modn x)
は、を計算するだけの関数です。
署名以降でたくさん使いますので、今のうちに用意しておきましょう。
8.1.3. ed25519
RFCを参考にします。
5.1.5. Key Generation
https://datatracker.ietf.org/doc/html/rfc8032
The private key is 32 octets (256 bits, corresponding to b) of
cryptographically secure random data.
つまりは256bitの乱数そのものです。
nで割る必要はありません。
さらに言うと、どうせ秘密鍵のハッシュが使われるので、0でも問題ありません。
(defun make-private-ed25519 () (make-private-256bit))
8.1.4. ed448
RFCを参考にします。
5.2.5. Key Generation
https://datatracker.ietf.org/doc/html/rfc8032
The private key is 57 octets (456 bits, corresponding to b) of
cryptographically secure random data.
SHA-512で乱数を作ってみます。
(defun make-private-ed448 (&optional (n 4)) (let ((hash (make-sha512encode))) (dotimes (i n) (little-endian-sha512encode hash (random (ash 1 512)) 64)) (vector-little-integer (calc-sha512encode hash) :end 57)))
面倒なら次の命令で代用できます。
(random (ash 1 456))
あとLittle Endian関数を使っていますが、 乱数を乱数にするだけなのでどっちでもいいです。
8.2. 公開鍵
こちらは種類によって出し方が大きく変わります。
ただどれもこれも基本的には、起点のスカラー倍が公開鍵です。
8.2.1. secp256k1, secp256r1
起点を秘密鍵でスカラー倍したものが公開鍵です。
簡単ですね。
(defun make-public-secp256k1 (private) (multiple private *elliptic-g*)) (defun make-public-secp256r1 (private) (multiple private *elliptic-g*))
8.2.2. ed25519
EdDSAの場合はいろいろと作業があります。
5.1.5. Key Generation
https://datatracker.ietf.org/doc/html/rfc8032
- 秘密鍵をSHA-512によって、64byteのハッシュ値を出力する。
- ハッシュ値の下位32byteを整数に変換
- 整数に対して次のbit値をセット
- 0 bitを0
- 1 bitを0
- 2 bitを0
- 254 bitを1
- 255 bitを0
- 起点のスカラー倍を公開鍵とする。
全部Little Endianとして扱って下さい。
SHA-512の下位32byteとは0~31byteの事です。
上位32byteの32~63byteは署名の時に使います。
実装の話をします。
いきなり公開鍵まで作成してしまうのではなく、
次の2つの値を返却する関数を作成します。
- 整数
- ハッシュ値の上位32byteの整数(署名時に使用)
実装は次のようになります。
(defun make-public-sign-ed25519 (private) (let ((sha (make-sha512encode))) (little-endian-sha512encode sha private 32) (let* ((v (calc-sha512encode sha)) (a (vector-little-integer v :start 0 :end 32)) (b (vector-little-integer v :start 32 :end 64))) (let ((v (ash 1 254))) (setq a (logand a (- v 8))) ;; cofactor (setq a (logior a v))) (values a b))))
公開鍵だけを生成したいときは、 最初の返却値のみを使います。
(defun make-public-ed25519 (private) (multiple (make-public-sign-ed25519 private) *elliptic-g*))
参考情報ですが、
1~3bitを0にクリアするのはcofactorが8だからだそうです。
ed448はcofactorが4なので、2ビットしかクリアしません。
なんでそういうことするのかは全然知りませんが。
8.2.3. ed448
ed25519とは微妙に異なっていますが、だいたい同じです。
5.2.5. Key Generation
https://datatracker.ietf.org/doc/html/rfc8032
- 秘密鍵をSHAKE-256によって、114byteのハッシュ値を出力する。
- ハッシュ値の下位57byteを整数に変換
- 整数に対して次のbit値をセット
- 0 bitを0
- 1 bitを0
- 447 bitを1
- 448~455 bitを0 (最後の1byte=8bitを、全部0にする)
- 起点のスカラー倍を公開鍵とする。
(defun make-public-sign-ed448 (private) (let ((sha (make-shake-256-encode))) (little-endian-sha3encode sha private 57) (let* ((v (result-sha3encode sha 114)) (a (vector-little-integer v :start 0 :end 57)) (b (vector-little-integer v :start 57 :end 114))) (let ((v (ash 1 447))) (setq a (logand a (- v 4))) ;; cofactor (setq a (logior a v))) (values a b)))) (defun make-public-ed448 (private) (multiple (make-public-sign-ed448 private) *elliptic-g*))
8.3. 座標のencode
公開鍵ができたので、事前にみんなに周知したくなります。
しかし公開鍵は座標です。
座標をそのまま誰かに送ったら、いや、そんなのわからんし、となると思います。
そこでわかりやすい形式に変換する、encodeを行いましょう。
secp256k1とsecp256r1は、まさにこのような目的で人のためにencodeします。
ed25519とed448は、それに加えて署名の処理でencodeを使います。
曲線によって用途が微妙に違います。
人に見て欲しいだけの場合、encodeは配列を返却することにしました。
一方、その後の署名の処理でも使う場合、encodeは整数を返却します。
- encodeの返却値は、
- secp256k1とsecp256r1は、byteの配列
- ed25519とed449は、整数
encodeの内容を見ていきましょう。
座標を、同等の意味をもつ値を(配列か整数)としたとき、
- 座標から、への変換が、encode
- から、座標への変換が、decode
となります。
座標はアフィン座標にすることでそれぞれ32byteくらいのデータになります。
ただ、この座標はたぶん曲線上に位置しているので、
片方あれば算出することができます。
どちらかの値だけを保存することで短くすることができることを覚えておきましょう。
8.3.1. secp256k1, secp256r1
ECDSAのencodeは、次の場所で定義されています。
2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion
https://www.secg.org/sec1-v2.pdf
このPDFにはencode/decodeという語は出てきません。
また、ECDSAだけの話になりますが、
compression, uncompressionという
2つの表現方法があります。
compressionは、X座標だけを表します(全33byte長)。
uncompressionは、X座標の次にY座標を表します(全65byte長)。
compressionかuncompressionはただの選択肢であり、
どっちでもいいので好きな方を選んでください。
それでは順にみていきましょう。
座標をencodeすることを考えます。
もしなら、次の1byteで終わりです。
#(0x00)
判定方法は、のときで問題ありません。
生成コードは次の通り。
(integer-big-vector #x00 1)
`であるときを考えます。
座標をアフィン座標に変換して話を進めます。
最初にuncompressの場合を見ていきます。
次の順番でデータを連結します。
例えばx=0x0100
, y=0x0200
であり、
例文のために32bit長としたときは次のようになります。
#(04 00 00 01 00 00 00 02 00)
返却値は配列であり、要素0番目が左(04の値)になります。
生成コードは次の通り。
(defun encode-uncompress-secp256k1 (x y) (integer-big-vector (logior (ash #x04 (* 256 2)) (ash x 256) y) (1+ 64)))
続いてcompressを示します。
最初の1byteは、の最下位ビットによって変わります。
次の順番でデータを連結します。
- の最下位ビットが、
0
のときは0x02
1
のときは0x03
- Big Endianの値
例えばx=0x00000100
, y=0x00000200
であり、
32bit長のときは次のようになります。
#(02 00 00 01 00)
他の例として、x=0x00000100
, y=0x00000201
であり、
32bit長のときは次のようになります。
#(03 00 00 01 00)
生成コードは次の通り。
(defun encode-compress-secp256k1 (x y) (integer-big-vector (logior (ash (if (logbitp 0 y) #x03 #x02) 256) x) (1+ 32)))
8.3.2. ed25519
座標をアフィン座標にします。
エンコードされた整数は下記のように作成されます。
- の最下位ビットをの最上位ビット(255bit)にコピー
整数は256bit長(32byte)の整数になります。
基本はの値だけを保存するのですが、
他にの符号情報も必要だとのこと。
の符号は最下位ビットです(最上位ではなく)。
生成コードは次の通り。
(defun encode-ed25519 (v) (let* ((a (affine v)) (x (point2-x a)) (y (point2-y a))) (when (logbitp 0 x) (setq y (logior y (ash 1 255)))) y))
返却値は、署名でも使うので整数です。
ところが公開鍵としてみんなに周知したいこともあると思います。
そんなときは、little endianで32byteの配列に直してください。
つまり、例として256bitは長すぎるので32bitで示しますが、
#x12345678 -> $(#x78 #x56 #x34 #x12)
のようになります。
8.3.3. ed448
ed25519とほぼ同じですが、特徴があります。
まずやり方から。
- の最下位ビットをの455bitにコピー
生成コードは次の通り。
(defun encode-ed448 (v) (let* ((a (affine v)) (x (point2-x a)) (y (point2-y a))) (when (logbitp 0 x) (setq y (logior y (ash 1 455)))) y))
8.4. 座標のdecode
encodeされたデータをdecodeして座標に戻しましょう。
すでに見てきた通り、曲線によってencodeの返却値の形式が違うので、
decodeの引数の形式も、それに合わせる必要があります。
次のようになります。
- decodeの引数は、
- secp256k1とsecp256r1は、byteの配列
- ed25519とed449は、整数
返却値はどちらも座標です。
8.4.1. secp256k1, secp256r1
引数のbyteの配列から、座標を求めます。
次の場所で定義されています。
2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion
https://www.secg.org/sec1-v2.pdf
の最初の1 byteを見て形式を判定します。
こんな感じで実装しました。
(defun decode-secp256k1 (k) (case (aref k 0) (#x00 (make-point3 0 0 0)) (#x02 (decode-compress-secp256k1 k 0)) (#x03 (decode-compress-secp256k1 k 1)) (#x04 (decode-uncompress-secp256k1 k))))
もし最初のbyteが0x00
なら、座標は無限遠点のです。
もし最初のbyteが0x04
なら、compressionではない座標データです。
続く32byteを、Big Endianの整数としての値にします。
続く32byteを、Big Endianの整数としての値にします。
得られた値から、射影座標で返却します。
配列からそのまま値を取り出しましょう。
値がvalid
かどうかは判定したほうがいいと思います。
(defun decode-uncompress-secp256k1 (k) (let ((p (make-point3 (vector-big-integer k :start 1 :end 33) (vector-big-integer k :start 33 :end 65)))) (when (valid p) p)))
もし最初のbyteが0x02
か0x03
なら、compressionです。
この値はの符号を表しています。
次のように符号を決めてください。
- 最初の1byteが
0x02
なら、 - 最初の1byteが
0x03
なら、
続く32byteを、Big Endianの整数としての値にします。
の値そのものは格納されていませんので、次の手順で算出する必要があります。
次の式よりを求めます。
ここで、とは曲線のパラメーターです。
次にの平方根を求めます。
平方根の求め方はすでに説明済みです。
最後に符号を合わせます。
算出した平方根の最下位ビットをとしたとき、
- なら、
- なら、
以上で、とが求まりました。
射影座標がDecodeの返却です。
実装は次の通りです。
(defun decode-compress-secp256k1 (k y0) (let* ((x (vector-big-integer k :start 1 :end 33)) (a (modp (+ (* x x x) (* *elliptic-a* x) *elliptic-b*))) (y (square-root-mod-4 a))) (when y (unless (= (logand y #x01) y0) (setq y (- *elliptic-p* y))) (make-point3 x y))))
8.4.2. ed25519
引数の整数から、座標を求めます。
まずはから次の値を算出します。
- の0~254bitをとする。
- の255bitをの符号とする。
encodeの逆の処理をしているだけなのでわかると思います。
求めたのチェックを行います。
- が以上の場合はdecode失敗
次にとを用いて、を求めましょう。
の算出は、曲線の式を用います。
次の, を求めます。
この値を用いて仮決めのを求めます。
確認のための値を計算します。
符号で場合分けをします。
- もしでありかつのときは、decodeは失敗
- もしの最下位ビットとが等しくないなら、を代入
結果のがdecodeの返却値です。
実装は、式の計算があり少々複雑です。
(defun decode-ed25519-x (y) (when (< y *elliptic-p*) (let* ((yy (* y y)) (u1 (modp (1- yy))) (v1 (1+ (* *elliptic-d* yy))) (v2 (* v1 v1)) (v3 (* v1 v2)) (v4 (* v2 v2)) (uv3 (* u1 v3)) (p (/ (- *elliptic-p* 5) 8)) (x (mulp uv3 (power-mod (* uv3 v4) p *elliptic-p*))) (v1x2 (mulp v1 x x))) (cond ((= v1x2 u1) x) ((= v1x2 (- *elliptic-p* u1)) (mulp x (power-mod 2 (/ (1- *elliptic-p*) 4) *elliptic-p*))))))) (defun decode-ed25519 (k) (let* ((y (ldb (byte 255 0) k)) (x0 (ldb (byte 1 255) k)) (x (decode-ed25519-x y))) (cond ((null x) nil) ((and (= x 0) (= x0 1)) nil) ((/= (logand x #x01) x0) (make-point4 (- *elliptic-p* x) y)) (t (make-point4 x y)))))
7.3.4. ed448
流れはed25519と同じです。
から次の値を算出します。
- の0~454bitをとする。
- の455bitをの符号とする。
求めたのチェックを行います。
- が以上の場合はdecode失敗
, を求めます。
ed25519とは符号が違うので注意。
仮決めのを求めます。
確認のための値を計算します。
符号で場合分けをします。
- もしでありかつのときは、decodeは失敗
- もしの最下位ビットとが等しくないなら、を代入
結果のがdecodeの返却値です。
(defun decode-ed448-x (y) (when (< y *elliptic-p*) (let* ((yy (* y y)) (u1 (modp (1- yy))) (u2 (* u1 u1)) (u3 (* u1 u2)) (u5 (* u2 u3)) (v1 (1- (* *elliptic-d* yy))) (v3 (* v1 v1 v1)) (u3v1 (* u3 v1)) (u5v3 (* u5 v3)) (p (/ (- *elliptic-p* 3) 4)) (x (mulp u3v1 (power-mod u5v3 p *elliptic-p*))) (v1x2 (mulp v1 x x))) (when (= v1x2 u1) x)))) (defun decode-ed448 (k) (let* ((y (ldb (byte 455 0) k)) (x0 (ldb (byte 1 455) k)) (x (decode-ed448-x y))) (cond ((null x) nil) ((and (= x 0) (= x0 1)) nil) ((/= (logand x #x01) x0) (make-point3 (- *elliptic-p* x) y)) (t (make-point3 x y)))))
続きます
次は署名と検証をやります。
Common Lispで楕円曲線DSAを実装する6(署名) - nptclのブログ