Skip to content

Instantly share code, notes, and snippets.

@steffentchr
Last active July 5, 2018 15:17
Show Gist options
  • Save steffentchr/9036789 to your computer and use it in GitHub Desktop.
Save steffentchr/9036789 to your computer and use it in GitHub Desktop.
An example of how to verify SAML/XML signatures, execute in Ruby. Adapted from https://github.com/zendesk/samlr/.

It's hard to find a good example of how to verify SAML signatures online. There are plenty magic that "just calls a Java method" -- but few clear step-by-step guide.

This gist covers the signature check of a SAML response in Ruby, and as such it's also an example of how to verify an XML Secure.

The code here is lifted entirely from Morten Primdahls and Zendesks awesome SAMLR library.

require "nokogiri"
require "openssl"
require "base64"
# Constants
C14N = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
NS_MAP = {
"c14n" => "http://www.w3.org/2001/10/xml-exc-c14n#",
"ds" => "http://www.w3.org/2000/09/xmldsig#",
"saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
"samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
"md" => "urn:oasis:names:tc:SAML:2.0:metadata",
"xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xs" => "http://www.w3.org/2001/XMLSchema"
}
SHA_MAP = {
1 => OpenSSL::Digest::SHA1,
256 => OpenSSL::Digest::SHA256,
384 => OpenSSL::Digest::SHA384,
512 => OpenSSL::Digest::SHA512
}
# Set up the certificate
certificate = OpenSSL::X509::Certificate.new(File.read("cert.pem"))
# Read the document
original = Nokogiri::XML(File.read("login.xml"))
document = original.dup
prefix = "/samlp:Response"
# Read, then clear, the signature
signature = document.at("#{prefix}/ds:Signature", NS_MAP)
signature.remove
# Verify the document digests to ensure that the document hasn't been modified
original.xpath("#{prefix}/ds:Signature/ds:SignedInfo/ds:Reference[@URI]", NS_MAP).each do |ref|
digest_value = ref.at("./ds:DigestValue", NS_MAP).text
decoded_digest_value = Base64.decode64(digest_value);
reference_id = ref["URI"][1..-1]
reference_node = document.xpath("//*[@ID='#{reference_id}']").first
reference_canoned = reference_node.canonicalize(C14N)
# Figure out which method has been used to the sign the node
digest_method = OpenSSL::Digest::SHA1
if ref.at("./ds:DigestMethod/@Algorithm", NS_MAP).text =~ /sha(\d+)$/
digest_method = SHA_MAP[$1.to_i]
end
# Verify the digest
digest = digest_method.digest(reference_canoned)
if digest == decoded_digest_value
print "Digest verified for #{reference_id}\n"
else
print "Digest check mismatch for #{reference_id}\n"
end
end
# Canonicalization: Stringify the node in a nice way
node = original.at("#{prefix}/ds:Signature/ds:SignedInfo", NS_MAP)
canoned = node.canonicalize(C14N)
# Figure out which method has been used to the sign the node
signature_method = OpenSSL::Digest::SHA1
if signature.at("./ds:SignedInfo/ds:SignatureMethod/@Algorithm", NS_MAP).text =~ /sha(\d+)$/
signature_method = SHA_MAP[$1.to_i]
end
# Read the signature
signature_value = signature.at("./ds:SignatureValue", NS_MAP).text
decoded_signature_value = Base64.decode64(signature_value);
# Finally, verify that the signature is correct
verify = certificate.public_key.verify(signature_method.new, decoded_signature_value, canoned)
if verify
print "Document signature is correct\n"
else
print "Document signature is incorrect\n"
end
@usethedata
Copy link

Thanks much. We found this very useful.

@guilpejon
Copy link

Thanks for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment