class XMLSecurity::SignedDocument

Attributes

errors[RW]
signed_element_id[RW]

Public Class Methods

new(response, errors = []) click to toggle source
Calls superclass method
# File lib/xml_security.rb, line 162
def initialize(response, errors = [])
  super(response)
  @errors = errors
  extract_signed_element_id
end

Public Instance Methods

validate_document(idp_cert_fingerprint, soft = true) click to toggle source
# File lib/xml_security.rb, line 168
def validate_document(idp_cert_fingerprint, soft = true)
  # get cert from response
  cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
  unless cert_element
    if soft
      return true # temp if no x509Ceritificate then whitelist this.
    else
      raise Samlsso::ValidationError.new("Certificate element missing in response (ds:X509Certificate)")
    end
  end
  base64_cert  = cert_element.text
  cert_text    = Base64.decode64(base64_cert)
  cert         = OpenSSL::X509::Certificate.new(cert_text)

  # check cert matches registered idp cert
  fingerprint = Digest::SHA1.hexdigest(cert.to_der)

  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
    @errors << "Fingerprint mismatch"
    return soft ? false : (raise Samlsso::ValidationError.new("Fingerprint mismatch"))
  end

  validate_signature(base64_cert, soft)
end
validate_signature(base64_cert, soft = true) click to toggle source
# File lib/xml_security.rb, line 193
def validate_signature(base64_cert, soft = true)
  # validate references

  # check for inclusive namespaces
  inclusive_namespaces = extract_inclusive_namespaces

  document = Nokogiri.parse(self.to_s)

  # create a working copy so we don't modify the original
  @working_copy ||= REXML::Document.new(self.to_s).root

  # store and remove signature node
  @sig_element ||= begin
    element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
    element.remove
  end

  # verify signature
  signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
  signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo") unless signed_info_element
  noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
  canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
  noko_sig_element.remove

  # check digests
  if REXML::XPath.first(@sig_element, "//ds:Reference", {"ds"=>DSIG})
    REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
      uri                           = ref.attributes.get_attribute("URI").value

      hashed_element                = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
      canon_algorithm               = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
      canon_hashed_element          = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)

      digest_algorithm_str          = REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG)
      digest_algorithm_str          = REXML::XPath.first(ref, "//ds:DigestMethod") unless digest_algorithm_str
      digest_algorithm              = algorithm(digest_algorithm_str)

      hash                          = digest_algorithm.digest(canon_hashed_element)

      base64_digest                 = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})
      base64_digest                 = REXML::XPath.first(ref, "//ds:DigestValue") unless base64_digest
      digest_value                  = Base64.decode64(base64_digest.text)

      unless digests_match?(hash, digest_value)
        @errors << "Digest mismatch"
        return soft ? false : (raise Samlsso::ValidationError.new("Digest mismatch"))
      end
    end
  else
    REXML::XPath.each(@sig_element, "//ds:Reference") do |ref|
      uri                           = ref.attributes.get_attribute("URI").value

      hashed_element                = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
      canon_algorithm               = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
      canon_hashed_element          = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)

      digest_algorithm_str          = REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG)
      digest_algorithm_str          = REXML::XPath.first(ref, "//ds:DigestMethod") unless digest_algorithm_str
      digest_algorithm              = algorithm(digest_algorithm_str)

      hash                          = digest_algorithm.digest(canon_hashed_element)

      base64_digest                 = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})
      base64_digest                 = REXML::XPath.first(ref, "//ds:DigestValue") unless base64_digest
      digest_value                  = Base64.decode64(base64_digest.text)

      unless digests_match?(hash, digest_value)
        @errors << "Digest mismatch"
        return soft ? false : (raise Samlsso::ValidationError.new("Digest mismatch"))
      end
    end
  end

  base64_signature        = REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG})
  base64_signature        = REXML::XPath.first(@sig_element, "//ds:SignatureValue") unless base64_signature
  signature               = Base64.decode64(base64_signature.text)

  # get certificate object
  cert_text               = Base64.decode64(base64_cert)
  cert                    = OpenSSL::X509::Certificate.new(cert_text)

  # signature method
  signature_method          = REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG})
  signature_method          = REXML::XPath.first(signed_info_element, "//ds:SignatureMethod") unless signature_method
  signature_algorithm       = algorithm(signature_method)

  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
    @errors << "Key validation error"
    return soft ? false : (raise Samlsso::ValidationError.new("Key validation error"))
  end

  return true
end

Private Instance Methods

digests_match?(hash, digest_value) click to toggle source
# File lib/xml_security.rb, line 291
def digests_match?(hash, digest_value)
  hash == digest_value
end
extract_inclusive_namespaces() click to toggle source
# File lib/xml_security.rb, line 300
def extract_inclusive_namespaces
  if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
    prefix_list = element.attributes.get_attribute("PrefixList").value
    prefix_list.split(" ")
  else
    []
  end
end
extract_signed_element_id() click to toggle source
# File lib/xml_security.rb, line 295
def extract_signed_element_id
  reference_element       = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
  self.signed_element_id  = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
end