samlr 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of samlr might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/LICENSE +176 -0
- data/README.md +182 -0
- data/Rakefile +12 -0
- data/bin/samlr +46 -0
- data/config/schemas/XMLSchema.xsd +2534 -0
- data/config/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/config/schemas/saml-schema-metadata-2.0.xsd +337 -0
- data/config/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/config/schemas/xenc-schema.xsd +146 -0
- data/config/schemas/xml.xsd +287 -0
- data/config/schemas/xmldsig-core-schema.xsd +318 -0
- data/lib/samlr.rb +52 -0
- data/lib/samlr/assertion.rb +91 -0
- data/lib/samlr/certificate.rb +23 -0
- data/lib/samlr/command.rb +41 -0
- data/lib/samlr/condition.rb +31 -0
- data/lib/samlr/errors.rb +22 -0
- data/lib/samlr/fingerprint.rb +44 -0
- data/lib/samlr/logout_request.rb +7 -0
- data/lib/samlr/reference.rb +32 -0
- data/lib/samlr/request.rb +37 -0
- data/lib/samlr/response.rb +68 -0
- data/lib/samlr/signature.rb +129 -0
- data/lib/samlr/tools.rb +108 -0
- data/lib/samlr/tools/certificate_builder.rb +74 -0
- data/lib/samlr/tools/logout_request_builder.rb +27 -0
- data/lib/samlr/tools/metadata_builder.rb +41 -0
- data/lib/samlr/tools/request_builder.rb +44 -0
- data/lib/samlr/tools/response_builder.rb +157 -0
- data/lib/samlr/tools/timestamp.rb +26 -0
- data/samlr.gemspec +19 -0
- data/test/fixtures/default_samlr_certificate.pem +11 -0
- data/test/fixtures/default_samlr_private_key.pem +9 -0
- data/test/fixtures/no_cert_response.xml +2 -0
- data/test/fixtures/sample_metadata.xml +7 -0
- data/test/fixtures/sample_response.xml +2 -0
- data/test/test_helper.rb +55 -0
- data/test/unit/test_assertion.rb +54 -0
- data/test/unit/test_condition.rb +71 -0
- data/test/unit/test_fingerprint.rb +45 -0
- data/test/unit/test_logout_request.rb +39 -0
- data/test/unit/test_reference.rb +32 -0
- data/test/unit/test_request.rb +34 -0
- data/test/unit/test_response.rb +94 -0
- data/test/unit/test_response_scenarios.rb +111 -0
- data/test/unit/test_signature.rb +54 -0
- data/test/unit/test_timestamp.rb +58 -0
- data/test/unit/test_tools.rb +100 -0
- data/test/unit/tools/test_certificate_builder.rb +41 -0
- data/test/unit/tools/test_logout_request_builder.rb +26 -0
- data/test/unit/tools/test_metadata_builder.rb +26 -0
- data/test/unit/tools/test_request_builder.rb +35 -0
- data/test/unit/tools/test_response_builder.rb +19 -0
- metadata +184 -0
data/lib/samlr/tools.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require "time"
|
2
|
+
require "uuidtools"
|
3
|
+
require "openssl"
|
4
|
+
require "cgi"
|
5
|
+
require "zlib"
|
6
|
+
|
7
|
+
require "samlr/tools/timestamp"
|
8
|
+
require "samlr/tools/certificate_builder"
|
9
|
+
require "samlr/tools/request_builder"
|
10
|
+
require "samlr/tools/response_builder"
|
11
|
+
require "samlr/tools/metadata_builder"
|
12
|
+
require "samlr/tools/logout_request_builder"
|
13
|
+
|
14
|
+
module Samlr
|
15
|
+
module Tools
|
16
|
+
SHA_MAP = {
|
17
|
+
1 => OpenSSL::Digest::SHA1,
|
18
|
+
256 => OpenSSL::Digest::SHA256,
|
19
|
+
384 => OpenSSL::Digest::SHA384,
|
20
|
+
512 => OpenSSL::Digest::SHA512
|
21
|
+
}
|
22
|
+
|
23
|
+
# Convert algorithm attribute value to Ruby implementation
|
24
|
+
def self.algorithm(value)
|
25
|
+
if value =~ /sha(\d+)$/
|
26
|
+
implementation = SHA_MAP[$1.to_i]
|
27
|
+
end
|
28
|
+
|
29
|
+
implementation || OpenSSL::Digest::SHA1
|
30
|
+
end
|
31
|
+
|
32
|
+
# Accepts a document and optionally :path => xpath, :c14n_mode => c14n_mode
|
33
|
+
def self.canonicalize(xml, options = {})
|
34
|
+
options = { :c14n_mode => C14N }.merge(options)
|
35
|
+
document = Nokogiri::XML(xml) { |c| c.strict.noblanks }
|
36
|
+
|
37
|
+
if path = options[:path]
|
38
|
+
node = document.at(path, NS_MAP)
|
39
|
+
else
|
40
|
+
node = document
|
41
|
+
end
|
42
|
+
|
43
|
+
node.canonicalize(options[:c14n_mode], options[:namespaces])
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generate an xs:NCName conforming UUID
|
47
|
+
def self.uuid
|
48
|
+
"samlr-#{UUIDTools::UUID.timestamp_create}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deflates, Base64 encodes and CGI escapes a string
|
52
|
+
def self.encode(string)
|
53
|
+
deflated = Zlib::Deflate.deflate(string, 9)[2..-5]
|
54
|
+
encoded = Base64.encode64(deflated)
|
55
|
+
escaped = CGI.escape(encoded)
|
56
|
+
escaped
|
57
|
+
end
|
58
|
+
|
59
|
+
# CGI unescapes, Base64 decodes and inflates a string
|
60
|
+
def self.decode(string)
|
61
|
+
unescaped = CGI.unescape(string)
|
62
|
+
decoded = Base64.decode64(unescaped)
|
63
|
+
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
64
|
+
inflated = inflater.inflate(decoded)
|
65
|
+
|
66
|
+
inflater.finish
|
67
|
+
inflater.close
|
68
|
+
|
69
|
+
inflated
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.validate!(options = {})
|
73
|
+
validate(options.merge(:bang => true))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Validate a SAML request or response against an XSD. Supply either :path or :document in the options and
|
77
|
+
# a :schema (defaults to SAML validation)
|
78
|
+
def self.validate(options = {})
|
79
|
+
document = options[:document] || File.read(options[:path])
|
80
|
+
schema = options.fetch(:schema, SAML_SCHEMA)
|
81
|
+
bang = options.fetch(:bang, false)
|
82
|
+
|
83
|
+
if document.is_a?(Nokogiri::XML::Document)
|
84
|
+
xml = document
|
85
|
+
else
|
86
|
+
xml = Nokogiri::XML(document) { |c| c.strict }
|
87
|
+
end
|
88
|
+
|
89
|
+
# All bundled schemas are using relative schemaLocation. This means we'll have to
|
90
|
+
# change working directory to find them during validation.
|
91
|
+
Dir.chdir(Samlr.schema_location) do
|
92
|
+
if schema.is_a?(Nokogiri::XML::Schema)
|
93
|
+
xsd = schema
|
94
|
+
else
|
95
|
+
xsd = Nokogiri::XML::Schema(File.read(schema))
|
96
|
+
end
|
97
|
+
|
98
|
+
result = xsd.validate(xml)
|
99
|
+
|
100
|
+
if bang && result.length != 0
|
101
|
+
raise Samlr::FormatError.new("Schema validation failed", "XSD validation errors: #{result.join(", ")}")
|
102
|
+
else
|
103
|
+
result.length == 0
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Samlr
|
2
|
+
module Tools
|
3
|
+
|
4
|
+
# Container for generating/referencing X509 and keys
|
5
|
+
class CertificateBuilder
|
6
|
+
attr_reader :key_size
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@key_size = options.fetch(:key_size, 4096)
|
10
|
+
@x509 = options[:x509]
|
11
|
+
@key_pair = options[:key_pair]
|
12
|
+
end
|
13
|
+
|
14
|
+
def x509
|
15
|
+
@x509 ||= begin
|
16
|
+
domain = "example.org"
|
17
|
+
name = OpenSSL::X509::Name.new([
|
18
|
+
[ 'C', 'US', OpenSSL::ASN1::PRINTABLESTRING ],
|
19
|
+
[ 'O', domain, OpenSSL::ASN1::UTF8STRING ],
|
20
|
+
[ 'OU', 'Samlr ResponseBuilder', OpenSSL::ASN1::UTF8STRING ],
|
21
|
+
[ 'CN', 'CA' ]
|
22
|
+
])
|
23
|
+
|
24
|
+
certificate = OpenSSL::X509::Certificate.new
|
25
|
+
certificate.subject = name
|
26
|
+
certificate.issuer = name
|
27
|
+
certificate.not_before = (Time.now - 5)
|
28
|
+
certificate.not_after = (Time.now + 60 * 60 * 24 * 365 * 20)
|
29
|
+
certificate.public_key = key_pair.public_key
|
30
|
+
certificate.serial = 1
|
31
|
+
certificate.version = 2
|
32
|
+
certificate.sign(key_pair, OpenSSL::Digest::SHA1.new)
|
33
|
+
|
34
|
+
certificate
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def x509_as_pem
|
39
|
+
pem = x509.to_pem.split("\n")
|
40
|
+
pem.pop
|
41
|
+
pem.shift
|
42
|
+
pem.join
|
43
|
+
end
|
44
|
+
|
45
|
+
def key_pair
|
46
|
+
@key_pair ||= OpenSSL::PKey::RSA.new(key_size)
|
47
|
+
end
|
48
|
+
|
49
|
+
def sign(string)
|
50
|
+
Base64.encode64(key_pair.sign(OpenSSL::Digest::SHA1.new, string)).delete("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def verify(signature, string)
|
54
|
+
key_pair.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), string)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_certificate
|
58
|
+
Samlr::Certificate.new(x509)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.dump(path, certificate, id = "samlr")
|
62
|
+
File.open(File.join(path, "#{id}_private_key.pem"), "w") { |f| f.write(certificate.key_pair.to_pem) }
|
63
|
+
File.open(File.join(path, "#{id}_certificate.pem"), "w") { |f| f.write(certificate.x509.to_pem) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.load(path, id = "samlr")
|
67
|
+
key_pair = OpenSSL::PKey::RSA.new(File.read(File.join(path, "#{id}_private_key.pem")))
|
68
|
+
x509_cert = OpenSSL::X509::Certificate.new(File.read(File.join(path, "#{id}_certificate.pem")))
|
69
|
+
|
70
|
+
new(:key_pair => key_pair, :x509 => x509_cert)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
3
|
+
module Samlr
|
4
|
+
module Tools
|
5
|
+
# Use this for building the SAML logout request XML
|
6
|
+
module LogoutRequestBuilder
|
7
|
+
def self.build(options = {})
|
8
|
+
name_id_format = options[:name_id_format] || EMAIL_FORMAT
|
9
|
+
|
10
|
+
# Mandatory
|
11
|
+
name_id = options.fetch(:name_id)
|
12
|
+
issuer = options.fetch(:issuer)
|
13
|
+
|
14
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
15
|
+
xml.LogoutRequest("xmlns:samlp" => NS_MAP["samlp"], "xmlns:saml" => NS_MAP["saml"], "ID" => Samlr::Tools.uuid, "IssueInstant" => Samlr::Tools::Timestamp.stamp, "Version" => "2.0") do
|
16
|
+
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
|
17
|
+
|
18
|
+
xml["saml"].Issuer(issuer)
|
19
|
+
xml["saml"].NameID(name_id, "Format" => name_id_format)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
builder.to_xml(COMPACT)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Samlr
|
2
|
+
module Tools
|
3
|
+
|
4
|
+
# Builds you some SP metadata. Accepts a hash with the below keys. No support for arrays
|
5
|
+
# of name id formats or asserion consumer services, build it if you need it.
|
6
|
+
#
|
7
|
+
# :entity_id => "https://sp.example.org/saml", # mandatory
|
8
|
+
# :name_identity_format => Samlr::EMAIL_FORMAT,
|
9
|
+
# :consumer_service_url => "https://sp.example.org/saml"
|
10
|
+
class MetadataBuilder
|
11
|
+
|
12
|
+
def self.build(options = {})
|
13
|
+
name_identity_format = options[:name_identity_format]
|
14
|
+
consumer_service_url = options[:consumer_service_url]
|
15
|
+
consumer_service_binding = options[:consumer_service_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
16
|
+
|
17
|
+
# Mandatory
|
18
|
+
entity_id = options.fetch(:entity_id)
|
19
|
+
|
20
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
21
|
+
xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "entityID" => entity_id) do
|
22
|
+
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "md" }
|
23
|
+
|
24
|
+
xml["md"].SPSSODescriptor("protocolSupportEnumeration" => NS_MAP["samlp"]) do
|
25
|
+
unless name_identity_format.nil?
|
26
|
+
xml["md"].NameIDFormat(name_identity_format)
|
27
|
+
end
|
28
|
+
|
29
|
+
unless consumer_service_url.nil?
|
30
|
+
xml["md"].AssertionConsumerService("index" => "0", "Binding" => consumer_service_binding, "Location" => consumer_service_url)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
builder.to_xml(COMPACT)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
3
|
+
module Samlr
|
4
|
+
module Tools
|
5
|
+
|
6
|
+
# Use this for building the SAML auth request XML
|
7
|
+
module RequestBuilder
|
8
|
+
def self.build(options = {})
|
9
|
+
consumer_service_url = options[:consumer_service_url]
|
10
|
+
issuer = options[:issuer]
|
11
|
+
name_identity_format = options[:name_identity_format]
|
12
|
+
allow_create = options[:allow_create] || "true"
|
13
|
+
authn_context = options[:authn_context]
|
14
|
+
|
15
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
16
|
+
xml.AuthnRequest("xmlns:samlp" => NS_MAP["samlp"], "xmlns:saml" => NS_MAP["saml"], "ID" => Samlr::Tools.uuid, "IssueInstant" => Samlr::Tools::Timestamp.stamp, "Version" => "2.0") do
|
17
|
+
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
|
18
|
+
|
19
|
+
unless consumer_service_url.nil?
|
20
|
+
xml.doc.root["AssertionConsumerServiceURL"] = consumer_service_url
|
21
|
+
end
|
22
|
+
|
23
|
+
unless issuer.nil?
|
24
|
+
xml["saml"].Issuer(issuer)
|
25
|
+
end
|
26
|
+
|
27
|
+
unless name_identity_format.nil?
|
28
|
+
xml["samlp"].NameIDPolicy("AllowCreate" => allow_create, "Format" => name_identity_format)
|
29
|
+
end
|
30
|
+
|
31
|
+
unless authn_context.nil?
|
32
|
+
xml["samlp"].RequestedAuthnContext("Comparison" => "exact") do
|
33
|
+
xml["saml"].AuthnContextClassRef(authn_context)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
builder.to_xml(COMPACT)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "time"
|
3
|
+
require "uuidtools"
|
4
|
+
|
5
|
+
module Samlr
|
6
|
+
module Tools
|
7
|
+
|
8
|
+
# Use this for building test data, not ready to use for production data
|
9
|
+
module ResponseBuilder
|
10
|
+
|
11
|
+
def self.build(options = {})
|
12
|
+
issue_instant = options[:issue_instant] || Samlr::Tools::Timestamp.stamp
|
13
|
+
response_id = options[:response_id] || Samlr::Tools.uuid
|
14
|
+
assertion_id = options[:assertion_id] || Samlr::Tools.uuid
|
15
|
+
status_code = options[:status_code] || "urn:oasis:names:tc:SAML:2.0:status:Success"
|
16
|
+
name_id_format = options[:name_id_format] || EMAIL_FORMAT
|
17
|
+
subject_conf_m = options[:subject_conf_m] || "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
18
|
+
version = options[:version] || "2.0"
|
19
|
+
auth_context = options[:auth_context] || "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
20
|
+
issuer = options[:issuer] || "ResponseBuilder IdP"
|
21
|
+
attributes = options[:attributes] || {}
|
22
|
+
|
23
|
+
# Mandatory for responses
|
24
|
+
destination = options.fetch(:destination)
|
25
|
+
in_response_to = options.fetch(:in_response_to)
|
26
|
+
name_id = options.fetch(:name_id)
|
27
|
+
not_on_or_after = options.fetch(:not_on_or_after)
|
28
|
+
not_before = options.fetch(:not_before)
|
29
|
+
audience = options.fetch(:audience)
|
30
|
+
|
31
|
+
# Signature settings
|
32
|
+
sign_assertion = [ true, false ].member?(options[:sign_assertion]) ? options[:sign_assertion] : true
|
33
|
+
sign_response = [ true, false ].member?(options[:sign_response]) ? options[:sign_response] : true
|
34
|
+
|
35
|
+
# Fixture controls
|
36
|
+
skip_assertion = options[:skip_assertion]
|
37
|
+
skip_conditions = options[:skip_conditions]
|
38
|
+
|
39
|
+
builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
|
40
|
+
xml.Response("xmlns:samlp" => NS_MAP["samlp"], "ID" => response_id, "InResponseTo" => in_response_to, "Version" => version, "IssueInstant" => issue_instant, "Destination" => destination) do
|
41
|
+
xml.doc.root.add_namespace_definition("saml", NS_MAP["saml"])
|
42
|
+
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
|
43
|
+
|
44
|
+
xml["saml"].Issuer(issuer)
|
45
|
+
xml["samlp"].Status { |xml| xml["samlp"].StatusCode("Value" => status_code) }
|
46
|
+
|
47
|
+
unless skip_assertion
|
48
|
+
xml["saml"].Assertion("xmlns:saml" => NS_MAP["saml"], "ID" => assertion_id, "IssueInstant" => issue_instant, "Version" => "2.0") do
|
49
|
+
xml["saml"].Issuer(issuer)
|
50
|
+
|
51
|
+
xml["saml"].Subject do
|
52
|
+
xml["saml"].NameID(name_id, "Format" => name_id_format)
|
53
|
+
|
54
|
+
xml["saml"].SubjectConfirmation("Method" => subject_conf_m) do
|
55
|
+
xml["saml"].SubjectConfirmationData("InResponseTo" => in_response_to, "NotOnOrAfter" => not_on_or_after, "Recipient" => destination)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
unless skip_conditions
|
60
|
+
xml["saml"].Conditions("NotBefore" => not_before, "NotOnOrAfter" => not_on_or_after) do
|
61
|
+
xml["saml"].AudienceRestriction do
|
62
|
+
xml["saml"].Audience(audience)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
xml["saml"].AuthnStatement("AuthnInstant" => issue_instant, "SessionIndex" => assertion_id) do
|
68
|
+
xml["saml"].AuthnContext do
|
69
|
+
xml["saml"].AuthnContextClassRef(auth_context)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
unless attributes.empty?
|
74
|
+
xml["saml"].AttributeStatement do
|
75
|
+
attributes.keys.sort.each do |name|
|
76
|
+
xml["saml"].Attribute("Name" => name) do
|
77
|
+
values = Array(attributes[name])
|
78
|
+
values.each do |value|
|
79
|
+
xml["saml"].AttributeValue(value, "xmlns:xsi" => NS_MAP["xsi"], "xmlns:xs" => NS_MAP["xs"], "xsi:type" => "xs:string")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# The core response is ready, not on to signing
|
91
|
+
response = builder.doc
|
92
|
+
|
93
|
+
response = sign(response, assertion_id, options) if sign_assertion
|
94
|
+
response = sign(response, response_id, options) if sign_response
|
95
|
+
|
96
|
+
response.to_xml(COMPACT)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.sign(document, element_id, options)
|
100
|
+
certificate = options[:certificate] || Samlr::Tools::CertificateBuilder.new
|
101
|
+
element = document.at("//*[@ID='#{element_id}']")
|
102
|
+
digest = digest(document, element, options)
|
103
|
+
canoned = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
|
104
|
+
signature = certificate.sign(canoned)
|
105
|
+
skip_keyinfo = options[:skip_keyinfo]
|
106
|
+
|
107
|
+
Nokogiri::XML::Builder.with(digest) do |xml|
|
108
|
+
xml.SignatureValue(signature)
|
109
|
+
xml.KeyInfo do
|
110
|
+
xml.X509Data do
|
111
|
+
xml.X509Certificate(certificate.x509_as_pem)
|
112
|
+
end
|
113
|
+
end unless skip_keyinfo
|
114
|
+
end
|
115
|
+
# digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
|
116
|
+
element.at("./saml:Issuer", NS_MAP).add_next_sibling(digest)
|
117
|
+
|
118
|
+
document
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.digest(document, element, options)
|
122
|
+
c14n_method = options[:c14n_method] || "http://www.w3.org/2001/10/xml-exc-c14n#"
|
123
|
+
sign_method = options[:sign_method] || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
124
|
+
digest_method = options[:digest_method] || "http://www.w3.org/2000/09/xmldsig#sha1"
|
125
|
+
env_signature = options[:env_signature] || "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
126
|
+
namespaces = options[:namespaces] || [ "#default", "samlp", "saml", "ds", "xs", "xsi" ]
|
127
|
+
|
128
|
+
canoned = element.canonicalize(C14N, namespaces)
|
129
|
+
digest_value = Base64.encode64(OpenSSL::Digest::SHA1.new.digest(canoned)).delete("\n")
|
130
|
+
|
131
|
+
builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
|
132
|
+
xml.Signature("xmlns" => NS_MAP["ds"]) do
|
133
|
+
|
134
|
+
xml.SignedInfo do
|
135
|
+
xml.CanonicalizationMethod("Algorithm" => c14n_method)
|
136
|
+
xml.SignatureMethod("Algorithm" => sign_method)
|
137
|
+
|
138
|
+
xml.Reference("URI" => "##{element['ID']}") do
|
139
|
+
xml.Transforms do
|
140
|
+
xml.Transform("Algorithm" => env_signature)
|
141
|
+
xml.Transform("Algorithm" => c14n_method) do
|
142
|
+
xml.InclusiveNamespaces("xmlns" => c14n_method, "PrefixList" => namespaces.join(" "))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
xml.DigestMethod("Algorithm" => digest_method)
|
146
|
+
xml.DigestValue(digest_value)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
builder.doc.root
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|