Skip to content

Commit 59b9ade

Browse files
committed
See #545 More specific error messages for signature validation
1 parent 6962bf5 commit 59b9ade

File tree

4 files changed

+74
-16
lines changed

4 files changed

+74
-16
lines changed

lib/onelogin/ruby-saml/response.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,14 +847,15 @@ def validate_signature
847847
end
848848

849849
if sig_elements.size != 1
850-
if sig_elements.size == 0
850+
if sig_elements.size == 0
851851
append_error("Signed element id ##{doc.signed_element_id} is not found")
852852
else
853853
append_error("Signed element id ##{doc.signed_element_id} is found more than once")
854854
end
855855
return append_error(error_msg)
856856
end
857857

858+
old_errors = @errors.clone
858859

859860
idp_certs = settings.get_idp_cert_multi
860861
if idp_certs.nil? || idp_certs[:signing].empty?
@@ -878,21 +879,27 @@ def validate_signature
878879
valid = false
879880
expired = false
880881
idp_certs[:signing].each do |idp_cert|
881-
valid = doc.validate_document_with_cert(idp_cert)
882+
valid = doc.validate_document_with_cert(idp_cert, true)
882883
if valid
883884
if settings.security[:check_idp_cert_expiration]
884885
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
885886
expired = true
886887
end
887888
end
889+
890+
# At least one certificate is valid, restore the old accumulated errors
891+
@errors = old_errors
888892
break
889893
end
894+
890895
end
891896
if expired
892897
error_msg = "IdP x509 certificate expired"
893898
return append_error(error_msg)
894899
end
895900
unless valid
901+
# Remove duplicated errors
902+
@errors = @errors.uniq
896903
return append_error(error_msg)
897904
end
898905
end

lib/xml_security.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {})
212212
begin
213213
cert = OpenSSL::X509::Certificate.new(cert_text)
214214
rescue OpenSSL::X509::CertificateError => _e
215-
return append_error("Certificate Error", soft)
215+
return append_error("Document Certificate Error", soft)
216216
end
217217

218218
if options[:fingerprint_alg]
@@ -224,7 +224,6 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {})
224224

225225
# check cert matches registered idp cert
226226
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
227-
@errors << "Fingerprint mismatch"
228227
return append_error("Fingerprint mismatch", soft)
229228
end
230229
else
@@ -255,12 +254,12 @@ def validate_document_with_cert(idp_cert, soft = true)
255254
begin
256255
cert = OpenSSL::X509::Certificate.new(cert_text)
257256
rescue OpenSSL::X509::CertificateError => _e
258-
return append_error("Certificate Error", soft)
257+
return append_error("Document Certificate Error", soft)
259258
end
260259

261260
# check saml response cert matches provided idp cert
262261
if idp_cert.to_pem != cert.to_pem
263-
return false
262+
return append_error("Certificate of the Signature element does not match provided certificate", soft)
264263
end
265264
else
266265
base64_cert = Base64.encode64(idp_cert.to_pem)
@@ -345,7 +344,6 @@ def validate_signature(base64_cert, soft = true)
345344
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
346345

347346
unless digests_match?(hash, digest_value)
348-
@errors << "Digest mismatch"
349347
return append_error("Digest mismatch", soft)
350348
end
351349

test/response_test.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ def generate_audience_error(expected, actual)
897897
settings.idp_cert_fingerprint = signature_fingerprint_1
898898
response.settings = settings
899899
assert !response.send(:validate_signature)
900+
assert_includes response.errors, "Fingerprint mismatch"
900901
assert_includes response.errors, "Invalid Signature on SAML Response"
901902
end
902903

@@ -917,15 +918,29 @@ def generate_audience_error(expected, actual)
917918
assert_includes response_valid_signed.errors, "IdP x509 certificate expired"
918919
end
919920

920-
it "return false when no X509Certificate and the cert provided at settings mismatches" do
921+
it "return false when X509Certificate and the cert provided at settings mismatches" do
921922
settings.idp_cert_fingerprint = nil
922923
settings.idp_cert = signature_1
923924
response_valid_signed_without_x509certificate.settings = settings
924925
assert !response_valid_signed_without_x509certificate.send(:validate_signature)
926+
assert_includes response_valid_signed_without_x509certificate.errors, "Key validation error"
925927
assert_includes response_valid_signed_without_x509certificate.errors, "Invalid Signature on SAML Response"
926928
end
927929

