main
1# frozen_string_literal: true
2
3RSpec.describe Saml::Kit::Signature do
4 subject { described_class.new(signed_document.at_xpath('//ds:Signature')) }
5
6 let(:key_pair) { ::Xml::Kit::KeyPair.generate(use: :signing) }
7 let(:signed_document) do
8 Saml::Kit::AuthenticationRequest.build do |x|
9 x.sign_with(key_pair)
10 end
11 end
12 let(:xml_hash) { Hash.from_xml(subject.to_xml) }
13
14 specify { expect(subject.digest_value).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestValue']) }
15 specify { expect(subject.digest_method).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestMethod']['Algorithm']) }
16 specify { expect(subject.signature_value).to eql(xml_hash['Signature']['SignatureValue']) }
17 specify { expect(subject.signature_method).to eql(xml_hash['Signature']['SignedInfo']['SignatureMethod']['Algorithm']) }
18 specify { expect(subject.canonicalization_method).to eql(xml_hash['Signature']['SignedInfo']['CanonicalizationMethod']['Algorithm']) }
19 specify { expect(subject.transforms).to eql(xml_hash['Signature']['SignedInfo']['Reference']['Transforms']['Transform'].map { |x| x['Algorithm'] }) }
20
21 specify do
22 expected = ::Xml::Kit::Certificate.new(xml_hash['Signature']['KeyInfo']['X509Data']['X509Certificate'], use: :signing)
23 expect(subject.certificate).to eql(expected)
24 end
25
26 describe '#valid?' do
27 it 'returns true when the signature is valid' do
28 expect(subject).to be_valid
29 end
30
31 it 'is invalid when the xml has been tampered' do
32 signed_document.at_xpath('//saml:Issuer').content = 'INVALID'
33 expect(subject).not_to be_valid
34 expect(subject.errors[:digest_value]).to be_present
35 end
36
37 it 'is invalid when the signature is missing' do
38 subject = described_class.new(nil)
39 expect(subject).not_to be_valid
40 expect(subject.errors[:base]).to match_array(['is missing.'])
41 end
42
43 it 'is invalid when the schema of the signature is invalid' do
44 signature_element = signed_document.at_xpath('//ds:Signature')
45 element = signature_element.at_xpath('./ds:SignedInfo', ds: Xml::Kit::Namespaces::XMLDSIG)
46 element.name = 'BLAH'
47 subject = described_class.new(signature_element)
48 expect(subject).not_to be_valid
49 expect(subject.errors[:base]).to include("1:0: ERROR: Element '{http://www.w3.org/2000/09/xmldsig#}BLAH': This element is not expected. Expected is ( {http://www.w3.org/2000/09/xmldsig#}SignedInfo ).")
50 end
51
52 describe 'certificate validation' do
53 let(:key_pair) { ::Xml::Kit::KeyPair.new(expired_certificate, private_key, nil, :signing) }
54 let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
55 let(:expired_certificate) do
56 certificate = OpenSSL::X509::Certificate.new
57 certificate.not_before = not_before
58 certificate.not_after = not_after
59 certificate.public_key = private_key.public_key
60 certificate.sign(private_key, OpenSSL::Digest::SHA256.new)
61 certificate
62 end
63
64 context 'when the certificate is expired' do
65 let(:not_before) { 10.minutes.ago }
66 let(:not_after) { 1.minute.ago }
67
68 it 'is invalid' do
69 expect(subject).to be_invalid
70 expect(subject.errors[:certificate]).to match_array([
71 "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
72 ])
73 end
74 end
75
76 context 'when the certificate is not active yet' do
77 let(:not_before) { 10.minutes.from_now }
78 let(:not_after) { 20.minute.from_now }
79
80 it 'invalid' do
81 expect(subject).to be_invalid
82 expect(subject.errors[:certificate]).to match_array([
83 "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
84 ])
85 end
86 end
87 end
88 end
89
90 describe '#to_h' do
91 it 'returns a hash representation of the signature' do
92 expected = Hash.from_xml(signed_document.to_s)['AuthnRequest']['Signature']
93 expect(subject.to_h).to eql(expected)
94 end
95 end
96
97 describe '#present?' do
98 context 'when a signature is not present' do
99 it 'return false' do
100 expect(described_class.new(nil)).not_to be_present
101 end
102 end
103
104 context 'when a signature is present' do
105 it 'returns true' do
106 expect(subject.present?).to be(true)
107 end
108 end
109 end
110
111 describe '#expected_digest_value' do
112 it 'returns the expected digest value' do
113 expected_digest = subject.digest_value
114
115 signed_document.at_xpath('//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue').content = 'INVALID'
116 subject = described_class.new(signed_document.at_xpath('//ds:Signature'))
117
118 expect(subject.expected_digest_value).to eql(expected_digest)
119 end
120 end
121
122 describe '#trusted?' do
123 context 'when trusted' do
124 let(:metadata) { instance_double(Saml::Kit::Metadata, matches?: Object.new) }
125
126 specify { expect(subject.trusted?(metadata)).to be(true) }
127 end
128
129 context 'when untrusted' do
130 let(:metadata) { instance_double(Saml::Kit::Metadata, matches?: nil) }
131
132 specify { expect(subject.trusted?(metadata)).to be(false) }
133 end
134 end
135end