lyrebird 1.0.0.alpha1 → 1.0.0
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/LICENSE.md +22 -0
- data/README.md +101 -61
- data/lib/lyrebird/assertion.rb +84 -48
- data/lib/lyrebird/certificate.rb +35 -23
- data/lib/lyrebird/defaults.rb +1 -1
- data/lib/lyrebird/encryption.rb +96 -0
- data/lib/lyrebird/namespaces.rb +3 -0
- data/lib/lyrebird/response.rb +69 -29
- data/lib/lyrebird/signature.rb +88 -40
- data/lib/lyrebird/version.rb +1 -1
- data/lib/lyrebird.rb +18 -1
- data/lyrebird.gemspec +9 -1
- data/test/lyrebird/assertion_test.rb +124 -90
- data/test/lyrebird/certificate_test.rb +70 -37
- data/test/lyrebird/defaults_test.rb +47 -0
- data/test/lyrebird/encryption_test.rb +139 -0
- data/test/lyrebird/id_test.rb +7 -0
- data/test/lyrebird/integration_test.rb +243 -0
- data/test/lyrebird/namespaces_test.rb +63 -0
- data/test/lyrebird/response_test.rb +141 -59
- data/test/lyrebird/signature_test.rb +77 -43
- metadata +55 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61005075359475ff6b492db62c80f7fea1724bb059c03f19a3f9d04c1f37871f
|
|
4
|
+
data.tar.gz: 0e44c46fe69c8701c7b5441f7a23b6bbf752ad957ae044e5f19d398c01a8db91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ccd8ff5e0c7a4f441dc275a6231924f8969993c7b670b83a60d01f16e4ebf8fff33ccedffaad21a45dc6bca056d1450c5b6566699f9fd733f71fc9342818bec
|
|
7
|
+
data.tar.gz: c0ebff2c3cc4134b84fb52327ee41c7d34bd9b2ab3d89ac7a50a3485bc912ca6936ad64388b04253001106a961f7f2caad313b88a2915e86c7bbd4c516eae673
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Simple SAML
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://badge.fury.io/rb/lyrebird)
|
|
4
|
+
|
|
1
5
|
# Lyrebird
|
|
2
6
|
A Ruby gem for mimicking SAML Identity Provider (IdP) responses in test
|
|
3
7
|
environments.
|
|
@@ -14,18 +18,19 @@ class SAMLTest < ActionDispatch::IntegrationTest
|
|
|
14
18
|
test "consume creates a session" do
|
|
15
19
|
user = users(:alice)
|
|
16
20
|
|
|
17
|
-
response = Lyrebird::Response.
|
|
18
|
-
issuer
|
|
19
|
-
destination
|
|
20
|
-
recipient
|
|
21
|
-
audience
|
|
22
|
-
name_id
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
response = Lyrebird::Response.build do |r|
|
|
22
|
+
r.issuer = "https://idp.example.com"
|
|
23
|
+
r.destination = saml_consume_url
|
|
24
|
+
r.recipient = saml_consume_url
|
|
25
|
+
r.audience = root_url
|
|
26
|
+
r.name_id = user.email
|
|
27
|
+
|
|
28
|
+
r.attributes do |a|
|
|
29
|
+
a.email = user.email
|
|
30
|
+
a.first_name = user.first_name
|
|
31
|
+
a.last_name = user.last_name
|
|
32
|
+
end
|
|
33
|
+
end
|
|
29
34
|
|
|
30
35
|
post saml_consume_path, params: { SAMLResponse: response.mimic }
|
|
31
36
|
|
|
@@ -36,45 +41,76 @@ end
|
|
|
36
41
|
```
|
|
37
42
|
|
|
38
43
|
## Response
|
|
39
|
-
|
|
44
|
+
Builds complete SAML responses with embedded assertions.
|
|
40
45
|
|
|
41
|
-
###
|
|
46
|
+
### Building a response
|
|
47
|
+
Defaults produce an SP-initiated response. See
|
|
48
|
+
[IdP-initiated SSO](#idp-initiated-sso) to omit `InResponseTo` and
|
|
49
|
+
`Destination`.
|
|
42
50
|
```ruby
|
|
43
|
-
# With defaults
|
|
44
|
-
response = Lyrebird::Response.
|
|
51
|
+
# With defaults (SP-initiated)
|
|
52
|
+
response = Lyrebird::Response.build
|
|
45
53
|
|
|
46
54
|
# 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
|
-
|
|
55
|
+
response = Lyrebird::Response.build do |r|
|
|
56
|
+
r.issuer = "https://idp.example.com"
|
|
57
|
+
r.destination = "https://sp.example.com/acs"
|
|
58
|
+
r.in_response_to = "_request_id"
|
|
59
|
+
r.name_id = "user@example.com"
|
|
60
|
+
r.name_id_format = Lyrebird::NAMEID_EMAIL
|
|
61
|
+
r.recipient = "https://sp.example.com/acs"
|
|
62
|
+
r.audience = "https://sp.example.com"
|
|
63
|
+
r.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
|
64
|
+
r.not_before = Time.now.utc
|
|
65
|
+
r.valid_for = 300 # seconds
|
|
66
|
+
r.sign_with = idp_cert
|
|
67
|
+
r.encrypt_with = sp_cert
|
|
68
|
+
|
|
69
|
+
r.attributes do |a|
|
|
70
|
+
a.email = "user@example.com"
|
|
71
|
+
a.groups = ["admin", "users"]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### IdP-initiated SSO
|
|
77
|
+
For unsolicited (IdP-initiated) flows where there is no AuthnRequest,
|
|
78
|
+
set `in_response_to` and `destination` to `nil` to omit them from the
|
|
79
|
+
XML entirely:
|
|
80
|
+
```ruby
|
|
81
|
+
response = Lyrebird::Response.build do |r|
|
|
82
|
+
r.in_response_to = nil
|
|
83
|
+
r.destination = nil
|
|
84
|
+
r.name_id = "user@example.com"
|
|
85
|
+
end
|
|
61
86
|
```
|
|
62
87
|
|
|
63
88
|
### Getting the encoded response
|
|
64
89
|
```ruby
|
|
65
90
|
response.mimic # Base64-encoded SAML response (for POST binding)
|
|
66
|
-
response.document #
|
|
91
|
+
response.document # Nokogiri::XML::Document for inspection
|
|
67
92
|
```
|
|
68
93
|
|
|
69
94
|
### Signing
|
|
95
|
+
Sign both the assertion and response with an IdP certificate:
|
|
70
96
|
```ruby
|
|
71
|
-
|
|
97
|
+
idp_cert = Lyrebird::Certificate.build
|
|
98
|
+
response = Lyrebird::Response.build(sign_with: idp_cert)
|
|
99
|
+
```
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
101
|
+
### Encryption
|
|
102
|
+
Encrypt assertions using the SP's certificate so only the SP can decrypt them:
|
|
103
|
+
```ruby
|
|
104
|
+
sp_cert = Lyrebird::Certificate.build
|
|
105
|
+
response = Lyrebird::Response.build(encrypt_with: sp_cert)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Signing and encryption can be combined:
|
|
109
|
+
```ruby
|
|
110
|
+
response = Lyrebird::Response.build do |r|
|
|
111
|
+
r.sign_with = idp_cert
|
|
112
|
+
r.encrypt_with = sp_cert
|
|
113
|
+
end
|
|
78
114
|
```
|
|
79
115
|
|
|
80
116
|
### NameID Formats
|
|
@@ -89,46 +125,50 @@ Lyrebird::NAMEID_UNSPECIFIED # unspecified
|
|
|
89
125
|
Override defaults globally for all responses/assertions:
|
|
90
126
|
```ruby
|
|
91
127
|
# test/test_helper.rb
|
|
92
|
-
Lyrebird
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
Lyrebird.configure do |d|
|
|
129
|
+
d.issuer = "https://custom.example.com"
|
|
130
|
+
d.recipient = "https://custom.example.com/acs"
|
|
131
|
+
d.audience = "https://custom.example.com"
|
|
132
|
+
d.name_id = "default@example.com"
|
|
133
|
+
d.valid_for = 600 # 10 minutes
|
|
134
|
+
d.attributes = { role: "user" }
|
|
135
|
+
end
|
|
98
136
|
```
|
|
137
|
+
Defaults are frozen after configuration for thread safety.
|
|
99
138
|
|
|
100
139
|
## Certificate
|
|
101
140
|
Generates and manages X.509 certificates for signing SAML responses.
|
|
102
141
|
|
|
103
|
-
###
|
|
142
|
+
### Building a certificate
|
|
104
143
|
```ruby
|
|
105
144
|
# With defaults
|
|
106
|
-
cert = Lyrebird::Certificate.
|
|
145
|
+
cert = Lyrebird::Certificate.build
|
|
107
146
|
|
|
108
147
|
# With options
|
|
109
|
-
cert = Lyrebird::Certificate.
|
|
110
|
-
bits
|
|
111
|
-
cn
|
|
112
|
-
o
|
|
113
|
-
valid_for
|
|
114
|
-
valid_until
|
|
115
|
-
|
|
148
|
+
cert = Lyrebird::Certificate.build do |c|
|
|
149
|
+
c.bits = 4096 # RSA key size (default: 2048)
|
|
150
|
+
c.cn = "example.com" # Common Name
|
|
151
|
+
c.o = "Acme" # Organization
|
|
152
|
+
c.valid_for = 30 # Days (default: 365)
|
|
153
|
+
c.valid_until = Time.new(2999, 12, 31) # Overrides valid_for
|
|
154
|
+
end
|
|
116
155
|
```
|
|
117
156
|
|
|
118
157
|
### Loading an existing certificate
|
|
119
158
|
```ruby
|
|
120
159
|
cert = Lyrebird::Certificate.load(
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
key_pem: File.read("private_key.pem"),
|
|
161
|
+
x509_pem: File.read("certificate.pem")
|
|
123
162
|
)
|
|
124
163
|
```
|
|
125
164
|
|
|
126
|
-
###
|
|
165
|
+
### Using a certificate
|
|
127
166
|
```ruby
|
|
128
|
-
cert.
|
|
129
|
-
cert.
|
|
130
|
-
cert.
|
|
131
|
-
cert.
|
|
132
|
-
cert.
|
|
133
|
-
cert.
|
|
167
|
+
cert.key # OpenSSL::PKey::RSA private key
|
|
168
|
+
cert.x509 # OpenSSL::X509::Certificate object
|
|
169
|
+
cert.key_pem # PEM-encoded private key
|
|
170
|
+
cert.x509_pem # PEM-encoded certificate
|
|
171
|
+
cert.sign(data) # Sign data with RSA-SHA256
|
|
172
|
+
cert.base64 # Base64-encoded certificate (for SAML metadata)
|
|
173
|
+
cert.fingerprint # SHA256 fingerprint
|
|
134
174
|
```
|
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
|
|
@@ -28,74 +28,110 @@ module Lyrebird
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def document
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
Nokogiri::XML::Document.new.tap do |doc|
|
|
32
|
+
doc.root = root(doc)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
|
|
38
|
-
def root
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
def root(doc)
|
|
39
|
+
doc.create_element("Assertion").tap do |a|
|
|
40
|
+
ns = a.add_namespace_definition("saml", SAML_ASSERTION_NS)
|
|
41
|
+
|
|
42
|
+
a.namespace = ns
|
|
43
|
+
a["ID"] = ID.generate
|
|
44
|
+
a["Version"] = "2.0"
|
|
45
|
+
a["IssueInstant"] = @issue_instant.iso8601
|
|
46
|
+
|
|
47
|
+
a.add_child(doc.create_element("Issuer")).tap do |i|
|
|
48
|
+
i.namespace = ns
|
|
49
|
+
i.content = @issuer
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
a.add_child(subject(doc, ns))
|
|
53
|
+
a.add_child(conditions(doc, ns))
|
|
54
|
+
a.add_child(authn_statement(doc, ns))
|
|
55
|
+
a.add_child(attribute_statement(doc, ns)) if @attributes.any?
|
|
49
56
|
end
|
|
50
57
|
end
|
|
51
58
|
|
|
52
|
-
def subject
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
def subject(doc, ns)
|
|
60
|
+
doc.create_element("Subject").tap do |s|
|
|
61
|
+
s.namespace = ns
|
|
62
|
+
|
|
63
|
+
s.add_child(doc.create_element("NameID")).tap do |nid|
|
|
64
|
+
nid.namespace = ns
|
|
65
|
+
nid["Format"] = @name_id_format
|
|
66
|
+
nid.content = @name_id
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
s.add_child(subject_confirmation(doc, ns))
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
def subject_confirmation
|
|
62
|
-
|
|
63
|
-
sc.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
def subject_confirmation(doc, ns)
|
|
74
|
+
doc.create_element("SubjectConfirmation").tap do |sc|
|
|
75
|
+
sc.namespace = ns
|
|
76
|
+
sc["Method"] = CM_BEARER
|
|
77
|
+
|
|
78
|
+
sc.add_child(doc.create_element("SubjectConfirmationData")).tap do |d|
|
|
79
|
+
d.namespace = ns
|
|
80
|
+
d["NotOnOrAfter"] = @not_on_or_after.iso8601
|
|
81
|
+
d["Recipient"] = @recipient
|
|
82
|
+
d["InResponseTo"] = @in_response_to if @in_response_to
|
|
83
|
+
end
|
|
68
84
|
end
|
|
69
85
|
end
|
|
70
86
|
|
|
71
|
-
def conditions
|
|
72
|
-
|
|
73
|
-
c.
|
|
74
|
-
c
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
def conditions(doc, ns)
|
|
88
|
+
doc.create_element("Conditions").tap do |c|
|
|
89
|
+
c.namespace = ns
|
|
90
|
+
c["NotBefore"] = @not_before.iso8601
|
|
91
|
+
c["NotOnOrAfter"] = @not_on_or_after.iso8601
|
|
92
|
+
|
|
93
|
+
c.add_child(doc.create_element("AudienceRestriction")).tap do |ar|
|
|
94
|
+
ar.namespace = ns
|
|
95
|
+
ar.add_child(doc.create_element("Audience")).tap do |a|
|
|
96
|
+
a.namespace = ns
|
|
97
|
+
a.content = @audience
|
|
98
|
+
end
|
|
99
|
+
end
|
|
77
100
|
end
|
|
78
101
|
end
|
|
79
102
|
|
|
80
|
-
def authn_statement
|
|
81
|
-
|
|
82
|
-
as.
|
|
83
|
-
as
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
def authn_statement(doc, ns)
|
|
104
|
+
doc.create_element("AuthnStatement").tap do |as|
|
|
105
|
+
as.namespace = ns
|
|
106
|
+
as["AuthnInstant"] = @issue_instant.iso8601
|
|
107
|
+
as["SessionIndex"] = ID.generate
|
|
108
|
+
|
|
109
|
+
as.add_child(doc.create_element("AuthnContext")).tap do |ac|
|
|
110
|
+
ac.namespace = ns
|
|
111
|
+
ac.add_child(doc.create_element("AuthnContextClassRef")).tap do |cr|
|
|
112
|
+
cr.namespace = ns
|
|
113
|
+
cr.content = @authn_context
|
|
114
|
+
end
|
|
115
|
+
end
|
|
87
116
|
end
|
|
88
117
|
end
|
|
89
118
|
|
|
90
|
-
def attribute_statement
|
|
91
|
-
|
|
119
|
+
def attribute_statement(doc, ns)
|
|
120
|
+
doc.create_element("AttributeStatement").tap do |as|
|
|
121
|
+
as.namespace = ns
|
|
122
|
+
|
|
92
123
|
@attributes.each do |name, values|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
as.add_child(doc.create_element("Attribute")).tap do |a|
|
|
125
|
+
a.namespace = ns
|
|
126
|
+
a["Name"] = name
|
|
127
|
+
a["NameFormat"] = ATTR_NAME_FORMAT
|
|
96
128
|
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
Array(values).each do |value|
|
|
130
|
+
a.add_child(doc.create_element("AttributeValue")).tap do |av|
|
|
131
|
+
av.namespace = ns
|
|
132
|
+
av.content = value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
99
135
|
end
|
|
100
136
|
end
|
|
101
137
|
end
|
data/lib/lyrebird/certificate.rb
CHANGED
|
@@ -2,59 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
module Lyrebird
|
|
4
4
|
class Certificate
|
|
5
|
-
attr_reader :
|
|
5
|
+
attr_reader :key, :x509
|
|
6
6
|
|
|
7
|
-
def self.
|
|
8
|
-
|
|
7
|
+
def self.build(**kwargs)
|
|
8
|
+
config = OpenStruct.new(kwargs)
|
|
9
|
+
yield config if block_given?
|
|
10
|
+
new(**config.to_h)
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def self.load(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
new(
|
|
13
|
+
def self.load(key_pem:, x509_pem:)
|
|
14
|
+
key = OpenSSL::PKey::RSA.new(key_pem)
|
|
15
|
+
x509 = OpenSSL::X509::Certificate.new(x509_pem)
|
|
16
|
+
new(key: key, x509: x509)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def initialize(
|
|
18
|
-
|
|
20
|
+
bits: 2048,
|
|
19
21
|
cn: nil,
|
|
20
22
|
o: nil,
|
|
21
23
|
valid_for: 365,
|
|
22
24
|
valid_until: nil,
|
|
23
|
-
|
|
25
|
+
key: nil,
|
|
26
|
+
x509: nil
|
|
24
27
|
)
|
|
25
|
-
@private_key = private_key
|
|
26
28
|
@common_name = cn
|
|
27
29
|
@organization = o
|
|
28
|
-
@
|
|
29
|
-
@
|
|
30
|
+
@valid_for = valid_for
|
|
31
|
+
@valid_until = valid_until
|
|
32
|
+
@key = key || OpenSSL::PKey::RSA.new(bits)
|
|
33
|
+
@x509 = x509 || build_x509
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
def
|
|
33
|
-
@
|
|
36
|
+
def key_pem
|
|
37
|
+
@key.to_pem
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
def
|
|
37
|
-
@
|
|
40
|
+
def x509_pem
|
|
41
|
+
@x509.to_pem
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def fingerprint
|
|
41
|
-
OpenSSL::Digest::SHA256.hexdigest(@
|
|
45
|
+
OpenSSL::Digest::SHA256.hexdigest(@x509.to_der)
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def base64
|
|
45
|
-
Base64.strict_encode64(@
|
|
49
|
+
Base64.strict_encode64(@x509.to_der)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sign(data)
|
|
53
|
+
@key.sign("SHA256", data)
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
private
|
|
49
57
|
|
|
50
|
-
def
|
|
58
|
+
def build_x509
|
|
59
|
+
now = Time.now.utc
|
|
60
|
+
|
|
51
61
|
OpenSSL::X509::Certificate.new.tap do |c|
|
|
52
|
-
c.
|
|
62
|
+
c.version = 2
|
|
63
|
+
c.serial = OpenSSL::BN.rand(64)
|
|
64
|
+
c.public_key = @key.public_key
|
|
53
65
|
c.subject = build_subject
|
|
54
66
|
c.issuer = c.subject
|
|
55
|
-
c.not_before =
|
|
56
|
-
c.not_after = @
|
|
57
|
-
c.sign(@
|
|
67
|
+
c.not_before = now
|
|
68
|
+
c.not_after = @valid_until || now + (@valid_for * 86_400)
|
|
69
|
+
c.sign(@key, OpenSSL::Digest::SHA256.new)
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
|
data/lib/lyrebird/defaults.rb
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lyrebird
|
|
4
|
+
class Encryption
|
|
5
|
+
def initialize(element, certificate)
|
|
6
|
+
@element = element
|
|
7
|
+
@certificate = certificate
|
|
8
|
+
@doc = element.document
|
|
9
|
+
@aes_key = SecureRandom.random_bytes(32)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def encrypt
|
|
13
|
+
build_encrypted_assertion
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def build_encrypted_assertion
|
|
19
|
+
@doc.create_element("EncryptedAssertion").tap do |ea|
|
|
20
|
+
@saml = ea.add_namespace_definition("saml", SAML_ASSERTION_NS)
|
|
21
|
+
ea.namespace = @saml
|
|
22
|
+
ea.add_child(build_encrypted_data)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_encrypted_data
|
|
27
|
+
@doc.create_element("EncryptedData").tap do |ed|
|
|
28
|
+
@xenc = ed.add_namespace_definition("xenc", XMLENC_NS)
|
|
29
|
+
ed.namespace = @xenc
|
|
30
|
+
ed["Type"] = "#{XMLENC_NS}Element"
|
|
31
|
+
|
|
32
|
+
ed.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
|
|
33
|
+
em.namespace = @xenc
|
|
34
|
+
em["Algorithm"] = AES256_CBC
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ed.add_child(build_key_info)
|
|
38
|
+
ed.add_child(build_cipher_data)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_key_info
|
|
43
|
+
@doc.create_element("KeyInfo").tap do |ki|
|
|
44
|
+
@ds = ki.add_namespace_definition("ds", XMLDSIG_NS)
|
|
45
|
+
ki.namespace = @ds
|
|
46
|
+
ki.add_child(build_encrypted_key)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_encrypted_key
|
|
51
|
+
@doc.create_element("EncryptedKey").tap do |ek|
|
|
52
|
+
ek.add_namespace_definition("xenc", XMLENC_NS)
|
|
53
|
+
ek.namespace = @xenc
|
|
54
|
+
|
|
55
|
+
ek.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
|
|
56
|
+
em.namespace = @xenc
|
|
57
|
+
em["Algorithm"] = RSA_OAEP
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
ek.add_child(build_encrypted_key_cipher_data)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_encrypted_key_cipher_data
|
|
65
|
+
public_key = @certificate.x509.public_key
|
|
66
|
+
padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
|
67
|
+
encrypted_aes_key = public_key.public_encrypt(@aes_key, padding)
|
|
68
|
+
|
|
69
|
+
@doc.create_element("CipherData").tap do |cd|
|
|
70
|
+
cd.namespace = @xenc
|
|
71
|
+
|
|
72
|
+
cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
|
|
73
|
+
cv.namespace = @xenc
|
|
74
|
+
cv.content = Base64.strict_encode64(encrypted_aes_key)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_cipher_data
|
|
80
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC")
|
|
81
|
+
cipher.encrypt
|
|
82
|
+
cipher.key = @aes_key
|
|
83
|
+
iv = cipher.random_iv
|
|
84
|
+
ciphertext = cipher.update(@element.to_xml) + cipher.final
|
|
85
|
+
|
|
86
|
+
@doc.create_element("CipherData").tap do |cd|
|
|
87
|
+
cd.namespace = @xenc
|
|
88
|
+
|
|
89
|
+
cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
|
|
90
|
+
cv.namespace = @xenc
|
|
91
|
+
cv.content = Base64.strict_encode64(iv + ciphertext)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
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
|