saml2 3.0.11 → 3.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/saml2/key.rb +38 -4
- data/lib/saml2/response.rb +7 -4
- data/lib/saml2/signable.rb +22 -19
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/response_with_rsa_key_value.xml +1 -0
- data/spec/lib/key_spec.rb +10 -0
- data/spec/lib/signable_spec.rb +15 -0
- data/spec/spec_helper.rb +1 -0
- metadata +12 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c6bee2a21b427f883ac61136eba8021c72f7f90a3a41654a964f165d1ae3641
|
4
|
+
data.tar.gz: 0e53c2b7e6d460f61f331c0d2cc4031ba3cea9895cc3b3b9d6fca153f1ca7653
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c427fc0852c531675c5573b1a51a3a6097414fc7ceee458a1c80d682dff696123e59d61a6e90cea0f6c221fbcef01a87f5a91f33dfebb2d0bf0f281e2b278794
|
7
|
+
data.tar.gz: a2281d564327fe52d98055559663cd4c26945aafc3b535c2d4adc2851480418f387e3a80f73f8bed665afc3eaf2ab771d94576c0332a1296ef8c0e73208467eb
|
data/lib/saml2/key.rb
CHANGED
@@ -11,6 +11,8 @@ module SAML2
|
|
11
11
|
class KeyInfo < Base
|
12
12
|
# @return [String] The PEM encoded certificate.
|
13
13
|
attr_reader :x509
|
14
|
+
# @return [OpenSSL::PKey::PKey] An RSA Public Key
|
15
|
+
attr_accessor :key
|
14
16
|
|
15
17
|
# @param x509 [String] The PEM encoded certificate.
|
16
18
|
def initialize(x509 = nil)
|
@@ -19,7 +21,15 @@ module SAML2
|
|
19
21
|
|
20
22
|
# (see Base#from_xml)
|
21
23
|
def from_xml(node)
|
22
|
-
self.x509 = node.at_xpath('dsig:
|
24
|
+
self.x509 = node.at_xpath('dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)&.content&.strip
|
25
|
+
if (rsa_key_value = node.at_xpath('dsig:KeyValue/dsig:RSAKeyValue', Namespaces::ALL))
|
26
|
+
modulus = crypto_binary_to_integer(rsa_key_value.at_xpath('dsig:Modulus', Namespaces::ALL)&.content&.strip)
|
27
|
+
exponent = crypto_binary_to_integer(rsa_key_value.at_xpath('dsig:Exponent', Namespaces::ALL)&.content&.strip)
|
28
|
+
if modulus && exponent
|
29
|
+
@key = OpenSSL::PKey::RSA.new
|
30
|
+
key.set_key(modulus, exponent, nil)
|
31
|
+
end
|
32
|
+
end
|
23
33
|
end
|
24
34
|
|
25
35
|
def x509=(value)
|
@@ -28,9 +38,15 @@ module SAML2
|
|
28
38
|
|
29
39
|
# @return [OpenSSL::X509::Certificate]
|
30
40
|
def certificate
|
41
|
+
return nil if x509.nil?
|
31
42
|
@certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
|
32
43
|
end
|
33
44
|
|
45
|
+
# @return [OpenSSL::PKey::PKey]
|
46
|
+
def public_key
|
47
|
+
key || certificate&.public_key
|
48
|
+
end
|
49
|
+
|
34
50
|
# Formats a fingerprint as all lowercase, with a : every two characters,
|
35
51
|
# stripping all non-hexadecimal characters.
|
36
52
|
# @param fingerprint [String]
|
@@ -41,17 +57,35 @@ module SAML2
|
|
41
57
|
|
42
58
|
# @return [String]
|
43
59
|
def fingerprint
|
60
|
+
return nil unless certificate
|
44
61
|
@fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
|
45
62
|
end
|
46
63
|
|
47
64
|
# (see Base#build)
|
48
65
|
def build(builder)
|
49
66
|
builder['dsig'].KeyInfo do |key_info|
|
50
|
-
|
51
|
-
|
67
|
+
if x509
|
68
|
+
key_info['dsig'].X509Data do |x509_data|
|
69
|
+
x509_data['dsig'].X509Certificate(x509)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
if key.is_a?(OpenSSL::PKey::RSA)
|
73
|
+
key_info['dsig'].KeyValue do |key_value|
|
74
|
+
key_value['dsig'].RSAKeyValue do |rsa_key_value|
|
75
|
+
rsa_key_value['dsig'].Modulus(Base64.encode64(key.n.to_s(2)))
|
76
|
+
rsa_key_value['dsig'].Exponent(Base64.encode64(key.e.to_s(2)))
|
77
|
+
end
|
78
|
+
end
|
52
79
|
end
|
53
80
|
end
|
54
81
|
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def crypto_binary_to_integer(str)
|
86
|
+
return nil unless str
|
87
|
+
OpenSSL::BN.new(Base64.decode64(str), 2)
|
88
|
+
end
|
55
89
|
end
|
56
90
|
|
57
91
|
class KeyDescriptor < KeyInfo
|
@@ -99,7 +133,7 @@ module SAML2
|
|
99
133
|
|
100
134
|
# (see Base#from_xml)
|
101
135
|
def from_xml(node)
|
102
|
-
super
|
136
|
+
super(node.at_xpath('dsig:KeyInfo', Namespaces::ALL))
|
103
137
|
self.use = node['use']
|
104
138
|
self.encryption_methods = load_object_array(node, 'md:EncryptionMethod', EncryptionMethod)
|
105
139
|
end
|
data/lib/saml2/response.rb
CHANGED
@@ -127,14 +127,16 @@ module SAML2
|
|
127
127
|
return errors
|
128
128
|
end
|
129
129
|
|
130
|
-
certificates = idp.signing_keys.map(&:certificate)
|
131
|
-
|
130
|
+
certificates = idp.signing_keys.map(&:certificate).compact
|
131
|
+
keys = idp.signing_keys.map(&:key).compact
|
132
|
+
if idp.fingerprints.empty? && certificates.empty? && keys.empty?
|
132
133
|
errors << "could not find certificate to validate message"
|
133
134
|
return errors
|
134
135
|
end
|
135
136
|
|
136
137
|
if signed?
|
137
|
-
unless (signature_errors = validate_signature(
|
138
|
+
unless (signature_errors = validate_signature(key: keys,
|
139
|
+
fingerprint: idp.fingerprints,
|
138
140
|
cert: certificates)).empty?
|
139
141
|
return errors.concat(signature_errors)
|
140
142
|
end
|
@@ -145,7 +147,8 @@ module SAML2
|
|
145
147
|
|
146
148
|
# this might be nil, if the assertion was encrypted
|
147
149
|
if assertion&.signed?
|
148
|
-
unless (signature_errors = assertion.validate_signature(
|
150
|
+
unless (signature_errors = assertion.validate_signature(key: keys,
|
151
|
+
fingerprint: idp.fingerprints,
|
149
152
|
cert: certificates)).empty?
|
150
153
|
return errors.concat(signature_errors)
|
151
154
|
end
|
data/lib/saml2/signable.rb
CHANGED
@@ -27,7 +27,7 @@ module SAML2
|
|
27
27
|
def signing_key
|
28
28
|
unless instance_variable_defined?(:@signing_key)
|
29
29
|
# don't use `... if signature.at_xpath(...)` - we need to make sure we assign the nil
|
30
|
-
@signing_key = signature.at_xpath('dsig:KeyInfo', Namespaces::ALL) ? KeyInfo.from_xml(
|
30
|
+
@signing_key = (key_info = signature.at_xpath('dsig:KeyInfo', Namespaces::ALL)) ? KeyInfo.from_xml(key_info) : nil
|
31
31
|
end
|
32
32
|
@signing_key
|
33
33
|
end
|
@@ -38,20 +38,21 @@ module SAML2
|
|
38
38
|
|
39
39
|
# Validate the signature on this object.
|
40
40
|
#
|
41
|
-
# At least one of +key+, +fingerprint+ or +cert+ must be provided.
|
41
|
+
# At least one of +key+, +fingerprint+ or +cert+ must be provided. If the signature
|
42
|
+
# doesn't specify which key to use, the first provided key will be used.
|
42
43
|
#
|
43
|
-
# @param key optional [String, OpenSSL::PKey::PKey]
|
44
|
-
#
|
44
|
+
# @param key optional [String, OpenSSL::PKey::PKey, Array<String>, Array<OpenSSL::PKey::PKey>]
|
45
|
+
# Public keys that are allowed to be the valid key.
|
45
46
|
# @param fingerprint optional [Array<String>, String]
|
46
47
|
# SHA1 fingerprints of trusted certificates. If provided, they will be
|
47
48
|
# checked against the {#signing_key} embedded in the {#signature}, and if
|
48
|
-
# a match is found, the
|
49
|
-
#
|
49
|
+
# a match is found, the key embedded in the signature will be
|
50
|
+
# used for verifying the signature.
|
50
51
|
# @param cert optional [Array<String>, String]
|
51
52
|
# A single or array of trusted certificates. If provided, they will be
|
52
|
-
#
|
53
|
-
# a match
|
54
|
-
#
|
53
|
+
# checked against the {#signing_key} embedded in the {#signature}, and if
|
54
|
+
# a match is found, the key embedded in the signature will be used for
|
55
|
+
# verifying the signature.
|
55
56
|
# @return [Array<String>] An empty array on success, details of errors on failure.
|
56
57
|
def validate_signature(key: nil,
|
57
58
|
fingerprint: nil,
|
@@ -67,23 +68,25 @@ module SAML2
|
|
67
68
|
end
|
68
69
|
certs = certs.uniq
|
69
70
|
|
70
|
-
trusted_keys =
|
71
|
+
trusted_keys = Array.wrap(key).map(&:to_s)
|
72
|
+
trusted_keys.concat(certs.map do |cert|
|
71
73
|
cert = cert.is_a?(String) ? OpenSSL::X509::Certificate.new(cert) : cert
|
72
74
|
cert.public_key.to_s
|
73
|
-
end
|
74
|
-
|
75
|
-
|
75
|
+
end)
|
76
|
+
|
77
|
+
if trusted_keys.include?(signing_key&.public_key&.to_s)
|
78
|
+
verification_key = signing_key.public_key.to_s
|
76
79
|
end
|
77
80
|
# signature doesn't say who signed it. hope and pray it's with the only certificate
|
78
81
|
# we know about
|
79
|
-
if signing_key.nil?
|
80
|
-
|
82
|
+
if signing_key.nil?
|
83
|
+
verification_key = trusted_keys.first
|
81
84
|
end
|
82
85
|
|
83
|
-
return ["no trusted signing key found"] if
|
86
|
+
return ["no trusted signing key found"] if verification_key.nil?
|
84
87
|
|
85
88
|
begin
|
86
|
-
result = signature.verify_with(key:
|
89
|
+
result = signature.verify_with(key: verification_key)
|
87
90
|
result ? [] : ["signature is invalid"]
|
88
91
|
rescue XMLSec::VerificationError => e
|
89
92
|
[e.message]
|
@@ -96,8 +99,8 @@ module SAML2
|
|
96
99
|
#
|
97
100
|
# @param (see #validate_signature)
|
98
101
|
# @return [Boolean]
|
99
|
-
def valid_signature?(
|
100
|
-
validate_signature(
|
102
|
+
def valid_signature?(**kwargs)
|
103
|
+
validate_signature(**kwargs).empty?
|
101
104
|
end
|
102
105
|
|
103
106
|
# Sign this object.
|
data/lib/saml2/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
<Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_f0b41a2d2b524b8c92027ef426b3cf3b" Version="2.0" IssueInstant="2021-08-02T16:53:20Z" Destination="https://whartononline.instructure.com/login/saml" xmlns="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://test.pep.siemens.knowledgeanywhere.com</Issuer><Status><StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></Status><Assertion Version="2.0" ID="_8be8f34e76e14fe3a990dcc00e60d13d" IssueInstant="2021-08-02T16:53:20Z" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"><Issuer>https://test.pep.siemens.knowledgeanywhere.com</Issuer><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /><Reference URI="#_8be8f34e76e14fe3a990dcc00e60d13d"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /><DigestValue>VQGa66oHsKF7zCXzob8WTCBLHUI=</DigestValue></Reference></SignedInfo><SignatureValue>S+He/m2bwH+QiGmR/eH9K71TXCQ2hC8BI5pgKWyN3gFfCfEvUYgnksXu/oE1wVxvpp2dtWPiT/8vejTJWfl4Yz8VT8gDcH9xKWhPzsHba8qCHui3U486dR1SwkwII3H0cK0Xhr/dKvco85QPAUsZTKZ+dq5HdeLu6n6aERzC/136iuw/eyCFDfFS2E3dfTHd2u+E9uUh6wI/nCpSzxMxQw9Wz9i941jdLKovuqILEVbbV33sg9VZRFNAs+D92YYiegKnnBBP2+H2fXFA2Wc5uP8yAAsrFMvHA7R1tIP7jci8vEplK/Hlkdta8KQVq4fIKsXUeYu17uNFfbSIXkL5oQ==</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>vyYuRxa/PI/r3N4lxOxL/O1OtwYexfmY3mF6mvjMr4YD762UZWNNhLZsFaWqUs3HQQwAZCg0sU65s4a+t4XANuRX1k3E1oPHBmqfLZ8spBeRfyHxc0KSah10RFfCO1LZdte2Avtpvh5RMkGkMhbWFAYQmc24u6sHvI1OVUkKmBiwss6QJAi6bttMsp0VNuW7TmmsPjxmaq5dddSryLHpVP/VbnnLu4fWOnLBMTtDY37l6ZAyqKW8PEaXWSFJSjGlA+9D76+bf5C+snk0yooARZduPny5biXtoHbC3DnPDBbCN70bvkxGNqMBiqPQrI2B7zZUXA1nDiK/iHPxl5X3ZQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature><Subject><NameID>chad@instructure.com</NameID><SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><SubjectConfirmationData Recipient="https://whartononline.instructure.com/login/saml" NotOnOrAfter="2021-08-02T16:58:20Z" /></SubjectConfirmation></Subject><Conditions NotBefore="2021-08-02T16:48:20Z" NotOnOrAfter="2021-08-02T16:58:20Z"><AudienceRestriction><Audience>http://whartononline.instructure.com/saml2</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant="2021-08-02T16:53:20Z"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name="FIRST_NAME"><AttributeValue>Chad</AttributeValue></Attribute><Attribute Name="LAST_NAME"><AttributeValue>Broadhead</AttributeValue></Attribute><Attribute Name="EMAIL"><AttributeValue>chad@instructure.com</AttributeValue></Attribute><Attribute Name="UserGuid"><AttributeValue>c555db14-fc81-4836-98de-62f592f36ede</AttributeValue></Attribute></AttributeStatement></Assertion></Response>
|
data/spec/lib/key_spec.rb
CHANGED
@@ -9,5 +9,15 @@ module SAML2
|
|
9
9
|
expect(KeyInfo.format_fingerprint("\u200F abcdefghijklmnop 1234567890-\n a1")).to eq("ab:cd:ef:12:34:56:78:90:a1")
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
describe '#certificate' do
|
14
|
+
it "doesn't asplode if the keyinfo is just an rsa key value" do
|
15
|
+
response = Nokogiri::XML(fixture("response_with_rsa_key_value.xml"))
|
16
|
+
key = KeyInfo.from_xml(response.at_xpath('//dsig:KeyInfo', Namespaces::ALL))
|
17
|
+
expect(key.certificate).to be_nil
|
18
|
+
expect(key.fingerprint).to be_nil
|
19
|
+
expect(key.key).not_to be_nil
|
20
|
+
end
|
21
|
+
end
|
12
22
|
end
|
13
23
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
module SAML2
|
6
|
+
describe Signable do
|
7
|
+
describe '#valid_signature?' do
|
8
|
+
it 'can work with an explicit key from metadata' do
|
9
|
+
response = Response.parse(fixture("response_with_rsa_key_value.xml"))
|
10
|
+
key = response.assertions.first.signing_key.key
|
11
|
+
expect(response.assertions.first.valid_signature?(key: [key])).to eq true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saml2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: 1.5.8
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '2.0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: 1.5.8
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '2.0'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: nokogiri-xmlsec-instructure
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,7 +59,7 @@ dependencies:
|
|
59
59
|
version: '3.2'
|
60
60
|
- - "<"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '7.1'
|
63
63
|
type: :runtime
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -69,21 +69,21 @@ dependencies:
|
|
69
69
|
version: '3.2'
|
70
70
|
- - "<"
|
71
71
|
- !ruby/object:Gem::Version
|
72
|
-
version: '
|
72
|
+
version: '7.1'
|
73
73
|
- !ruby/object:Gem::Dependency
|
74
74
|
name: byebug
|
75
75
|
requirement: !ruby/object:Gem::Requirement
|
76
76
|
requirements:
|
77
77
|
- - "~>"
|
78
78
|
- !ruby/object:Gem::Version
|
79
|
-
version: '
|
79
|
+
version: '11.0'
|
80
80
|
type: :development
|
81
81
|
prerelease: false
|
82
82
|
version_requirements: !ruby/object:Gem::Requirement
|
83
83
|
requirements:
|
84
84
|
- - "~>"
|
85
85
|
- !ruby/object:Gem::Version
|
86
|
-
version: '
|
86
|
+
version: '11.0'
|
87
87
|
- !ruby/object:Gem::Dependency
|
88
88
|
name: rake
|
89
89
|
requirement: !ruby/object:Gem::Requirement
|
@@ -197,6 +197,7 @@ files:
|
|
197
197
|
- spec/fixtures/response_tampered_signature.xml
|
198
198
|
- spec/fixtures/response_with_attribute_signed.xml
|
199
199
|
- spec/fixtures/response_with_encrypted_assertion.xml
|
200
|
+
- spec/fixtures/response_with_rsa_key_value.xml
|
200
201
|
- spec/fixtures/response_with_signed_assertion_and_encrypted_subject.xml
|
201
202
|
- spec/fixtures/response_without_keyinfo.xml
|
202
203
|
- spec/fixtures/service_provider.xml
|
@@ -222,6 +223,7 @@ files:
|
|
222
223
|
- spec/lib/message_spec.rb
|
223
224
|
- spec/lib/response_spec.rb
|
224
225
|
- spec/lib/service_provider_spec.rb
|
226
|
+
- spec/lib/signable_spec.rb
|
225
227
|
- spec/spec_helper.rb
|
226
228
|
homepage: https://github.com/instructure/ruby-saml2
|
227
229
|
licenses:
|
@@ -251,6 +253,7 @@ test_files:
|
|
251
253
|
- spec/lib/logout_response_spec.rb
|
252
254
|
- spec/lib/indexed_object_spec.rb
|
253
255
|
- spec/lib/attribute_spec.rb
|
256
|
+
- spec/lib/signable_spec.rb
|
254
257
|
- spec/lib/entity_spec.rb
|
255
258
|
- spec/lib/attribute_consuming_service_spec.rb
|
256
259
|
- spec/lib/key_spec.rb
|
@@ -271,6 +274,7 @@ test_files:
|
|
271
274
|
- spec/fixtures/xml_missigned_assertion.xml
|
272
275
|
- spec/fixtures/certificate.pem
|
273
276
|
- spec/fixtures/noconditions_response.xml
|
277
|
+
- spec/fixtures/response_with_rsa_key_value.xml
|
274
278
|
- spec/fixtures/entities.xml
|
275
279
|
- spec/fixtures/response_assertion_signed_reffed_from_response.xml
|
276
280
|
- spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml
|