lyrebird 1.0.0.alpha1 → 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/README.md +69 -37
- data/lib/lyrebird/assertion.rb +6 -6
- data/lib/lyrebird/certificate.rb +6 -3
- data/lib/lyrebird/encryption.rb +75 -0
- data/lib/lyrebird/namespaces.rb +3 -0
- data/lib/lyrebird/response.rb +28 -15
- data/lib/lyrebird/signature.rb +3 -3
- data/lib/lyrebird/version.rb +1 -1
- data/lib/lyrebird.rb +2 -0
- data/lyrebird.gemspec +1 -0
- data/test/lyrebird/assertion_test.rb +15 -0
- data/test/lyrebird/encryption_test.rb +126 -0
- data/test/lyrebird/response_test.rb +104 -32
- data/test/lyrebird/signature_test.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b18c9229d638025f9882d18957a043c890e128ae5cbf9541d97f0cce078ce8e1
|
|
4
|
+
data.tar.gz: 28ce7ba82169a7fa5b1ac90299eb09af37781adefc74522a6ee43e16f822f740
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7edb0e358a6f295f80731bcf2d6544d55843b69c69bd25fcfbffc9a750240af558dfad598ba2939e6b9fa96033b17a7e827715908e08f3005ece03284c75f9a2
|
|
7
|
+
data.tar.gz: c7ba17cac0ffe23dcc264c52c6b8c6ce8fc44900bc0a302866e9c9aee2f874fd6f12ae5feda573c2d864047aac56a6bb5d67a000f570b018be54062b1e0acaab
|
data/README.md
CHANGED
|
@@ -14,18 +14,19 @@ class SAMLTest < ActionDispatch::IntegrationTest
|
|
|
14
14
|
test "consume creates a session" do
|
|
15
15
|
user = users(:alice)
|
|
16
16
|
|
|
17
|
-
response = Lyrebird::Response.
|
|
18
|
-
issuer
|
|
19
|
-
destination
|
|
20
|
-
recipient
|
|
21
|
-
audience
|
|
22
|
-
name_id
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
response = Lyrebird::Response.build do |r|
|
|
18
|
+
r.issuer = "https://idp.example.com"
|
|
19
|
+
r.destination = saml_consume_url
|
|
20
|
+
r.recipient = saml_consume_url
|
|
21
|
+
r.audience = root_url
|
|
22
|
+
r.name_id = user.email
|
|
23
|
+
|
|
24
|
+
r.attributes do |a|
|
|
25
|
+
a.email = user.email
|
|
26
|
+
a.first_name = user.first_name
|
|
27
|
+
a.last_name = user.last_name
|
|
28
|
+
end
|
|
29
|
+
end
|
|
29
30
|
|
|
30
31
|
post saml_consume_path, params: { SAMLResponse: response.mimic }
|
|
31
32
|
|
|
@@ -36,28 +37,48 @@ end
|
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
## Response
|
|
39
|
-
|
|
40
|
+
Builds complete SAML responses with embedded assertions.
|
|
40
41
|
|
|
41
|
-
###
|
|
42
|
+
### Building a response
|
|
43
|
+
Defaults produce an SP-initiated response. See
|
|
44
|
+
[IdP-initiated SSO](#idp-initiated-sso) to omit `InResponseTo` and
|
|
45
|
+
`Destination`.
|
|
42
46
|
```ruby
|
|
43
|
-
# With defaults
|
|
44
|
-
response = Lyrebird::Response.
|
|
47
|
+
# With defaults (SP-initiated)
|
|
48
|
+
response = Lyrebird::Response.build
|
|
45
49
|
|
|
46
50
|
# With options
|
|
47
|
-
response = Lyrebird::Response.
|
|
48
|
-
issuer
|
|
49
|
-
destination
|
|
50
|
-
in_response_to
|
|
51
|
-
name_id
|
|
52
|
-
name_id_format
|
|
53
|
-
recipient
|
|
54
|
-
audience
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
response = Lyrebird::Response.build do |r|
|
|
52
|
+
r.issuer = "https://idp.example.com"
|
|
53
|
+
r.destination = "https://sp.example.com/acs"
|
|
54
|
+
r.in_response_to = "_request_id"
|
|
55
|
+
r.name_id = "user@example.com"
|
|
56
|
+
r.name_id_format = Lyrebird::NAMEID_EMAIL
|
|
57
|
+
r.recipient = "https://sp.example.com/acs"
|
|
58
|
+
r.audience = "https://sp.example.com"
|
|
59
|
+
r.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
|
60
|
+
r.not_before = Time.now.utc
|
|
61
|
+
r.valid_for = 300 # seconds
|
|
62
|
+
r.sign_with = idp_cert
|
|
63
|
+
r.encrypt_with = sp_cert
|
|
64
|
+
|
|
65
|
+
r.attributes do |a|
|
|
66
|
+
a.email = "user@example.com"
|
|
67
|
+
a.groups = ["admin", "users"]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### IdP-initiated SSO
|
|
73
|
+
For unsolicited (IdP-initiated) flows where there is no AuthnRequest,
|
|
74
|
+
set `in_response_to` and `destination` to `nil` to omit them from the
|
|
75
|
+
XML entirely:
|
|
76
|
+
```ruby
|
|
77
|
+
response = Lyrebird::Response.build do |r|
|
|
78
|
+
r.in_response_to = nil
|
|
79
|
+
r.destination = nil
|
|
80
|
+
r.name_id = "user@example.com"
|
|
81
|
+
end
|
|
61
82
|
```
|
|
62
83
|
|
|
63
84
|
### Getting the encoded response
|
|
@@ -67,14 +88,25 @@ response.document # REXML::Document for inspection
|
|
|
67
88
|
```
|
|
68
89
|
|
|
69
90
|
### Signing
|
|
91
|
+
Sign both the assertion and response with an IdP certificate:
|
|
70
92
|
```ruby
|
|
71
|
-
|
|
93
|
+
idp_cert = Lyrebird::Certificate.generate
|
|
94
|
+
response = Lyrebird::Response.build(sign_with: idp_cert)
|
|
95
|
+
```
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
97
|
+
### Encryption
|
|
98
|
+
Encrypt assertions using the SP's certificate so only the SP can decrypt them:
|
|
99
|
+
```ruby
|
|
100
|
+
sp_cert = Lyrebird::Certificate.generate # In practice, provided by the SP
|
|
101
|
+
response = Lyrebird::Response.build(encrypt_with: sp_cert)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Signing and encryption can be combined:
|
|
105
|
+
```ruby
|
|
106
|
+
response = Lyrebird::Response.build do |r|
|
|
107
|
+
r.sign_with = idp_cert
|
|
108
|
+
r.encrypt_with = sp_cert
|
|
109
|
+
end
|
|
78
110
|
```
|
|
79
111
|
|
|
80
112
|
### NameID Formats
|
|
@@ -111,7 +143,7 @@ cert = Lyrebird::Certificate.generate(
|
|
|
111
143
|
cn: "example.com", # Common Name
|
|
112
144
|
o: "Acme", # Organization
|
|
113
145
|
valid_for: 30, # Validity in days (default: 365)
|
|
114
|
-
valid_until: Time.new(
|
|
146
|
+
valid_until: Time.new(2999, 12, 31) # Specific expiration (overrides valid_for)
|
|
115
147
|
)
|
|
116
148
|
```
|
|
117
149
|
|
data/lib/lyrebird/assertion.rb
CHANGED
|
@@ -8,19 +8,19 @@ module Lyrebird
|
|
|
8
8
|
name_id_format: DEFAULTS.name_id_format,
|
|
9
9
|
recipient: DEFAULTS.recipient,
|
|
10
10
|
in_response_to: DEFAULTS.in_response_to,
|
|
11
|
+
not_before: nil,
|
|
11
12
|
valid_for: DEFAULTS.valid_for,
|
|
12
13
|
audience: DEFAULTS.audience,
|
|
13
14
|
authn_context: DEFAULTS.authn_context,
|
|
14
15
|
attributes: DEFAULTS.attributes
|
|
15
16
|
)
|
|
16
|
-
@id = ID.generate
|
|
17
|
-
@session_index = ID.generate
|
|
18
17
|
@issue_instant = Time.now.utc
|
|
19
18
|
@issuer = issuer
|
|
20
19
|
@name_id = name_id
|
|
21
20
|
@name_id_format = name_id_format
|
|
22
21
|
@recipient = recipient
|
|
23
22
|
@in_response_to = in_response_to
|
|
23
|
+
@not_before = not_before || @issue_instant
|
|
24
24
|
@not_on_or_after = @issue_instant + valid_for
|
|
25
25
|
@audience = audience
|
|
26
26
|
@authn_context = authn_context
|
|
@@ -38,7 +38,7 @@ module Lyrebird
|
|
|
38
38
|
def root
|
|
39
39
|
REXML::Element.new("saml:Assertion").tap do |r|
|
|
40
40
|
r.add_namespace("saml", SAML_ASSERTION_NS)
|
|
41
|
-
r.add_attribute("ID",
|
|
41
|
+
r.add_attribute("ID", ID.generate)
|
|
42
42
|
r.add_attribute("Version", "2.0")
|
|
43
43
|
r.add_attribute("IssueInstant", @issue_instant.iso8601)
|
|
44
44
|
r.add_element("saml:Issuer").text = @issuer
|
|
@@ -64,13 +64,13 @@ module Lyrebird
|
|
|
64
64
|
data = sc.add_element("saml:SubjectConfirmationData")
|
|
65
65
|
data.add_attribute("NotOnOrAfter", @not_on_or_after.iso8601)
|
|
66
66
|
data.add_attribute("Recipient", @recipient)
|
|
67
|
-
data.add_attribute("InResponseTo", @in_response_to)
|
|
67
|
+
data.add_attribute("InResponseTo", @in_response_to) if @in_response_to
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def conditions
|
|
72
72
|
REXML::Element.new("saml:Conditions").tap do |c|
|
|
73
|
-
c.add_attribute("NotBefore", @
|
|
73
|
+
c.add_attribute("NotBefore", @not_before.iso8601)
|
|
74
74
|
c.add_attribute("NotOnOrAfter", @not_on_or_after.iso8601)
|
|
75
75
|
ar = c.add_element("saml:AudienceRestriction")
|
|
76
76
|
ar.add_element("saml:Audience").text = @audience
|
|
@@ -80,7 +80,7 @@ module Lyrebird
|
|
|
80
80
|
def authn_statement
|
|
81
81
|
REXML::Element.new("saml:AuthnStatement").tap do |as|
|
|
82
82
|
as.add_attribute("AuthnInstant", @issue_instant.iso8601)
|
|
83
|
-
as.add_attribute("SessionIndex",
|
|
83
|
+
as.add_attribute("SessionIndex", ID.generate)
|
|
84
84
|
ac = as.add_element("saml:AuthnContext")
|
|
85
85
|
cr = ac.add_element("saml:AuthnContextClassRef")
|
|
86
86
|
cr.text = @authn_context
|
data/lib/lyrebird/certificate.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Lyrebird
|
|
|
25
25
|
@private_key = private_key
|
|
26
26
|
@common_name = cn
|
|
27
27
|
@organization = o
|
|
28
|
-
@
|
|
28
|
+
@valid_for = valid_for
|
|
29
|
+
@valid_until = valid_until
|
|
29
30
|
@certificate = certificate || build_certificate
|
|
30
31
|
end
|
|
31
32
|
|
|
@@ -48,12 +49,14 @@ module Lyrebird
|
|
|
48
49
|
private
|
|
49
50
|
|
|
50
51
|
def build_certificate
|
|
52
|
+
now = Time.now
|
|
53
|
+
|
|
51
54
|
OpenSSL::X509::Certificate.new.tap do |c|
|
|
52
55
|
c.public_key = @private_key.public_key
|
|
53
56
|
c.subject = build_subject
|
|
54
57
|
c.issuer = c.subject
|
|
55
|
-
c.not_before =
|
|
56
|
-
c.not_after = @
|
|
58
|
+
c.not_before = now
|
|
59
|
+
c.not_after = @valid_until || now + (@valid_for * 86_400)
|
|
57
60
|
c.sign(@private_key, OpenSSL::Digest::SHA256.new)
|
|
58
61
|
end
|
|
59
62
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lyrebird
|
|
4
|
+
class Encryption
|
|
5
|
+
def initialize(element, certificate)
|
|
6
|
+
@element = element
|
|
7
|
+
@certificate = certificate
|
|
8
|
+
@aes_key = SecureRandom.random_bytes(32)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def encrypt
|
|
12
|
+
encrypted_assertion
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def encrypted_assertion
|
|
18
|
+
REXML::Element.new("saml:EncryptedAssertion").tap do |ea|
|
|
19
|
+
ea.add_namespace("saml", SAML_ASSERTION_NS)
|
|
20
|
+
ea.add_element(encrypted_data)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def encrypted_data
|
|
25
|
+
REXML::Element.new("xenc:EncryptedData").tap do |ed|
|
|
26
|
+
ed.add_namespace("xenc", XMLENC_NS)
|
|
27
|
+
ed.add_attribute("Type", "#{XMLENC_NS}Element")
|
|
28
|
+
em = ed.add_element("xenc:EncryptionMethod")
|
|
29
|
+
em.add_attribute("Algorithm", AES256_CBC)
|
|
30
|
+
ed.add_element(key_info)
|
|
31
|
+
ed.add_element(cipher_data)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_info
|
|
36
|
+
REXML::Element.new("ds:KeyInfo").tap do |ki|
|
|
37
|
+
ki.add_namespace("ds", XMLDSIG_NS)
|
|
38
|
+
ki.add_element(encrypted_key)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def encrypted_key
|
|
43
|
+
REXML::Element.new("xenc:EncryptedKey").tap do |ek|
|
|
44
|
+
ek.add_namespace("xenc", XMLENC_NS)
|
|
45
|
+
em = ek.add_element("xenc:EncryptionMethod")
|
|
46
|
+
em.add_attribute("Algorithm", RSA_OAEP)
|
|
47
|
+
ek.add_element(encrypted_key_cipher_data)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def encrypted_key_cipher_data
|
|
52
|
+
public_key = @certificate.certificate.public_key
|
|
53
|
+
padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
|
54
|
+
encrypted_aes_key = public_key.public_encrypt(@aes_key, padding)
|
|
55
|
+
|
|
56
|
+
REXML::Element.new("xenc:CipherData").tap do |cd|
|
|
57
|
+
cv = Base64.strict_encode64(encrypted_aes_key)
|
|
58
|
+
cd.add_element("xenc:CipherValue").text = cv
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cipher_data
|
|
63
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC")
|
|
64
|
+
cipher.encrypt
|
|
65
|
+
cipher.key = @aes_key
|
|
66
|
+
iv = cipher.random_iv
|
|
67
|
+
ciphertext = cipher.update(@element.to_s) + cipher.final
|
|
68
|
+
|
|
69
|
+
REXML::Element.new("xenc:CipherData").tap do |cd|
|
|
70
|
+
cv = Base64.strict_encode64(iv + ciphertext)
|
|
71
|
+
cd.add_element("xenc:CipherValue").text = cv
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/lyrebird/namespaces.rb
CHANGED
|
@@ -11,4 +11,7 @@ module Lyrebird
|
|
|
11
11
|
CM_BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
|
12
12
|
ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
|
|
13
13
|
STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
14
|
+
XMLENC_NS = "http://www.w3.org/2001/04/xmlenc#"
|
|
15
|
+
AES256_CBC = "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
|
|
16
|
+
RSA_OAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
|
|
14
17
|
end
|
data/lib/lyrebird/response.rb
CHANGED
|
@@ -2,23 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Lyrebird
|
|
4
4
|
class Response
|
|
5
|
+
def self.build(**kwargs)
|
|
6
|
+
config = OpenStruct.new(kwargs)
|
|
7
|
+
|
|
8
|
+
config.define_singleton_method(:attributes) do |&block|
|
|
9
|
+
self.attributes = OpenStruct.new.tap(&block).to_h
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
yield config if block_given?
|
|
13
|
+
new(**config.to_h)
|
|
14
|
+
end
|
|
15
|
+
|
|
5
16
|
def initialize(
|
|
6
17
|
issuer: DEFAULTS.issuer,
|
|
7
18
|
destination: DEFAULTS.recipient,
|
|
8
19
|
in_response_to: DEFAULTS.in_response_to,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sign_response: false,
|
|
20
|
+
sign_with: nil,
|
|
21
|
+
encrypt_with: nil,
|
|
12
22
|
**assertion_options
|
|
13
23
|
)
|
|
14
|
-
@id = ID.generate
|
|
15
|
-
@issue_instant = Time.now.utc
|
|
16
24
|
@issuer = issuer
|
|
17
25
|
@destination = destination
|
|
18
26
|
@in_response_to = in_response_to
|
|
19
|
-
@
|
|
20
|
-
@
|
|
21
|
-
@sign_response = sign_response
|
|
27
|
+
@sign_with = sign_with
|
|
28
|
+
@encrypt_with = encrypt_with
|
|
22
29
|
|
|
23
30
|
@assertion = Assertion.new(
|
|
24
31
|
issuer: issuer,
|
|
@@ -43,19 +50,25 @@ module Lyrebird
|
|
|
43
50
|
REXML::Element.new("samlp:Response").tap do |r|
|
|
44
51
|
r.add_namespace("samlp", SAML_PROTOCOL_NS)
|
|
45
52
|
r.add_namespace("saml", SAML_ASSERTION_NS)
|
|
46
|
-
r.add_attribute("ID",
|
|
53
|
+
r.add_attribute("ID", ID.generate)
|
|
47
54
|
r.add_attribute("Version", "2.0")
|
|
48
|
-
r.add_attribute("IssueInstant",
|
|
49
|
-
r.add_attribute("Destination", @destination)
|
|
50
|
-
r.add_attribute("InResponseTo", @in_response_to)
|
|
55
|
+
r.add_attribute("IssueInstant", Time.now.utc.iso8601)
|
|
56
|
+
r.add_attribute("Destination", @destination) if @destination
|
|
57
|
+
r.add_attribute("InResponseTo", @in_response_to) if @in_response_to
|
|
51
58
|
r.add_element("saml:Issuer").text = @issuer
|
|
52
59
|
r.add_element(status)
|
|
53
|
-
|
|
54
|
-
Signature.new(
|
|
55
|
-
Signature.new(r, certificate: @certificate).sign! if @sign_response
|
|
60
|
+
r.add_element(assertion_element)
|
|
61
|
+
Signature.new(r, @sign_with).sign! if @sign_with
|
|
56
62
|
end
|
|
57
63
|
end
|
|
58
64
|
|
|
65
|
+
def assertion_element
|
|
66
|
+
element = @assertion.document.root
|
|
67
|
+
Signature.new(element, @sign_with).sign! if @sign_with
|
|
68
|
+
return element unless @encrypt_with
|
|
69
|
+
Encryption.new(element, @encrypt_with).encrypt
|
|
70
|
+
end
|
|
71
|
+
|
|
59
72
|
def status
|
|
60
73
|
REXML::Element.new("samlp:Status").tap do |s|
|
|
61
74
|
sc = s.add_element("samlp:StatusCode")
|
data/lib/lyrebird/signature.rb
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Lyrebird
|
|
4
4
|
class Signature
|
|
5
|
-
def initialize(element, certificate
|
|
5
|
+
def initialize(element, certificate)
|
|
6
6
|
@element = element
|
|
7
7
|
@certificate = certificate
|
|
8
|
-
@element_id = @element.attributes["ID"]
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
def sign!
|
|
12
11
|
issuer = @element.elements["saml:Issuer"]
|
|
13
12
|
@element.insert_after(issuer, signature_element)
|
|
13
|
+
self
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -50,7 +50,7 @@ module Lyrebird
|
|
|
50
50
|
|
|
51
51
|
def reference
|
|
52
52
|
REXML::Element.new("ds:Reference").tap do |ref|
|
|
53
|
-
ref.add_attribute("URI", "##{@
|
|
53
|
+
ref.add_attribute("URI", "##{@element.attributes["ID"]}")
|
|
54
54
|
ref.add_element(transforms)
|
|
55
55
|
dm = ref.add_element("ds:DigestMethod")
|
|
56
56
|
dm.add_attribute("Algorithm", SHA256_DIGEST)
|
data/lib/lyrebird/version.rb
CHANGED
data/lib/lyrebird.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "openssl"
|
|
5
|
+
require "ostruct"
|
|
5
6
|
require "rexml"
|
|
6
7
|
require "securerandom"
|
|
7
8
|
require "time"
|
|
@@ -9,6 +10,7 @@ require "time"
|
|
|
9
10
|
require_relative "lyrebird/assertion"
|
|
10
11
|
require_relative "lyrebird/certificate"
|
|
11
12
|
require_relative "lyrebird/defaults"
|
|
13
|
+
require_relative "lyrebird/encryption"
|
|
12
14
|
require_relative "lyrebird/id"
|
|
13
15
|
require_relative "lyrebird/namespaces"
|
|
14
16
|
require_relative "lyrebird/response"
|
data/lyrebird.gemspec
CHANGED
|
@@ -131,6 +131,14 @@ module Lyrebird
|
|
|
131
131
|
assert_equal in_response_to, scd.attributes["InResponseTo"]
|
|
132
132
|
end
|
|
133
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
|
+
|
|
134
142
|
def test_valid_for_override
|
|
135
143
|
valid_for = 600 # 10 minutes
|
|
136
144
|
refute_equal valid_for, DEFAULTS.valid_for
|
|
@@ -153,6 +161,13 @@ module Lyrebird
|
|
|
153
161
|
assert_in_delta Time.now.to_i, not_before.to_i, 1
|
|
154
162
|
end
|
|
155
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
|
+
|
|
156
171
|
def test_conditions_not_on_or_after
|
|
157
172
|
not_on_or_after = Time.iso8601(@conditions.attributes["NotOnOrAfter"])
|
|
158
173
|
expected = Time.now.utc + DEFAULTS.valid_for
|
|
@@ -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
|
|
@@ -9,6 +9,78 @@ module Lyrebird
|
|
|
9
9
|
@root = @response.document.root
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def test_build_with_defaults
|
|
13
|
+
response = Response.build
|
|
14
|
+
root = response.document.root
|
|
15
|
+
assert_equal DEFAULTS.issuer, root.elements["saml:Issuer"].text
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_build_with_kwargs
|
|
19
|
+
issuer = "https://test.example.com"
|
|
20
|
+
refute_equal issuer, DEFAULTS.issuer
|
|
21
|
+
response = Response.build(issuer: issuer)
|
|
22
|
+
root = response.document.root
|
|
23
|
+
assert_equal issuer, root.elements["saml:Issuer"].text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_build_with_block
|
|
27
|
+
issuer = "https://test.example.com"
|
|
28
|
+
refute_equal issuer, DEFAULTS.issuer
|
|
29
|
+
response = Response.build { |r| r.issuer = issuer }
|
|
30
|
+
root = response.document.root
|
|
31
|
+
assert_equal issuer, root.elements["saml:Issuer"].text
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_build_with_kwargs_and_block
|
|
35
|
+
issuer = "https://test.example.com"
|
|
36
|
+
email = "test@example.com"
|
|
37
|
+
|
|
38
|
+
refute_equal issuer, DEFAULTS.issuer
|
|
39
|
+
refute_equal email, DEFAULTS.name_id
|
|
40
|
+
|
|
41
|
+
response = Response.build(issuer: issuer) do |r|
|
|
42
|
+
r.name_id = email
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
root = response.document.root
|
|
46
|
+
assert_equal issuer, root.elements["saml:Issuer"].text
|
|
47
|
+
name_id = root.elements["saml:Assertion/saml:Subject/saml:NameID"]
|
|
48
|
+
assert_equal email, name_id.text
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_build_with_attributes_block
|
|
52
|
+
email = "test@example.com"
|
|
53
|
+
role = "admin"
|
|
54
|
+
|
|
55
|
+
response = Response.build do |r|
|
|
56
|
+
r.attributes do |a|
|
|
57
|
+
a.email = email
|
|
58
|
+
a.role = role
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
root = response.document.root
|
|
63
|
+
statement = root.elements["saml:Assertion/saml:AttributeStatement"]
|
|
64
|
+
email_element = statement.elements["saml:Attribute[@Name='email']"]
|
|
65
|
+
role_element = statement.elements["saml:Attribute[@Name='role']"]
|
|
66
|
+
|
|
67
|
+
assert_equal email, email_element.elements["saml:AttributeValue"].text
|
|
68
|
+
assert_equal role, role_element.elements["saml:AttributeValue"].text
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_build_with_attributes_hash
|
|
72
|
+
email = "user@example.com"
|
|
73
|
+
|
|
74
|
+
response = Response.build do |r|
|
|
75
|
+
r.attributes = { email: email }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
root = response.document.root
|
|
79
|
+
statement = root.elements["saml:Assertion/saml:AttributeStatement"]
|
|
80
|
+
email_element = statement.elements["saml:Attribute[@Name='email']"]
|
|
81
|
+
assert_equal email, email_element.elements["saml:AttributeValue"].text
|
|
82
|
+
end
|
|
83
|
+
|
|
12
84
|
def test_mimic_returns_base64
|
|
13
85
|
encoded = @response.mimic
|
|
14
86
|
decoded = Base64.strict_decode64(encoded)
|
|
@@ -64,6 +136,16 @@ module Lyrebird
|
|
|
64
136
|
assert_equal in_response_to, root.attributes["InResponseTo"]
|
|
65
137
|
end
|
|
66
138
|
|
|
139
|
+
def test_destination_omitted_when_nil
|
|
140
|
+
root = Response.new(destination: nil).document.root
|
|
141
|
+
assert_nil root.attributes["Destination"]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_in_response_to_omitted_when_nil
|
|
145
|
+
root = Response.new(in_response_to: nil).document.root
|
|
146
|
+
assert_nil root.attributes["InResponseTo"]
|
|
147
|
+
end
|
|
148
|
+
|
|
67
149
|
def test_issuer
|
|
68
150
|
issuer = @root.elements["saml:Issuer"]
|
|
69
151
|
assert_equal "Issuer", issuer.name
|
|
@@ -121,48 +203,38 @@ module Lyrebird
|
|
|
121
203
|
assert_equal email, name_id.text
|
|
122
204
|
end
|
|
123
205
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
assert_nil
|
|
206
|
+
def test_unsigned_by_default
|
|
207
|
+
assert_nil @root.elements["ds:Signature"]
|
|
208
|
+
assert_nil @root.elements["saml:Assertion/ds:Signature"]
|
|
127
209
|
end
|
|
128
210
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
root
|
|
132
|
-
|
|
133
|
-
signature = assertion.elements["ds:Signature"]
|
|
134
|
-
assert_equal "Signature", signature.name
|
|
135
|
-
assert_equal "ds", signature.prefix
|
|
211
|
+
def test_sign_with_signs_response_and_assertion
|
|
212
|
+
root = Response.new(sign_with: Certificate.generate).document.root
|
|
213
|
+
refute_nil root.elements["ds:Signature"]
|
|
214
|
+
refute_nil root.elements["saml:Assertion/ds:Signature"]
|
|
136
215
|
end
|
|
137
216
|
|
|
138
|
-
def
|
|
139
|
-
assert_nil @root.elements["
|
|
217
|
+
def test_not_encrypted_by_default
|
|
218
|
+
assert_nil @root.elements["saml:EncryptedAssertion"]
|
|
140
219
|
end
|
|
141
220
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert_equal "
|
|
147
|
-
assert_equal "ds", signature.prefix
|
|
221
|
+
def test_encrypt_with_creates_encrypted_assertion
|
|
222
|
+
root = Response.new(encrypt_with: Certificate.generate).document.root
|
|
223
|
+
ea = root.elements["saml:EncryptedAssertion"]
|
|
224
|
+
assert_equal "EncryptedAssertion", ea.name
|
|
225
|
+
assert_equal "saml", ea.prefix
|
|
148
226
|
end
|
|
149
227
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
sign_assertion: true,
|
|
154
|
-
sign_response: true
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
root = Response.new(**args).document.root
|
|
158
|
-
refute_nil root.elements["ds:Signature"]
|
|
159
|
-
refute_nil root.elements["saml:Assertion/ds:Signature"]
|
|
228
|
+
def test_encrypt_with_removes_plain_assertion
|
|
229
|
+
root = Response.new(encrypt_with: Certificate.generate).document.root
|
|
230
|
+
assert_nil root.elements["saml:Assertion"]
|
|
160
231
|
end
|
|
161
232
|
|
|
162
|
-
def
|
|
163
|
-
root = Response.new(
|
|
164
|
-
|
|
165
|
-
|
|
233
|
+
def test_encrypt_with_contains_encrypted_data
|
|
234
|
+
root = Response.new(encrypt_with: Certificate.generate).document.root
|
|
235
|
+
ed = root.elements["saml:EncryptedAssertion/xenc:EncryptedData"]
|
|
236
|
+
assert_equal "EncryptedData", ed.name
|
|
166
237
|
end
|
|
238
|
+
|
|
167
239
|
end
|
|
168
240
|
end
|
|
@@ -8,7 +8,7 @@ module Lyrebird
|
|
|
8
8
|
@certificate = Certificate.generate
|
|
9
9
|
@assertion = Assertion.new.document
|
|
10
10
|
@element = @assertion.root
|
|
11
|
-
Signature.new(@element,
|
|
11
|
+
Signature.new(@element, @certificate).sign!
|
|
12
12
|
@signature = @element.elements["ds:Signature"]
|
|
13
13
|
@signed_info = @signature.elements["ds:SignedInfo"]
|
|
14
14
|
@reference = @signed_info.elements["ds:Reference"]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lyrebird
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.
|
|
4
|
+
version: 1.0.0.alpha2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ostruct
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rexml
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,6 +94,7 @@ files:
|
|
|
80
94
|
- lib/lyrebird/assertion.rb
|
|
81
95
|
- lib/lyrebird/certificate.rb
|
|
82
96
|
- lib/lyrebird/defaults.rb
|
|
97
|
+
- lib/lyrebird/encryption.rb
|
|
83
98
|
- lib/lyrebird/id.rb
|
|
84
99
|
- lib/lyrebird/namespaces.rb
|
|
85
100
|
- lib/lyrebird/response.rb
|
|
@@ -90,6 +105,7 @@ files:
|
|
|
90
105
|
- test/lyrebird/assertion_test.rb
|
|
91
106
|
- test/lyrebird/certificate_test.rb
|
|
92
107
|
- test/lyrebird/defaults_test.rb
|
|
108
|
+
- test/lyrebird/encryption_test.rb
|
|
93
109
|
- test/lyrebird/id_test.rb
|
|
94
110
|
- test/lyrebird/response_test.rb
|
|
95
111
|
- test/lyrebird/signature_test.rb
|