[ruby] ruby-openid で signature アルゴリズムに SHA256 を使う

解決方法

  OpenID::DefaultNegotiator.allowed_types = [['HMAC-SHA256', 'DH-SHA256']]
  server = OpenID::Server::Server.new(ActiveRecordStore.new, servers_url)

consumer 実装の場合も一緒。
ruby-openid のサンプルコードに従っているのだけど openid.mode = associate 時の provider 側挙動が怪しい。
だんだん ruby-openidデバッグになってくる。
OpenID カンペキ理解のためにも ruby-openid 再実装しちゃろうかーとか考え始める。

以下は調査ログ

  • -

自分のために情報整理を兼ねてメモ。
ruby-openid(2.0.4) を使って OpenID の処理を書くとき signature アルゴリズムSHA1 でなく SHA256 が選びたいのだけど、どうも OpenID::Consumer の実装が SHA1 以外選べないようになっている気がする。
今日は追跡記録を書いて、明日、SHA256 を使う為の実装を書いてみよう。

RailsOpenID リクエストを発行するには以下のように

consumer = OpenID::Consumer.new(session, store)
oidreq = consumer.begin(params[:openid_identifier])

begin を実行すると OpenID でいう discovery, association 両方のプロセスが実行される。
begin(consumer.rb)の中身は

    def begin(openid_identifier, anonymous=false)
      # discovery プロセスの実行
      manager = discovery_manager(openid_identifier)
      service = manager.get_next_service(&method(:discover))

      if service.nil?
        raise DiscoveryFailure.new("No usable OpenID services were found "\
                                   "for #{openid_identifier.inspect}", nil)
      else
        # association プロセスの実行
        begin_without_discovery(service, anonymous)
      end
    end

begin_without_discovery の中身は

    def begin_without_discovery(service, anonymous)
      # get_association で OP と通信を行い Association を確立する
      assoc = association_manager(service).get_association
      checkid_request = CheckIDRequest.new(assoc, service)
      checkid_request.anonymous = anonymous

      if service.compatibility_mode
        rt_args = checkid_request.return_to_args
        rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce
        rt_args[Consumer.openid1_return_to_claimed_id_name] =
          service.claimed_id
      end

      self.last_requested_endpoint = service
      return checkid_request
    end

get_association(associationmanager.rb)は

      def get_association
        if @store.nil?
          return nil
        end
        assoc = @store.get_association(@server_url)
        if assoc.nil? || assoc.expires_in <= 0
          # ここで association  生成
          assoc = negotiate_association
          if !assoc.nil?
            @store.store_association(@server_url, assoc)
          end
        end

        return assoc
      end

で、また negotiate_association を追う

      def negotiate_association
        assoc_type, session_type = @negotiator.get_allowed_type
        begin
          return request_association(assoc_type, session_type)
        rescue ServerError => why
          supported_types = extract_supported_association_type(why, assoc_type)
          if !supported_types.nil?
            # Attempt to create an association from the assoc_type and
            # session_type that the server told us it supported.
            assoc_type, session_type = supported_types
            begin
              return request_association(assoc_type, session_type)
            rescue ServerError => why
              Util.log("Server #{@server_url} refused its suggested " \
                       "association type: session_type=#{session_type}, " \
                       "assoc_type=#{assoc_type}")
              return nil
            end
          end
        end
      end

assoc_type と session_type の中身を見るとそれぞれ HMAC-SHA1 と DH-SHA1アサインされている。
で、どこで決まっているのかと get_allowed_type を追うと
get_allowed_type(association.rb)

    def get_allowed_type
      @allowed_types.empty? ? nil : @allowed_types[0]
    end

allowed_types は AssociationNegotiator オブジェクトのメンバで association.rb から AssociationNegotiator 関連の実装を抜き出すと

module OpenID
  class AssociationNegotiator
    attr_reader :allowed_types

    def self.get_session_types(assoc_type)
      case assoc_type
      when 'HMAC-SHA1'
        ['DH-SHA1', 'no-encryption']
      when 'HMAC-SHA256'
        ['DH-SHA256', 'no-encryption']
      else
        raise StandardError, "Unknown association type #{assoc_type.inspect}"
      end
    end

    def initialize(allowed_types)
      self.allowed_types=(allowed_types)
    end

    def allowed_types=(allowed_types)
      allowed_types.each do |assoc_type, session_type|
        self.class.check_session_type(assoc_type, session_type)
      end
      @allowed_types = allowed_types
    end

    def get_allowed_type
      @allowed_types.empty? ? nil : @allowed_types[0]
    end
  end
  DefaultNegotiator =
    AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
                               ['HMAC-SHA1', 'no-encryption'],
                               ['HMAC-SHA256', 'DH-SHA256'],
                               ['HMAC-SHA256', 'no-encryption']])

  EncryptedNegotiator =
    AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
                               ['HMAC-SHA256', 'DH-SHA256']])
end

これ、DefaultNegotiator でも EncryptoNegotiator でも get_allowed_type を使う限り SHA1 が常に呼ばれてしまう。

と同時に、「新しい Negotiator を定義する」方法が使えそう。
では negotiator の決定をどこで行っているのかと探すと consumer.rb の begin_without_discovery で決まっている。

      assoc = association_manager(service).get_association

association_manager の実装は

    def association_manager(service)
      AssociationManager.new(@store, service.server_url,
                             service.compatibility_mode, negotiator)
    end

negotiator の実装は

    def negotiator
      DefaultNegotiator
    end

追跡編は以上。

明日は SHA256 を使えるようにしてみよう。