HealthCards::Verification.verify_using_key_set
def verify_using_key_set(verifiable, key_set = nil, resolve_keys: true)
jws = verifiable.is_a?(HealthCards::HealthCard) ? verifiable.jws : JWS.from_jws(verifiable)
key_set ||= HealthCards::KeySet.new
key_set.add_keys(resolve_key(jws)) if resolve_keys && key_set.find_key(jws.kid).nil?
key = key_set.find_key(jws.kid)
unless key
raise MissingPublicKeyError,
'Verifier does not contain public key that is able to verify this signature'
end
jws.public_key = key
jws.verify
end
まずはコメントアウト部分を読んでみます。
指定されたKeySetでヘルスカードを確認します。
verifiable (HealthCards::JWS または String) : 確認するヘルスカード
key_set(HealthCards::KeySet または nil) : キーを取得または追加する必要があるKeySet
resolve_keys(Boolean) : キーを解決する必要がある場合は true
返り値はBoolean
一通り概要は掴めました。具体的な処理も見ていきます。
verifiable.jws
$ jws = verifiable.is_a?(HealthCards::HealthCard) ? verifiable.jws : JWS.from_jws(verifiable)
=>
@header={"zip"=>"DEF", "alg"=>"ES256", "kid"=>"kp4-CyUBQV4x5nBpAzITzdkJJPVrRekiiHs2JrMqpPw"},
@key=
@key=
@public_key=
@coordinates=
{:x=>"BePU7QxXPU9lSSKJ7w_7uYg-67qAic347XKfNUhs_lQ",
:y=>"6QVRjH1arEJcFYsBw1wd_CbvpW-iT5HV7LcSzsH3v_0",
:kty=>"EC",
:crv=>"P-256"},
@key=
@payload=
"-\x8DM\x0E\x820\x10\x85\xEF2n\xB1@D\xA2]z\x05\x8D\e\xE3\xA2\x94!\xADi\v\x99\x0EFC\xBC\xBB-:\xBB7\xEF\xE7[\xC0\xC6\b\x12\f\xF3$\xCB\x12_\xCAO\x0E\x85\x1E=\x14\x10\xBA\x01d\xDD6uu\xDC\x1D\xF6m\x01O\rr\x01~O\b\xF2\xB6vb*E\xAF\x88\r*\xC7FhE}\xDC\xFC\xC46\v\xB8\x17\xA0\t{\fl\x95;\xCF\xDD\x035\xE7\x95\xC1X\xBA\"E;\x86\xC4oD%\xEA\x84\xCC\xDF\xD3\x1Cz\x879C\x18\xC7\x994^V\"\xFC\x8DO\xBA/",
@public_key=
@coordinates=
{:x=>"BePU7QxXPU9lSSKJ7w_7uYg-67qAic347XKfNUhs_lQ",
:y=>"6QVRjH1arEJcFYsBw1wd_CbvpW-iT5HV7LcSzsH3v_0",
:kty=>"EC",
:crv=>"P-256"},
@key=
HealthCardクラスの処理を見たところ、initializeメソッドでjwsが定義されているようでした。
github.com
module HealthCards
class HealthCard
extend Forwardable
attr_reader :jws
def_delegator :@qr_codes, :code_by_ordinal
def_delegators :@payload, :bundle, :issuer
def initialize(jws)
@jws = JWS.from_jws(jws)
@payload = Payload.from_payload(@jws.payload)
@qr_codes = QRCodes.from_jws(@jws)
end
end
次に key_set.find_key の処理を見てみます。
HealthCards::KeySet#find_key
github.com
def find_key(kid)
@key_map[kid]
end
KeySetインスタンス生成時に定義される@key_map変数の中の値を引数のkidで走査して、該当するkidを持つ値があればそれを返却するメソッドでした。
取得したkeyをjwsのpublic_keyとして代入し、その後 HealthCards::JWS#verifyを呼び出しているようなのでコードを読んでみます。
HealthCards::JWS#verify
github.com
def verify
raise MissingPublicKeyError unless public_key
public_key.verify(jws_signing_input, signature)
end
public_key属性が nil または falseの場合は例外をraiseさせていました。
検証する処理はHealthCard::PublicKeyのインスタンスが担っているっぽいですね。
その前にpublic_key.verifyに渡している引数の中身を見てみましょう。
From: health_cards/lib/health_cards/jws.rb:105 HealthCards::JWS
102: def verify
103: raise MissingPublicKeyError unless public_key
104:
=> 105: public_key.verify(jws_signing_input, signature)
106: end
$ jws_signing_input
=> "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtwNC1DeVVCUVY0eDVuQnBBeklUemRrSkpQVnJSZWtpaUhzMkpyTXFwUHcifQ.LY1NDoIwEIXvMm6xQESiXXoFjRvjopQhrWkLmQ5GQ7y7LTq7N-_nW8DGCBIM8yTLEl_KTw6FHj0UELoBZN02dXXcHfZtAU8NcgF-TwjytnZiKkWviA0qx0ZoRX3c_MQ2C7gXoAl7DGyVO8_dAzXnlcFYuiJFO4bEb0Ql6oTM39Mceoc5QxjHmTReViL8jU-6Lw"
$ signature
=>"\x11\xB7C\xF3!z\x18\xA0\xDC\x1F\xFE\xB7\x9A\xCD\xDCo[\xCAM\xAF:\xE8\x15\xFE\x00\x10\f8\xF0\xB5H\x87\xD8\x7FU\x92\xE5\xC3\x06\xC7\xE6|\x84\xB3\xBB{\xDB>\x8F\xDF]\xA2\xAE?\x88\xB4]\xD9\x8AC\xD1t_\x0F"
$ method(:jws_signing_input).source_location
=> ["health_cards/lib/health_cards/jws.rb", 110]
jws_signing_inputのコードはこちら。
github.com
def jws_signing_input
"#{JWS.encode(@header.to_json)}.#{encoded_payload}"
end
stepメソッドで処理を追います。
From: health_cards/lib/health_cards/jws.rb:111 HealthCards::JWS
110: def jws_signing_input
=> 111: "#{JWS.encode(@header.to_json)}.#{encoded_payload}"
112: end
$ @header.to_json
=> "{\"zip\":\"DEF\",\"alg\":\"ES256\",\"kid\":\"kp4-CyUBQV4x5nBpAzITzdkJJPVrRekiiHs2JrMqpPw\"}"
$ JWS.encode(@header.to_json)
=> "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtwNC1DeVVCUVY0eDVuQnBBeklUemRrSkpQVnJSZWtpaUhzMkpyTXFwUHcifQ"
HealthCard::JWS.encodeのコードはこちら。
github.com
require 'base64'
module HealthCards
module Encoding
def encode(data)
Base64.urlsafe_encode64(data, padding: false).gsub("\n", '')
end
def decode(data)
Base64.urlsafe_decode64(data)
end
end
end
Base64モジュールのurlsafe_encode64を padiing: false で呼び出して、改行文字を消したdataを返すメソッドになっていました。
padding: falseにして呼び出すと、==を消した上でencodeしてくれるようです。
docs.ruby-lang.org
encoded_payloadメソッドも内部でJWS.encodeメソッドを呼んでいました。
さて、ようやく HealthCard::PublicKey#verifyメソッドにたどり着きました、、、
HealthCard::PublicKey#verify
github.com
def verify(payload, signature)
@key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), payload)
end
コンソールで試します。
From: health_cards/lib/health_cards/public_key.rb:11 HealthCards::PublicKey
10: def verify(payload, signature)
=> 11: @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), payload)
12: end
$ @key
=>
$ payload
=> "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtwNC1DeVVCUVY0eDVuQnBBeklUemRrSkpQVnJSZWtpaUhzMkpyTXFwUHcifQ.LY1NDoIwEIXvMm6xQESiXXoFjRvjopQhrWkLmQ5GQ7y7LTq7N-_nW8DGCBIM8yTLEl_KTw6FHj0UELoBZN02dXXcHfZtAU8NcgF-TwjytnZiKkWviA0qx0ZoRX3c_MQ2C7gXoAl7DGyVO8_dAzXnlcFYuiJFO4bEb0Ql6oTM39Mceoc5QxjHmTReViL8jU-6Lw"
$ signature
=> "\x11\xB7C\xF3!z\x18\xA0\xDC\x1F\xFE\xB7\x9A\xCD\xDCo[\xCAM\xAF:\xE8\x15\xFE\x00\x10\f8\xF0\xB5H\x87\xD8\x7FU\x92\xE5\xC3\x06\xC7\xE6|\x84\xB3\xBB{\xDB>\x8F\xDF]\xA2\xAE?\x88\xB4]\xD9\x8AC\xD1t_\x0F"
$ OpenSSL::Digest.new('SHA256')
=>
$ raw_to_asn1(signature, self)
=> "0E\x02 \x11\xB7C\xF3!z\x18\xA0\xDC\x1F\xFE\xB7\x9A\xCD\xDCo[\xCAM\xAF:\xE8\x15\xFE\x00\x10\f8\xF0\xB5H\x87\x02!\x00\xD8\x7FU\x92\xE5\xC3\x06\xC7\xE6|\x84\xB3\xBB{\xDB>\x8F\xDF]\xA2\xAE?\x88\xB4]\xD9\x8AC\xD1t_\x0F"
$ method(:raw_to_asn1).source_location
=> ["health_cards/lib/health_cards/public_key.rb", 22]
raw_to_asn1メソッドを読んでみますか。
HealthCards::PublicKey.raw_to_asn1
github.com
def raw_to_asn1(signature, key)
byte_size = (key.group.degree + 7) / 8
sig_bytes = signature[0..(byte_size - 1)]
sig_char = signature[byte_size..] || ''
OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map do |int|
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2))
end).to_der
end
ANS1がよくわからなかったので簡単に調べてみました。
ASN.1 【Abstract Syntax Notation One】
ASN.1とは、データ構造の表現形式を定義するための標準的な記法の一つ。主に通信プロトコル(通信規約)で扱われるデータの送受信単位(PDU:Protocol Data Unit)の定義に用いられる。
出典: https://e-words.jp/w/ASN.1.html
概要は掴めました。(この記事内での深掘りは避けます、、、)
次に、More info のリンク先を確認してみます。
全てアクセスして確認したところ、重要そうなのは以下のリンクでした。
github.com
以下、引用と翻訳の大意です。
ruby-jwt fails with a Signature verification failed (JWT::VerificationError) error when decoding a valid JWT signed with ES512.
I believe this is due to an issue with improper ECDSA signature serialization format where the signature parameters are not concatenated in the proper order.
The improper serialization results in an implementation being able to successfully verify its own JWTs but not those generated by other conforming libraries.
We encountered the same issue and fixed it on PyJWT a little while ago but some users are now reporting issues with ruby-jwt validating ECDSA-signed tokens and I wanted to pass along the information.
PyJWT now has tests validating it against RFC 7520 test vectors to ensure all algorithms serialize properly. It may be a good idea to do a similar thing here.
ES512署名アルゴリズムで署名された有効なJWTをデコードすると、ruby-jwtがエラーで失敗します。Signature verification failed (JWT::VerificationError)
これは、署名パラメータが適切な順序で連結されていない、不適切なECDSA署名シリアル化形式の問題が原因であると考えられます。
不適切なシリアル化により、実装は自身のJWTを正常に検証できますが、他の準拠ライブラリによって生成されたJWTは検証できません。
出典: https://github.com/jwt/ruby-jwt/issues/84
このissueの指摘事項を修正したPRが以下でした。
github.com
修正内容を見ると、ASN.1 DERフォーマットを考慮した実装に修正したようです。
なぜASN.1 DERフォーマットを考慮する必要があるのでしょうか。
参考になりそうな記事を見つけたので引用します。
SHA-256 with ECDSAによる電子署名は、各32オクテットのRとSの値で表現されます。
JWTのシグネチャに格納する際は、RFC 7518に記載されているように、RとSを単純に連結して64オクテットのバイナリデータとしてシグネチャを生成し、これをURLセーフBase64にエンコードします。
WebCrypto APIでcrypto.subtle.sign()を実行してSHA-256 with ECDSAによって署名する場合は、このRとSの単純連結した64オクテットのUint8Arrayを実行結果として取得できます。
よって、これをURLセーフBase64にエンコードして、ペイロードと.を挟んで連結することで、JWTを生成することができます。
一方、Java 8等では、ECDSAによる電子署名はASN.1 DERフォーマットで扱われるため、JWTの作成や検証の際はRとSの単純連結との相互変換を行う必要があります。
なお、WebCrypto APIでは、このASN.1 DERフォーマットには対応していません。
出典: https://qiita.com/tomoyukilabs/items/b346a71a920eb7a93501
なるほど、ASN.1DERフォーマットを扱うパターンもあるために考慮の必要があったようです。
かなり寄り道しましたが、最後に OpenSSL::PKey::PKey#verify を見てみましょう。
OpenSSL::PKey::PKey#verify
公式ドキュメントがありました。
docs.ruby-lang.org
verify(digest, sign, data) -> bool
data を秘密鍵で署名したその署名文字列が sign であることを公開鍵を使って検証し、検証に成功すれば true を返します。
digest は利用するハッシュ関数の名前を "sha256" や "md5" といった文字列で指定します。
DSA で検証をする場合はハッシュ関数には "dss1" を指定してください。
検証に失敗した、つまり署名時と異なるハッシュ関数を使った、 sign が正しい署名でなかった場合などは false を返します。
[PARAM] digest:
利用するハッシュ関数の名前
[PARAM] sign:
検証に利用する署名文字列
[PARAM] data:
検証対象の文字列
[EXCEPTION] OpenSSL::PKey::PKeyError:
検証時にエラーが起きた場合に発生します。正しい署名でなかった場合など、検証に失敗した場合はこの例外は発生しないことに注意してください
出典: https://docs.ruby-lang.org/ja/latest/method/OpenSSL=3a=3aPKey=3a=3aPKey/i/verify.html
こちらもコンソールで確認してみましょう。
From: health_cards/lib/health_cards/public_key.rb:11 HealthCards::PublicKey
10: def verify(payload, signature)
=> 11: @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), payload)
12: end
$ @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), payload)
=> true
$ @key.verify(OpenSSL::Digest.new('SHA512'), raw_to_asn1(signature, self), payload)
=> false
$ _signature = _signature.gsub("\x11", "\x12")
$ @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(_signature, self), payload)
=> false
$ _payload = payload.gsub("e", "f")
$ @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), _payload)
=> false
署名時と異なるハッシュ関数を使ったり、signatureが間違っていた場合は検証に失敗することが確認できました。