RubyでGoogleのJWKにあるmodulusとexponentからOpenSSLを使って公開鍵を生成する

# JWKとは

> JWK は JWS や JWE などで利用する鍵を JSON 形式で表現するための仕様です。上記2つとよくセットで利用されます。OpenID Connect でも、OpenID Connect Discovery をサポートしているような IdP では大体公開鍵を JWK Set 形式で公開していますね。

（http://oauth.jp/blog/2015/05/20/jose-and-oauth-assertion-rfcs/ より引用）

[JWKの仕様](http://tools.ietf.org/html/rfc7517)

# GoogleのOpenID ConnectでJWTの検証について

https://developers.google.com/identity/protocols/OpenIDConnect?hl=ja#validatinganidtoken

取得したID tokenをtokeninfoエンドポイントに投げれば簡単に検証できるけど、デバッグ目的なので実際には公開鍵を取得してローカルで検証する方法を推奨している。

で、JWKの公開先である https://www.googleapis.com/oauth2/v3/certs にアクセスすると下記のようなJWKが取得できる。

```json
{
 "keys": [
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "78b0247ef5fd27b3ce7273b5a404c4e86a92a24d",
   "n": "61W3abyQw6JJUrCXDjn7w-SozhaGrqiqO-ykjd0B2r0jHi-dlsgIrj8fREOXZaJnRVZb0pFrUEgUcvBBuLIXHxGN-8k-XatRuwrOxH_m2GQMgtemLEc-xD_iE-4V7vwySSW6_Jb6Ltpen9gGakkG0p-ZTcBbtVsJZbRsS5RtiryeOtwss7_bbGGl33X0qKNKPFygTEDskUSnW5Fwd1vuAQfVT1dJk2PE99WjUGnO3-GbYTiueo5Gf5C9j3E1UBkLgjzax_YqqAiVlpv2jTR6--N6bVmOaeMjC_Sjxlaw1YfmULzlEK5IZi5DA-HvYJhfwTA65IHcakU6PM67skTsuw",
   "e": "AQAB"
  },
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "1bfc384edfa2933c1fe97f20579f5dbadc62c1a8",
   "n": "2hZF8h1a0KOPUZcZhJdyUKrLoigrgW7mD6MqKT8P7dUcD-JJkCBhj7xtSysmPsY4fzXRkCvziRlogft0bz5UTWVnmlqUtpHQo39h-pNhZ5r9FekCqo6g5r_nmGag36j-5iZ4Z-dDFMhtSTzIM61UkYKaEhl2OhPba0NQBdP5NuoNwTtQ5ELvoqkmpHVIoBCN8iCsY9IOei_mDqJRlHpAmn8un1bWRunvk9y4jWCzV-alxdap4VRAXcOXtHYe9Yx4jJsVZafukyuS9EKiCKGz95we_4v3qWu-gCUK2o7Uf-V6jo9Ff4IFasQaG7zJoKYnIYIfOzDm093IrUDsqsafrQ",
   "e": "AQAB"
  }
 ]
}
```

ここにあるn(modulus)とe(exponent)がRSA暗号の公開鍵となっており、これらとOpenSSLを使って公開鍵を生成するサンプルプログラムが下記の通り（Ruby 2.2.2で確認した）。

```rb:jwk.rb
require 'json'
require 'base64'
require 'openssl'

def public_key(jwk)
  modulus = openssl_bn(jwk['n'])
  exponent = openssl_bn(jwk['e'])
  sequence = OpenSSL::ASN1::Sequence.new(
    [OpenSSL::ASN1::Integer.new(modulus),
      OpenSSL::ASN1::Integer.new(exponent)])
  OpenSSL::PKey::RSA.new(sequence.to_der)
end

def openssl_bn(n)
  n = n + '=' * (4 - n.size % 4) if n.size % 4 != 0
  decoded = Base64.urlsafe_decode64 n
  unpacked = decoded.unpack('H*').first
  OpenSSL::BN.new unpacked, 16
end

jwk = <<'EOS'
{
  "kty": "RSA",
  "alg": "RS256",
  "use": "sig",
  "kid": "78b0247ef5fd27b3ce7273b5a404c4e86a92a24d",
  "n": "61W3abyQw6JJUrCXDjn7w-SozhaGrqiqO-ykjd0B2r0jHi-dlsgIrj8fREOXZaJnRVZb0pFrUEgUcvBBuLIXHxGN-8k-XatRuwrOxH_m2GQMgtemLEc-xD_iE-4V7vwySSW6_Jb6Ltpen9gGakkG0p-ZTcBbtVsJZbRsS5RtiryeOtwss7_bbGGl33X0qKNKPFygTEDskUSnW5Fwd1vuAQfVT1dJk2PE99WjUGnO3-GbYTiueo5Gf5C9j3E1UBkLgjzax_YqqAiVlpv2jTR6--N6bVmOaeMjC_Sjxlaw1YfmULzlEK5IZi5DA-HvYJhfwTA65IHcakU6PM67skTsuw",
  "e": "AQAB"
}
EOS

jwk_public_key = public_key(JSON.parse(jwk))

puts "=== JWK ==="
puts "Modulus: #{jwk_public_key.n}"
puts "Exponent: #{jwk_public_key.e}"
puts
puts "JWK Public Key"
puts jwk_public_key.to_pem
```

生成した公開鍵を使って検証すれば署名が正しいか確認できるはず。

ちなみに、https://www.googleapis.com/oauth2/v1/certs にあるX.509の署名を使うこともできる（バージョニングされてるぐらいなのでいつまで使えるかは不明）。

```json
{
 "78b0247ef5fd27b3ce7273b5a404c4e86a92a24d": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIcOQ8thj+cTYwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xNTEyMDYxMzI4MzRaFw0xNTEyMDgwMjI4MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrVbdpvJDDoklSsJcOOfvD5KjOFoauqKo7\n7KSN3QHavSMeL52WyAiuPx9EQ5dlomdFVlvSkWtQSBRy8EG4shcfEY37yT5dq1G7\nCs7Ef+bYZAyC16YsRz7EP+IT7hXu/DJJJbr8lvou2l6f2AZqSQbSn5lNwFu1Wwll\ntGxLlG2KvJ463Cyzv9tsYaXfdfSoo0o8XKBMQOyRRKdbkXB3W+4BB9VPV0mTY8T3\n1aNQac7f4ZthOK56jkZ/kL2PcTVQGQuCPNrH9iqoCJWWm/aNNHr743ptWY5p4yML\n9KPGVrDVh+ZQvOUQrkhmLkMD4e9gmF/BMDrkgdxqRTo8zruyROy7AgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQCPuQVBoC1ffCG1N9ceFAanXezPU7vj\n/o9AMR6YpARH4hUrThn0GvqamboRzPQYaRwP9KCqYyCITV/knl/gey5tWAUTdTWY\nRUUzFKdEgNdCsOvMkbpEttiG+bX2rSesxB4/TW0gY1C5TCUdmGKhra7Qj1dBhYAe\nVVJZBPotUmR78cuoF/WzQ398U49cS5F1dAfrFJ433BZ/BIz9zruYiZeg75wZRMb1\ni9muMcC6OwfAMMrQOTNSx7Mc2uJ5tMRCYOAAyQm0/fV5u22bQhkEwz/NhL4OHl0s\n0qe1QaDedOhSevrN4aWzFisyc4S0UrHIMvTQdZ3EjGsO32Ea1kgcW4DT\n-----END CERTIFICATE-----\n",
 "1bfc384edfa2933c1fe97f20579f5dbadc62c1a8": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIOVKk2TYhEccwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xNTEyMDcxMzEzMzRaFw0xNTEyMDkwMjEzMzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaFkXyHVrQo49RlxmEl3JQqsuiKCuBbuYP\noyopPw/t1RwP4kmQIGGPvG1LKyY+xjh/NdGQK/OJGWiB+3RvPlRNZWeaWpS2kdCj\nf2H6k2Fnmv0V6QKqjqDmv+eYZqDfqP7mJnhn50MUyG1JPMgzrVSRgpoSGXY6E9tr\nQ1AF0/k26g3BO1DkQu+iqSakdUigEI3yIKxj0g56L+YOolGUekCafy6fVtZG6e+T\n3LiNYLNX5qXF1qnhVEBdw5e0dh71jHiMmxVlp+6TK5L0QqIIobP3nB7/i/epa76A\nJQrajtR/5XqOj0V/ggVqxBobvMmgpichgh87MObT3citQOyqxp+tAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQAdkbYwkosB442yYnSt0vjx53Tah6u3\n60f9jg9ulv/Go0OzIQ5Py5o9Nl0NHzd/GmWUvJjiFGQThe5MfDJJwzZSlDJGRLoc\n3IvI0nUVSlbOfk/GYu0Ft6zfE/INNdSbcbTI3/s35uRWnUqvP3K742ffeq2rF7EZ\nptfXJHYVYAqmbwn/V48Y9yqOKhrHmXPaD6KKOaY7iecpyjVTSjBYh7Q+t/vd95F1\n5Vq8UPQl8aYmjPlTdL/aQAVYhShsKS7wcVK/pWr+gwt29kVq0ZSNZX9VwVo1Fvjp\nH4UGQPnHLe3SperP2zesT1LK/5klqJ2GCLpUTmo/r52OLvdn+hpAqu5g\n-----END CERTIFICATE-----\n"
}
```

```rb:cert.rb
require 'openssl'

certificate = <<'EOS'
-----BEGIN CERTIFICATE-----
MIIDJjCCAg6gAwIBAgIIcOQ8thj+cTYwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE
AxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe
Fw0xNTEyMDYxMzI4MzRaFw0xNTEyMDgwMjI4MzRaMDYxNDAyBgNVBAMTK2ZlZGVy
YXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrVbdpvJDDoklSsJcOOfvD5KjOFoauqKo7
7KSN3QHavSMeL52WyAiuPx9EQ5dlomdFVlvSkWtQSBRy8EG4shcfEY37yT5dq1G7
Cs7Ef+bYZAyC16YsRz7EP+IT7hXu/DJJJbr8lvou2l6f2AZqSQbSn5lNwFu1Wwll
tGxLlG2KvJ463Cyzv9tsYaXfdfSoo0o8XKBMQOyRRKdbkXB3W+4BB9VPV0mTY8T3
1aNQac7f4ZthOK56jkZ/kL2PcTVQGQuCPNrH9iqoCJWWm/aNNHr743ptWY5p4yML
9KPGVrDVh+ZQvOUQrkhmLkMD4e9gmF/BMDrkgdxqRTo8zruyROy7AgMBAAGjODA2
MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG
AQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQCPuQVBoC1ffCG1N9ceFAanXezPU7vj
/o9AMR6YpARH4hUrThn0GvqamboRzPQYaRwP9KCqYyCITV/knl/gey5tWAUTdTWY
RUUzFKdEgNdCsOvMkbpEttiG+bX2rSesxB4/TW0gY1C5TCUdmGKhra7Qj1dBhYAe
VVJZBPotUmR78cuoF/WzQ398U49cS5F1dAfrFJ433BZ/BIz9zruYiZeg75wZRMb1
i9muMcC6OwfAMMrQOTNSx7Mc2uJ5tMRCYOAAyQm0/fV5u22bQhkEwz/NhL4OHl0s
0qe1QaDedOhSevrN4aWzFisyc4S0UrHIMvTQdZ3EjGsO32Ea1kgcW4DT
-----END CERTIFICATE-----
EOS

x509_cert = OpenSSL::X509::Certificate.new(certificate)

puts
puts "=== X.509 ==="
puts "Modulus: #{x509_cert.public_key.n}"
puts "Exponent: #{x509_cert.public_key.e}"
puts
puts "X.509 Public Key"
puts x509_cert.public_key.to_pem
```

（ちなみにちなみに、nのBase64パディングを残したままの https://www.googleapis.com/oauth2/v2/certs もある。）