lyrebird 1.0.1 → 2.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/README.md +50 -13
- data/lib/lyrebird/assertion.rb +5 -3
- data/lib/lyrebird/certificate.rb +20 -3
- data/lib/lyrebird/encryption.rb +2 -2
- data/lib/lyrebird/response.rb +13 -6
- data/lib/lyrebird/signature.rb +3 -2
- data/lib/lyrebird/version.rb +1 -1
- metadata +6 -18
- data/.github/workflows/ci.yml +0 -25
- data/.github/workflows/publish.yml +0 -20
- data/Rakefile +0 -11
- data/lyrebird.gemspec +0 -33
- data/sig/lyrebird.rbs +0 -4
- data/test/lyrebird/assertion_test.rb +0 -353
- data/test/lyrebird/certificate_test.rb +0 -113
- data/test/lyrebird/defaults_test.rb +0 -58
- data/test/lyrebird/encryption_test.rb +0 -139
- data/test/lyrebird/id_test.rb +0 -18
- data/test/lyrebird/integration_test.rb +0 -243
- data/test/lyrebird/namespaces_test.rb +0 -63
- data/test/lyrebird/response_test.rb +0 -258
- data/test/lyrebird/signature_test.rb +0 -171
- data/test/test_helper.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b9457c5f260c26c5adf43de4262d67077e1f5729b4506ff0efa92ec61519346
|
|
4
|
+
data.tar.gz: 16a5dbf97973cd9c3673c26212fa0be146955ef9ddaf877768d40120416d6e54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc83af3b91fb52553dc96733c2a8ea0e89c561fe124bddc1b769cb39f681e03c1e18ff78ec2c83ffe886114b369e4a1630e637e56a1cf2dcee2137a5fe88680b
|
|
7
|
+
data.tar.gz: 138bd3e26e223c69dc10e435b21c12e0e1fcf3322e64df65d1775695d8920e6a274caf0d1323c5722244d73a47a09296d4d2a1d2037f2761084f470fcff6202c
|
data/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A Ruby gem for mimicking SAML Identity Provider (IdP) responses in test
|
|
|
7
7
|
environments.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
|
+
Requires Ruby 3.2 or newer.
|
|
10
11
|
```ruby
|
|
11
12
|
gem "lyrebird", group: :test
|
|
12
13
|
```
|
|
@@ -15,10 +16,6 @@ gem "lyrebird", group: :test
|
|
|
15
16
|
```ruby
|
|
16
17
|
# test/integration/saml_test.rb
|
|
17
18
|
class SAMLTest < ActionDispatch::IntegrationTest
|
|
18
|
-
setup do
|
|
19
|
-
@idp_cert = Lyrebird::Certificate.build
|
|
20
|
-
end
|
|
21
|
-
|
|
22
19
|
test "consume creates a session" do
|
|
23
20
|
user = users(:alice)
|
|
24
21
|
|
|
@@ -28,7 +25,7 @@ class SAMLTest < ActionDispatch::IntegrationTest
|
|
|
28
25
|
r.recipient = saml_consume_url
|
|
29
26
|
r.audience = root_url
|
|
30
27
|
r.name_id = user.email
|
|
31
|
-
r.sign_with =
|
|
28
|
+
r.sign_with = Lyrebird::Certificate.default
|
|
32
29
|
|
|
33
30
|
r.attributes do |a|
|
|
34
31
|
a.email = user.email
|
|
@@ -93,8 +90,12 @@ end
|
|
|
93
90
|
### Getting the encoded response
|
|
94
91
|
```ruby
|
|
95
92
|
response.mimic # Base64-encoded SAML response (for POST binding)
|
|
96
|
-
response.
|
|
93
|
+
response.to_xml # the same response as plain XML (for reading)
|
|
94
|
+
response.document # the response as a Nokogiri document (for searching)
|
|
97
95
|
```
|
|
96
|
+
Use `response.to_xml`, not `document.to_xml`. Going through `document`
|
|
97
|
+
adds spaces and line breaks, which breaks the signature, and the SP
|
|
98
|
+
rejects the response.
|
|
98
99
|
|
|
99
100
|
### Signing
|
|
100
101
|
Sign both the assertion and response with an IdP certificate:
|
|
@@ -117,6 +118,35 @@ response = Lyrebird::Response.build do |r|
|
|
|
117
118
|
r.encrypt_with = sp_cert
|
|
118
119
|
end
|
|
119
120
|
```
|
|
121
|
+
The assertion is signed first and then encrypted, and the response is
|
|
122
|
+
signed over the ciphertext.
|
|
123
|
+
|
|
124
|
+
### Testing rejection paths
|
|
125
|
+
Build deliberately invalid responses the SP should reject:
|
|
126
|
+
```ruby
|
|
127
|
+
# Expired (NotOnOrAfter in the past)
|
|
128
|
+
response = Lyrebird::Response.build do |r|
|
|
129
|
+
r.sign_with = idp_cert
|
|
130
|
+
r.valid_for = -10
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Not yet valid (NotBefore in the future)
|
|
134
|
+
response = Lyrebird::Response.build do |r|
|
|
135
|
+
r.sign_with = idp_cert
|
|
136
|
+
r.not_before = Time.now.utc + 600
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Wrong audience
|
|
140
|
+
response = Lyrebird::Response.build do |r|
|
|
141
|
+
r.sign_with = idp_cert
|
|
142
|
+
r.audience = "https://wrong.example.com"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Untrusted signature (a certificate the SP does not know)
|
|
146
|
+
response = Lyrebird::Response.build do |r|
|
|
147
|
+
r.sign_with = Lyrebird::Certificate.build
|
|
148
|
+
end
|
|
149
|
+
```
|
|
120
150
|
|
|
121
151
|
### NameID Formats
|
|
122
152
|
```ruby
|
|
@@ -139,13 +169,19 @@ Lyrebird.configure do |d|
|
|
|
139
169
|
d.attributes = { role: "user" }
|
|
140
170
|
end
|
|
141
171
|
```
|
|
142
|
-
Defaults are frozen after configuration for thread safety
|
|
172
|
+
Defaults are frozen after configuration for thread safety, so
|
|
173
|
+
`configure` can only be called once.
|
|
143
174
|
|
|
144
175
|
## Certificate
|
|
145
176
|
Generates and manages X.509 certificates for signing SAML responses.
|
|
146
177
|
|
|
147
178
|
### Building a certificate
|
|
179
|
+
Key generation is slow, so `Lyrebird::Certificate.default` generates one
|
|
180
|
+
certificate and reuses it.
|
|
148
181
|
```ruby
|
|
182
|
+
# Generated once and reused
|
|
183
|
+
cert = Lyrebird::Certificate.default
|
|
184
|
+
|
|
149
185
|
# With defaults
|
|
150
186
|
cert = Lyrebird::Certificate.build
|
|
151
187
|
|
|
@@ -169,10 +205,11 @@ cert = Lyrebird::Certificate.load(
|
|
|
169
205
|
|
|
170
206
|
### Using a certificate
|
|
171
207
|
```ruby
|
|
172
|
-
cert.key
|
|
173
|
-
cert.x509
|
|
174
|
-
cert.key_pem
|
|
175
|
-
cert.x509_pem
|
|
176
|
-
cert.base64
|
|
177
|
-
cert.fingerprint
|
|
208
|
+
cert.key # OpenSSL::PKey::RSA private key
|
|
209
|
+
cert.x509 # OpenSSL::X509::Certificate object
|
|
210
|
+
cert.key_pem # PEM-encoded private key
|
|
211
|
+
cert.x509_pem # PEM-encoded certificate
|
|
212
|
+
cert.base64 # Base64-encoded certificate (for SAML metadata)
|
|
213
|
+
cert.fingerprint # SHA256 fingerprint, colon-separated
|
|
214
|
+
cert.fingerprint(:sha1) # SHA1 fingerprint
|
|
178
215
|
```
|
data/lib/lyrebird/assertion.rb
CHANGED
|
@@ -20,8 +20,8 @@ module Lyrebird
|
|
|
20
20
|
@name_id_format = name_id_format
|
|
21
21
|
@recipient = recipient
|
|
22
22
|
@in_response_to = in_response_to
|
|
23
|
-
@not_before = not_before || @issue_instant
|
|
24
|
-
@not_on_or_after = @issue_instant + valid_for
|
|
23
|
+
@not_before = (not_before || @issue_instant).to_time.getutc
|
|
24
|
+
@not_on_or_after = [@issue_instant, @not_before].max + valid_for
|
|
25
25
|
@audience = audience
|
|
26
26
|
@authn_context = authn_context
|
|
27
27
|
@attributes = attributes
|
|
@@ -121,12 +121,14 @@ module Lyrebird
|
|
|
121
121
|
as.namespace = ns
|
|
122
122
|
|
|
123
123
|
@attributes.each do |name, values|
|
|
124
|
+
list = values.is_a?(Enumerable) ? values.to_a : [values].compact
|
|
125
|
+
|
|
124
126
|
as.add_child(doc.create_element("Attribute")).tap do |a|
|
|
125
127
|
a.namespace = ns
|
|
126
128
|
a["Name"] = name
|
|
127
129
|
a["NameFormat"] = ATTR_NAME_FORMAT
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
list.each do |value|
|
|
130
132
|
a.add_child(doc.create_element("AttributeValue")).tap do |av|
|
|
131
133
|
av.namespace = ns
|
|
132
134
|
av.content = value
|
data/lib/lyrebird/certificate.rb
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Lyrebird
|
|
4
4
|
class Certificate
|
|
5
|
+
LOCK = Mutex.new
|
|
6
|
+
|
|
7
|
+
EXTENSIONS = {
|
|
8
|
+
"basicConstraints" => "CA:FALSE",
|
|
9
|
+
"keyUsage" => "digitalSignature,keyEncipherment"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
5
12
|
attr_reader :key, :x509
|
|
6
13
|
|
|
14
|
+
def self.default
|
|
15
|
+
LOCK.synchronize { @default ||= build }
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
def self.build(**kwargs)
|
|
8
19
|
config = OpenStruct.new(kwargs)
|
|
9
20
|
yield config if block_given?
|
|
@@ -18,7 +29,7 @@ module Lyrebird
|
|
|
18
29
|
|
|
19
30
|
def initialize(
|
|
20
31
|
bits: 2048,
|
|
21
|
-
cn:
|
|
32
|
+
cn: "lyrebird",
|
|
22
33
|
o: nil,
|
|
23
34
|
valid_for: 365,
|
|
24
35
|
valid_until: nil,
|
|
@@ -41,8 +52,9 @@ module Lyrebird
|
|
|
41
52
|
@x509.to_pem
|
|
42
53
|
end
|
|
43
54
|
|
|
44
|
-
def fingerprint
|
|
45
|
-
OpenSSL::Digest
|
|
55
|
+
def fingerprint(algorithm = :sha256)
|
|
56
|
+
digest = OpenSSL::Digest.new(algorithm.to_s)
|
|
57
|
+
digest.hexdigest(@x509.to_der).upcase.scan(/../).join(":")
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
def base64
|
|
@@ -62,6 +74,11 @@ module Lyrebird
|
|
|
62
74
|
c.issuer = c.subject
|
|
63
75
|
c.not_before = now
|
|
64
76
|
c.not_after = @valid_until || now + (@valid_for * 86_400)
|
|
77
|
+
|
|
78
|
+
EXTENSIONS.each do |oid, value|
|
|
79
|
+
c.add_extension(OpenSSL::X509::Extension.new(oid, value, true))
|
|
80
|
+
end
|
|
81
|
+
|
|
65
82
|
c.sign(@key, OpenSSL::Digest::SHA256.new)
|
|
66
83
|
end
|
|
67
84
|
end
|
data/lib/lyrebird/encryption.rb
CHANGED
|
@@ -49,7 +49,6 @@ module Lyrebird
|
|
|
49
49
|
|
|
50
50
|
def build_encrypted_key
|
|
51
51
|
@doc.create_element("EncryptedKey").tap do |ek|
|
|
52
|
-
ek.add_namespace_definition("xenc", XMLENC_NS)
|
|
53
52
|
ek.namespace = @xenc
|
|
54
53
|
|
|
55
54
|
ek.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
|
|
@@ -81,7 +80,8 @@ module Lyrebird
|
|
|
81
80
|
cipher.encrypt
|
|
82
81
|
cipher.key = @aes_key
|
|
83
82
|
iv = cipher.random_iv
|
|
84
|
-
|
|
83
|
+
plaintext = @element.to_xml(save_with: 0)
|
|
84
|
+
ciphertext = cipher.update(plaintext) + cipher.final
|
|
85
85
|
|
|
86
86
|
@doc.create_element("CipherData").tap do |cd|
|
|
87
87
|
cd.namespace = @xenc
|
data/lib/lyrebird/response.rb
CHANGED
|
@@ -6,7 +6,11 @@ module Lyrebird
|
|
|
6
6
|
config = OpenStruct.new(kwargs)
|
|
7
7
|
|
|
8
8
|
config.define_singleton_method(:attributes) do |&block|
|
|
9
|
-
self
|
|
9
|
+
current = self[:attributes] || {}
|
|
10
|
+
return current unless block
|
|
11
|
+
|
|
12
|
+
fresh = OpenStruct.new.tap(&block).to_h
|
|
13
|
+
self[:attributes] = current.transform_keys(&:to_sym).merge(fresh)
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
yield config if block_given?
|
|
@@ -35,7 +39,11 @@ module Lyrebird
|
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def mimic
|
|
38
|
-
Base64.strict_encode64(
|
|
42
|
+
Base64.strict_encode64(to_xml)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_xml
|
|
46
|
+
document.to_xml(save_with: 0)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def document
|
|
@@ -66,13 +74,12 @@ module Lyrebird
|
|
|
66
74
|
end
|
|
67
75
|
|
|
68
76
|
r.add_child(build_status(doc))
|
|
69
|
-
r.add_child(build_assertion_element
|
|
77
|
+
r.add_child(build_assertion_element)
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
|
|
73
|
-
def build_assertion_element
|
|
74
|
-
|
|
75
|
-
element = assertion_doc.root
|
|
81
|
+
def build_assertion_element
|
|
82
|
+
element = @assertion.document.root
|
|
76
83
|
|
|
77
84
|
if @encrypt_with
|
|
78
85
|
Signature.new(element, @sign_with).sign! if @sign_with
|
data/lib/lyrebird/signature.rb
CHANGED
|
@@ -11,7 +11,9 @@ module Lyrebird
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def sign!
|
|
14
|
-
|
|
14
|
+
ns = { "saml" => SAML_ASSERTION_NS, "ds" => XMLDSIG_NS }
|
|
15
|
+
@element.at_xpath("ds:Signature", ns)&.remove
|
|
16
|
+
issuer = @element.at_xpath("saml:Issuer", ns)
|
|
15
17
|
issuer.add_next_sibling(build_signature)
|
|
16
18
|
populate_signature_value
|
|
17
19
|
self
|
|
@@ -33,7 +35,6 @@ module Lyrebird
|
|
|
33
35
|
def build_signed_info
|
|
34
36
|
@doc.create_element("SignedInfo").tap do |si|
|
|
35
37
|
@signed_info = si
|
|
36
|
-
si.add_namespace_definition("ds", XMLDSIG_NS)
|
|
37
38
|
si.namespace = @ds
|
|
38
39
|
|
|
39
40
|
si.add_child(@doc.create_element("CanonicalizationMethod")).tap do |cm|
|
data/lib/lyrebird/version.rb
CHANGED
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:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -114,11 +114,8 @@ executables: []
|
|
|
114
114
|
extensions: []
|
|
115
115
|
extra_rdoc_files: []
|
|
116
116
|
files:
|
|
117
|
-
- ".github/workflows/ci.yml"
|
|
118
|
-
- ".github/workflows/publish.yml"
|
|
119
117
|
- LICENSE.md
|
|
120
118
|
- README.md
|
|
121
|
-
- Rakefile
|
|
122
119
|
- lib/lyrebird.rb
|
|
123
120
|
- lib/lyrebird/assertion.rb
|
|
124
121
|
- lib/lyrebird/certificate.rb
|
|
@@ -129,22 +126,13 @@ files:
|
|
|
129
126
|
- lib/lyrebird/response.rb
|
|
130
127
|
- lib/lyrebird/signature.rb
|
|
131
128
|
- lib/lyrebird/version.rb
|
|
132
|
-
- lyrebird.gemspec
|
|
133
|
-
- sig/lyrebird.rbs
|
|
134
|
-
- test/lyrebird/assertion_test.rb
|
|
135
|
-
- test/lyrebird/certificate_test.rb
|
|
136
|
-
- test/lyrebird/defaults_test.rb
|
|
137
|
-
- test/lyrebird/encryption_test.rb
|
|
138
|
-
- test/lyrebird/id_test.rb
|
|
139
|
-
- test/lyrebird/integration_test.rb
|
|
140
|
-
- test/lyrebird/namespaces_test.rb
|
|
141
|
-
- test/lyrebird/response_test.rb
|
|
142
|
-
- test/lyrebird/signature_test.rb
|
|
143
|
-
- test/test_helper.rb
|
|
144
129
|
homepage: https://github.com/simplesaml/lyrebird
|
|
145
130
|
licenses:
|
|
146
131
|
- MIT
|
|
147
|
-
metadata:
|
|
132
|
+
metadata:
|
|
133
|
+
source_code_uri: https://github.com/simplesaml/lyrebird
|
|
134
|
+
changelog_uri: https://github.com/simplesaml/lyrebird/releases
|
|
135
|
+
bug_tracker_uri: https://github.com/simplesaml/lyrebird/issues
|
|
148
136
|
post_install_message:
|
|
149
137
|
rdoc_options: []
|
|
150
138
|
require_paths:
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
fail-fast: false
|
|
14
|
-
matrix:
|
|
15
|
-
ruby-version: ["3.2", "3.3", "3.4", "4.0"]
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
20
|
-
uses: ruby/setup-ruby@v1
|
|
21
|
-
with:
|
|
22
|
-
ruby-version: ${{ matrix.ruby-version }}
|
|
23
|
-
bundler-cache: true
|
|
24
|
-
- name: Run tests
|
|
25
|
-
run: bundle exec rake test
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
steps:
|
|
11
|
-
- uses: actions/checkout@v4
|
|
12
|
-
- uses: ruby/setup-ruby@v1
|
|
13
|
-
with:
|
|
14
|
-
ruby-version: "3.2"
|
|
15
|
-
- name: Build gem
|
|
16
|
-
run: gem build lyrebird.gemspec
|
|
17
|
-
- name: Publish to RubyGems
|
|
18
|
-
run: gem push lyrebird-*.gem
|
|
19
|
-
env:
|
|
20
|
-
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/Rakefile
DELETED
data/lyrebird.gemspec
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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.homepage = "https://github.com/simplesaml/lyrebird"
|
|
12
|
-
spec.license = "MIT"
|
|
13
|
-
spec.required_ruby_version = ">= 3.2.0"
|
|
14
|
-
|
|
15
|
-
spec.files = `git ls-files -z`.split("\x0")
|
|
16
|
-
spec.files.delete("Gemfile")
|
|
17
|
-
spec.files.delete(".gitignore")
|
|
18
|
-
spec.files.reject! { |f| f.start_with?("bin/") }
|
|
19
|
-
|
|
20
|
-
spec.require_paths = ["lib"]
|
|
21
|
-
|
|
22
|
-
spec.add_dependency "base64"
|
|
23
|
-
spec.add_dependency "nokogiri"
|
|
24
|
-
spec.add_dependency "ostruct"
|
|
25
|
-
|
|
26
|
-
# ruby-saml's gemspec has a conditional dependency on logger for Ruby >= 3.4,
|
|
27
|
-
# but it was evaluated at gem build time (on Ruby < 3.4), so the published gem
|
|
28
|
-
# doesn't include it.
|
|
29
|
-
spec.add_development_dependency "logger"
|
|
30
|
-
spec.add_development_dependency "minitest"
|
|
31
|
-
spec.add_development_dependency "rake"
|
|
32
|
-
spec.add_development_dependency "ruby-saml"
|
|
33
|
-
end
|
data/sig/lyrebird.rbs
DELETED