RSAを使ったデジタル署名について勉強する

2023/02/25

python セキュリティ プログラミング

 前回の記事ではRSA暗号を使った暗号化を実施しました。今回はデジタル署名について勉強していきます。

↓前回の記事

デジタル署名とは

コンピュータの世界でも実現するために考えられた仕組みです。

現実世界では他の人と契約をする際に紙の契約書に自筆で署名をする場合があると思います。紙に自筆する場合は筆跡が他人に真似されにくいため、本人が同意したという証拠になります。

しかし、デジタルの世界ではデータを簡単にコピーできるため簡単に偽装が出来てしまいます。(例えば本人が署名した契約書の画像を入手して、署名部分をコピペして別の書類に張り付けるなど)

そこでデジタルの世界でも本人が同意したという事を保証するために考えられたのがデジタル署名です。

RSA暗号方式を用いたデジタル署名について

ここでは前回勉強したRSA暗号を用いた署名について勉強します。RSA暗号については前回の記事RSA暗号についてpython-RSAを使って動作確認をする~暗号化と復号について~を参照お願いします。

全体の流れとしては下記図の通りです。各項目の詳細については次以降で記載します。

①署名鍵、署名検証鍵の生成

まず初めにメッセージ送信者は署名鍵と署名検証鍵を生成します。全体の図でいうと下記赤枠部分について説明となります。2つの鍵を生成したら署名検証鍵をメッセージの受信者に渡します。
※RSA暗号の仕組みを用いたデジタル署名では前回のRSA暗号で登場した秘密鍵を署名鍵と呼び、公開鍵のことを署名検証鍵というみたいです。(署名鍵署名検証鍵より)

②署名の生成

鍵の生成及び鍵の引き渡しが終わったら、メッセージ送信者は署名を生成します。全体の図でいうと下記赤枠の部分についての説明になります。

署名の生成については
①ハッシュ値を求める
②ハッシュ値に署名をする
という2つの処理が必要なので、順番に説明していきます。

ハッシュ値を求める

メッセージ送信者は送りたいメッセージと署名鍵を元に署名を作成します。署名の作り方ですが、最初にハッシュ関数と呼ばれるものにメッセージを入れてハッシュ値というものを求めます。

(補足)ハッシュ関数/ハッシュ値とは

ここで新しくハッシュ関数という言葉が出てきました。ハッシュ関数とは入力された値を特定のアルゴリズムで計算して、入力値のデータ長によらず決まった文字列を出力する関数のことで、出力値をハッシュ値といいます。

ハッシュ関数/ハッシュ値の特徴として以下のものが挙げられます。

  1. ハッシュ値から元のデータを推測することが出来ない(推測することが非常に困難)
  2. 入力値が同じなら同じハッシュ値を返す
  3. 入力値が少しでも異なると全く異なるハッシュ値を返す
この特徴については後ほど確認していきたいと思います。

ハッシュについてイメージがしにくい人がいると思うので、例を挙げると人間でいうところの指紋などの生体情報に近いものだと思います。

人間ごとに指紋は異なりますが、指紋がばれたところでその人のことについて推測することが出来ませんし、人が同じなら同じ指紋となります。(ハッシュ関数の特徴の1番目、2番目の内容と同等です)

ハッシュについてイメージがしにくい人がいると思うので、例を挙げると人間でいうところの指紋などの生体情報に近いものだと思います。

人間ごとに指紋は異なりますが、指紋がばれたところでその人のことについて推測することが出来ませんし、人が同じなら同じ指紋となります。(ハッシュ関数の特徴の1番目、2番目の内容と同等です)

また、一卵性双生児でも指紋が異なるため、入力値が少しでも違うと異なる値を返すという特徴とも一致します。(指紋は遺伝で決まりますか?~一卵性双生児でも指紋は異なる?~)

少し話が脱線してしまいましたが、話をハッシュ関数に戻します。ハッシュ関数には種類があるのですが、現在はSHA-256というものが主流みたいです。具体的にどのような処理をしているのかは下記仕様書を参照してください。

(簡単に読みましたが、入力されたメッセージを64ビットに分割して、配列に格納し右ローテートシフトなどを組み合わせて値を計算していくような処理をしてそうです。)

SHA-256が具体的にどのようなプロセスで計算しているかは下記サイトで確認することが出来ます。これを見てなんとなくの生成方法を理解することが出来ました。

