samlr 2.7.1 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/samlr/assertion.rb +7 -7
- data/lib/samlr/command.rb +8 -11
- data/lib/samlr/condition.rb +8 -8
- data/lib/samlr/fingerprint.rb +5 -5
- data/lib/samlr/fingerprint_sha1.rb +1 -1
- data/lib/samlr/fingerprint_sha256.rb +1 -1
- data/lib/samlr/reference.rb +2 -3
- data/lib/samlr/request.rb +2 -2
- data/lib/samlr/response.rb +1 -2
- data/lib/samlr/signature.rb +18 -18
- data/lib/samlr/tools/certificate_builder.rb +19 -20
- data/lib/samlr/tools/logout_request_builder.rb +5 -5
- data/lib/samlr/tools/logout_response_builder.rb +1 -1
- data/lib/samlr/tools/metadata_builder.rb +5 -7
- data/lib/samlr/tools/request_builder.rb +3 -5
- data/lib/samlr/tools/response_builder.rb +44 -46
- data/lib/samlr/tools/timestamp.rb +0 -2
- data/lib/samlr/tools.rb +25 -26
- data/lib/samlr/version.rb +1 -1
- data/lib/samlr.rb +14 -14
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb88cef552d1a5b96160425d90b32459139c39d8a94395ce923af66b9be8eef4
|
|
4
|
+
data.tar.gz: 7fa242a6642c7f8a0188f78c3b3df386e78d3c70b57efead6ef7b810bfe0f04c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fe8bf321fab3c2a6a258e88147543be16232524cfa2e0568dfbcdc972bd5edf7557643beaf27d3b4705e8d9cb93e50234592774ce4ac211325b79d2778d5667
|
|
7
|
+
data.tar.gz: 676d91f15ae1cd3729d1720ca9c3dce41272740bcd92107b6e77eb8bd802888224eef6798427a3a550b800843ac2d4cc49e79cdbef58bf8c51e242265a403eaa
|
data/lib/samlr/assertion.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Samlr
|
|
|
5
5
|
|
|
6
6
|
def initialize(document, options)
|
|
7
7
|
@document = document
|
|
8
|
-
@options
|
|
8
|
+
@options = options
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def verify!
|
|
@@ -31,15 +31,15 @@ module Samlr
|
|
|
31
31
|
def attributes
|
|
32
32
|
@attributes ||= {}.tap do |attrs|
|
|
33
33
|
assertion.xpath("./saml:AttributeStatement/saml:Attribute", NS_MAP).each do |statement|
|
|
34
|
-
name
|
|
34
|
+
name = statement["Name"]
|
|
35
35
|
values = statement.xpath("./saml:AttributeValue", NS_MAP)
|
|
36
36
|
|
|
37
|
-
if values.size == 0
|
|
38
|
-
|
|
37
|
+
value = if values.size == 0
|
|
38
|
+
nil
|
|
39
39
|
elsif values.size == 1
|
|
40
|
-
|
|
40
|
+
values.first.text
|
|
41
41
|
else
|
|
42
|
-
|
|
42
|
+
values.map { |value| value.text }
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
attrs[name] = value
|
|
@@ -56,7 +56,7 @@ module Samlr
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def name_id_options
|
|
59
|
-
@name_id_options ||=
|
|
59
|
+
@name_id_options ||= name_id_node.attributes.map { |k, v| [k, v.value] }.to_h
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def conditions
|
data/lib/samlr/command.rb
CHANGED
|
@@ -4,10 +4,10 @@ require "logger"
|
|
|
4
4
|
module Samlr
|
|
5
5
|
# Helper module for command line options
|
|
6
6
|
module Command
|
|
7
|
-
COMMANDS = [
|
|
7
|
+
COMMANDS = [:verify, :schema_validate, :print]
|
|
8
8
|
|
|
9
9
|
def self.execute(options, path = nil)
|
|
10
|
-
Samlr.logger.level
|
|
10
|
+
Samlr.logger.level = Logger::DEBUG if options[:verbose]
|
|
11
11
|
Samlr.validation_mode = :log if options[:skip_validation]
|
|
12
12
|
|
|
13
13
|
if options[:verify]
|
|
@@ -21,21 +21,18 @@ module Samlr
|
|
|
21
21
|
execute_verify(path, options)
|
|
22
22
|
end
|
|
23
23
|
elsif options[:schema_validate]
|
|
24
|
-
Samlr::Tools.validate(:
|
|
24
|
+
Samlr::Tools.validate(path: path)
|
|
25
25
|
elsif options[:print]
|
|
26
26
|
Samlr::Response.parse(File.read(path)).to_xml
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
private
|
|
31
|
-
|
|
32
30
|
def self.execute_verify(path, options)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"Verification failed for #{path}: #{e.message}"
|
|
38
|
-
end
|
|
31
|
+
Samlr::Response.new(File.read(path), options).verify!
|
|
32
|
+
"Verification passed for #{path}"
|
|
33
|
+
rescue Samlr::SamlrError => e
|
|
34
|
+
"Verification failed for #{path}: #{e.message}"
|
|
39
35
|
end
|
|
36
|
+
private_class_method :execute_verify
|
|
40
37
|
end
|
|
41
38
|
end
|
data/lib/samlr/condition.rb
CHANGED
|
@@ -3,10 +3,10 @@ module Samlr
|
|
|
3
3
|
attr_reader :audience, :not_before, :not_on_or_after, :options
|
|
4
4
|
|
|
5
5
|
def initialize(condition, options)
|
|
6
|
-
@options
|
|
7
|
-
@not_before
|
|
6
|
+
@options = options
|
|
7
|
+
@not_before = (condition || {})["NotBefore"]
|
|
8
8
|
@not_on_or_after = (condition || {})["NotOnOrAfter"]
|
|
9
|
-
@audience
|
|
9
|
+
@audience = extract_audience(condition)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def verify!
|
|
@@ -35,9 +35,9 @@ module Samlr
|
|
|
35
35
|
|
|
36
36
|
def audience_satisfied?
|
|
37
37
|
options[:audience].nil? ||
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
audience.nil? ||
|
|
39
|
+
audience.empty? ||
|
|
40
|
+
audience.any? { |a| options[:audience] === a }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
private
|
|
@@ -45,10 +45,10 @@ module Samlr
|
|
|
45
45
|
def extract_audience(condition)
|
|
46
46
|
return unless condition
|
|
47
47
|
|
|
48
|
-
audience_restriction_node = condition.at(
|
|
48
|
+
audience_restriction_node = condition.at("./saml:AudienceRestriction", NS_MAP)
|
|
49
49
|
return unless audience_restriction_node
|
|
50
50
|
|
|
51
|
-
audience_nodes = audience_restriction_node.search(
|
|
51
|
+
audience_nodes = audience_restriction_node.search("./saml:Audience", NS_MAP)
|
|
52
52
|
return unless audience_nodes.any?
|
|
53
53
|
|
|
54
54
|
audience_nodes.map(&:text)
|
data/lib/samlr/fingerprint.rb
CHANGED
|
@@ -3,16 +3,16 @@ module Samlr
|
|
|
3
3
|
attr_accessor :value
|
|
4
4
|
|
|
5
5
|
def initialize(value)
|
|
6
|
-
if value.is_a?(OpenSSL::X509::Certificate)
|
|
7
|
-
|
|
6
|
+
@value = if value.is_a?(OpenSSL::X509::Certificate)
|
|
7
|
+
self.class.x509(value)
|
|
8
8
|
else
|
|
9
|
-
|
|
9
|
+
self.class.normalize(value)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.from_string(string)
|
|
14
14
|
normalized = normalize(string)
|
|
15
|
-
if string.
|
|
15
|
+
if string.delete(":").length == 64
|
|
16
16
|
FingerprintSHA256.new(normalized)
|
|
17
17
|
else
|
|
18
18
|
FingerprintSHA1.new(normalized)
|
|
@@ -46,7 +46,7 @@ module Samlr
|
|
|
46
46
|
|
|
47
47
|
# Extracts a fingerprint for an x509 certificate
|
|
48
48
|
def self.x509(certificate)
|
|
49
|
-
raise NotImplementedError,
|
|
49
|
+
raise NotImplementedError, "subclass must implement x509"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Converts a string to fingerprint normal form
|
|
@@ -4,7 +4,7 @@ module Samlr
|
|
|
4
4
|
class FingerprintSHA1 < Fingerprint
|
|
5
5
|
# Extracts a fingerprint for an x509 certificate
|
|
6
6
|
def self.x509(certificate)
|
|
7
|
-
normalize(OpenSSL::Digest
|
|
7
|
+
normalize(OpenSSL::Digest.new("SHA1").hexdigest(certificate.to_der))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Samlr
|
|
|
4
4
|
class FingerprintSHA256 < Fingerprint
|
|
5
5
|
# Extracts a fingerprint for an x509 certificate
|
|
6
6
|
def self.x509(certificate)
|
|
7
|
-
normalize(OpenSSL::Digest
|
|
7
|
+
normalize(OpenSSL::Digest.new("SHA256").hexdigest(certificate.to_der))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
data/lib/samlr/reference.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Samlr
|
|
|
6
6
|
|
|
7
7
|
def initialize(node)
|
|
8
8
|
@node = node
|
|
9
|
-
@uri
|
|
9
|
+
@uri = node["URI"][1..]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def digest_method
|
|
@@ -14,7 +14,7 @@ module Samlr
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def digest_value
|
|
17
|
-
@digest_value
|
|
17
|
+
@digest_value ||= node.at("./ds:DigestValue", NS_MAP).text
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def decoded_digest_value
|
|
@@ -27,6 +27,5 @@ module Samlr
|
|
|
27
27
|
attribute ? attribute.split(" ") : []
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
-
|
|
31
30
|
end
|
|
32
31
|
end
|
data/lib/samlr/request.rb
CHANGED
|
@@ -41,14 +41,14 @@ module Samlr
|
|
|
41
41
|
Samlr::Tools.parse(data, compressed: true)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def get_attribute_or_element(x_path,attribute=nil)
|
|
44
|
+
def get_attribute_or_element(x_path, attribute = nil)
|
|
45
45
|
if document
|
|
46
46
|
element = document.xpath(x_path)
|
|
47
47
|
if element.length == 0
|
|
48
48
|
nil
|
|
49
49
|
elsif attribute
|
|
50
50
|
value = element.attr(attribute)
|
|
51
|
-
value
|
|
51
|
+
value&.to_s
|
|
52
52
|
else
|
|
53
53
|
element
|
|
54
54
|
end
|
data/lib/samlr/response.rb
CHANGED
|
@@ -2,7 +2,6 @@ require "forwardable"
|
|
|
2
2
|
require "nokogiri"
|
|
3
3
|
|
|
4
4
|
module Samlr
|
|
5
|
-
|
|
6
5
|
# This is the object interface to the XML response object.
|
|
7
6
|
class Response
|
|
8
7
|
extend Forwardable
|
|
@@ -11,7 +10,7 @@ module Samlr
|
|
|
11
10
|
attr_reader :document, :options
|
|
12
11
|
|
|
13
12
|
def initialize(data, options)
|
|
14
|
-
@options
|
|
13
|
+
@options = options
|
|
15
14
|
@document = Response.parse(data)
|
|
16
15
|
end
|
|
17
16
|
|
data/lib/samlr/signature.rb
CHANGED
|
@@ -13,11 +13,11 @@ module Samlr
|
|
|
13
13
|
# Signature validations require document alterations
|
|
14
14
|
@original = original
|
|
15
15
|
@document = original.dup
|
|
16
|
-
@prefix
|
|
17
|
-
@options
|
|
16
|
+
@prefix = prefix
|
|
17
|
+
@options = options
|
|
18
18
|
@signature = nil
|
|
19
19
|
|
|
20
|
-
id = @document.at(
|
|
20
|
+
id = @document.at(prefix.to_s, NS_MAP)&.attribute("ID")
|
|
21
21
|
@signature = find_signature_for_element_id(id) if id
|
|
22
22
|
|
|
23
23
|
@fingerprint = if options[:fingerprint]
|
|
@@ -51,7 +51,6 @@ module Samlr
|
|
|
51
51
|
refs_xpath.each do |ref|
|
|
52
52
|
refs << Samlr::Reference.new(ref)
|
|
53
53
|
end
|
|
54
|
-
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
|
|
@@ -70,7 +69,7 @@ module Samlr
|
|
|
70
69
|
def verify_digests!
|
|
71
70
|
# Check if we need to remove an enveloped signature
|
|
72
71
|
if @signature && !@signature_removed
|
|
73
|
-
signed_element = @document.at(
|
|
72
|
+
signed_element = @document.at(prefix.to_s, NS_MAP)
|
|
74
73
|
is_enveloped = signed_element&.xpath(".//ds:Signature", NS_MAP)&.include?(@signature)
|
|
75
74
|
|
|
76
75
|
# Remove enveloped signature for digest verification
|
|
@@ -81,9 +80,9 @@ module Samlr
|
|
|
81
80
|
end
|
|
82
81
|
|
|
83
82
|
references.each do |reference|
|
|
84
|
-
node
|
|
83
|
+
node = referenced_node(reference.uri)
|
|
85
84
|
canoned = node.canonicalize(C14N, reference.namespaces)
|
|
86
|
-
digest
|
|
85
|
+
digest = reference.digest_method.digest(canoned)
|
|
87
86
|
|
|
88
87
|
if digest != reference.decoded_digest_value
|
|
89
88
|
raise SignatureError.new("Reference validation error: Digest mismatch for #{reference.uri}")
|
|
@@ -128,14 +127,10 @@ module Samlr
|
|
|
128
127
|
end
|
|
129
128
|
|
|
130
129
|
def certificate
|
|
131
|
-
@certificate ||=
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Certificate.new(cert)
|
|
136
|
-
else
|
|
137
|
-
nil
|
|
138
|
-
end
|
|
130
|
+
@certificate ||= if (node = certificate_node)
|
|
131
|
+
Certificate.new(Base64.decode64(node.text))
|
|
132
|
+
elsif (cert = options[:certificate])
|
|
133
|
+
Certificate.new(cert)
|
|
139
134
|
end
|
|
140
135
|
end
|
|
141
136
|
|
|
@@ -150,9 +145,14 @@ module Samlr
|
|
|
150
145
|
def find_signature_for_element_id(element_id)
|
|
151
146
|
return nil unless element_id
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
# Prevent XPath injection by using parameterized XPath queries with variable bindings.
|
|
149
|
+
# Nokogiri's xpath() method supports passing variables separately from the query,
|
|
150
|
+
# which prevents injection attacks like "_x'or'1'='1" from breaking out of the predicate.
|
|
151
|
+
@document.at_xpath(
|
|
152
|
+
"//ds:Signature[ds:SignedInfo/ds:Reference[@URI=$uri]]",
|
|
153
|
+
NS_MAP,
|
|
154
|
+
{uri: "##{element_id}"}
|
|
155
|
+
)
|
|
155
156
|
end
|
|
156
|
-
|
|
157
157
|
end
|
|
158
158
|
end
|
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
module Samlr
|
|
2
2
|
module Tools
|
|
3
|
-
|
|
4
3
|
# Container for generating/referencing X509 and keys
|
|
5
4
|
class CertificateBuilder
|
|
6
5
|
attr_reader :key_size
|
|
7
6
|
|
|
8
7
|
def initialize(options = {})
|
|
9
8
|
@key_size = options.fetch(:key_size, 4096)
|
|
10
|
-
@x509
|
|
9
|
+
@x509 = options[:x509]
|
|
11
10
|
@key_pair = options[:key_pair]
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def x509
|
|
15
14
|
@x509 ||= begin
|
|
16
15
|
domain = "example.org"
|
|
17
|
-
name
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
|
|
16
|
+
name = OpenSSL::X509::Name.new([
|
|
17
|
+
["C", "US", OpenSSL::ASN1::PRINTABLESTRING],
|
|
18
|
+
["O", domain, OpenSSL::ASN1::UTF8STRING],
|
|
19
|
+
["OU", "Samlr ResponseBuilder", OpenSSL::ASN1::UTF8STRING],
|
|
20
|
+
["CN", "CA"]
|
|
21
|
+
])
|
|
23
22
|
|
|
24
23
|
certificate = OpenSSL::X509::Certificate.new
|
|
25
|
-
certificate.subject
|
|
26
|
-
certificate.issuer
|
|
24
|
+
certificate.subject = name
|
|
25
|
+
certificate.issuer = name
|
|
27
26
|
certificate.not_before = (Time.now - 5)
|
|
28
|
-
certificate.not_after
|
|
27
|
+
certificate.not_after = (Time.now + 60 * 60 * 24 * 365 * 20)
|
|
29
28
|
certificate.public_key = key_pair.public_key
|
|
30
|
-
certificate.serial
|
|
31
|
-
certificate.version
|
|
32
|
-
certificate.sign(key_pair, OpenSSL::Digest
|
|
29
|
+
certificate.serial = 1
|
|
30
|
+
certificate.version = 2
|
|
31
|
+
certificate.sign(key_pair, OpenSSL::Digest.new("SHA1"))
|
|
33
32
|
|
|
34
33
|
certificate
|
|
35
34
|
end
|
|
@@ -47,11 +46,11 @@ module Samlr
|
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def sign(string)
|
|
50
|
-
Base64.encode64(key_pair.sign(OpenSSL::Digest
|
|
49
|
+
Base64.encode64(key_pair.sign(OpenSSL::Digest.new("SHA1"), string)).delete("\n")
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def verify(signature, string)
|
|
54
|
-
key_pair.public_key.verify(OpenSSL::Digest
|
|
53
|
+
key_pair.public_key.verify(OpenSSL::Digest.new("SHA1"), Base64.decode64(signature), string)
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
def to_certificate
|
|
@@ -59,15 +58,15 @@ module Samlr
|
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def self.dump(path, certificate, id = "samlr")
|
|
62
|
-
File.
|
|
63
|
-
File.
|
|
61
|
+
File.write(File.join(path, "#{id}_private_key.pem"), certificate.key_pair.to_pem)
|
|
62
|
+
File.write(File.join(path, "#{id}_certificate.pem"), certificate.x509.to_pem)
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
def self.load(path, id = "samlr")
|
|
67
|
-
key_pair
|
|
66
|
+
key_pair = OpenSSL::PKey::RSA.new(File.read(File.join(path, "#{id}_private_key.pem")))
|
|
68
67
|
x509_cert = OpenSSL::X509::Certificate.new(File.read(File.join(path, "#{id}_certificate.pem")))
|
|
69
68
|
|
|
70
|
-
new(:
|
|
69
|
+
new(key_pair: key_pair, x509: x509_cert)
|
|
71
70
|
end
|
|
72
71
|
end
|
|
73
72
|
end
|
|
@@ -7,7 +7,7 @@ module Samlr
|
|
|
7
7
|
def self.build(options = {})
|
|
8
8
|
# Mandatory
|
|
9
9
|
name_id = options.fetch(:name_id)
|
|
10
|
-
issuer
|
|
10
|
+
issuer = options.fetch(:issuer)
|
|
11
11
|
|
|
12
12
|
builder = Nokogiri::XML::Builder.new do |xml|
|
|
13
13
|
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
|
|
@@ -21,10 +21,10 @@ module Samlr
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def self.logout_options(options)
|
|
24
|
-
name_id_options
|
|
25
|
-
options = {
|
|
26
|
-
options
|
|
27
|
-
options
|
|
24
|
+
name_id_options = options[:name_id_options] || {}
|
|
25
|
+
options = {"Format" => format_option(options)}
|
|
26
|
+
options["NameQualifier"] = name_id_options[:name_qualifier] if name_id_options[:name_qualifier]
|
|
27
|
+
options["SPNameQualifier"] = name_id_options[:spname_qualifier] if name_id_options[:spname_qualifier]
|
|
28
28
|
options
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -25,7 +25,7 @@ module Samlr
|
|
|
25
25
|
"Version" => "2.0"
|
|
26
26
|
}
|
|
27
27
|
result["InResponseTo"] = options[:in_response_to] if options[:in_response_to]
|
|
28
|
-
result["Destination"] = options[:destination]
|
|
28
|
+
result["Destination"] = options[:destination] if options[:destination]
|
|
29
29
|
result
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
module Samlr
|
|
2
2
|
module Tools
|
|
3
|
-
|
|
4
3
|
# Builds you some SP metadata. Accepts a hash with the below keys. No support for arrays
|
|
5
4
|
# of name id formats or asserion consumer services, build it if you need it.
|
|
6
5
|
#
|
|
@@ -8,16 +7,15 @@ module Samlr
|
|
|
8
7
|
# :name_identity_format => Samlr::EMAIL_FORMAT,
|
|
9
8
|
# :consumer_service_url => "https://sp.example.org/saml"
|
|
10
9
|
class MetadataBuilder
|
|
11
|
-
|
|
12
10
|
def self.build(options = {})
|
|
13
|
-
name_identity_format
|
|
14
|
-
consumer_service_url
|
|
11
|
+
name_identity_format = options[:name_identity_format]
|
|
12
|
+
consumer_service_url = options[:consumer_service_url]
|
|
15
13
|
consumer_service_binding = options[:consumer_service_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
16
|
-
metadata_id
|
|
17
|
-
sign_metadata
|
|
14
|
+
metadata_id = options[:metadata_id] || Samlr::Tools.uuid
|
|
15
|
+
sign_metadata = options[:sign_metadata] || false
|
|
18
16
|
|
|
19
17
|
# Mandatory
|
|
20
|
-
entity_id
|
|
18
|
+
entity_id = options.fetch(:entity_id)
|
|
21
19
|
|
|
22
20
|
builder = Nokogiri::XML::Builder.new do |xml|
|
|
23
21
|
xml.EntityDescriptor("xmlns:md" => NS_MAP["md"], "ID" => metadata_id, "entityID" => entity_id) do
|
|
@@ -2,15 +2,14 @@ require "nokogiri"
|
|
|
2
2
|
|
|
3
3
|
module Samlr
|
|
4
4
|
module Tools
|
|
5
|
-
|
|
6
5
|
# Use this for building the SAML auth request XML
|
|
7
6
|
module RequestBuilder
|
|
8
7
|
def self.build(options = {})
|
|
9
8
|
consumer_service_url = options[:consumer_service_url]
|
|
10
|
-
issuer
|
|
9
|
+
issuer = options[:issuer]
|
|
11
10
|
name_identity_format = options[:name_identity_format]
|
|
12
|
-
allow_create
|
|
13
|
-
authn_context
|
|
11
|
+
allow_create = options[:allow_create] || "true"
|
|
12
|
+
authn_context = options[:authn_context]
|
|
14
13
|
|
|
15
14
|
builder = Nokogiri::XML::Builder.new do |xml|
|
|
16
15
|
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
|
|
@@ -38,7 +37,6 @@ module Samlr
|
|
|
38
37
|
|
|
39
38
|
builder.to_xml(COMPACT)
|
|
40
39
|
end
|
|
41
|
-
|
|
42
40
|
end
|
|
43
41
|
end
|
|
44
42
|
end
|
|
@@ -4,41 +4,39 @@ require "uuidtools"
|
|
|
4
4
|
|
|
5
5
|
module Samlr
|
|
6
6
|
module Tools
|
|
7
|
-
|
|
8
7
|
# Use this for building test data, not ready to use for production data
|
|
9
8
|
module ResponseBuilder
|
|
10
|
-
|
|
11
9
|
def self.build(options = {})
|
|
12
|
-
issue_instant
|
|
13
|
-
response_id
|
|
14
|
-
assertion_id
|
|
15
|
-
status_code
|
|
16
|
-
name_id_format
|
|
17
|
-
subject_conf_m
|
|
18
|
-
version
|
|
19
|
-
auth_context
|
|
20
|
-
issuer
|
|
21
|
-
attributes
|
|
22
|
-
name_id
|
|
23
|
-
name_qualifier
|
|
10
|
+
issue_instant = options[:issue_instant] || Samlr::Tools::Timestamp.stamp
|
|
11
|
+
response_id = options[:response_id] || Samlr::Tools.uuid
|
|
12
|
+
assertion_id = options[:assertion_id] || Samlr::Tools.uuid
|
|
13
|
+
status_code = options[:status_code] || "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
14
|
+
name_id_format = options[:name_id_format] || EMAIL_FORMAT
|
|
15
|
+
subject_conf_m = options[:subject_conf_m] || "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
|
16
|
+
version = options[:version] || "2.0"
|
|
17
|
+
auth_context = options[:auth_context] || "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
|
18
|
+
issuer = options[:issuer] || "ResponseBuilder IdP"
|
|
19
|
+
attributes = options[:attributes] || {}
|
|
20
|
+
name_id = options[:name_id]
|
|
21
|
+
name_qualifier = options[:name_qualifier]
|
|
24
22
|
sp_name_qualifier = options[:sp_name_qualifier]
|
|
25
23
|
|
|
26
24
|
# Mandatory for responses
|
|
27
|
-
destination
|
|
28
|
-
in_response_to
|
|
25
|
+
destination = options.fetch(:destination)
|
|
26
|
+
in_response_to = options.fetch(:in_response_to)
|
|
29
27
|
not_on_or_after = options.fetch(:not_on_or_after)
|
|
30
|
-
not_before
|
|
31
|
-
audience
|
|
28
|
+
not_before = options.fetch(:not_before)
|
|
29
|
+
audience = options.fetch(:audience)
|
|
32
30
|
|
|
33
31
|
# Signature settings
|
|
34
|
-
sign_assertion
|
|
35
|
-
sign_response
|
|
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
|
|
36
34
|
|
|
37
35
|
# Fixture controls
|
|
38
|
-
skip_assertion
|
|
36
|
+
skip_assertion = options[:skip_assertion]
|
|
39
37
|
skip_conditions = options[:skip_conditions]
|
|
40
38
|
|
|
41
|
-
builder = Nokogiri::XML::Builder.new(:
|
|
39
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
42
40
|
xml.Response("xmlns:samlp" => NS_MAP["samlp"], "ID" => response_id, "InResponseTo" => in_response_to, "Version" => version, "IssueInstant" => issue_instant, "Destination" => destination) do
|
|
43
41
|
xml.doc.root.add_namespace_definition("saml", NS_MAP["saml"])
|
|
44
42
|
xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }
|
|
@@ -51,9 +49,9 @@ module Samlr
|
|
|
51
49
|
xml["saml"].Issuer(issuer)
|
|
52
50
|
|
|
53
51
|
xml["saml"].Subject do
|
|
54
|
-
name_id_options = {
|
|
55
|
-
name_id_options
|
|
56
|
-
name_id_options
|
|
52
|
+
name_id_options = {"Format" => name_id_format}
|
|
53
|
+
name_id_options["NameQualifier"] = name_qualifier unless name_qualifier.nil?
|
|
54
|
+
name_id_options["SPNameQualifier"] = sp_name_qualifier unless sp_name_qualifier.nil?
|
|
57
55
|
|
|
58
56
|
xml["saml"].NameID(name_id, name_id_options) unless name_id.nil?
|
|
59
57
|
|
|
@@ -99,30 +97,32 @@ module Samlr
|
|
|
99
97
|
|
|
100
98
|
# The core response is ready, not on to signing
|
|
101
99
|
response = builder.doc
|
|
102
|
-
assertion_options = options.merge(:
|
|
100
|
+
assertion_options = options.merge(skip_keyinfo: options[:skip_assertion_keyinfo])
|
|
103
101
|
response = sign(response, assertion_id, assertion_options) if sign_assertion
|
|
104
102
|
|
|
105
|
-
response_options = options.merge(:
|
|
106
|
-
response = sign(response, response_id, response_options)
|
|
103
|
+
response_options = options.merge(skip_keyinfo: options[:skip_response_keyinfo])
|
|
104
|
+
response = sign(response, response_id, response_options) if sign_response
|
|
107
105
|
|
|
108
106
|
response.to_xml(COMPACT)
|
|
109
107
|
end
|
|
110
108
|
|
|
111
109
|
def self.sign(document, element_id, options)
|
|
112
|
-
certificate
|
|
113
|
-
element
|
|
114
|
-
digest
|
|
115
|
-
canoned
|
|
116
|
-
signature
|
|
110
|
+
certificate = options[:certificate] || Samlr::Tools::CertificateBuilder.new
|
|
111
|
+
element = document.at("//*[@ID='#{element_id}']")
|
|
112
|
+
digest = digest(document, element, options)
|
|
113
|
+
canoned = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
|
|
114
|
+
signature = certificate.sign(canoned)
|
|
117
115
|
skip_keyinfo = options[:skip_keyinfo]
|
|
118
116
|
|
|
119
117
|
Nokogiri::XML::Builder.with(digest) do |xml|
|
|
120
118
|
xml.SignatureValue(signature)
|
|
121
|
-
|
|
122
|
-
xml.
|
|
123
|
-
xml.
|
|
119
|
+
unless skip_keyinfo
|
|
120
|
+
xml.KeyInfo do
|
|
121
|
+
xml.X509Data do
|
|
122
|
+
xml.X509Certificate(certificate.x509_as_pem)
|
|
123
|
+
end
|
|
124
124
|
end
|
|
125
|
-
end
|
|
125
|
+
end
|
|
126
126
|
end
|
|
127
127
|
# digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
|
|
128
128
|
if element.at("./saml:Issuer", NS_MAP)
|
|
@@ -135,23 +135,22 @@ module Samlr
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def self.digest(document, element, options)
|
|
138
|
-
c14n_method
|
|
139
|
-
sign_method
|
|
138
|
+
c14n_method = options[:c14n_method] || "http://www.w3.org/2001/10/xml-exc-c14n#"
|
|
139
|
+
sign_method = options[:sign_method] || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
|
140
140
|
digest_method = options[:digest_method] || "http://www.w3.org/2000/09/xmldsig#sha1"
|
|
141
141
|
env_signature = options[:env_signature] || "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
|
142
|
-
namespaces
|
|
142
|
+
namespaces = options[:namespaces] || ["#default", "samlp", "saml", "ds", "xs", "xsi"]
|
|
143
143
|
|
|
144
|
-
canoned
|
|
145
|
-
digest_value
|
|
144
|
+
canoned = element.canonicalize(C14N, namespaces)
|
|
145
|
+
digest_value = Base64.encode64(OpenSSL::Digest.new("SHA1").digest(canoned)).delete("\n")
|
|
146
146
|
|
|
147
|
-
builder = Nokogiri::XML::Builder.new(:
|
|
147
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
148
148
|
xml.Signature("xmlns" => NS_MAP["ds"]) do
|
|
149
|
-
|
|
150
149
|
xml.SignedInfo do
|
|
151
150
|
xml.CanonicalizationMethod("Algorithm" => c14n_method)
|
|
152
151
|
xml.SignatureMethod("Algorithm" => sign_method)
|
|
153
152
|
|
|
154
|
-
xml.Reference("URI" => "##{element[
|
|
153
|
+
xml.Reference("URI" => "##{element["ID"]}") do
|
|
155
154
|
xml.Transforms do
|
|
156
155
|
xml.Transform("Algorithm" => env_signature)
|
|
157
156
|
xml.Transform("Algorithm" => c14n_method) do
|
|
@@ -167,7 +166,6 @@ module Samlr
|
|
|
167
166
|
|
|
168
167
|
builder.doc.root
|
|
169
168
|
end
|
|
170
|
-
|
|
171
169
|
end
|
|
172
170
|
end
|
|
173
171
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Samlr
|
|
2
2
|
module Tools
|
|
3
3
|
module Timestamp
|
|
4
|
-
|
|
5
4
|
# Generate a current timestamp in ISO8601 format
|
|
6
5
|
def self.stamp(time = Time.now)
|
|
7
6
|
time.utc.iso8601
|
|
@@ -20,7 +19,6 @@ module Samlr
|
|
|
20
19
|
def self.not_before?(time)
|
|
21
20
|
Time.now.to_i >= (time.to_i - Samlr.jitter.to_i)
|
|
22
21
|
end
|
|
23
|
-
|
|
24
22
|
end
|
|
25
23
|
end
|
|
26
24
|
end
|
data/lib/samlr/tools.rb
CHANGED
|
@@ -15,10 +15,10 @@ require "samlr/tools/logout_response_builder"
|
|
|
15
15
|
module Samlr
|
|
16
16
|
module Tools
|
|
17
17
|
SHA_MAP = {
|
|
18
|
-
1
|
|
19
|
-
256
|
|
20
|
-
384
|
|
21
|
-
512
|
|
18
|
+
1 => OpenSSL::Digest::SHA1,
|
|
19
|
+
256 => OpenSSL::Digest::SHA256,
|
|
20
|
+
384 => OpenSSL::Digest::SHA384,
|
|
21
|
+
512 => OpenSSL::Digest::SHA512
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
# Convert algorithm attribute value to Ruby implementation
|
|
@@ -32,13 +32,13 @@ module Samlr
|
|
|
32
32
|
|
|
33
33
|
# Accepts a document and optionally :path => xpath, :c14n_mode => c14n_mode
|
|
34
34
|
def self.canonicalize(xml, options = {})
|
|
35
|
-
options
|
|
35
|
+
options = {c14n_mode: C14N}.merge(options)
|
|
36
36
|
document = Nokogiri::XML(xml) { |c| c.strict.noblanks }
|
|
37
37
|
|
|
38
|
-
if path = options[:path]
|
|
39
|
-
|
|
38
|
+
node = if (path = options[:path])
|
|
39
|
+
document.at(path, NS_MAP)
|
|
40
40
|
else
|
|
41
|
-
|
|
41
|
+
document
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
node.canonicalize(options[:c14n_mode], options[:namespaces])
|
|
@@ -52,17 +52,16 @@ module Samlr
|
|
|
52
52
|
# Deflates, Base64 encodes and CGI escapes a string
|
|
53
53
|
def self.encode(string)
|
|
54
54
|
deflated = Zlib::Deflate.deflate(string, 9)[2..-5]
|
|
55
|
-
encoded
|
|
56
|
-
|
|
57
|
-
escaped
|
|
55
|
+
encoded = Base64.encode64(deflated)
|
|
56
|
+
CGI.escape(encoded)
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
# CGI unescapes, Base64 decodes and inflates a string
|
|
61
60
|
def self.decode(string)
|
|
62
61
|
unescaped = CGI.unescape(string)
|
|
63
|
-
decoded
|
|
64
|
-
inflater
|
|
65
|
-
inflated
|
|
62
|
+
decoded = Base64.decode64(unescaped)
|
|
63
|
+
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
64
|
+
inflated = inflater.inflate(decoded)
|
|
66
65
|
|
|
67
66
|
inflater.finish
|
|
68
67
|
inflater.close
|
|
@@ -71,29 +70,29 @@ module Samlr
|
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
def self.validate!(options = {})
|
|
74
|
-
validate(options.merge(:
|
|
73
|
+
validate(options.merge(bang: true))
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
# Validate a SAML request or response against an XSD. Supply either :path or :document in the options and
|
|
78
77
|
# a :schema (defaults to SAML validation)
|
|
79
78
|
def self.validate(options = {})
|
|
80
79
|
document = options[:document] || File.read(options[:path])
|
|
81
|
-
schema
|
|
82
|
-
bang
|
|
80
|
+
schema = options.fetch(:schema, SAML_SCHEMA)
|
|
81
|
+
bang = options.fetch(:bang, false)
|
|
83
82
|
|
|
84
|
-
if document.is_a?(Nokogiri::XML::Document)
|
|
85
|
-
|
|
83
|
+
xml = if document.is_a?(Nokogiri::XML::Document)
|
|
84
|
+
document
|
|
86
85
|
else
|
|
87
|
-
|
|
86
|
+
Nokogiri::XML(document) { |c| c.strict }
|
|
88
87
|
end
|
|
89
88
|
|
|
90
89
|
# All bundled schemas are using relative schemaLocation. This means we'll have to
|
|
91
90
|
# change working directory to find them during validation.
|
|
92
91
|
Dir.chdir(Samlr.schema_location) do
|
|
93
|
-
if schema.is_a?(Nokogiri::XML::Schema)
|
|
94
|
-
|
|
92
|
+
xsd = if schema.is_a?(Nokogiri::XML::Schema)
|
|
93
|
+
schema
|
|
95
94
|
else
|
|
96
|
-
|
|
95
|
+
Nokogiri::XML::Schema(File.read(schema))
|
|
97
96
|
end
|
|
98
97
|
|
|
99
98
|
result = xsd.validate(xml)
|
|
@@ -113,7 +112,7 @@ module Samlr
|
|
|
113
112
|
def self.parse(data, compressed: false)
|
|
114
113
|
return unless data
|
|
115
114
|
decoded = Base64.decode64(data)
|
|
116
|
-
decoded =
|
|
115
|
+
decoded = inflate(decoded) if compressed
|
|
117
116
|
return unless decoded
|
|
118
117
|
begin
|
|
119
118
|
doc = Nokogiri::XML(decoded) { |config| config.strict }
|
|
@@ -126,7 +125,7 @@ module Samlr
|
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
begin
|
|
129
|
-
Samlr::Tools.validate!(:
|
|
128
|
+
Samlr::Tools.validate!(document: doc)
|
|
130
129
|
rescue Samlr::SamlrError => e
|
|
131
130
|
Samlr.logger.warn("Accepting non schema conforming response: #{e.message}, #{e.details}")
|
|
132
131
|
raise e unless Samlr.validation_mode == :log
|
|
@@ -135,7 +134,7 @@ module Samlr
|
|
|
135
134
|
end
|
|
136
135
|
|
|
137
136
|
def self.inflate(data)
|
|
138
|
-
inflater
|
|
137
|
+
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
139
138
|
decoded = inflater.inflate(data)
|
|
140
139
|
inflater.finish
|
|
141
140
|
inflater.close
|
data/lib/samlr/version.rb
CHANGED
data/lib/samlr.rb
CHANGED
|
@@ -2,22 +2,22 @@ require "nokogiri"
|
|
|
2
2
|
require "logger"
|
|
3
3
|
|
|
4
4
|
module Samlr
|
|
5
|
-
C14N
|
|
6
|
-
COMPACT = {
|
|
5
|
+
C14N = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
|
6
|
+
COMPACT = {indent: 0, save_with: Nokogiri::XML::Node::SaveOptions::AS_XML}
|
|
7
7
|
|
|
8
|
-
NS_MAP
|
|
9
|
-
"c14n"
|
|
10
|
-
"ds"
|
|
11
|
-
"saml"
|
|
8
|
+
NS_MAP = {
|
|
9
|
+
"c14n" => "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
10
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#",
|
|
11
|
+
"saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
12
12
|
"samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
13
|
-
"md"
|
|
14
|
-
"xsi"
|
|
15
|
-
"xs"
|
|
13
|
+
"md" => "urn:oasis:names:tc:SAML:2.0:metadata",
|
|
14
|
+
"xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
|
15
|
+
"xs" => "http://www.w3.org/2001/XMLSchema"
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
EMAIL_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
19
|
-
SAML_SCHEMA
|
|
20
|
-
META_SCHEMA
|
|
19
|
+
SAML_SCHEMA = "saml-schema-protocol-2.0.xsd"
|
|
20
|
+
META_SCHEMA = "saml-schema-metadata-2.0.xsd"
|
|
21
21
|
|
|
22
22
|
class << self
|
|
23
23
|
attr_accessor :schema_location
|
|
@@ -28,9 +28,9 @@ module Samlr
|
|
|
28
28
|
|
|
29
29
|
self.schema_location = File.join(File.dirname(__FILE__), "..", "config", "schemas")
|
|
30
30
|
self.validation_mode = :reject
|
|
31
|
-
self.jitter
|
|
32
|
-
self.logger
|
|
33
|
-
|
|
31
|
+
self.jitter = 0
|
|
32
|
+
self.logger = Logger.new($stderr)
|
|
33
|
+
logger.level = Logger::UNKNOWN
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
unless Object.new.respond_to?(:try)
|