nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

Common Lispで楕円曲線DSAを実装する5(鍵生成)

前回の続きからです。
Common Lispで楕円曲線DSAを実装する4(確認) - nptclのブログ

7. DSAとは何か

あまりにも今さら過ぎますが、DSAの説明をします。
DSAとは、ディジタル署名アルゴリズム(Digital Signature Algorithm)だそうです。
ディジタル署名とは、「そのメッセージは、確かにあの人が書きましたよ」というのを確認するためのものです。

例として「私」が、みんなに向けてHelloというメッセージを 送信することを考えます。
Helloは大変重要なメッセージなので、 悪意がある人たちによって「私」のなりすましがあったり、 あるいはHelloが改ざんされてHalloになったりすることが懸念されます。

事前に私は、秘密鍵と公開鍵を作成します。
秘密鍵は、単なる乱数です。
秘密なので隠しておきましょう。

公開鍵は、起点 G秘密鍵やらhashやらを用いてスカラー倍したものです。
公開なので自分のページやメールで、 みんながわかる所に公開しておきましょう。

それでは署名を行います。
署名には下記のものが必要です。

署名によって次のものが得られます。

  • 署名 r \| s

 r sは、それぞれ32byteくらいのデータです。
 r sを連結したものを r\|sと表現し、それを署名データとします。

ではみんなにメッセージを送りましょう。
すでにみんなに周知されているものは下記になります。

  • 公開鍵

今回新たにみんなに送信するものは下記になります。

  • メッセージHello
  • 署名 r\|s

これで私の作業は終わりです。

場面が変わり、メッセージHelloを受け取ったみなさんの立場になります。
みなさんは、本当に私がHelloというメッセージを送信したのか疑っているわけです。
そこで次のデータをそろえます。

  • 公開鍵
  • メッセージHello
  • 署名 r\|s

これらのデータを使い、 署名が正しいかどうかを「はい」か「いいえ」で答えるというのが検証です。
「はい」ならHelloは「私」が書いたものです。
「いいえ」ならHelloは「私」が書いたものではありません。
なりすましか改ざんが行われたということです。

7.1 アリスとボブについて

何なのあいつら

7.2 曲線とエンディアン

ここからはDSAそのものを作成していくことになります。

自分が思ったことなのですが、 曲線ごとに使うエンディアンが違うようです。
作った人が違うからなのでしょう。
次のような感じを受けました。

  • secp256k1, secp256r1
    • Big Endianを使用(ただし未確認)
  • ed25519, ed448
    • Little Endianを使用(こちらは確定)

未確認だの確定だのあいまいですが、 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の場合はやり直しましょう。
あとあとでエラーになるっぽい。

★注意
乱数の範囲は、曲線パラメーターの n未満です。
素数 p未満ではないようです。
私は間違えました!

実装例を示します。

(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)は、 x \mod nを計算するだけの関数です。
署名以降でたくさん使いますので、今のうちに用意しておきましょう。

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. 公開鍵

こちらは種類によって出し方が大きく変わります。
ただどれもこれも基本的には、起点 Gスカラー倍が公開鍵です。

8.2.1. secp256k1, secp256r1

起点 G秘密鍵スカラー倍したものが公開鍵です。
簡単ですね。

