sso_login_box_for_ntu/ruby-saml-custom/lib/xml_security.rb

422 lines
14 KiB
Ruby
Raw Permalink Normal View History

2022-05-14 03:53:04 +00:00
# The contents of this file are subject to the terms
# of the Common Development and Distribution License
# (the License). You may not use this file except in
# compliance with the License.
#
# You can obtain a copy of the License at
# https://opensso.dev.java.net/public/CDDLv1.0.html or
# opensso/legal/CDDLv1.0.txt
# See the License for the specific language governing
# permission and limitations under the License.
#
# When distributing Covered Code, include this CDDL
# Header Notice in each file and include the License file
# at opensso/legal/CDDLv1.0.txt.
# If applicable, add the following below the CDDL Header,
# with the fields enclosed by brackets [] replaced by
# your own identifying information:
# "Portions Copyrighted [year] [name of copyright owner]"
#
# $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
#
# Copyright 2007 Sun Microsystems Inc. All Rights Reserved
# Portions Copyrighted 2007 Todd W Saxton.
require 'rubygems'
require "rexml/document"
require "rexml/xpath"
require "openssl"
require 'nokogiri'
require "digest/sha1"
require "digest/sha2"
require "onelogin/ruby-saml/utils"
require "onelogin/ruby-saml/error_handling"
module XMLSecurity
class BaseDocument < REXML::Document
REXML::Document::entity_expansion_limit = 0
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
DSIG = "http://www.w3.org/2000/09/xmldsig#"
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
Nokogiri::XML::ParseOptions::NONET
def canon_algorithm(element)
algorithm = element
if algorithm.is_a?(REXML::Element)
algorithm = element.attribute('Algorithm').value
end
case algorithm
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
Nokogiri::XML::XML_C14N_1_0
when "http://www.w3.org/2006/12/xml-c14n11",
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
Nokogiri::XML::XML_C14N_1_1
else
Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
end
end
def algorithm(element)
algorithm = element
if algorithm.is_a?(REXML::Element)
algorithm = element.attribute("Algorithm").value
end
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
case algorithm
when 256 then OpenSSL::Digest::SHA256
when 384 then OpenSSL::Digest::SHA384
when 512 then OpenSSL::Digest::SHA512
else
OpenSSL::Digest::SHA1
end
end
end
class Document < BaseDocument
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
attr_writer :uuid
def uuid
@uuid ||= begin
document.root.nil? ? nil : document.root.attributes['ID']
end
end
#<Signature>
#<SignedInfo>
#<CanonicalizationMethod />
#<SignatureMethod />
#<Reference>
#<Transforms>
#<DigestMethod>
#<DigestValue>
#</Reference>
#<Reference /> etc.
#</SignedInfo>
#<SignatureValue />
#<KeyInfo />
#<Object />
#</Signature>
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
noko = Nokogiri::XML(self.to_s) do |config|
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
end
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
signed_info_element = signature_element.add_element("ds:SignedInfo")
signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method})
# Add Reference
reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
# Add Transforms
transforms_element = reference_element.add_element("ds:Transforms")
transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method})
inclusive_namespaces = INC_PREFIX_LIST.split(" ")
canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces)
reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
# add SignatureValue
noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
end
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
signature_element.add_element("ds:SignatureValue").text = signature
# add KeyInfo
key_info_element = signature_element.add_element("ds:KeyInfo")
x509_element = key_info_element.add_element("ds:X509Data")
x509_cert_element = x509_element.add_element("ds:X509Certificate")
if certificate.is_a?(String)
certificate = OpenSSL::X509::Certificate.new(certificate)
end
x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
# add the signature
issuer_element = elements["//saml:Issuer"]
if issuer_element
root.insert_after(issuer_element, signature_element)
elsif first_child = root.children[0]
root.insert_before(first_child, signature_element)
else
root.add_element(signature_element)
end
end
protected
def compute_signature(private_key, signature_algorithm, document)
Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
end
def compute_digest(document, digest_algorithm)
digest = digest_algorithm.digest(document)
2023-02-13 15:21:45 +00:00
Base64.encode64(digest).strip
2022-05-14 03:53:04 +00:00
end
end
class SignedDocument < BaseDocument
include OneLogin::RubySaml::ErrorHandling
attr_writer :signed_element_id
def initialize(response, errors = [])
super(response)
@errors = errors
end
def signed_element_id
@signed_element_id ||= extract_signed_element_id
end
def validate_document(idp_cert_fingerprint, soft = true, options = {})
# get cert from response
cert_element = REXML::XPath.first(
self,
"//ds:X509Certificate",
{ "ds"=>DSIG }
)
if cert_element
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
cert_text = Base64.decode64(base64_cert)
begin
cert = OpenSSL::X509::Certificate.new(cert_text)
rescue OpenSSL::X509::CertificateError => _e
return append_error("Document Certificate Error", soft)
end
if options[:fingerprint_alg]
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
else
2023-02-13 15:21:45 +00:00
fingerprint_alg = OpenSSL::Digest.new('SHA1')
2022-05-14 03:53:04 +00:00
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
return append_error("Fingerprint mismatch", soft)
end
else
if options[:cert]
base64_cert = Base64.encode64(options[:cert].to_pem)
else
if soft
return false
else
return append_error("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings", soft)
end
end
end
validate_signature(base64_cert, soft)
end
def validate_document_with_cert(idp_cert, soft = true)
# get cert from response
cert_element = REXML::XPath.first(
self,
"//ds:X509Certificate",
{ "ds"=>DSIG }
)
if cert_element
base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
cert_text = Base64.decode64(base64_cert)
begin
cert = OpenSSL::X509::Certificate.new(cert_text)
rescue OpenSSL::X509::CertificateError => _e
return append_error("Document Certificate Error", soft)
end
# check saml response cert matches provided idp cert
if idp_cert.to_pem != cert.to_pem
return append_error("Certificate of the Signature element does not match provided certificate", soft)
end
else
base64_cert = Base64.encode64(idp_cert.to_pem)
end
validate_signature(base64_cert, true)
end
def validate_signature(base64_cert, soft = true)
document = Nokogiri::XML(self.to_s) do |config|
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
end
# create a rexml document
@working_copy ||= REXML::Document.new(self.to_s).root
# get signature node
sig_element = REXML::XPath.first(
@working_copy,
"//ds:Signature",
{"ds"=>DSIG}
)
# signature method
sig_alg_value = REXML::XPath.first(
sig_element,
"./ds:SignedInfo/ds:SignatureMethod",
{"ds"=>DSIG}
)
signature_algorithm = algorithm(sig_alg_value)
# get signature
base64_signature = REXML::XPath.first(
sig_element,
"./ds:SignatureValue",
{"ds" => DSIG}
)
signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
# canonicalization method
canon_algorithm = canon_algorithm REXML::XPath.first(
sig_element,
'./ds:SignedInfo/ds:CanonicalizationMethod',
'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_string = noko_signed_info_element.canonicalize(canon_algorithm)
noko_sig_element.remove
# get inclusive namespaces
inclusive_namespaces = extract_inclusive_namespaces
# check digests
ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
canon_algorithm = canon_algorithm REXML::XPath.first(
ref,
'//ds:CanonicalizationMethod',
{ "ds" => DSIG }
)
canon_algorithm = process_transforms(ref, canon_algorithm)
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 }
)
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
unless digests_match?(hash, digest_value)
return append_error("Digest mismatch", soft)
end
# get certificate object
cert_text = Base64.decode64(base64_cert)
cert = OpenSSL::X509::Certificate.new(cert_text)
# verify signature
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
return append_error("Key validation error", soft)
end
return true
end
private
def process_transforms(ref, canon_algorithm)
transforms = REXML::XPath.match(
ref,
"//ds:Transforms/ds:Transform",
{ "ds" => DSIG }
)
transforms.each do |transform_element|
if transform_element.attributes && transform_element.attributes["Algorithm"]
algorithm = transform_element.attributes["Algorithm"]
case algorithm
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
canon_algorithm = Nokogiri::XML::XML_C14N_1_0
when "http://www.w3.org/2006/12/xml-c14n11",
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
canon_algorithm = Nokogiri::XML::XML_C14N_1_1
when "http://www.w3.org/2001/10/xml-exc-c14n#",
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
end
end
end
canon_algorithm
end
def digests_match?(hash, digest_value)
hash == digest_value
end
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]
sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
end
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
end
end