以前調べたJWTについて、jwt.ioで検索できるライブラリを使わずに、Googleで認証した署名の検証を試してみようと思い立った。
ただ、自作のスクリプトで署名の検証を計算している記事をいくつか見かけたが、さすがにそこまでフルスクラッチでやる気にはなれず。
OpenSSLで公開鍵を作り、それで検証できないかと思い、試してみたらうまくいったのでメモ。
環境
Windows 10 Pro 64bit、Groovy 2.5.7、Git for Windows v2.24.1.2付属のGit Bash、OpenSSL 1.1.1dで確認。
RSA公開鍵の作成
e(RSA暗号の公開指数)とn(RSA暗号のモジュラス)から公開鍵を作成する方法があるか調べたところ、以下のページを見つけた。
openssl asn1parse -genconf ${入力ファイル} -out ${出力ファイル}
として、ASN.1形式の入力ファイルを渡すと、DERエンコードされたRSA公開鍵が出力される。
asn1=SEQUENCE:pubkeyinfo [pubkeyinfo] algorithm=SEQUENCE:rsa_alg pubkey=BITWRAP,SEQUENCE:rsapubkey [rsa_alg] algorithm=OID:rsaEncryption parameter=NULL # nとeの順序は固定、逆にすると公開鍵の内容が変わって署名検証に失敗する [rsapubkey] n=INTEGER:0x${nの16進数表記} e=INTEGER:0x010001
あとは openssl rsa -pubin -inform der -in ${DERエンコードされたRSA公開鍵ファイル} -out ${出力ファイル}
でPEMエンコードに変換すればいい。
スクリプト
Googleで認証して、返ってきたJWTから公開鍵情報を取得し、署名検証を行ってみる。
JWTのデコードなどにはGroovyを使用。
Groovyスクリプト
公開鍵情報を取得し、もろもろのファイルを ./output/
配下に出力する以下のGroovyスクリプトを JWT.groovy
として作成。
@Grapes( @Grab(group='commons-codec', module='commons-codec', version='1.13') ) import groovy.json.JsonSlurper import org.apache.commons.codec.binary.Base64 def jsonSlurper = new JsonSlurper() def jwt = args[0].split(java.util.regex.Pattern.quote('.')) def base64Header = jwt[0] def base64Payload = jwt[1] def base64Signature = jwt[2] // Java8のBase64やGroovyのString#decodeBase64では、URLセーフなデコードができない // Commons CodecのBase64#decodeBase64は、うまいことやってくれる def header = jsonSlurper.parseText(new String(Base64.decodeBase64(base64Header))) def keys = jsonSlurper.parseText(new URL('https://www.googleapis.com/oauth2/v3/certs').text).keys def key = keys.find { it.alg == header.alg && it.kid == header.kid } @groovy.transform.SourceURI def sourceURI def outputDir = new File((sourceURI.path as File).parentFile, 'output') outputDir.mkdirs() new File(outputDir, 'plain.txt').write("${base64Header}.${base64Payload}", 'UTF-8') new File(outputDir, 'pubkey.ini').write("""\ asn1=SEQUENCE:pubkeyinfo [pubkeyinfo] algorithm=SEQUENCE:rsa_alg pubkey=BITWRAP,SEQUENCE:rsapubkey [rsa_alg] algorithm=OID:rsaEncryption parameter=NULL [rsapubkey] n=INTEGER:0x${Base64.decodeBase64(key.n).encodeHex().toString().toUpperCase()} e=INTEGER:0x${Base64.decodeBase64(key.e).encodeHex().toString().toUpperCase()} """, 'UTF-8') new FileOutputStream(new File(outputDir, 'sig.bin')).withCloseable { it.write(Base64.decodeBase64(base64Signature)) }
シェルスクリプト
先のGroovyスクリプトを呼び出し、出力されたASN.1形式のファイルから公開鍵を作成、署名検証する以下のシェルスクリプトを、Groovyスクリプトと同じディレクトリに jwt.sh
として作成。
#!/bin/bash cd $(dirname ${0}) groovy ./JWT.groovy ${1} cd output # RSA公開鍵の作成 echo '# create pubkey.pem' openssl asn1parse -genconf pubkey.ini -out pubkey.der openssl rsa -pubin -inform der -in pubkey.der -out pubkey.pem # 公開鍵の確認 echo '# show pubkey.pem' openssl rsa -text -noout -pubin -in pubkey.pem # 署名検証 echo '# verify signature' cat plain.txt | openssl dgst -verify pubkey.pem -sha256 -signature sig.bin
実行結果
Google認証時のJWTトークンを引数として jwt.sh
を実行すると、以下の出力が得られた。
# create pubkey.pem 0:d=0 hl=4 l= 290 cons: SEQUENCE 4:d=1 hl=2 l= 13 cons: SEQUENCE 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption 17:d=2 hl=2 l= 0 prim: NULL 19:d=1 hl=4 l= 271 prim: BIT STRING writing RSA key # show pubkey.pem RSA Public-Key: (2048 bit) Modulus: ... Exponent: 65537 (0x10001) # verify signature Verified OK
振り返り
実運用では当然ライブラリを使うが、手作業でやってみるとRSAにおける署名の勉強になった。
あと、自前で計算してる人たちはすごいな...