(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を整数 sに変換
  • 整数 sに対して次のbit値をセット
    • 0 bitを0
    • 1 bitを0
    • 2 bitを0
    • 254 bitを1
    • 255 bitを0
  • 起点 Gスカラー sを公開鍵とする。

全部Little Endianとして扱って下さい。
SHA-512の下位32byteとは0~31byteの事です。
上位32byteの32~63byteは署名の時に使います。

実装の話をします。
いきなり公開鍵まで作成してしまうのではなく、 次の2つの値を返却する関数を作成します。

実装は次のようになります。

(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を整数 sに変換
  • 整数 sに対して次のbit値をセット
    • 0 bitを0
    • 1 bitを0
    • 447 bitを1
    • 448~455 bitを0 (最後の1byte=8bitを、全部0にする)
  • 起点 Gスカラー sを公開鍵とする。
(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の内容を見ていきましょう。
座標を A、同等の意味をもつ値を k(配列か整数)としたとき、

  • 座標 Aから、 kへの変換が、encode
  •  kから、座標 Aへの変換が、decode

となります。

座標 Aはアフィン座標にすることで x, yそれぞれ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はただの選択肢であり、 どっちでもいいので好きな方を選んでください。
それでは順にみていきましょう。

座標 Aをencodeすることを考えます。
もし A=Oなら、次の1byteで終わりです。

#(0x00)

判定方法は、 Z=0のときで問題ありません。

生成コードは次の通り。

(integer-big-vector #x00 1)

 A \neq O`であるときを考えます。
座標 Aをアフィン座標に変換して話を進めます。

最初に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は、 yの最下位ビットによって変わります。 次の順番でデータを連結します。

  •  yの最下位ビットが、
    • 0のときは0x02
    • 1のときは0x03
  • Big Endian x

例えば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

座標 Aをアフィン座標 (x,y)にします。
エンコードされた整数 kは下記のように作成されます。

  •  k \leftarrow y
  •  xの最下位ビットを kの最上位ビット(255bit)にコピー

整数 kは256bit長(32byte)の整数になります。
基本は yの値だけを保存するのですが、 他に xの符号情報も必要だとのこと。
 xの符号は最下位ビットです(最上位ではなく)。

生成コードは次の通り。

(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))

返却値 kは、署名でも使うので整数です。
ところが公開鍵としてみんなに周知したいこともあると思います。
そんなときは、little endianで32byteの配列に直してください。
つまり、例として256bitは長すぎるので32bitで示しますが、

#x12345678 -> $(#x78 #x56 #x34 #x12)

のようになります。

8.3.3. ed448

ed25519とほぼ同じですが、特徴があります。
まずやり方から。

  •  k \leftarrow y
  •  xの最下位ビットを kの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の配列 kから、座標を求めます。

次の場所で定義されています。

2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion
https://www.secg.org/sec1-v2.pdf

 kの最初の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なら、座標は無限遠点の Oです。

もし最初のbyteが0x04なら、compressionではない座標データです。
続く32byteを、Big Endianの整数として xの値にします。
続く32byteを、Big Endianの整数として yの値にします。
得られた値から、射影座標 (x, y, 1)で返却します。

配列 kからそのまま値を取り出しましょう。
値が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が0x020x03なら、compressionです。
この値は yの符号を表しています。
次のように符号 y_0を決めてください。

  • 最初の1byteが0x02なら、 y_0=0
  • 最初の1byteが0x03なら、 y_0=1

続く32byteを、Big Endianの整数として xの値にします。
 yの値そのものは格納されていませんので、次の手順で算出する必要があります。

次の式より \alphaを求めます。

 \displaystyle \alpha \equiv x^{3} + a x + b \mod p

ここで、 a bは曲線のパラメーターです。

次に \alpha平方根 \betaを求めます。
平方根の求め方はすでに説明済みです。

最後に符号を合わせます。
算出した平方根 \betaの最下位ビットを \beta_0としたとき、

  •  \beta_0 = y_0なら、 y = p - \beta
  •  \beta_0 \neq y_0なら、 y = \beta

以上で、 x yが求まりました。
射影座標 (x, y, 1)が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

引数の整数 kから、座標を求めます。

まずは kから次の値を算出します。

  •  kの0~254bitを yとする。
  •  kの255bitを xの符号 x_0とする。

encodeの逆の処理をしているだけなのでわかると思います。
求めた yのチェックを行います。

  •  y p以上の場合はdecode失敗

次に y x_0を用いて、 xを求めましょう。
 xの算出は、曲線の式を用います。
次の u,  vを求めます。

 \displaystyle u = y^{2} - 1
 \displaystyle v = d \ y^{2} + 1

この値を用いて仮決めの xを求めます。

 \displaystyle x \equiv u \ v^{3} (u \ v^{7}) ^{(p-5)/8} \mod p

確認のための値 v \ x^{2}を計算します。

  1.  v \ x^{2} \equiv u \mod pなら、 x平方根
  2.  v \ x^{2} \equiv -u \mod pなら、平方根 xは、  x \leftarrow x \cdot 2^{(p-1)/4}
  3. それ以外なら平方根はなし(decode失敗)

符号 x_0で場合分けをします。

  • もし x=0でありかつ x_0=1のときは、decodeは失敗
  • もし xの最下位ビットと x_0が等しくないなら、 x \leftarrow p - xを代入

結果の (x, y)が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と同じです。
 kから次の値を算出します。

  •  kの0~454bitを yとする。
  •  kの455bitを xの符号 x_0とする。

求めた yのチェックを行います。

  •  y p以上の場合はdecode失敗

 u,  vを求めます。

 \displaystyle u = y^{2} - 1
 \displaystyle v = d \ y^{2} - 1

ed25519とは符号が違うので注意。
仮決めの xを求めます。

 \displaystyle x \equiv u^{3} \ v (u^{5} \ v^{3}) ^{(p-3)/4} \mod p

確認のための値 v \ x^{2}を計算します。

  1.  v \ x^{2} \equiv u \mod pなら、 x平方根
  2. それ以外なら平方根はなし(decode失敗)

符号 x_0で場合分けをします。

  • もし x=0でありかつ x_0=1のときは、decodeは失敗
  • もし xの最下位ビットと x_0が等しくないなら、 x \leftarrow p - xを代入

結果の (x, y)が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のブログ