tool_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. FIXTURES_FILES_PATH = Rails.root.join('spec/fixtures/files/pgp').freeze
  4. RSpec.describe SecureMailing::PGP::Tool, :aggregate_failures do
  5. before do
  6. Setting.set('pgp_integration', true)
  7. end
  8. let(:instance) { described_class.new }
  9. describe '#with_private_keyring' do
  10. it 'sets GNUPGHOME to a temporary directory' do
  11. expect(instance.with_private_keyring(&:gnupg_home)).to be_present
  12. expect(instance.with_private_keyring { |t| File.exist?("#{t.gnupg_home}/pubring.kbx") }).to be false
  13. expect(instance.with_private_keyring do |t|
  14. t.send(:gpg, 'list-keys')
  15. File.exist?("#{t.gnupg_home}/pubring.kbx")
  16. end).to be true
  17. end
  18. it 'removes the temporary directory afterwards' do
  19. expect(Dir.exist?(instance.with_private_keyring(&:gnupg_home))).to be false
  20. end
  21. end
  22. describe '#call (private method)' do
  23. it 'only works from with_private_keyring' do
  24. expect { instance.send(:gpg, 'version') }.to raise_error(RuntimeError)
  25. end
  26. end
  27. describe '#import' do
  28. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.pub.asc').read }
  29. let(:private_key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  30. let(:import) do
  31. instance.with_private_keyring do |t|
  32. t.import(private_key)
  33. t.import(key)
  34. end
  35. end
  36. it 'imports public and private keys successfully' do
  37. expect(import.status.success?).to be true
  38. expect(import.stdout).to be_empty
  39. end
  40. context 'with an invalid key' do
  41. let(:key) { 'invalid' }
  42. it 'raises an error' do
  43. expect { import }.to raise_error(SecureMailing::PGP::Tool::Error::NoData)
  44. end
  45. end
  46. end
  47. describe '#passphrase' do
  48. let(:private_key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  49. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  50. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  51. let(:passphrase_result) do
  52. instance.with_private_keyring do |t|
  53. t.import(private_key)
  54. t.passphrase(fingerprint, passphrase)
  55. end
  56. end
  57. it 'validates the passphrase successfully' do
  58. expect(passphrase_result.status.success?).to be true
  59. end
  60. context 'with an invalid passphrase' do
  61. let(:passphrase) { 'invalid' }
  62. it 'raises an error' do
  63. expect { passphrase_result }.to raise_error(SecureMailing::PGP::Tool::Error::BadPassphrase)
  64. end
  65. end
  66. context 'with an empty passphrase' do
  67. let(:passphrase) { '' }
  68. it 'raises an error' do
  69. expect { passphrase_result }.to raise_error(SecureMailing::PGP::Tool::Error::NoPassphrase)
  70. end
  71. end
  72. end
  73. describe '#info' do
  74. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.pub.asc').read }
  75. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  76. let(:created_at) { DateTime.parse(FIXTURES_FILES_PATH.join('zammad@localhost.created_at').read) }
  77. let(:expires_at) { DateTime.parse(FIXTURES_FILES_PATH.join('zammad@localhost.expires_at').read) }
  78. let(:info) do
  79. instance.with_private_keyring { |t| t.info(key) }
  80. end
  81. it 'returns information of a public key successfully' do
  82. expect(info).to have_attributes(fingerprint: fingerprint, uids: ['zammad@localhost'], created_at: created_at, expires_at: expires_at, secret: false)
  83. end
  84. context 'with an invalid key' do
  85. let(:key) { 'invalid' }
  86. it 'raises an error' do
  87. expect { info }.to raise_error(SecureMailing::PGP::Tool::Error::NoData)
  88. end
  89. end
  90. context 'with an key including a revoke subkey' do
  91. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.revoker.pub.asc').read }
  92. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.revoker.fingerprint').read }
  93. let(:created_at) { DateTime.parse(FIXTURES_FILES_PATH.join('zammad@localhost.revoker.created_at').read) }
  94. let(:expires_at) { DateTime.parse(FIXTURES_FILES_PATH.join('zammad@localhost.revoker.expires_at').read) }
  95. it 'returns information of a public key successfully' do
  96. expect(info).to have_attributes(fingerprint: fingerprint, uids: ['zammad@localhost'], created_at: created_at, expires_at: expires_at, secret: false)
  97. end
  98. end
  99. context 'with an key including a revoked uid' do
  100. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.revuid.pub.asc').read }
  101. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.revuid.fingerprint').read }
  102. let(:created_at) { DateTime.parse(FIXTURES_FILES_PATH.join('zammad@localhost.revuid.created_at').read) }
  103. let(:expires_at) { nil }
  104. let(:revuid) { FIXTURES_FILES_PATH.join('zammad@localhost.revuid.uid').read }
  105. it 'returns information of a public key successfully' do
  106. expect(info.uids.exclude?(revuid)).to be true
  107. expect(info).to have_attributes(fingerprint: fingerprint, uids: ['zammad@localhost'], created_at: created_at, expires_at: expires_at, secret: false)
  108. end
  109. end
  110. end
  111. describe '#export' do
  112. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  113. let(:export) do
  114. instance.with_private_keyring do |t|
  115. t.import(key)
  116. t.export(fingerprint, passphrase, secret: secret)
  117. end
  118. end
  119. context 'with public key' do
  120. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.pub.asc').read }
  121. let(:passphrase) { nil }
  122. let(:secret) { false }
  123. it 'exports a public key successfully' do
  124. expect(export.status.success?).to be true
  125. expect(export.stdout).to eq(key)
  126. end
  127. context 'with an unknown fingerprint' do
  128. let(:fingerprint) { 'invalid' }
  129. it 'raises an error' do
  130. expect { export }.to raise_error(SecureMailing::PGP::Tool::Error::NoPublicKey)
  131. end
  132. end
  133. end
  134. context 'with private key' do
  135. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  136. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  137. let(:secret) { true }
  138. let(:info) do
  139. described_class.new.with_private_keyring do |t|
  140. t.info(key)
  141. end
  142. end
  143. it 'exports a private key successfully' do
  144. # The exported key differs from the imported key because the exported key is encrypted with a random salt.
  145. # For that we compare the key information instead of the key itself.
  146. expect(described_class.new.with_private_keyring { |t| t.info(export.stdout) }).to eq(info)
  147. end
  148. context 'with an unknown fingerprint' do
  149. let(:fingerprint) { 'invalid' }
  150. it 'raises an error' do
  151. expect { export }.to raise_error(SecureMailing::PGP::Tool::Error::NoSecretKey)
  152. end
  153. end
  154. end
  155. end
  156. describe '#sign' do
  157. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  158. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  159. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  160. let(:data) { 'Hello, World.' }
  161. let(:sign) do
  162. instance.with_private_keyring do |t|
  163. t.import(key)
  164. t.sign(data, fingerprint, passphrase)
  165. end
  166. end
  167. it 'signs data successfully' do
  168. expect(sign.status.success?).to be true
  169. expect(sign.stdout).to be_present and expect(sign.stdout).to include('-----BEGIN PGP SIGNATURE-----')
  170. end
  171. context 'with an invalid passphrase' do
  172. let(:passphrase) { 'invalid' }
  173. it 'raises an error' do
  174. expect { sign }.to raise_error(SecureMailing::PGP::Tool::Error::BadPassphrase)
  175. end
  176. end
  177. end
  178. describe '#verify' do
  179. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  180. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  181. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  182. let(:data) { FIXTURES_FILES_PATH.join('zammad@localhost.data').read }
  183. let(:signature) { FIXTURES_FILES_PATH.join('zammad@localhost.data.sig.asc').read }
  184. let(:verify) do
  185. instance.with_private_keyring do |t|
  186. t.import(key) if key.present?
  187. t.verify(data, signature: signature)
  188. end
  189. end
  190. it 'verifies signature successfully' do
  191. expect(verify.status.success?).to be true
  192. expect(verify.stderr).to be_present and expect(verify.stderr).to include('Good signature')
  193. end
  194. context 'with corrupted data' do
  195. let(:data) { 'invalid' }
  196. it 'raises an error' do
  197. expect { verify }.to raise_error(SecureMailing::PGP::Tool::Error::BadSignature)
  198. end
  199. end
  200. context 'with an invalid signature' do
  201. let(:signature) { 'invalid' }
  202. it 'raises an error' do
  203. expect { verify }.to raise_error(SecureMailing::PGP::Tool::Error::NoData)
  204. end
  205. end
  206. context 'with an missing public key' do
  207. let(:key) { nil }
  208. it 'raises an error' do
  209. expect { verify }.to raise_error(SecureMailing::PGP::Tool::Error::NoPublicKey)
  210. end
  211. end
  212. end
  213. describe '#encrypt' do
  214. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  215. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  216. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  217. let(:data) { 'Hello, World.' }
  218. let(:encrypt) do
  219. instance.with_private_keyring do |t|
  220. t.import(key)
  221. t.encrypt(data, [fingerprint])
  222. end
  223. end
  224. it 'encrypts data successfully' do
  225. expect(encrypt.status.success?).to be true
  226. expect(encrypt.stdout).to be_present and expect(encrypt.stdout).to include('-----BEGIN PGP MESSAGE-----')
  227. end
  228. context 'with an unknown recipient' do
  229. let(:fingerprint) { 'invalid' }
  230. it 'raises an error' do
  231. expect { encrypt }.to raise_error(SecureMailing::PGP::Tool::Error::InvalidRecipient)
  232. end
  233. end
  234. end
  235. describe '#decrypt' do
  236. let(:key) { FIXTURES_FILES_PATH.join('zammad@localhost.asc').read }
  237. let(:passphrase) { FIXTURES_FILES_PATH.join('zammad@localhost.passphrase').read }
  238. let(:fingerprint) { FIXTURES_FILES_PATH.join('zammad@localhost.fingerprint').read }
  239. let(:encrypted_data) { FIXTURES_FILES_PATH.join('zammad@localhost.data.enc.asc').read }
  240. let(:decrypt) do
  241. instance.with_private_keyring do |t|
  242. t.import(key)
  243. t.decrypt(encrypted_data, passphrase)
  244. end
  245. end
  246. it 'decrypts data successfully' do
  247. expect(decrypt.status.success?).to be true
  248. expect(decrypt.stdout).to eq("Hello, World.\n")
  249. end
  250. context 'with an invalid passphrase' do
  251. let(:passphrase) { 'invalid' }
  252. it 'raises an error' do
  253. expect { decrypt }.to raise_error(SecureMailing::PGP::Tool::Error::BadPassphrase)
  254. end
  255. end
  256. end
  257. end