|
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 |
|
|
|
|
|
|
Thanks much. We found this very useful.