ハッシュ値に対して署名する

次に先ほど生成したハッシュ値に対して署名を行います。RSAの場合は秘密鍵で暗号化するのと同じ処理を行いますが、デジタル署名において"暗号化"という言葉を使わない方がいいです。詳細はこの記事の補足に記載しました。

③メッセージを受け取った人が検証する

最後にメッセージ受信者は受け取った署名に対して署名検証鍵を使って検証を行います。
全体の図でいうと下記赤枠の説明になります。

まず初めにメッセージの受信者は署名とメッセージを受け取ります。

そして、受け取ったメッセージ及び署名を元に内容に問題がないかを検証していきます。検証の流れとしては下記図を参照お願いします。

以上が署名から検証までの流れとなります。

(補足)デジタル署名の特徴について

デジタル署名の特徴として「真正性」「改ざん検知」「否認防止」があります。

真正性とは簡単に言うとメッセージの送信者が本人であることを示すことが出来るという事です。下記図のように本人がデータを送信したものであることを確認することが出来ます。

理由としてはメッセージ受信者が持っている検証鍵はメッセージ送信者が持っている署名鍵と紐づいているため、異なる鍵で署名されたメッセージは検証時に気が付くことが出来るからです。

続いて改ざん検知についてです。一言でいうと悪意のある第三者がメッセージを改ざんした場合でも気が付くことが出来るという事です。

本当は"大好きです"というメッセージを送りたかったのに悪意のある第三者が改ざんして"大嫌いです"というメッセージとして送信した場合を考えてみます。

この場合は"大嫌いです"というメッセージから求めたハッシュ値と署名されたハッシュ値の値が一致しないため、メッセージが改ざんされていることに気が付くことが出来ます。


最後に否認防止についてです。これは電子情報が本人によって作られたものであるという事を証明することが出来ます。

例えば"1万円借りました。○○までに返します。"というメッセージに署名したとします。
この署名は本人しか作成することが出来ないため、あとからそんな約束してないと否認することが出来なくなります。


ここまででデジタル署名の流れ及び特徴について勉強しましたが一つ注意点があります。。
今まで紹介したデジタル署名の特徴は署名検証鍵が正しいものであるという前提のもとに成り立ちます。

悪意のある第三者が署名検証鍵も偽装した場合は正しくデジタル署名が機能しなくなってしまいます。

このように署名検証鍵も偽装された場合は署名検証鍵を使って取り出したハッシュ値とメッセージを元に算出したハッシュ値が一致してしまうため、デジタル署名が想定通り機能しません。

したがって受領した署名検証鍵が正しいかどうかを確認する環境が必要となるという事が分かります。これはPublic Key Infrastructure(PKI)というものを使うことで鍵の妥当性を保証することが出来るそうなのですが、次回勉強したいと思います。

pythonのプログラムでデジタル署名の流れを理解する

ここからは実際にpythonのプログラムを使ってデジタル署名の流れを理解していきます。
使うライブラリは前回の記事同様"python-RSA"です。

①ハッシュ値を求め、署名を行う

import rsa

(pubkey,privkey) = rsa.newkeys(512)#鍵ぺア生成(pubkey:署名検証鍵、pribkey:署名鍵)
message = "大好きです。".encode()#送信者が送りたいメッセージ
hash1 = rsa.compute_hash(message,"SHA-256")#メッセージを元にSHA-256を使ってハッシュ生成
signature_hash = rsa.sign_hash(hash1,privkey,"SHA-256")#ハッシュに署名鍵で署名
print(hash1)#何故か16進数で正しく表示されない
hash_int = int.from_bytes(hash1,"big")#16進数で正しく表示されないため一度intに変換
print("hash=")
print(hex(hash_int))#16進数に変換
これを実行すると下記のような出力が得られ、今回送信したいメッセージのハッシュ値が"0xd95343b15789411366b608496c67a64646fb207ec128680bbeffb4b757a03a73"であることが分かります。(普通にハッシュ値を出力すると、正しく16進数表記で出力されませんでした。。。原因知っている方いましたらご教示お願いします。。。)
b'\xd9SC\xb1W\x89A\x13f\xb6\x08Ilg\xa6FF\xfb ~\xc1(h\x0b\xbe\xff\xb4\xb7W\xa0:s'
hash=
0xd95343b15789411366b608496c67a64646fb207ec128680bbeffb4b757a03a73
先ほど紹介したSHA256の計算プロセスが分かるサイトで今回送信するメッセージのハッシュ値を計算したところ、同じ値が出てきました。(先ほどハッシュ関数の特徴として、"入力値が同じなら同じハッシュ値を返す"というものを紹介しましたが、これで確認することが出来ました。)

自分が送りたいメッセージのハッシュ値がどのようにして算出されているか気になる方は同じようにこのサイトを使って確認してみてください。

また、今回は勉強のために、ハッシュ値を求めるのと署名を別の関数を呼び出して実行しましたが、sign関数を使うことでハッシュ値算出から署名まで一気に実行することが出来ます。

signature1 = rsa.sign(message,privkey,"SHA-256")#ハッシュ生成から署名までをまとめて実施する場合
また、先ほどハッシュ関数の特徴として"入力値が少しでも異なると全く異なるハッシュ値を返す"というのを紹介しましたが、ここで確認してみます。先ほどは"大好きです。"という文字列のハッシュ関数を求めましたが、ここでは最後の。が無い"大好きです"という文字列のハッシュ値を求めてみます。

実行した結果下記のようなハッシュ値が返ってきました。
"ae4d019298310b62a879b0d3d796a45d328864326119383ca9e866b4ae7b94e1"

先ほどのハッシュ値
"d95343b15789411366b608496c67a64646fb207ec128680bbeffb4b757a03a73"
と比較して、一文字違うだけで全く別のハッシュ値が返ってくることが確認でき、ハッシュ値から元のメッセージが何かを推定するのが困難であることが分かります。

②署名の検証を行う
次にメッセージ受信者側の署名検証の流れについて確認していきます。
verify関数を使うことで署名検証を行うことが出来ます。先ほどのプログラムの末尾に署名検証の処理を追加しました。
import rsa

(pubkey,privkey) = rsa.newkeys(512)#鍵ぺア生成(pubkey:署名検証鍵、pribkey:署名鍵)
message = "大好きです。".encode()#送信者が送りたいメッセージ
hash1 = rsa.compute_hash(message,"SHA-256")#メッセージを元にSHA-256を使ってハッシュ生成
signature_hash = rsa.sign_hash(hash1,privkey,"SHA-256")#ハッシュに署名鍵で署名
hash_int = int.from_bytes(hash1,"big")#16進数で正しく表示されないため一度intに変換
print("hash=")
print(hex(hash_int))#16進数に変換=
print("署名検証結果=")
print(rsa.verify(message,signature_hash,pubkey))#改ざんされてないとハッシュアルゴリズムをリターンする
出力結果は下記の通りになり、正しく署名検証が出来たため"SHA-256"という結果が返ってきました。
hash=
0xd95343b15789411366b608496c67a64646fb207ec128680bbeffb4b757a03a73
署名検証結果=
SHA-256
(補足)署名の検証が通らないパターンについて
ここでは署名検証が通らないパターンの動きについてみてみます。
署名検証が通らないパターンとして大きく分けてハッシュ値が正しくない場合と署名検証鍵が間違っている場合があると思います。

ここでは例としてメッセージが改ざんされてハッシュ値が異なるものになった場合について動作を確認します。改ざんされたメッセージとして変数"message2"を用意して"大嫌いです。"を格納します。

最後のverify関数の引数として改ざんされたメッセージ"message2"を渡して、メッセージが改ざんされたことを検知してみたいと思います。
import rsa

(pubkey,privkey) = rsa.newkeys(512)#鍵ぺア生成(pubkey:署名検証鍵、pribkey:署名鍵)
message = "大好きです。".encode()#送信者が送りたいメッセージ
message2 = "大嫌いです".encode()#偽装されたメッセージ
hash1 = rsa.compute_hash(message,"SHA-256")#メッセージを元にSHA-256を使ってハッシュ生成
signature_hash = rsa.sign_hash(hash1,privkey,"SHA-256")#ハッシュに署名鍵で署名
hash_int = int.from_bytes(hash1,"big")#16進数で正しく表示されないため一度intに変換
print("hash=")
print(hex(hash_int))#16進数に変換=
print("署名検証結果=")
print(rsa.verify(message2,signature_hash,pubkey))#メッセージが改ざんされているためエラーとなる

