lyrebird 0.0.0 → 1.0.0.alpha2
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/.github/workflows/ci.yml +25 -0
- data/.github/workflows/publish.yml +20 -0
- data/README.md +148 -17
- data/Rakefile +8 -1
- data/lib/lyrebird/assertion.rb +104 -0
- data/lib/lyrebird/certificate.rb +71 -0
- data/lib/lyrebird/defaults.rb +41 -0
- data/lib/lyrebird/encryption.rb +75 -0
- data/lib/lyrebird/id.rb +9 -0
- data/lib/lyrebird/namespaces.rb +17 -0
- data/lib/lyrebird/response.rb +79 -0
- data/lib/lyrebird/signature.rb +75 -0
- data/lib/lyrebird/version.rb +1 -1
- data/lib/lyrebird.rb +15 -1
- data/lyrebird.gemspec +26 -0
- data/test/lyrebird/assertion_test.rb +329 -0
- data/test/lyrebird/certificate_test.rb +87 -0
- data/test/lyrebird/defaults_test.rb +11 -0
- data/test/lyrebird/encryption_test.rb +126 -0
- data/test/lyrebird/id_test.rb +11 -0
- data/test/lyrebird/response_test.rb +240 -0
- data/test/lyrebird/signature_test.rb +137 -0
- data/test/test_helper.rb +4 -0
- metadata +103 -8
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lyrebird
|
|
4
|
+
class Signature
|
|
5
|
+
def initialize(element, certificate)
|
|
6
|
+
@element = element
|
|
7
|
+
@certificate = certificate
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sign!
|
|
11
|
+
issuer = @element.elements["saml:Issuer"]
|
|
12
|
+
@element.insert_after(issuer, signature_element)
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def signature_element
|
|
19
|
+
REXML::Element.new("ds:Signature").tap do |sig|
|
|
20
|
+
sig.add_namespace("ds", XMLDSIG_NS)
|
|
21
|
+
sig.add_element(signed_info)
|
|
22
|
+
sig.add_element(signature_value)
|
|
23
|
+
sig.add_element(key_info)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def signed_info
|
|
28
|
+
REXML::Element.new("ds:SignedInfo").tap do |si|
|
|
29
|
+
cm = si.add_element("ds:CanonicalizationMethod")
|
|
30
|
+
cm.add_attribute("Algorithm", EXC_C14N)
|
|
31
|
+
sm = si.add_element("ds:SignatureMethod")
|
|
32
|
+
sm.add_attribute("Algorithm", RSA_SHA256)
|
|
33
|
+
si.add_element(reference)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def signature_value
|
|
38
|
+
REXML::Element.new("ds:SignatureValue").tap do |sv|
|
|
39
|
+
sig = @certificate.private_key.sign("SHA256", signed_info.to_s)
|
|
40
|
+
sv.text = Base64.strict_encode64(sig)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key_info
|
|
45
|
+
REXML::Element.new("ds:KeyInfo").tap do |ki|
|
|
46
|
+
x = ki.add_element("ds:X509Data")
|
|
47
|
+
x.add_element("ds:X509Certificate").text = @certificate.base64
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reference
|
|
52
|
+
REXML::Element.new("ds:Reference").tap do |ref|
|
|
53
|
+
ref.add_attribute("URI", "##{@element.attributes["ID"]}")
|
|
54
|
+
ref.add_element(transforms)
|
|
55
|
+
dm = ref.add_element("ds:DigestMethod")
|
|
56
|
+
dm.add_attribute("Algorithm", SHA256_DIGEST)
|
|
57
|
+
ref.add_element("ds:DigestValue").text = compute_digest(@element)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def transforms
|
|
62
|
+
REXML::Element.new("ds:Transforms").tap do |t|
|
|
63
|
+
enveloped = t.add_element("ds:Transform")
|
|
64
|
+
enveloped.add_attribute("Algorithm", ENVELOPED_SIG)
|
|
65
|
+
c14n = t.add_element("ds:Transform")
|
|
66
|
+
c14n.add_attribute("Algorithm", EXC_C14N)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compute_digest(element)
|
|
71
|
+
digest = OpenSSL::Digest::SHA256.digest(element.to_s)
|
|
72
|
+
Base64.strict_encode64(digest)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/lyrebird/version.rb
CHANGED
data/lib/lyrebird.rb
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "ostruct"
|
|
6
|
+
require "rexml"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
require "time"
|
|
9
|
+
|
|
10
|
+
require_relative "lyrebird/assertion"
|
|
11
|
+
require_relative "lyrebird/certificate"
|
|
12
|
+
require_relative "lyrebird/defaults"
|
|
13
|
+
require_relative "lyrebird/encryption"
|
|
14
|
+
require_relative "lyrebird/id"
|
|
15
|
+
require_relative "lyrebird/namespaces"
|
|
16
|
+
require_relative "lyrebird/response"
|
|
17
|
+
require_relative "lyrebird/signature"
|
|
3
18
|
require_relative "lyrebird/version"
|
|
4
19
|
|
|
5
20
|
module Lyrebird
|
|
6
21
|
class Error < StandardError; end
|
|
7
|
-
# Your code goes here...
|
|
8
22
|
end
|
data/lyrebird.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/lyrebird/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "lyrebird"
|
|
7
|
+
spec.version = Lyrebird::VERSION
|
|
8
|
+
spec.authors = ["Josh"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "Mimics SAML Identity Provider (IdP) responses for testing"
|
|
11
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
12
|
+
|
|
13
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
14
|
+
spec.files.delete("Gemfile")
|
|
15
|
+
spec.files.delete(".gitignore")
|
|
16
|
+
spec.files.reject! { |f| f.start_with?("bin/") }
|
|
17
|
+
|
|
18
|
+
spec.require_paths = ["lib"]
|
|
19
|
+
|
|
20
|
+
spec.add_dependency "base64"
|
|
21
|
+
spec.add_dependency "ostruct"
|
|
22
|
+
spec.add_dependency "rexml"
|
|
23
|
+
|
|
24
|
+
spec.add_development_dependency "minitest"
|
|
25
|
+
spec.add_development_dependency "rake"
|
|
26
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module Lyrebird
|
|
6
|
+
class AssertionTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@assertion = Assertion.new.document
|
|
9
|
+
@root = @assertion.root
|
|
10
|
+
@subject = @root.elements["saml:Subject"]
|
|
11
|
+
@sc = @subject.elements["saml:SubjectConfirmation"]
|
|
12
|
+
@scd = @sc.elements["saml:SubjectConfirmationData"]
|
|
13
|
+
@conditions = @root.elements["saml:Conditions"]
|
|
14
|
+
@audience_restriction = @conditions.elements["saml:AudienceRestriction"]
|
|
15
|
+
@authn_statement = @root.elements["saml:AuthnStatement"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_root_name
|
|
19
|
+
assert_equal "Assertion", @root.name
|
|
20
|
+
assert_equal "saml", @root.prefix
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_root_namespace
|
|
24
|
+
assert_equal SAML_ASSERTION_NS, @root.namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_root_id
|
|
28
|
+
assert @root.attributes["ID"].start_with?("_")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_root_version
|
|
32
|
+
assert_equal "2.0", @root.attributes["Version"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_root_issue_instant
|
|
36
|
+
instant = Time.iso8601(@root.attributes["IssueInstant"])
|
|
37
|
+
assert_in_delta Time.now.to_i, instant.to_i, 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_issuer
|
|
41
|
+
issuer = @root.elements["saml:Issuer"]
|
|
42
|
+
assert_equal "Issuer", issuer.name
|
|
43
|
+
assert_equal "saml", issuer.prefix
|
|
44
|
+
assert_equal DEFAULTS.issuer, issuer.text
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_issuer_override
|
|
48
|
+
assertion = Assertion.new(issuer: "https://test.example.com").document
|
|
49
|
+
issuer = assertion.root.elements["saml:Issuer"]
|
|
50
|
+
assert_equal "https://test.example.com", issuer.text
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_subject
|
|
54
|
+
assert_equal "Subject", @subject.name
|
|
55
|
+
assert_equal "saml", @subject.prefix
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_name_id
|
|
59
|
+
name_id = @subject.elements["saml:NameID"]
|
|
60
|
+
assert_equal "NameID", name_id.name
|
|
61
|
+
assert_equal "saml", name_id.prefix
|
|
62
|
+
assert_equal DEFAULTS.name_id, name_id.text
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_name_id_format_default
|
|
66
|
+
name_id = @subject.elements["saml:NameID"]
|
|
67
|
+
assert_equal NAMEID_EMAIL, name_id.attributes["Format"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_name_id_override
|
|
71
|
+
email = "user@test.com"
|
|
72
|
+
refute_equal email, DEFAULTS.name_id
|
|
73
|
+
assertion = Assertion.new(name_id: email).document
|
|
74
|
+
name_id = assertion.root.elements["saml:Subject/saml:NameID"]
|
|
75
|
+
assert_equal email, name_id.text
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_name_id_format_override
|
|
79
|
+
format = NAMEID_PERSISTENT
|
|
80
|
+
refute_equal format, DEFAULTS.name_id_format
|
|
81
|
+
assertion = Assertion.new(name_id_format: format).document
|
|
82
|
+
name_id = assertion.root.elements["saml:Subject/saml:NameID"]
|
|
83
|
+
assert_equal format, name_id.attributes["Format"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_subject_confirmation
|
|
87
|
+
assert_equal "SubjectConfirmation", @sc.name
|
|
88
|
+
assert_equal "saml", @sc.prefix
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_subject_confirmation_method
|
|
92
|
+
assert_equal CM_BEARER, @sc.attributes["Method"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_subject_confirmation_data
|
|
96
|
+
assert_equal "SubjectConfirmationData", @scd.name
|
|
97
|
+
assert_equal "saml", @scd.prefix
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_valid_for_default
|
|
101
|
+
not_on_or_after = Time.iso8601(@scd.attributes["NotOnOrAfter"])
|
|
102
|
+
expected = Time.now.utc + DEFAULTS.valid_for
|
|
103
|
+
assert_in_delta expected.to_i, not_on_or_after.to_i, 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_recipient_default
|
|
107
|
+
assert_equal DEFAULTS.recipient, @scd.attributes["Recipient"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_in_response_to_default
|
|
111
|
+
assert_equal DEFAULTS.in_response_to, @scd.attributes["InResponseTo"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_recipient_override
|
|
115
|
+
recipient = "https://custom.example.com/acs"
|
|
116
|
+
refute_equal recipient, DEFAULTS.recipient
|
|
117
|
+
assertion = Assertion.new(recipient: recipient).document
|
|
118
|
+
subject = assertion.root.elements["saml:Subject"]
|
|
119
|
+
sc = subject.elements["saml:SubjectConfirmation"]
|
|
120
|
+
scd = sc.elements["saml:SubjectConfirmationData"]
|
|
121
|
+
assert_equal recipient, scd.attributes["Recipient"]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_in_response_to_override
|
|
125
|
+
in_response_to = "_custom_request"
|
|
126
|
+
refute_equal in_response_to, DEFAULTS.in_response_to
|
|
127
|
+
assertion = Assertion.new(in_response_to: in_response_to).document
|
|
128
|
+
subject = assertion.root.elements["saml:Subject"]
|
|
129
|
+
sc = subject.elements["saml:SubjectConfirmation"]
|
|
130
|
+
scd = sc.elements["saml:SubjectConfirmationData"]
|
|
131
|
+
assert_equal in_response_to, scd.attributes["InResponseTo"]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_in_response_to_omitted_when_nil
|
|
135
|
+
assertion = Assertion.new(in_response_to: nil).document
|
|
136
|
+
subject = assertion.root.elements["saml:Subject"]
|
|
137
|
+
sc = subject.elements["saml:SubjectConfirmation"]
|
|
138
|
+
scd = sc.elements["saml:SubjectConfirmationData"]
|
|
139
|
+
assert_nil scd.attributes["InResponseTo"]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_valid_for_override
|
|
143
|
+
valid_for = 600 # 10 minutes
|
|
144
|
+
refute_equal valid_for, DEFAULTS.valid_for
|
|
145
|
+
assertion = Assertion.new(valid_for: valid_for).document
|
|
146
|
+
subject = assertion.root.elements["saml:Subject"]
|
|
147
|
+
sc = subject.elements["saml:SubjectConfirmation"]
|
|
148
|
+
scd = sc.elements["saml:SubjectConfirmationData"]
|
|
149
|
+
not_on_or_after = Time.iso8601(scd.attributes["NotOnOrAfter"])
|
|
150
|
+
expected = Time.now.utc + valid_for
|
|
151
|
+
assert_in_delta expected.to_i, not_on_or_after.to_i, 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_conditions
|
|
155
|
+
assert_equal "Conditions", @conditions.name
|
|
156
|
+
assert_equal "saml", @conditions.prefix
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_conditions_not_before
|
|
160
|
+
not_before = Time.iso8601(@conditions.attributes["NotBefore"])
|
|
161
|
+
assert_in_delta Time.now.to_i, not_before.to_i, 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_conditions_not_before_override
|
|
165
|
+
not_before = Time.now.utc - 60
|
|
166
|
+
assertion = Assertion.new(not_before: not_before).document
|
|
167
|
+
conditions = assertion.root.elements["saml:Conditions"]
|
|
168
|
+
assert_equal not_before.iso8601, conditions.attributes["NotBefore"]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_conditions_not_on_or_after
|
|
172
|
+
not_on_or_after = Time.iso8601(@conditions.attributes["NotOnOrAfter"])
|
|
173
|
+
expected = Time.now.utc + DEFAULTS.valid_for
|
|
174
|
+
assert_in_delta expected.to_i, not_on_or_after.to_i, 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_conditions_valid_for_override
|
|
178
|
+
valid_for = 600 # 10 minutes
|
|
179
|
+
refute_equal valid_for, DEFAULTS.valid_for
|
|
180
|
+
assertion = Assertion.new(valid_for: valid_for).document
|
|
181
|
+
conditions = assertion.root.elements["saml:Conditions"]
|
|
182
|
+
not_on_or_after = Time.iso8601(conditions.attributes["NotOnOrAfter"])
|
|
183
|
+
expected = Time.now.utc + valid_for
|
|
184
|
+
assert_in_delta expected.to_i, not_on_or_after.to_i, 1
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_audience_restriction
|
|
188
|
+
assert_equal "AudienceRestriction", @audience_restriction.name
|
|
189
|
+
assert_equal "saml", @audience_restriction.prefix
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def test_audience
|
|
193
|
+
audience = @audience_restriction.elements["saml:Audience"]
|
|
194
|
+
assert_equal "Audience", audience.name
|
|
195
|
+
assert_equal "saml", audience.prefix
|
|
196
|
+
assert_equal DEFAULTS.audience, audience.text
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_audience_override
|
|
200
|
+
audience = "https://custom.sp.example.com"
|
|
201
|
+
refute_equal audience, DEFAULTS.audience
|
|
202
|
+
assertion = Assertion.new(audience: audience).document
|
|
203
|
+
conditions = assertion.root.elements["saml:Conditions"]
|
|
204
|
+
ar = conditions.elements["saml:AudienceRestriction"]
|
|
205
|
+
assert_equal audience, ar.elements["saml:Audience"].text
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_authn_statement
|
|
209
|
+
assert_equal "AuthnStatement", @authn_statement.name
|
|
210
|
+
assert_equal "saml", @authn_statement.prefix
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_authn_statement_authn_instant
|
|
214
|
+
authn_instant = Time.iso8601(@authn_statement.attributes["AuthnInstant"])
|
|
215
|
+
assert_in_delta Time.now.to_i, authn_instant.to_i, 1
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_authn_statement_session_index
|
|
219
|
+
assert @authn_statement.attributes["SessionIndex"].start_with?("_")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_authn_context
|
|
223
|
+
authn_context = @authn_statement.elements["saml:AuthnContext"]
|
|
224
|
+
assert_equal "AuthnContext", authn_context.name
|
|
225
|
+
assert_equal "saml", authn_context.prefix
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_authn_context_class_ref
|
|
229
|
+
ac = @authn_statement.elements["saml:AuthnContext"]
|
|
230
|
+
class_ref = ac.elements["saml:AuthnContextClassRef"]
|
|
231
|
+
assert_equal "AuthnContextClassRef", class_ref.name
|
|
232
|
+
assert_equal "saml", class_ref.prefix
|
|
233
|
+
assert_equal DEFAULTS.authn_context, class_ref.text
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_authn_context_override
|
|
237
|
+
custom_ref = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
|
238
|
+
refute_equal custom_ref, DEFAULTS.authn_context
|
|
239
|
+
assertion = Assertion.new(authn_context: custom_ref).document
|
|
240
|
+
as = assertion.root.elements["saml:AuthnStatement"]
|
|
241
|
+
ac = as.elements["saml:AuthnContext"]
|
|
242
|
+
class_ref = ac.elements["saml:AuthnContextClassRef"]
|
|
243
|
+
assert_equal custom_ref, class_ref.text
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_default_attributes
|
|
247
|
+
as = @root.elements["saml:AttributeStatement"]
|
|
248
|
+
attrs = as.elements.to_a("saml:Attribute")
|
|
249
|
+
assert_equal 2, attrs.size
|
|
250
|
+
|
|
251
|
+
first = attrs.find { |a| a.attributes["Name"] == "first_name" }
|
|
252
|
+
assert_equal "Test", first.elements["saml:AttributeValue"].text
|
|
253
|
+
|
|
254
|
+
last = attrs.find { |a| a.attributes["Name"] == "last_name" }
|
|
255
|
+
assert_equal "User", last.elements["saml:AttributeValue"].text
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_no_attribute_statement_when_empty
|
|
259
|
+
assertion = Assertion.new(attributes: {}).document
|
|
260
|
+
assert_nil assertion.root.elements["saml:AttributeStatement"]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_attribute_statement_with_single_value
|
|
264
|
+
attributes = { "email" => "user@example.com" }
|
|
265
|
+
assertion = Assertion.new(attributes: attributes).document
|
|
266
|
+
as = assertion.root.elements["saml:AttributeStatement"]
|
|
267
|
+
assert_equal "AttributeStatement", as.name
|
|
268
|
+
assert_equal "saml", as.prefix
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_attribute_name_and_format
|
|
272
|
+
attributes = { "email" => "user@example.com" }
|
|
273
|
+
assertion = Assertion.new(attributes: attributes).document
|
|
274
|
+
attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
|
|
275
|
+
assert_equal "Attribute", attr.name
|
|
276
|
+
assert_equal "saml", attr.prefix
|
|
277
|
+
assert_equal "email", attr.attributes["Name"]
|
|
278
|
+
assert_equal ATTR_NAME_FORMAT, attr.attributes["NameFormat"]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_attribute_single_value
|
|
282
|
+
attributes = { "email" => "user@example.com" }
|
|
283
|
+
assertion = Assertion.new(attributes: attributes).document
|
|
284
|
+
attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
|
|
285
|
+
value = attr.elements["saml:AttributeValue"]
|
|
286
|
+
assert_equal "AttributeValue", value.name
|
|
287
|
+
assert_equal "saml", value.prefix
|
|
288
|
+
assert_equal "user@example.com", value.text
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_attribute_multi_value
|
|
292
|
+
attributes = { "groups" => ["admin", "users", "developers"] }
|
|
293
|
+
assertion = Assertion.new(attributes: attributes).document
|
|
294
|
+
attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
|
|
295
|
+
values = attr.elements.to_a("saml:AttributeValue")
|
|
296
|
+
assert_equal 3, values.size
|
|
297
|
+
assert_equal "admin", values[0].text
|
|
298
|
+
assert_equal "users", values[1].text
|
|
299
|
+
assert_equal "developers", values[2].text
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_multiple_attributes
|
|
303
|
+
attributes = {
|
|
304
|
+
email: "user@example.com",
|
|
305
|
+
name: "Test User",
|
|
306
|
+
groups: ["admin", "users"]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
assertion = Assertion.new(attributes: attributes).document
|
|
310
|
+
as = assertion.root.elements["saml:AttributeStatement"]
|
|
311
|
+
attrs = as.elements.to_a("saml:Attribute")
|
|
312
|
+
assert_equal 3, attrs.size
|
|
313
|
+
|
|
314
|
+
email_attr = attrs.find { |a| a.attributes["Name"] == "email" }
|
|
315
|
+
email_value = email_attr.elements["saml:AttributeValue"].text
|
|
316
|
+
assert_equal "user@example.com", email_value
|
|
317
|
+
|
|
318
|
+
name_attr = attrs.find { |a| a.attributes["Name"] == "name" }
|
|
319
|
+
name_value = name_attr.elements["saml:AttributeValue"].text
|
|
320
|
+
assert_equal "Test User", name_value
|
|
321
|
+
|
|
322
|
+
groups_attr = attrs.find { |a| a.attributes["Name"] == "groups" }
|
|
323
|
+
group_values = groups_attr.elements.to_a("saml:AttributeValue")
|
|
324
|
+
assert_equal 2, group_values.size
|
|
325
|
+
assert_equal "admin", group_values[0].text
|
|
326
|
+
assert_equal "users", group_values[1].text
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module Lyrebird
|
|
6
|
+
class CertificateTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@certificate = Certificate.generate
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_private_key_is_rsa
|
|
12
|
+
assert_instance_of OpenSSL::PKey::RSA, @certificate.private_key
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_private_key_defaults_to_2048_bits
|
|
16
|
+
assert_equal 2048, @certificate.private_key.n.num_bits
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_private_key_with_custom_bits
|
|
20
|
+
certificate = Certificate.generate(bits: 4096)
|
|
21
|
+
assert_equal 4096, certificate.private_key.n.num_bits
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_certificate_is_x509
|
|
25
|
+
assert_instance_of OpenSSL::X509::Certificate, @certificate.certificate
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_certificate_not_before_is_now
|
|
29
|
+
assert_in_delta Time.now, @certificate.certificate.not_before, 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_certificate_not_after_is_one_year_from_now
|
|
33
|
+
one_year = 365 * 24 * 60 * 60
|
|
34
|
+
assert_in_delta Time.now + one_year, @certificate.certificate.not_after, 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_certificate_is_signed
|
|
38
|
+
assert @certificate.certificate.verify(@certificate.private_key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_certificate_with_custom_subject
|
|
42
|
+
certificate = Certificate.generate(cn: "Test", o: "Acme")
|
|
43
|
+
assert_equal "/CN=Test/O=Acme", certificate.certificate.subject.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_certificate_with_custom_valid_for
|
|
47
|
+
certificate = Certificate.generate(valid_for: 30).certificate
|
|
48
|
+
thirty_days = 30 * 24 * 60 * 60
|
|
49
|
+
assert_in_delta Time.now + thirty_days, certificate.not_after, 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_certificate_with_valid_until
|
|
53
|
+
valid_until = Time.new(2030, 1, 1)
|
|
54
|
+
certificate = Certificate.generate(valid_until: valid_until)
|
|
55
|
+
assert_equal valid_until, certificate.certificate.not_after
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_private_key_pem
|
|
59
|
+
assert @certificate.private_key_pem.start_with?("-----BEGIN")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_certificate_pem
|
|
63
|
+
assert @certificate.certificate_pem.start_with?("-----BEGIN")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_fingerprint
|
|
67
|
+
der = @certificate.certificate.to_der
|
|
68
|
+
expected = OpenSSL::Digest::SHA256.hexdigest(der)
|
|
69
|
+
assert_equal expected, @certificate.fingerprint
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_base64
|
|
73
|
+
der = @certificate.certificate.to_der
|
|
74
|
+
expected = Base64.strict_encode64(der)
|
|
75
|
+
assert_equal expected, @certificate.base64
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_load
|
|
79
|
+
certificate = Certificate.load(
|
|
80
|
+
private_key_pem: @certificate.private_key_pem,
|
|
81
|
+
certificate_pem: @certificate.certificate_pem,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert_equal @certificate.fingerprint, certificate.fingerprint
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module Lyrebird
|
|
6
|
+
class EncryptionTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@assertion = Assertion.new.document
|
|
9
|
+
@element = @assertion.root
|
|
10
|
+
@certificate = Certificate.generate
|
|
11
|
+
@encrypted = Encryption.new(@element, @certificate).encrypt
|
|
12
|
+
@ed = @encrypted.elements["xenc:EncryptedData"]
|
|
13
|
+
@ki = @ed.elements["ds:KeyInfo"]
|
|
14
|
+
@ek = @ki.elements["xenc:EncryptedKey"]
|
|
15
|
+
@cd = @ed.elements["xenc:CipherData"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_returns_encrypted_assertion_element
|
|
19
|
+
assert_equal "EncryptedAssertion", @encrypted.name
|
|
20
|
+
assert_equal "saml", @encrypted.prefix
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_encrypted_assertion_namespace
|
|
24
|
+
assert_equal SAML_ASSERTION_NS, @encrypted.namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_encrypted_data_element
|
|
28
|
+
assert_equal "EncryptedData", @ed.name
|
|
29
|
+
assert_equal "xenc", @ed.prefix
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_encrypted_data_namespace
|
|
33
|
+
assert_equal XMLENC_NS, @ed.namespace
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_encrypted_data_type
|
|
37
|
+
assert_equal "#{XMLENC_NS}Element", @ed.attributes["Type"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_encryption_method_element
|
|
41
|
+
em = @ed.elements["xenc:EncryptionMethod"]
|
|
42
|
+
assert_equal "EncryptionMethod", em.name
|
|
43
|
+
assert_equal "xenc", em.prefix
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_encryption_method_algorithm
|
|
47
|
+
em = @ed.elements["xenc:EncryptionMethod"]
|
|
48
|
+
assert_equal AES256_CBC, em.attributes["Algorithm"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_cipher_data_element
|
|
52
|
+
assert_equal "CipherData", @cd.name
|
|
53
|
+
assert_equal "xenc", @cd.prefix
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_cipher_value_element
|
|
57
|
+
cv = @cd.elements["xenc:CipherValue"]
|
|
58
|
+
assert_equal "CipherValue", cv.name
|
|
59
|
+
assert_equal "xenc", cv.prefix
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_cipher_value_is_base64
|
|
63
|
+
cv = @cd.elements["xenc:CipherValue"]
|
|
64
|
+
decoded = Base64.strict_decode64(cv.text)
|
|
65
|
+
assert decoded.bytesize > 16
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_cipher_value_starts_with_iv
|
|
69
|
+
cv = @cd.elements["xenc:CipherValue"]
|
|
70
|
+
decoded = Base64.strict_decode64(cv.text)
|
|
71
|
+
iv = decoded[0, 16]
|
|
72
|
+
assert_equal 16, iv.bytesize
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_key_info_element
|
|
76
|
+
assert_equal "KeyInfo", @ki.name
|
|
77
|
+
assert_equal "ds", @ki.prefix
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_key_info_namespace
|
|
81
|
+
assert_equal XMLDSIG_NS, @ki.namespace
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_encrypted_key_element
|
|
85
|
+
assert_equal "EncryptedKey", @ek.name
|
|
86
|
+
assert_equal "xenc", @ek.prefix
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_encrypted_key_namespace
|
|
90
|
+
assert_equal XMLENC_NS, @ek.namespace
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_encrypted_key_encryption_method
|
|
94
|
+
em = @ek.elements["xenc:EncryptionMethod"]
|
|
95
|
+
assert_equal "EncryptionMethod", em.name
|
|
96
|
+
assert_equal RSA_OAEP, em.attributes["Algorithm"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_encrypted_key_cipher_data
|
|
100
|
+
cd = @ek.elements["xenc:CipherData"]
|
|
101
|
+
assert_equal "CipherData", cd.name
|
|
102
|
+
assert_equal "xenc", cd.prefix
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_encrypted_key_cipher_value
|
|
106
|
+
cv = @ek.elements["xenc:CipherData/xenc:CipherValue"]
|
|
107
|
+
assert_equal "CipherValue", cv.name
|
|
108
|
+
assert_equal "xenc", cv.prefix
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_encrypted_key_cipher_value_is_base64
|
|
112
|
+
cv = @ek.elements["xenc:CipherData/xenc:CipherValue"]
|
|
113
|
+
decoded = Base64.strict_decode64(cv.text)
|
|
114
|
+
assert decoded.bytesize > 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_encrypted_key_can_be_decrypted
|
|
118
|
+
cv = @ek.elements["xenc:CipherData/xenc:CipherValue"]
|
|
119
|
+
encrypted_key = Base64.strict_decode64(cv.text)
|
|
120
|
+
private_key = @certificate.private_key
|
|
121
|
+
padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
|
122
|
+
decrypted_key = private_key.private_decrypt(encrypted_key, padding)
|
|
123
|
+
assert_equal 32, decrypted_key.bytesize
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|