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