😽

🔒NodeJSで秘密鍵で署名して公開鍵で検証する。

2023/01/19に公開

まえがき

OpenID Connectを理解しようと勉強しているが、その前に秘密鍵で署名作成、公開鍵で署名検証について理解するための記事である。

総括

✅署名とは、公開鍵/秘密鍵を用いて改ざんを検知する技術/仕組み。
✅キーペア(公開鍵/秘密鍵)は、ssh-keygenコマンドopensslコマンドで作成可能。
✅NodeJSの暗号ライブラリは、Trend的にcrypto-js>>>>>>>crypto
✅署名アルゴリズムとしてRS256(非対称)とHS256(対象)がある。
✅RSA256 = RSA(公開鍵暗号)+HMAC256(ハッシュ関数)であるので、不可逆なハッシュ関数と可逆な暗号復号も組み合わせたもの
👉JWTの署名アルゴリズムとしてこの2つのいずれか(RS256,HS256)を利用できるってこと。
✅鍵ファイルのフォーマットとしてRFC4716, PEM, JWK, PKCS8が存在する。
PEM(Base64Encodeしたもの)とJWK(JSON形式にしたもの)は良く使う。
jwt.ioで生成した鍵(PEM)を、PEM to JWKでJWK形式に変換可能👍

署名とは

👉公開鍵/秘密鍵を用いて改ざんを検知する技術/仕組み。
👉秘密鍵で署名を作成(暗号化) -> 公開鍵で署名を検証(復号化)する。

公開鍵,秘密鍵を実際に作ってみる。

ssh-keygenとは

SSHの認証キーの生成と管理と変更を行うコマンド。
SSHを公開鍵認証方式で認証したい場合にそのキーペアを生成できる。

👉opensslコマンドでも公開鍵,秘密鍵の作成は可能
https://security.stackexchange.com/questions/29876/what-are-the-differences-between-ssh-generated-keysssh-keygen-and-openssl-keys

ssh-keygenコマンドで鍵作ってみる。

C:\Users\daisu>ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\daisu/.ssh/id_rsa): oidc_practice_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in oidc_practice_rsa.
Your public key has been saved in oidc_practice_rsa.pub.
The key fingerprint is:
SHA256:P15De/W2FKC67PMP9yp9oYamf2d2Rv/siWcpFKxyqGA daisuke@TABLET-G9B5BEIQ
The key's randomart image is:
+---[RSA 3072]----+
|                 |
|                 |
|            ..   |
|            .o.  |
|        S ..o ...|
|     E   +.+ o o+|
|    . . ..=.B.oo*|
|       ..o.*oO+@*|
|        .=Bo=+X**|
+----[SHA256]-----+

下記2ファイルが生成された。

ファイル名 役割
oidc_practice_rsa 秘密鍵の保存場所。
oidc_practice_rsa.pub 公開鍵の保存場所。

オプション未指定のssh-keygenコマンドだとPEM形式ではないものが出来る。

オプション未指定のssh-keygenで生成した2ファイルの中身がこんな感じである。

oidc_practice_rsa(秘密鍵)
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBsjnpCyP
DEV+oJ0vmpBPm4AAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQC/yqdSx3W9
5oHqbjLyZwlNTv6aZSivBznKcZm1o/ygKI3hJY+MaetmF9yxLvNe5XccLtQc5G5jImey2p
Jv4n4q2QsTQ2iQKJ7v1/LwCZBhbdMEPVh04a4ULWcVarchkdEHvshl2p0tkvY5HRpoWg18
// 以下省略
-----END OPENSSH PRIVATE KEY-----
oidc_practice_rsa.pub(公開鍵)
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/yqdSx3W95oHqbjLyZwlNTv6aZSivBznKcZm1o/ygKI3hJY+MaetmF9yxLvNe5XccLtQc5G5jImey2pJv4n4q2QsTQ2iQKJ7v1/LwCZBhbdMEPVh04a4ULWcVarchkdEHvshl2p0tkvY5HRpoWg18Z6tDtKR4/ONq1aix1lCheHH8lHMgKPFTwDreyt4sZp3pwEHpgNuvIEzSO8YDxMgIkD97CCu

