こんにちは!kossyです!
今回はSSOを実現するGem「ruby-saml」で「Invalid Signature on SAML Response」が返ってきた場合の調査方法について、
備忘録としてブログに残してみたいと思います。
環境
Ruby 2.6.8
Rails 6.0.4
MacOS BigSur
ruby-saml 1.13.0
なお、サンプルのアプリケーションはoneloginが提供している以下のアプリを使っているものとします。
コードリーディング
fail.html.erbをレンダリングしている処理はこちらでした。
def acs settings = Account.get_saml_settings(get_url_base) response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: settings) if response.is_valid? session[:nameid] = response.nameid session[:attributes] = response.attributes @attrs = session[:attributes] logger.info "Sucessfully logged" logger.info "NAMEID: #{response.nameid}" render :action => :index else logger.info "Response Invalid. Errors: #{response.errors}" @errors = response.errors render :action => :fail end end
なので、acsアクションにbinding.pryを定義し、pry-byebug Gemを入れることで使えるようになるstepメソッドを使って、
原因を探りたいと思います。
20: def acs 21: settings = Account.get_saml_settings(get_url_base) 22: response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: settings) 23: 24: binding.pry 25: => 26: if response.is_valid? 27: session[:nameid] = response.nameid 28: session[:attributes] = response.attributes 29: @attrs = session[:attributes] 30: logger.info "Sucessfully logged" 31: logger.info "NAMEID: #{response.nameid}" 32: render :action => :index 33: else 34: logger.info "Response Invalid. Errors: #{response.errors}" 35: @errors = response.errors 36: render :action => :fail 37: end 38: end $ response.is_valid? => false $ step From: /usr/local/bundle/gems/ruby-saml-1.13.0/lib/onelogin/ruby-saml/response.rb:79 OneLogin::RubySaml::Response#is_valid?: 78: def is_valid?(collect_errors = false) => 79: validate(collect_errors) 80: end $ step # Validates the SAML Response (calls several validation methods) # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate(collect_errors = false) reset_errors! return false unless validate_response_state validations = [ :validate_version, :validate_id, :validate_success_status, :validate_num_assertion, :validate_no_duplicated_attributes, :validate_signed_elements, :validate_structure, :validate_in_response_to, :validate_one_conditions, :validate_conditions, :validate_one_authnstatement, :validate_audience, :validate_destination, :validate_issuer, :validate_session_expiration, :validate_subject_confirmation, :validate_name_id, :validate_signature ] if collect_errors validations.each { |validation| send(validation) } @errors.empty? else validations.all? { |validation| send(validation) } end end # ですよね()という結果 $ validations.all? { |validation| send(validation) } => false
実際にvalidationを実行しているのは、send(validation)の部分ですね。
ここは全て読むのは辛いので、「Invalid Signature」でruby-samlのコードを検索してみます。
# Validates the Signature # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_signature error_msg = "Invalid Signature on SAML Response" # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response # otherwise, review if the decrypted assertion contains a signature sig_elements = REXML::XPath.match( document, "/p:Response[@ID=$id]/ds:Signature", { "p" => PROTOCOL, "ds" => DSIG }, { 'id' => document.signed_element_id } ) use_original = sig_elements.size == 1 || decrypted_document.nil? doc = use_original ? document : decrypted_document # Check signature nodes if sig_elements.nil? || sig_elements.size == 0 sig_elements = REXML::XPath.match( doc, "/p:Response/a:Assertion[@ID=$id]/ds:Signature", {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG}, { 'id' => doc.signed_element_id } ) end if sig_elements.size != 1 if sig_elements.size == 0 append_error("Signed element id ##{doc.signed_element_id} is not found") else append_error("Signed element id ##{doc.signed_element_id} is found more than once") end return append_error(error_msg) end old_errors = @errors.clone idp_certs = settings.get_idp_cert_multi if idp_certs.nil? || idp_certs[:signing].empty? opts = {} opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm idp_cert = settings.get_idp_cert fingerprint = settings.get_fingerprint opts[:cert] = idp_cert if fingerprint && doc.validate_document(fingerprint, @soft, opts) if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) error_msg = "IdP x509 certificate expired" return append_error(error_msg) end end else return append_error(error_msg) end else valid = false expired = false idp_certs[:signing].each do |idp_cert| valid = doc.validate_document_with_cert(idp_cert, true) if valid if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) expired = true end end # At least one certificate is valid, restore the old accumulated errors @errors = old_errors break end end if expired error_msg = "IdP x509 certificate expired" return append_error(error_msg) end unless valid # Remove duplicated errors @errors = @errors.uniq return append_error(error_msg) end end true end
validate_signatureメソッドがエラーメッセージを返していそうなので、処理を追ってみます。
$ send(:validate_signature) => false # stepでvalidate_signatureメソッドの内部に移動 # どうやらここがfalseなのが原因 $ if fingerprint && doc.validate_document(fingerprint, @soft, opts) => false $ fingerprint => "AB:CD:EF:AB:CD:EF:A1:B2:C3:D4:E5:F6:94:C1:B5:8B:00:23:0A:D8:3D:1B:55:DF:41:EA:42:5F:9E:62:07:85" $ doc.validate_document(fingerprint, @soft, opts) => false
どうやらfingerprintが間違っているのが原因のようです。
# IdP section settings.idp_entity_id = "#{idp_base_url}/#{ENV['IDP_GROUP_NAME']}" settings.idp_sso_target_url = "#{idp_base_url}/idp/#{ENV['IDP_GROUP_NAME']}" settings.idp_slo_target_url = "#{idp_base_url}/idp/#{ENV['IDP_GROUP_NAME']}/logout" settings.idp_cert_fingerprint = "#{ENV['IDP_FINGERPRINT']}" settings.idp_cert_fingerprint_algorithm = XMLSecurity::Document::SHA256
settings.idp_cert_fingerprint属性の値に正しいfingerprintを渡してやればOKでした。
まとめ
もし本番運用で「Invalid Signature on SAML Response」に遭遇して、原因が「fingerprintの値が間違っている」だったとして、
原因を特定するまでにかなりの労力を使うような気がしています、、、(エラーメッセージが不親切)
XmlSecurityモジュールのvalidate_documentメソッドも読んでみようと思ったんですが、何をしているのかさっぱりわからなかったので、
Pull Requestのコメントやcommitメッセージを読みつつコードリーディングを進めるのと、
そもそもなぜSAMLによるSSO認証にfingerprintが使われているか、等を調べて知見を深めようと思います、、、