saml-kit 1.4.0 → 1.5.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/CHANGELOG.md +40 -2
- data/Gemfile.lock +53 -27
- data/lib/saml/kit/assertion.rb +7 -3
- data/lib/saml/kit/bindings/http_redirect.rb +12 -1
- data/lib/saml/kit/bindings/url_builder.rb +1 -1
- data/lib/saml/kit/concerns/trustable.rb +39 -2
- data/lib/saml/kit/configuration.rb +8 -0
- data/lib/saml/kit/locales/en.yml +3 -0
- data/lib/saml/kit/response.rb +17 -1
- data/lib/saml/kit/signature.rb +26 -18
- data/lib/saml/kit/signature_reference.rb +85 -0
- data/lib/saml/kit/version.rb +1 -1
- data/lib/saml/kit.rb +1 -0
- data/saml-kit.gemspec +1 -1
- metadata +5 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6758626dbf26ac6ea50535978aead6d9046a6dd31662cff9eb962154a682380e
|
|
4
|
+
data.tar.gz: 8fd7df975fa700f8d969f77c2fff0f59e9829ef3bde7ab6f41a6b5a94cb208d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e80fc106bbc6e06e167c0e5b6ce7b7f09d3bf7e471558533ec2a061089dc0367d60da5a393436a9feb3e8f5b12745193d7652c29f5d9cc9dae9ecd151a42a19
|
|
7
|
+
data.tar.gz: a3a39fcbff995f9ac1dceca502412efd165690fdd66b91d093f423d8cc1573486c95655b1f958746f98c682c48eba69357a8b058fef53d216e770c7e5b9d8620
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Version 1.
|
|
1
|
+
Version 1.5.0
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
All notable changes to this project will be documented in this file.
|
|
@@ -8,6 +8,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] - 2026-08-17
|
|
12
|
+
### Security
|
|
13
|
+
- Reject a `Response` or `Assertion` that carries no signature. A registered
|
|
14
|
+
issuer was previously enough to make an unsigned document valid, so anyone
|
|
15
|
+
who could reach the assertion consumer service could forge an assertion for
|
|
16
|
+
any user. (GHSA-hx7c-98jm-7r55)
|
|
17
|
+
- Require an xml signature to reference the element it is attached to. A
|
|
18
|
+
signature was verified without checking which element it covered, so a valid
|
|
19
|
+
signature could be copied onto a forged element with the element it really
|
|
20
|
+
covered hidden in `saml:Advice` or `ds:Object`. (GHSA-q726-45rx-qvrw)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
This release rejects documents that 1.4.1 accepted. Each case below was
|
|
24
|
+
exploitable or is a side effect of closing one that was.
|
|
25
|
+
|
|
26
|
+
- An unsigned `Response` or `Assertion` is now invalid, even when its issuer is
|
|
27
|
+
registered. Set `configuration.signature_required = false` to restore the old
|
|
28
|
+
behaviour while an identity provider is being fixed; doing so accepts forged
|
|
29
|
+
assertions. Signing only the `Assertion` is still accepted, as is an unsigned
|
|
30
|
+
`AuthnRequest`, `LogoutRequest` or `LogoutResponse`.
|
|
31
|
+
- `Response.build` on a `Configuration` with no signing key pair emits an
|
|
32
|
+
unsigned document, which now fails `valid?`. Call
|
|
33
|
+
`generate_key_pair_for(use: :signing)` or `add_key_pair`.
|
|
34
|
+
- `Assertion.new` with an unsigned assertion as a `String` is now invalid,
|
|
35
|
+
because a bare assertion is the whole document and nothing else vouches for
|
|
36
|
+
it. Passing the same assertion as a node nested in a `Response` is unchanged.
|
|
37
|
+
- A signature whose `ds:Reference` does not resolve to the element it hangs
|
|
38
|
+
off, or resolves to more than one element, adds an error on `:reference`.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `Saml::Kit::Configuration#signature_required`, defaulting to `true`.
|
|
42
|
+
|
|
43
|
+
## [1.4.1] - 2026-08-14
|
|
44
|
+
### Changed
|
|
45
|
+
- fix: decode encoded signature
|
|
46
|
+
|
|
11
47
|
## [1.4.0] - 2025-03-26
|
|
12
48
|
### Added
|
|
13
49
|
- Add support for converting `Assertion` xml to `Saml::Document`
|
|
@@ -106,7 +142,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
106
142
|
### Removed
|
|
107
143
|
- Removed optional SessionNotOnOrAfter attribute from AuthnStatement.
|
|
108
144
|
|
|
109
|
-
[Unreleased]: https://github.com/xlgmokha/saml-kit/compare/v1.
|
|
145
|
+
[Unreleased]: https://github.com/xlgmokha/saml-kit/compare/v1.5.0...HEAD
|
|
146
|
+
[1.5.0]: https://github.com/xlgmokha/saml-kit/compare/v1.4.1...v1.5.0
|
|
147
|
+
[1.4.1]: https://github.com/xlgmokha/saml-kit/compare/v1.4.0...v1.4.1
|
|
110
148
|
[1.4.0]: https://github.com/xlgmokha/saml-kit/compare/v1.3.0...v1.4.0
|
|
111
149
|
[1.3.0]: https://github.com/xlgmokha/saml-kit/compare/v1.2.0...v1.3.0
|
|
112
150
|
[1.2.0]: https://github.com/xlgmokha/saml-kit/compare/v1.1.0...v1.2.0
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
saml-kit (1.
|
|
4
|
+
saml-kit (1.5.0)
|
|
5
5
|
activemodel (>= 5.1)
|
|
6
6
|
base64 (~> 0.1)
|
|
7
7
|
cgi (~> 0.1)
|
|
@@ -16,66 +16,84 @@ PATH
|
|
|
16
16
|
GEM
|
|
17
17
|
remote: https://rubygems.org/
|
|
18
18
|
specs:
|
|
19
|
-
activemodel (
|
|
20
|
-
activesupport (=
|
|
21
|
-
activesupport (
|
|
19
|
+
activemodel (8.1.3.1)
|
|
20
|
+
activesupport (= 8.1.3.1)
|
|
21
|
+
activesupport (8.1.3.1)
|
|
22
22
|
base64
|
|
23
|
-
benchmark (>= 0.3)
|
|
24
23
|
bigdecimal
|
|
25
24
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
26
25
|
connection_pool (>= 2.2.5)
|
|
27
26
|
drb
|
|
28
27
|
i18n (>= 1.6, < 2)
|
|
28
|
+
json
|
|
29
29
|
logger (>= 1.4.2)
|
|
30
30
|
minitest (>= 5.1)
|
|
31
31
|
securerandom (>= 0.3)
|
|
32
32
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
uri (>= 0.13.1)
|
|
34
|
+
addressable (2.9.0)
|
|
35
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
35
36
|
ast (2.4.3)
|
|
36
|
-
base64 (0.
|
|
37
|
-
benchmark (0.4.0)
|
|
37
|
+
base64 (0.3.0)
|
|
38
38
|
benchmark-malloc (0.2.0)
|
|
39
39
|
benchmark-perf (0.6.0)
|
|
40
40
|
benchmark-trend (0.4.0)
|
|
41
|
-
bigdecimal (
|
|
41
|
+
bigdecimal (4.1.2)
|
|
42
42
|
builder (3.3.0)
|
|
43
43
|
bundler-audit (0.9.2)
|
|
44
44
|
bundler (>= 1.2.0, < 3)
|
|
45
45
|
thor (~> 1.0)
|
|
46
|
-
cgi (0.
|
|
47
|
-
concurrent-ruby (1.3.
|
|
48
|
-
connection_pool (
|
|
46
|
+
cgi (0.5.2)
|
|
47
|
+
concurrent-ruby (1.3.8)
|
|
48
|
+
connection_pool (3.0.2)
|
|
49
49
|
crack (1.0.0)
|
|
50
50
|
bigdecimal
|
|
51
51
|
rexml
|
|
52
52
|
date (3.4.1)
|
|
53
53
|
diff-lcs (1.6.1)
|
|
54
54
|
docile (1.4.1)
|
|
55
|
-
drb (2.2.
|
|
56
|
-
erb (4.0.4)
|
|
55
|
+
drb (2.2.3)
|
|
56
|
+
erb (4.0.4.1)
|
|
57
57
|
cgi (>= 0.3.3)
|
|
58
58
|
ffaker (2.24.0)
|
|
59
59
|
forwardable (1.3.3)
|
|
60
60
|
hashdiff (1.1.2)
|
|
61
|
-
i18n (1.
|
|
61
|
+
i18n (1.15.2)
|
|
62
62
|
concurrent-ruby (~> 1.0)
|
|
63
63
|
io-console (0.8.0)
|
|
64
64
|
irb (1.15.1)
|
|
65
65
|
pp (>= 0.6.0)
|
|
66
66
|
rdoc (>= 4.0.0)
|
|
67
67
|
reline (>= 0.4.2)
|
|
68
|
-
json (2.
|
|
68
|
+
json (2.21.2)
|
|
69
69
|
language_server-protocol (3.17.0.4)
|
|
70
70
|
lint_roller (1.1.0)
|
|
71
|
-
logger (1.
|
|
72
|
-
mini_portile2 (2.8.
|
|
73
|
-
minitest (
|
|
71
|
+
logger (1.7.0)
|
|
72
|
+
mini_portile2 (2.8.9)
|
|
73
|
+
minitest (6.0.6)
|
|
74
|
+
drb (~> 2.0)
|
|
75
|
+
prism (~> 1.5)
|
|
74
76
|
net-hippie (1.2.0)
|
|
75
77
|
logger (~> 1.0)
|
|
76
|
-
nokogiri (1.
|
|
78
|
+
nokogiri (1.19.4)
|
|
77
79
|
mini_portile2 (~> 2.8.2)
|
|
78
80
|
racc (~> 1.4)
|
|
81
|
+
nokogiri (1.19.4-aarch64-linux-gnu)
|
|
82
|
+
racc (~> 1.4)
|
|
83
|
+
nokogiri (1.19.4-aarch64-linux-musl)
|
|
84
|
+
racc (~> 1.4)
|
|
85
|
+
nokogiri (1.19.4-arm-linux-gnu)
|
|
86
|
+
racc (~> 1.4)
|
|
87
|
+
nokogiri (1.19.4-arm-linux-musl)
|
|
88
|
+
racc (~> 1.4)
|
|
89
|
+
nokogiri (1.19.4-arm64-darwin)
|
|
90
|
+
racc (~> 1.4)
|
|
91
|
+
nokogiri (1.19.4-x86_64-darwin)
|
|
92
|
+
racc (~> 1.4)
|
|
93
|
+
nokogiri (1.19.4-x86_64-linux-gnu)
|
|
94
|
+
racc (~> 1.4)
|
|
95
|
+
nokogiri (1.19.4-x86_64-linux-musl)
|
|
96
|
+
racc (~> 1.4)
|
|
79
97
|
parallel (1.26.3)
|
|
80
98
|
parser (3.3.7.3)
|
|
81
99
|
ast (~> 2.4.1)
|
|
@@ -83,11 +101,11 @@ GEM
|
|
|
83
101
|
pp (0.6.2)
|
|
84
102
|
prettyprint
|
|
85
103
|
prettyprint (0.2.0)
|
|
86
|
-
prism (1.
|
|
104
|
+
prism (1.9.0)
|
|
87
105
|
psych (5.2.3)
|
|
88
106
|
date
|
|
89
107
|
stringio
|
|
90
|
-
public_suffix (
|
|
108
|
+
public_suffix (7.0.5)
|
|
91
109
|
racc (1.8.1)
|
|
92
110
|
rainbow (3.1.1)
|
|
93
111
|
rake (13.2.1)
|
|
@@ -96,7 +114,7 @@ GEM
|
|
|
96
114
|
regexp_parser (2.10.0)
|
|
97
115
|
reline (0.6.0)
|
|
98
116
|
io-console (~> 0.5)
|
|
99
|
-
rexml (3.4.
|
|
117
|
+
rexml (3.4.4)
|
|
100
118
|
rspec (3.13.0)
|
|
101
119
|
rspec-core (~> 3.13.0)
|
|
102
120
|
rspec-expectations (~> 3.13.0)
|
|
@@ -141,15 +159,15 @@ GEM
|
|
|
141
159
|
simplecov_json_formatter (~> 0.1)
|
|
142
160
|
simplecov-html (0.13.1)
|
|
143
161
|
simplecov_json_formatter (0.1.4)
|
|
144
|
-
stringio (3.
|
|
162
|
+
stringio (3.2.0)
|
|
145
163
|
thor (1.3.2)
|
|
146
164
|
tilt (2.6.0)
|
|
147
165
|
tzinfo (2.0.6)
|
|
148
166
|
concurrent-ruby (~> 1.0)
|
|
149
167
|
unicode-display_width (3.1.4)
|
|
150
168
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
|
151
|
-
unicode-emoji (4.0
|
|
152
|
-
uri (1.
|
|
169
|
+
unicode-emoji (4.2.0)
|
|
170
|
+
uri (1.1.1)
|
|
153
171
|
webmock (3.25.1)
|
|
154
172
|
addressable (>= 2.8.0)
|
|
155
173
|
crack (>= 0.3.2)
|
|
@@ -164,7 +182,15 @@ GEM
|
|
|
164
182
|
nokogiri (>= 1.6.8, < 2.0.0)
|
|
165
183
|
|
|
166
184
|
PLATFORMS
|
|
185
|
+
aarch64-linux-gnu
|
|
186
|
+
aarch64-linux-musl
|
|
187
|
+
arm-linux-gnu
|
|
188
|
+
arm-linux-musl
|
|
189
|
+
arm64-darwin
|
|
167
190
|
ruby
|
|
191
|
+
x86_64-darwin
|
|
192
|
+
x86_64-linux-gnu
|
|
193
|
+
x86_64-linux-musl
|
|
168
194
|
|
|
169
195
|
DEPENDENCIES
|
|
170
196
|
bundler (~> 2.0)
|
data/lib/saml/kit/assertion.rb
CHANGED
|
@@ -89,9 +89,7 @@ module Saml
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def decryptable?
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
!@cannot_decrypt
|
|
92
|
+
!encrypted? || !@cannot_decrypt
|
|
95
93
|
end
|
|
96
94
|
|
|
97
95
|
def to_s
|
|
@@ -100,6 +98,12 @@ module Saml
|
|
|
100
98
|
|
|
101
99
|
private
|
|
102
100
|
|
|
101
|
+
# A nested Assertion is covered by the enclosing Response's policy; a
|
|
102
|
+
# bare one is the whole document and has nothing else vouching for it.
|
|
103
|
+
def signature_required_by_type?
|
|
104
|
+
to_nokogiri == to_nokogiri.document.root
|
|
105
|
+
end
|
|
106
|
+
|
|
103
107
|
def decrypt(decryptor)
|
|
104
108
|
encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
|
|
105
109
|
@encrypted = encrypted_assertion.present?
|
|
@@ -49,13 +49,24 @@ module Saml
|
|
|
49
49
|
|
|
50
50
|
return document.signature_verified! if provider.verify(
|
|
51
51
|
algorithm_for(params[:SigAlg]),
|
|
52
|
-
decode(signature),
|
|
52
|
+
decode(signature_from(signature)),
|
|
53
53
|
canonicalize(params)
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
raise ArgumentError, 'Invalid Signature'
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# The signature arrives percent encoded from any conformant peer, but
|
|
60
|
+
# versions of this library before 1.4.1 emitted it raw. A percent sign
|
|
61
|
+
# is not in the base64 alphabet, so its presence distinguishes the two
|
|
62
|
+
# without a guess. Unescaping unconditionally would corrupt a raw
|
|
63
|
+
# signature, because CGI.unescape turns + into a space.
|
|
64
|
+
#
|
|
65
|
+
# @param signature [String] the Signature query parameter.
|
|
66
|
+
def signature_from(signature)
|
|
67
|
+
signature.include?('%') ? unescape(signature) : signature
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
def canonicalize(params)
|
|
60
71
|
%i[SAMLRequest SAMLResponse RelayState SigAlg].map do |key|
|
|
61
72
|
value = params[key]
|
|
@@ -21,7 +21,7 @@ module Saml
|
|
|
21
21
|
destination = document.destination
|
|
22
22
|
if configuration.sign?
|
|
23
23
|
payload = canonicalize(document, relay_state)
|
|
24
|
-
"#{destination}?#{payload}&Signature=#{signature_for(payload)}"
|
|
24
|
+
"#{destination}?#{payload}&Signature=#{escape(signature_for(payload))}"
|
|
25
25
|
else
|
|
26
26
|
"#{destination}?" + to_query_string(
|
|
27
27
|
document.query_string_parameter => serialize(document.to_xml),
|
|
@@ -11,9 +11,15 @@ module Saml
|
|
|
11
11
|
included do
|
|
12
12
|
validate :must_have_valid_signature, unless: :signature_verified
|
|
13
13
|
validate :must_be_registered
|
|
14
|
+
validate :must_be_signed, if: :signature_required?
|
|
14
15
|
validate :must_be_trusted
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# Returns true when this document must carry a signature to be valid.
|
|
19
|
+
def signature_required?
|
|
20
|
+
configuration.signature_required && signature_required_by_type?
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
# Returns true when the document has an embedded XML Signature or has
|
|
18
24
|
# been verified externally.
|
|
19
25
|
def signed?
|
|
@@ -65,9 +71,40 @@ module Saml
|
|
|
65
71
|
errors.add(:provider, error_message(:unregistered))
|
|
66
72
|
end
|
|
67
73
|
|
|
74
|
+
# Whether a document of this type is worthless unless signed.
|
|
75
|
+
#
|
|
76
|
+
# A request may legitimately arrive unsigned, which is what
|
|
77
|
+
# `WantAuthnRequestsSigned="false"` means, so the default is false. A
|
|
78
|
+
# document that asserts an identity carries no authenticity guarantee at
|
|
79
|
+
# all when unsigned, so Response and Assertion override this.
|
|
80
|
+
def signature_required_by_type?
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def must_be_signed
|
|
85
|
+
return unless expected_type?
|
|
86
|
+
return if signature_covers_document?
|
|
87
|
+
|
|
88
|
+
errors.add(:base, error_message(:unsigned))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Whether a signature exists over the content this document asserts.
|
|
92
|
+
# Response overrides this, because a signature on either the Response or
|
|
93
|
+
# the Assertion covers the identity being asserted.
|
|
94
|
+
def signature_covers_document?
|
|
95
|
+
signed?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The trust counterpart of `signature_covers_document?`. Kept separate
|
|
99
|
+
# from the public `trusted?`, which answers the narrower question of
|
|
100
|
+
# whether *this* element's own signature is trusted.
|
|
101
|
+
def signature_trusted?
|
|
102
|
+
trusted?
|
|
103
|
+
end
|
|
104
|
+
|
|
68
105
|
def must_be_trusted
|
|
69
|
-
return if
|
|
70
|
-
return if provider.present? && !signed?
|
|
106
|
+
return if signature_trusted?
|
|
107
|
+
return if !signature_required? && provider.present? && !signed?
|
|
71
108
|
|
|
72
109
|
errors.add(:fingerprint, error_message(:invalid_fingerprint))
|
|
73
110
|
end
|
|
@@ -46,6 +46,13 @@ module Saml
|
|
|
46
46
|
attr_accessor :logger
|
|
47
47
|
# The total allowable clock drift for session timeout validation.
|
|
48
48
|
attr_accessor :clock_drift
|
|
49
|
+
# Whether a document that asserts an identity must carry a signature.
|
|
50
|
+
#
|
|
51
|
+
# An unsigned Response or Assertion proves nothing about who issued it,
|
|
52
|
+
# so this defaults to true and should stay that way. Set it to false only
|
|
53
|
+
# to keep an identity provider that signs nothing working while it is
|
|
54
|
+
# being fixed, and understand that doing so accepts forged assertions.
|
|
55
|
+
attr_accessor :signature_required
|
|
49
56
|
|
|
50
57
|
def initialize
|
|
51
58
|
@clock_drift = 30.seconds
|
|
@@ -55,6 +62,7 @@ module Saml
|
|
|
55
62
|
@registry = DefaultRegistry.new
|
|
56
63
|
@session_timeout = 3.hours
|
|
57
64
|
@signature_method = :SHA256
|
|
65
|
+
@signature_required = true
|
|
58
66
|
yield self if block_given?
|
|
59
67
|
end
|
|
60
68
|
|
data/lib/saml/kit/locales/en.yml
CHANGED
|
@@ -11,6 +11,7 @@ en:
|
|
|
11
11
|
must_contain_single_assertion: "must contain single Assertion."
|
|
12
12
|
must_match_issuer: "must match entityId."
|
|
13
13
|
unregistered: "is unregistered."
|
|
14
|
+
unsigned: "must be signed."
|
|
14
15
|
AuthnRequest:
|
|
15
16
|
invalid: "must contain AuthnRequest."
|
|
16
17
|
invalid_fingerprint: "is not registered."
|
|
@@ -34,10 +35,12 @@ en:
|
|
|
34
35
|
invalid_version: "must be 2.0."
|
|
35
36
|
unregistered: "must originate from registered identity provider."
|
|
36
37
|
must_contain_single_assertion: "must contain a single Assertion."
|
|
38
|
+
unsigned: "must be signed."
|
|
37
39
|
Signature:
|
|
38
40
|
certificate: "Not valid before %{not_before}. Not valid after %{not_after}."
|
|
39
41
|
digest_value: "is invalid."
|
|
40
42
|
empty: "is missing."
|
|
43
|
+
reference: "must reference the element it is attached to."
|
|
41
44
|
signature: "is invalid."
|
|
42
45
|
SPSSODescriptor:
|
|
43
46
|
invalid: "must contain SPSSODescriptor."
|
data/lib/saml/kit/response.rb
CHANGED
|
@@ -41,10 +41,26 @@ module Saml
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
+
def signature_required_by_type?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# `signed?` and `trusted?` deliberately report only this element's own
|
|
49
|
+
# signature, but signing only the Assertion is a common identity provider
|
|
50
|
+
# configuration, and that signature covers the identity being asserted.
|
|
51
|
+
def signature_covers_document?
|
|
52
|
+
signed? || assertion.signed?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def signature_trusted?
|
|
56
|
+
super || (assertion.signed? && assertion.trusted?)
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
def must_be_valid_assertion
|
|
45
60
|
assertion.valid?
|
|
46
61
|
assertion.each_error do |attribute, error|
|
|
47
|
-
|
|
62
|
+
attribute = :assertion if attribute == :base
|
|
63
|
+
errors.add(attribute, error) unless errors.added?(attribute, error)
|
|
48
64
|
end
|
|
49
65
|
end
|
|
50
66
|
|
data/lib/saml/kit/signature.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Saml
|
|
|
11
11
|
|
|
12
12
|
validate :validate_signature
|
|
13
13
|
validate :validate_certificate
|
|
14
|
+
validate :validate_reference
|
|
14
15
|
|
|
15
16
|
attr_reader :name
|
|
16
17
|
|
|
@@ -20,18 +21,24 @@ module Saml
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Returns the embedded X509 Certificate
|
|
24
|
+
#
|
|
25
|
+
# Memoized with a `defined?` guard rather than `||=` because a signature
|
|
26
|
+
# without KeyInfo legitimately has no certificate, and callers depend on
|
|
27
|
+
# the nil.
|
|
23
28
|
def certificate
|
|
24
|
-
|
|
25
|
-
value = at_xpath(xpath).try(:text)
|
|
26
|
-
return if value.nil?
|
|
29
|
+
return @certificate if defined?(@certificate)
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
|
|
32
|
+
@certificate =
|
|
33
|
+
value.nil? ? nil : ::Xml::Kit::Certificate.new(value, use: :signing)
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
# Returns true when the fingerprint of the certificate matches one of
|
|
32
37
|
# the certificates registered in the metadata.
|
|
33
38
|
def trusted?(metadata)
|
|
34
39
|
return false if metadata.nil?
|
|
40
|
+
return false if certificate.nil?
|
|
41
|
+
return false unless reference.bound?
|
|
35
42
|
|
|
36
43
|
metadata.matches?(certificate.fingerprint, use: :signing).present?
|
|
37
44
|
end
|
|
@@ -66,15 +73,9 @@ module Saml
|
|
|
66
73
|
end
|
|
67
74
|
|
|
68
75
|
def transforms
|
|
69
|
-
xpath =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'ds:Reference',
|
|
73
|
-
'ds:Transforms',
|
|
74
|
-
'ds:Transform',
|
|
75
|
-
'@Algorithm',
|
|
76
|
-
])
|
|
77
|
-
node.search(xpath, Saml::Kit::Document::NAMESPACES).try(:map, &:value)
|
|
76
|
+
xpath = './ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform'
|
|
77
|
+
node.search("#{xpath}/@Algorithm", Saml::Kit::Document::NAMESPACES)
|
|
78
|
+
.try(:map, &:value)
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Returns the XML Hash.
|
|
@@ -109,6 +110,17 @@ module Saml
|
|
|
109
110
|
errors.add(:base, error.message)
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
def validate_reference
|
|
114
|
+
return unless present?
|
|
115
|
+
return if reference.bound?
|
|
116
|
+
|
|
117
|
+
errors.add(:reference, error_message(:reference))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def reference
|
|
121
|
+
@reference ||= SignatureReference.new(node)
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
def validate_certificate(now = Time.now.utc)
|
|
113
125
|
return unless certificate.present?
|
|
114
126
|
return if certificate.active?(now)
|
|
@@ -128,11 +140,7 @@ module Saml
|
|
|
128
140
|
end
|
|
129
141
|
|
|
130
142
|
def dsignature
|
|
131
|
-
@dsignature ||= Xmldsig::Signature.new(node,
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def xpath_for(segments)
|
|
135
|
-
segments.join('/')
|
|
143
|
+
@dsignature ||= Xmldsig::Signature.new(node, SignatureReference::ID_ATTR)
|
|
136
144
|
end
|
|
137
145
|
end
|
|
138
146
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Saml
|
|
4
|
+
module Kit
|
|
5
|
+
# This class is responsible for deciding whether an xml digital signature
|
|
6
|
+
# actually covers the element it is attached to.
|
|
7
|
+
#
|
|
8
|
+
# A cryptographically intact signature only proves that *some* element in
|
|
9
|
+
# the document is unmodified. xmldsig resolves a Reference by searching the
|
|
10
|
+
# whole document for a matching ID, so without this check an attacker can
|
|
11
|
+
# copy a valid signature onto a forged element and hide the element it
|
|
12
|
+
# really covers somewhere the schema still permits, e.g. saml:Advice or
|
|
13
|
+
# ds:Object.
|
|
14
|
+
class SignatureReference
|
|
15
|
+
# The id_attr handed to Xmldsig::Signature, which expands it into
|
|
16
|
+
# "//*[@ID=$uri or @Id=$uri]". Both resolvers must agree on this or the
|
|
17
|
+
# element we check is not the element that was digested.
|
|
18
|
+
ID_ATTR = 'ID=$uri or @Id'
|
|
19
|
+
XPATH = './ds:SignedInfo/ds:Reference'
|
|
20
|
+
|
|
21
|
+
def initialize(node)
|
|
22
|
+
@node = node
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns true when some Reference resolves to the element the signature
|
|
26
|
+
# hangs off, and resolves to nothing else.
|
|
27
|
+
#
|
|
28
|
+
# Every Reference lives inside ds:SignedInfo, which ds:SignatureValue
|
|
29
|
+
# covers, so an attacker replaying a signature cannot add a Reference or
|
|
30
|
+
# retarget an existing one. Finding our element among them is therefore
|
|
31
|
+
# enough. SAML 2.0 core 5.4.2 permits only one, but documents carrying
|
|
32
|
+
# more exist, and each extra Reference only widens what the signature
|
|
33
|
+
# already commits to.
|
|
34
|
+
def bound?
|
|
35
|
+
return @bound if defined?(@bound)
|
|
36
|
+
|
|
37
|
+
@bound = binds_parent_element?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :node
|
|
43
|
+
|
|
44
|
+
def binds_parent_element?
|
|
45
|
+
return false if node.nil? || signed.nil?
|
|
46
|
+
|
|
47
|
+
references.any? { |reference| covers_signed_element?(reference) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def covers_signed_element?(reference)
|
|
51
|
+
uri = reference.attribute('URI').try(:value).to_s
|
|
52
|
+
# An empty URI digests the whole document, so it covers every element in
|
|
53
|
+
# it and leaves nowhere to hide an original.
|
|
54
|
+
return true if uri.empty?
|
|
55
|
+
|
|
56
|
+
id.present? && uri == "##{id}" && resolves_to_signed_element?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `bound?` memoizes the one answer this class exists to give, so only the
|
|
60
|
+
# XPath search below is worth caching; the rest are O(1) node lookups.
|
|
61
|
+
def signed
|
|
62
|
+
node.parent
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def references
|
|
66
|
+
@references ||= node.search(XPATH, Saml::Kit::Document::NAMESPACES)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def id
|
|
70
|
+
signed.attribute('ID').try(:value) || signed.attribute('Id').try(:value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Duplicate IDs are only rejected when schema validation runs, and an
|
|
74
|
+
# XPath lookup silently returns the first match, so require exactly one.
|
|
75
|
+
def resolves_to_signed_element?
|
|
76
|
+
matches = node.document.xpath(
|
|
77
|
+
"//*[@#{ID_ATTR}=$uri]",
|
|
78
|
+
Saml::Kit::Document::NAMESPACES,
|
|
79
|
+
'uri' => id
|
|
80
|
+
)
|
|
81
|
+
matches.count == 1 && matches.first == signed
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/saml/kit/version.rb
CHANGED
data/lib/saml/kit.rb
CHANGED
|
@@ -44,6 +44,7 @@ require 'saml/kit/logout_request'
|
|
|
44
44
|
require 'saml/kit/metadata'
|
|
45
45
|
require 'saml/kit/deprecated/metadata'
|
|
46
46
|
require 'saml/kit/null_assertion'
|
|
47
|
+
require 'saml/kit/signature_reference'
|
|
47
48
|
require 'saml/kit/organization'
|
|
48
49
|
require 'saml/kit/parser'
|
|
49
50
|
require 'saml/kit/composite_metadata'
|
data/saml-kit.gemspec
CHANGED
|
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
|
14
14
|
spec.description = 'A simple toolkit for working with SAML.'
|
|
15
15
|
spec.homepage = 'https://github.com/xlgmokha/saml-kit'
|
|
16
16
|
spec.license = 'MIT'
|
|
17
|
-
spec.required_ruby_version = '>= 3.
|
|
17
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
18
18
|
|
|
19
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
20
20
|
(
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: saml-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mo khan
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activemodel
|
|
@@ -414,6 +413,7 @@ files:
|
|
|
414
413
|
- lib/saml/kit/rspec/have_xpath.rb
|
|
415
414
|
- lib/saml/kit/service_provider_metadata.rb
|
|
416
415
|
- lib/saml/kit/signature.rb
|
|
416
|
+
- lib/saml/kit/signature_reference.rb
|
|
417
417
|
- lib/saml/kit/version.rb
|
|
418
418
|
- lib/saml/kit/xsd/MetadataExchange.xsd
|
|
419
419
|
- lib/saml/kit/xsd/oasis-200401-wss-wssecurity-secext-1.0.xsd
|
|
@@ -448,7 +448,6 @@ licenses:
|
|
|
448
448
|
- MIT
|
|
449
449
|
metadata:
|
|
450
450
|
yard.run: yri
|
|
451
|
-
post_install_message:
|
|
452
451
|
rdoc_options: []
|
|
453
452
|
require_paths:
|
|
454
453
|
- lib
|
|
@@ -456,15 +455,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
456
455
|
requirements:
|
|
457
456
|
- - ">="
|
|
458
457
|
- !ruby/object:Gem::Version
|
|
459
|
-
version: 3.
|
|
458
|
+
version: 3.2.0
|
|
460
459
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
461
460
|
requirements:
|
|
462
461
|
- - ">="
|
|
463
462
|
- !ruby/object:Gem::Version
|
|
464
463
|
version: '0'
|
|
465
464
|
requirements: []
|
|
466
|
-
rubygems_version:
|
|
467
|
-
signing_key:
|
|
465
|
+
rubygems_version: 4.0.18
|
|
468
466
|
specification_version: 4
|
|
469
467
|
summary: A simple toolkit for working with SAML.
|
|
470
468
|
test_files: []
|