しかし今回期待しているのは、以下のようなファイルである。

-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAu9AMgGH5CTIS532iAJ+Q1PKkFvvwmHZgHyscHJiy9qtQGy+q
JRNtyOsgywWnOVvQIC25uhJMHCh9LHDCngHbD/ej/RrCp+Edhso/jOTqEF6UrTY8

ssh-keygenのオプション

(´_ゝ`)「オプション未指定だと、秘密鍵と公開鍵で中身の形式が違うのなんで?」
(´_ゝ`)「どうしたら-----BEGIN RSA PRIVATE KEY-----となる?ていうか-----BEGIN RSA PRIVATE KEY-----って何?」

これらの疑問を解決するために、まずはssh-keygen helpでオプション一覧を見てみよう。

C:\Users\daisu>ssh-keygen help
Too many arguments.
usage: ssh-keygen [-q] [-b bits] [-C comment] [-f output_keyfile] [-m format]
                  [-N new_passphrase] [-t dsa | ecdsa | ed25519 | rsa]
       ssh-keygen -p [-f keyfile] [-m format] [-N new_passphrase]
                   [-P old_passphrase]
       ssh-keygen -i [-f input_keyfile] [-m key_format]
       ssh-keygen -e [-f input_keyfile] [-m key_format]
       ssh-keygen -y [-f input_keyfile]
       ssh-keygen -c [-C comment] [-f keyfile] [-P passphrase]
       ssh-keygen -l [-v] [-E fingerprint_hash] [-f input_keyfile]
       ssh-keygen -B [-f input_keyfile]
       ssh-keygen -D pkcs11
       ssh-keygen -F hostname [-lv] [-f known_hosts_file]
       ssh-keygen -H [-f known_hosts_file]
       ssh-keygen -R hostname [-f known_hosts_file]
       ssh-keygen -r hostname [-g] [-f input_keyfile]
       ssh-keygen -G output_file [-v] [-b bits] [-M memory] [-S start_point]
       ssh-keygen -f input_file -T output_file [-v] [-a rounds] [-J num_lines]
                  [-j start_line] [-K checkpt] [-W generator]
       ssh-keygen -I certificate_identity -s ca_key [-hU] [-D pkcs11_provider]
                  [-n principals] [-O option] [-V validity_interval]
                  [-z serial_number] file ...
       ssh-keygen -L [-f input_keyfile]
       ssh-keygen -A [-f prefix_path]
       ssh-keygen -k -f krl_file [-u] [-s ca_public] [-z version_number]
                  file ...
       ssh-keygen -Q -f krl_file file ...
       ssh-keygen -Y check-novalidate -n namespace -s signature_file
       ssh-keygen -Y sign -f key_file -n namespace file ...
       ssh-keygen -Y verify -f allowed_signers_file -I signer_identity
                -n namespace -s signature_file [-r revocation_file]

たくさんあるので主要なものをピックアップする。

オプション名 用途
-t ✅type。公開鍵暗号方式のこと。dsa、ecdsa、ed25519、rsaのいずれか。
参考:SSHの公開鍵暗号には「RSA」「DSA」「ECDSA」「EdDSA」のどれを使えばよいのか?
-b ✅bits。作成する鍵のbit数。
-f ✅file。読み込むファイルパス。
-i ✅input。暗号化されていない鍵ファイルを読み出し、OpenSSH互換形式に変換してから標準出力に出力する
-e ✅OpenSSH形式の鍵ファイルを読み出し、RFC 4716形式または-mオプションで指定した形式で標準出力に出力する
-m ✅「-i」オプションで入力する鍵の形式を、「-e」オプションで出力する鍵の形式を、「RFC4716」(デフォルト)、「PKCS8」「PEM」から指定する。