928-
it "return true when no X509Certificate and the cert provided at settings matches" do
930+
it "return false when X509Certificate has invalid content" do
931+
settings.idp_cert_fingerprint = nil
932+
settings.idp_cert = ruby_saml_cert_text
933+
content = read_response('response_with_signed_message_and_assertion.xml')
934+
content = content.sub(/<ds:X509Certificate>.*<\/ds:X509Certificate>/,
935+
"<ds:X509Certificate>an-invalid-certificate</ds:X509Certificate>")
936+
response_invalid_x509certificate = OneLogin::RubySaml::Response.new(content)
937+
response_invalid_x509certificate.settings = settings
938+
assert !response_invalid_x509certificate.send(:validate_signature)
939+
assert_includes response_invalid_x509certificate.errors, "Document Certificate Error"
940+
assert_includes response_invalid_x509certificate.errors, "Invalid Signature on SAML Response"
941+
end
942+
943+
it "return true when X509Certificate and the cert provided at settings matches" do
929944
settings.idp_cert_fingerprint = nil
930945
settings.idp_cert = ruby_saml_cert_text
931946
response_valid_signed_without_x509certificate.settings = settings
@@ -953,7 +968,7 @@ def generate_audience_error(expected, actual)
953968
:encryption => []
954969
}
955970
response_valid_signed.settings = settings
956-
assert response_valid_signed.send(:validate_signature)
971+
res = response_valid_signed.send(:validate_signature)
957972
assert_empty response_valid_signed.errors
958973
end
959974

@@ -965,6 +980,7 @@ def generate_audience_error(expected, actual)
965980
}
966981
response_valid_signed.settings = settings
967982
assert !response_valid_signed.send(:validate_signature)
983+
assert_includes response_valid_signed.errors, "Certificate of the Signature element does not match provided certificate"
968984
assert_includes response_valid_signed.errors, "Invalid Signature on SAML Response"
969985
end
970986
end

test/xml_security_test.rb

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,11 @@ class XmlSecurityTest < Minitest::Test
395395
end
396396

397397
describe '#validate_document_with_cert' do
398+
let(:document_data) { read_response('response_with_signed_message_and_assertion.xml') }
399+
let(:document) { OneLogin::RubySaml::Response.new(document_data).document }
400+
let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) }
401+
let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' }
402+
398403
describe 'with invalid document ' do
399404
describe 'when certificate is invalid' do
400405
let(:document_data) { read_response('response_with_signed_message_and_assertion.xml')
@@ -408,13 +413,8 @@ class XmlSecurityTest < Minitest::Test
408413
end
409414
end
410415

411-
describe 'with valid document ' do
416+
describe 'with valid document' do
412417
describe 'when response has cert' do
413-
let(:document_data) { read_response('response_with_signed_message_and_assertion.xml') }
414-
let(:document) { OneLogin::RubySaml::Response.new(document_data).document }
415-
let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) }
416-
let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' }
417-
418418
it 'is valid' do
419419
assert document.validate_document_with_cert(idp_cert), 'Document should be valid'
420420
end
@@ -429,6 +429,43 @@ class XmlSecurityTest < Minitest::Test
429429
end
430430
end
431431
end
432+
433+
describe 'when response has no cert but you have local cert' do
434+
let(:document_data) { response_document_valid_signed_without_x509certificate }
435+
436+
it 'is valid' do
437+
assert document.validate_document_with_cert(idp_cert), 'Document should be valid'
438+
end
439+
end
440+
441+
describe 'when response cert is invalid' do
442+
let(:document_data) do
443+
contents = read_response('response_with_signed_message_and_assertion.xml')
444+
contents.sub(/<ds:X509Certificate>.*<\/ds:X509Certificate>/,
445+
"<ds:X509Certificate>an-invalid-certificate</ds:X509Certificate>")
446+
end
447+
448+
it 'is not valid' do
449+
assert !document.validate_document_with_cert(idp_cert), 'Document should be valid'
450+
assert_equal(["Document Certificate Error"], document.errors)
451+
end
452+
end
453+
454+
describe 'when response cert is different from idp cert' do
455+
let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text2) }
456+
457+
it 'is not valid' do
458+
exception = assert_raises(OneLogin::RubySaml::ValidationError) do
459+
document.validate_document_with_cert(idp_cert, false)
460+
end
461+
assert_equal("Certificate of the Signature element does not match provided certificate", exception.message)
462+
end
463+
464+
it 'is not valid (soft = true)' do
465+
document.validate_document_with_cert(idp_cert)
466+
assert_equal(["Certificate of the Signature element does not match provided certificate"], document.errors)
467+
end
468+
end
432469
end
433470
end
434471
end

0 commit comments

Comments
 (0)