出力結果としては下記のようなエラーが出力され、改ざん検知が出来ていることを確認できました。
Verification failed
  File "C:\Users\kinao\Documents\python\RSA\RSA_signed.py", line 12, in <module>
    print(rsa.verify(message2,signature_hash,pubkey))#メッセージが改ざんされているためエラーとなる
rsa.pkcs1.VerificationError: Verification failed

(補足)デジタル署名において署名鍵を使って"暗号化"という言葉は間違っている

一部の他の記事で「秘密鍵を使って暗号化するのがデジタル署名」という旨の記載がされていると思います。

例えば下記の記事では以下のように"秘密鍵で暗号化したものが含まれている"という記載があります。
「デジタル(電子)署名には、差出人のメールアドレスが記載された電子証明書と、配布する文書の「特徴(ダイジェスト)」を差出人の秘密鍵で暗号化したものが含まれています。」

 (デジタル(電子)署名や暗号化の仕組みより)

RSA暗号においては秘密鍵を使ってデジタル署名を作成しますが、暗号化しているわけではございません。

以下総務省のサイトより引用した"暗号化"の定義です。

大事な情報を他人には知られないようにするため、データを見てもその内容がわからないように、定められた処理手順でデータを変えてしまうこと。

暗号化されたデータは、復号という処理によって元のデータに戻すことができます。
基礎知識: 暗号化の仕組み
上記用語の定義で大事なのは、"暗号化されたデータは復号処理を行うことで元のデータに戻すことが出来る"という事です。

今回説明した通り、署名は元のデータからハッシュ値を生成(元のデータを推定することが困難な値)し、ハッシュ値に対して秘密鍵を使って"署名"を作成します。(実施している計算としてはRSA暗号でいう秘密鍵を使った暗号化の処理と同じです。)

仮に"署名"を"暗号化"と同じだとすると、暗号化の定義から"復号"の処理をすると元のデータに戻すことが出来る必要があります。

署名に対して"復号"処理をした場合、ハッシュ値しか出てこないため元のデータに戻すことが出来ません。なので、RSA署名において、秘密鍵を使って"暗号化"という表現が誤りだという事になります。(RSAにおいて計算自体は署名も暗号化も同じなのですが、"暗号化"という用語の定義を正しく理解できてないと後々混乱が生じてしまうと思います。)

PKEとPKCについて

デジタル署名の説明において暗号化するという説明が間違っている理由としてPKE(Public Key Encryption)とPKC(Public Key Cryptography)を混同して使っていると下記本に紹介がありました。


私の理解では秘密鍵と公開鍵といった公開情報と秘密情報を扱う技術全般のことをPKCといい、PKEはPKCの一部で情報を暗号化する際に使う技術のことを言うと理解しました。

PKCとPKEの関係については書籍内にあった下記図が分かりやすいと思います。


暗号と認証のしくみと理論がしっかり分かる教科書より

秘密鍵で暗号化についての他の意見

他にも下記のような記述がありました。興味がある方はこちらのサイトも参照お願いします。
どちらも正しいと言ったものの、やっぱり「秘密鍵で暗号化・公開鍵で復号」は気持ちわるいなあ。RSA暗号では、平文と暗号文が一対一対応になっているから、「暗号化して復号すると元に戻る」だけでなく、「復号してから暗号化しても元に戻る」という性質があることを説明したうえで、「ハッシュ値を秘密鍵で復号・署名を公開鍵で暗号化」と説明するのが良いのではないかな。

まとめ

今回はRSA暗号を用いたデジタル署名について勉強し、pythonのプログラムでデジタル署名の流れについて確認しました。

今回勉強した内容についてサイトによって記載されている内容にばらつきがあり、誤っている内容が記載されている可能性があります。

もし、私の理解が間違っており間違っている内容がある場合はお手数をおかけしますが、連絡をしていただけると幸いです。

また、今回の例では悪意のある第三者が公開鍵を偽装するような例を出さなかったですが、実際の環境ではそういったことを考慮しなければいけません。PKIという仕組みで保証が出来るみたいなので、次回はPKIについて勉強したいと思います。

参考文献

自己紹介

はじめまして 社会人になってからバイクやプログラミングなどを始めました。 プログラミングや整備の記事を書いていますが、独学なので間違った情報が多いかもしれません。 間違っている情報や改善点がありましたらコメントしていただけると幸いです。

X(旧Twitter)

フォローお願いします!

QooQ