参考:【 ssh-keygen 】コマンド――SSHの公開鍵と秘密鍵を作成する

利用例

ssh-keygen -t rsa -b 4096
ssh-keygen -f keys/fuga.pub -e -m pem > keys/fuga.pub_

RFC4716, PEM, JWK, PKCS8

RFC4716(デフォルト)

👉SSH公開鍵のファイルフォーマットの標準仕様。

PEM

👉ファイルの先頭に-- BEGIN...という行があるやつ。

PEM形式のファイル
-----BEGIN RSA PRIVATE KEY-----
MIIG4gIBAAKCAYEA5bGGox4U3VW5EotikwTEC6beaqDIvusDaNYJBd6/0jMJn1+X
tQAicv+QCjbECG9NCqVsRVRzUiyfLy5OUw0jeHYVE0CTJ2gMAKeG/8sMwop0PpjZ

これはバイナリデータをPEMでエンコーディング(Base64化によってテキスト化)したものである。PEM形式でエンコーディングされてなければ、人間がみても意味不明なやつだろう。
※opensslコマンドのデフォルトのエンコーディング。

JWK

👉暗号鍵JSONで表現する規格。
👉PEM ⇔ JWKの変換用サイト:https://irrte.ch/jwt-js-decode/pem2jwk.html

{
	"kid": "test",
	"kty": "RSA",
	"n": "u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw",
	"e": "AQAB"
}

PKCS8

👉PKCS = Public Key Cryptography Standards
👉公開鍵暗号のための仕様。
👉PKCS1, PKCS2, ... と色々ある。Wiki - PKCS
👉PKCS8 = 秘密鍵用(private certificate keypairs (encrypted or unencrypted).

参考:RSA鍵、証明書のファイルフォーマットについて

PEM形式の公開鍵ファイル/秘密鍵ファイルを生成する。

手順1: -mオプションでpemを選択して2ファイルを生成する。

名前はhoge -> hogeとhoge.pubの鍵ファイルが生成される。
C:\Users\daisu\Desktop\oidc-practice\keys>ssh-keygen -m pem
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\daisu/.ssh/id_rsa): hoge

中身

hoge(秘密鍵ファイル)
-----BEGIN RSA PRIVATE KEY-----
MIIG4gIBAAKCAYEA5bGGox4U3VW5EotikwTEC6beaqDIvusDaNYJBd6/0jMJn1+X
tQAicv+QCjbECG9NCqVsRVRzUiyfLy5OUw0jeHYVE0CTJ2gMAKeG/8sMwop0PpjZ
hoge.pub(公開鍵ファイル)
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDlsYajHhTdVbkSi2KTBMQLpt5qo

