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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 953f52449cc1cbab86280c3265ecf74cde4eb8db37946a99aa2d85f59d7a4754
4
- data.tar.gz: f211ab22f71726b292c5182fba1b5040284c4a243f03458b84f33983f95dadbc
3
+ metadata.gz: 4c6bee2a21b427f883ac61136eba8021c72f7f90a3a41654a964f165d1ae3641
4
+ data.tar.gz: 0e53c2b7e6d460f61f331c0d2cc4031ba3cea9895cc3b3b9d6fca153f1ca7653
5
5
  SHA512:
6
- metadata.gz: 271dd3d0bd8325c59b61ed416fd0ed16d1cbc284d957593220f905c3f5c5f8d001bdcc125fcd96e1d28ffe3ad2ec22a6fec027c768df82ddb5dca609ba435c58
7
- data.tar.gz: 87337ae1fd36828023da486e932582009b76f666e04fd336e4d4b4e0a244366c62c7491255b320b2575c6439d56c856200f883a2087fdc70b82f746927fef8db
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:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)&.content&.strip
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
- key_info['dsig'].X509Data do |x509_data|
51
- x509_data['dsig'].X509Certificate(x509)
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
@@ -127,14 +127,16 @@ module SAML2
127
127
  return errors
128
128
  end
129
129
 
130
- certificates = idp.signing_keys.map(&:certificate)
131
- if idp.fingerprints.empty? && certificates.empty?
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(fingerprint: idp.fingerprints,
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(fingerprint: idp.fingerprints,
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
@@ -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(signature) : nil
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
- # The exact public key to use. +fingerprint+ and +cert+ are ignored.
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 certificate embedded in the signature will be
49
- # added to the list of certificates used for verifying the signature.
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
- # check against the {#signing_key} embedded in the {#signature}, and if
53
- # a match of public keys only is found, that key will be considered trusted
54
- # and used to verify the signature.
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 = certs.map do |cert|
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
- if signing_key&.certificate && trusted_keys.include?(signing_key.certificate.public_key.to_s)
75
- key ||= signing_key.certificate.public_key.to_s
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? && key.nil? && trusted_keys.length == 1
80
- key = trusted_keys.first
82
+ if signing_key.nil?
83
+ verification_key = trusted_keys.first
81
84
  end
82
85
 
83
- return ["no trusted signing key found"] if key.nil?
86
+ return ["no trusted signing key found"] if verification_key.nil?
84
87
 
85
88
  begin
86
- result = signature.verify_with(key: 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?(fingerprint: nil, cert: nil)
100
- validate_signature(fingerprint: fingerprint, cert: cert).empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SAML2
4
- VERSION = '3.0.11'
4
+ VERSION = '3.1.2'
5
5
  end
@@ -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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'byebug'
3
4
  require 'saml2'
4
5
 
5
6
  def fixture(name)
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.0.11
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: 2021-01-08 00:00:00.000000000 Z
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: '1.12'
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: '1.12'
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: '6.2'
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: '6.2'
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: '10.0'
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: '10.0'
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