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 186
def initialize(response, errors = [])
  super(response)
  @errors = errors
end

Public Instance Methods

validate_document(idp_cert_fingerprint, soft = true, options = {}) click to toggle source
# File lib/xml_security.rb, line 195
def validate_document(idp_cert_fingerprint, soft = true, options = {})
  # get cert from response
  cert_element = REXML::XPath.first(
    self,
    "//ds:X509Certificate",
    { "ds"=>DSIG }
  )
  unless cert_element
    if soft
      return false
    else
      raise OneLogin::RubySaml::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)

  if options[:fingerprint_alg]
    fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
  else
    fingerprint_alg = OpenSSL::Digest::SHA1.new
  end
  fingerprint = fingerprint_alg.hexdigest(cert.to_der)

  # check cert matches registered idp cert
  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
    @errors << "Fingerprint mismatch"
    return soft ? false : (raise OneLogin::RubySaml::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 229
def validate_signature(base64_cert, soft = true)
  # validate references

  # check for inclusive namespaces
  inclusive_namespaces = extract_inclusive_namespaces

  document = Nokogiri.parse(self.to_s) do |options|
    options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
  end

  # 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}
  )
  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
  )

  noko_signed_info_reference_element_uri_attr = noko_signed_info_element.at_xpath('./ds:Reference', 'ds' => DSIG).attributes["URI"]
  if (noko_signed_info_reference_element_uri_attr.value.empty?)
    noko_signed_info_reference_element_uri_attr.value = "##{document.root.attribute('ID')}"
  end

  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
  noko_sig_element.remove

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

    hashed_element = uri.empty? ? document : document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => uri[1..-1] })
    # hashed_element = document.at_xpath("//*[@ID=$uri]", nil, { 'uri' => 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 = algorithm(REXML::XPath.first(
      ref,
      "//ds:DigestMethod",
      { "ds" => DSIG }
    ))
    hash = digest_algorithm.digest(canon_hashed_element)
    encoded_digest_value = REXML::XPath.first(
      ref,
      "//ds:DigestValue",
      { "ds" => DSIG }
    ).text
    digest_value = Base64.decode64(encoded_digest_value)

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

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

  signature = Base64.decode64(base64_signature)

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

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

  unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
    @errors << "Key validation error"
    return soft ? false : (raise OneLogin::RubySaml::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 336
def digests_match?(hash, digest_value)
  hash == digest_value
end
extract_inclusive_namespaces() click to toggle source
# File lib/xml_security.rb, line 353
def extract_inclusive_namespaces
  element = REXML::XPath.first(
    self,
    "//ec:InclusiveNamespaces",
    { "ec" => C14N }
  )
  if element
    prefix_list = element.attributes.get_attribute("PrefixList").value
    prefix_list.split(" ")
  else
    nil
  end
end
extract_signed_element_id() click to toggle source
# File lib/xml_security.rb, line 340
def extract_signed_element_id
  reference_element = REXML::XPath.first(
    self,
    "//ds:Signature/ds:SignedInfo/ds:Reference",
    {"ds"=>DSIG}
  )

  return nil if reference_element.nil?

  sei = reference_element.attribute("URI").value[1..-1] 
  self.signed_element_id = sei.nil? ? self.root.attribute("ID") : sei
end