(´_ゝ`)「なぜか公開鍵ファイルの方だけPEM形式ではないので変換してあげる。」

手順2: hoge.pubをINPUTに、PEM形式に変換したものを新しいファイルfuga.pubに出力する。

ssh-keygen -f hoge.pub -e -m pem > fuga.pub

🎉PEM形式になってる

fuga.pub
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA5bGGox4U3VW5EotikwTEC6beaqDIvusDaNYJBd6/0jMJn1+XtQAi
cv+QCjbECG9NCqVsRVRzUiyfLy5OUw0jeHYVE0CTJ2gMAKeG/8sMwop0PpjZDOK3

秘密鍵で署名を作成、公開鍵で署名を検証する。

作成した秘密鍵と公開鍵を使って、署名の作成&検証を行う。
👉動作環境:NodeJS
👉署名の作成&検証するためのライブラリ:crypto
👉署名アルゴリズム:RS256

crypto vs crypto-js

cryptoはNodeJS標準搭載なのでnpm installする必要がない。
同じ暗号ライブラリであるcrypto-jsの方がTrend的には人気だが、今回利用したい署名アルゴリズムRS256cryptoの方でしか利用できなそう?なので今回はcryptoを使う。

参考:npmtrend

RS256 vs HS256

署名するためのアルゴリズム。公開鍵/秘密鍵を利用した非対称アルゴリズム(RS256)に対して、共通鍵を利用した対称アルゴリズム(HS256)も存在する。

署名アルゴリズム 説明
RS256 RSA Signature with SHA-256の略称。
RSA(公開鍵暗号)✕ハッシュ関数(SHA-256)を利用した署名(RSA Signature)
HS256 HMAC with SHA-256

署名=公開鍵を用いて改ざんを検知する技術/仕組み

参考:RS256 と HS256 ってなにが違うの

NodeJSプロジェクト作成

NodeJSプロジェクト作成
mkdir oidc-practice
npm init
先ほど作成した鍵ファイル2つをkeysフォルダに配置
mkdir keys
// keysフォルダ配下に2ファイル配置

秘密鍵で署名を作成 → 公開鍵で署名を検証する。

const fs = require(`fs`);
const path = require(`path`);
const cryptor = require('crypto');

// 署名する文字列
const signTarget = 'some data to sign';

const signer = cryptor.createSign('RSA-SHA256');
// 署名する文字列を書き込む
signer.update(signTarget);
const privateKey = fs.readFileSync(path.resolve('./keys/hoge'), "utf8");

// 秘密鍵で署名を作成
const signature = signer.sign(privateKey, 'base64');

const verifyer = cryptor.createVerify('RSA-SHA256');
// 署名する文字列を書き込む
verifyer.write(signTarget);
const publicKey = fs.readFileSync(path.resolve('./keys/fuga.pub'), "utf8");

// 公開鍵で署名を検証
console.log(verifyer.verify(publicKey, signature, 'base64'));

🎉成功

C:\Users\daisu\Desktop\oidc-practice>node index.js
true

自分用メモ:手こずったところ

ssh-keygen オプション未指定で秘密鍵/公開鍵を作成。
 ↓
✕秘密鍵による署名作成部分でエラー

Error: error:0909006C:PEM routines:get_name:no start line

👉秘密鍵がでPEM形式ではないのが原因。

こうじゃなくて
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACF>
NhAAAAAwEAAQAAAgEAsOvD4B2eDNB+IjNe3P6uU/KIzkO+nwHAGyjc4hB>
こう
-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAu9AMgGH5CTIS532iAJ+Q1PKkFvvwmHZgHyscHJiy9qtQGy+q
JRNtyOsgywWnOVvQIC25uhJMHCh9LHDCngHbD/ej/RrCp+Edhso/jOTqEF6UrTY8

参考:ssh-keygen で生成された OPENSSH フォーマットの秘密鍵を pem フォーマットへ変換する

ssh-keygen -m pemでPEM形式の秘密鍵を生成。
 ↓
✅秘密鍵による署名作成はOK。
✕公開鍵による署名検証部分でまた同じエラー。

Error: error:0909006C:PEM routines:get_name:no start line

👉ssh-keygen -t rsa -b 4096だと秘密鍵はPEM形式なのに公開鍵はPEM形式ではないのが原因。下記のコマンドで、公開鍵をPEM形式に変換する。

ssh-keygen -f keys/hoge.pub -e -m pem > keys/fuga.pub
こうじゃなくて
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC70AyAYfkJMhLnfaIAn5DU8qQW+/CYdmAfKxwcmLL2q1AbL6olE23I6yDLBac5W9AgLbm6EkwcKH0scMKeAdsP96P9GsKn4R2Gyj+M5OoQXpStNjwl456BnH7UL1q6lE7QVQXv1Odi
こう
-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAu9AMgGH5CTIS532iAJ+Q1PKkFvvwmHZgHyscHJiy9qtQGy+qJRNt
yOsgywWnOVvQIC25uhJMHCh9LHDCngHbD/ej/RrCp+Edhso/jOTqEF6UrTY8JeOe

参考:【JWT(JSON Web Token)】Node.jsで実際に使ってみた(公開鍵・秘密鍵方式)

🎉成功

C:\Users\daisu\Desktop\oidc-practice>node index.js
true

Discussion