rspec-pgp_matchers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ require "rspec/pgp_matchers/version"
5
+ require "rspec/pgp_matchers/gpg_matcher_helper"
6
+ require "rspec/pgp_matchers/be_a_pgp_encrypted_message"
7
+ require "rspec/pgp_matchers/be_a_valid_pgp_signature_of"
8
+
9
+ module RSpec
10
+ module PGPMatchers
11
+ class << self
12
+ attr_accessor :homedir
13
+ end
14
+ # Your code goes here...
15
+ end
16
+ end
@@ -0,0 +1,113 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ # A following PGP matcher calls the GPG executable in a subshell, then
5
+ # parses command output. This is a poor pattern in general, because output
6
+ # messages may subtly change over GPG evolution, and some maintenance work
7
+ # may be required.
8
+ #
9
+ # However, GPG executables do not provide any machine-readable output which
10
+ # could be used instead. Furthermore, using RNP or GPGME from here is also
11
+ # a poor idea, because this gem is going to be tested against various
12
+ # combinations of software, in some of which that dependency may be unavailable.
13
+ #
14
+ # If parsing the output of GPG commands will become a burden, then the preferred
15
+ # solution is to develop a minimalist validator which, if executed in
16
+ # a subshell, returns some useful machine-readable output. A validator tool
17
+ # running in a separate process may leverage GPGME, as it won't be exposed
18
+ # outside the validator. A previous implementation of this matcher may provide
19
+ # some useful ideas. See commit 2e2bd0da090d7d31ecacc2d1ea6bd3e13479e675.
20
+ #
21
+ # rubocop:disable Metrics/BlockLength
22
+ RSpec::Matchers.define :be_a_pgp_encrypted_message do
23
+ # rubocop:enable Metrics/BlockLength
24
+ include RSpec::PGPMatchers::GPGMatcherHelper
25
+
26
+ attr_reader :err, :expected_recipients
27
+
28
+ match do |encrypted_string|
29
+ @err = validate_encrypted_message(encrypted_string)
30
+ @err.nil?
31
+ end
32
+
33
+ chain :containing, :expected_cleartext
34
+ chain :signed_by, :expected_signer
35
+
36
+ chain :encrypted_for do |*recipients|
37
+ @expected_recipients = [*recipients]
38
+ end
39
+
40
+ failure_message do
41
+ err
42
+ end
43
+
44
+ # Returns +nil+ if signature is valid, or an error message otherwise.
45
+ def validate_encrypted_message(encrypted_string)
46
+ cmd_output = run_gpg_decrypt(encrypted_string)
47
+ cmd_result = analyse_decrypt_output(*cmd_output)
48
+
49
+ if cmd_result[:well_formed_pgp_data]
50
+ match_constraints(**cmd_result)
51
+ else
52
+ msg_no_pgg_data(encrypted_string)
53
+ end
54
+ end
55
+
56
+ def run_gpg_decrypt(encrypted_string)
57
+ enc_file = make_tempfile_containing(encrypted_string)
58
+ cmd = gpg_decrypt_command(enc_file)
59
+ run_command(cmd)
60
+ end
61
+
62
+ def analyse_decrypt_output(stdout_str, stderr_str, status)
63
+ {
64
+ well_formed_pgp_data: (status.exitstatus != 2),
65
+ recipients: detect_recipients(stderr_str),
66
+ signature: detect_signers(stderr_str).first,
67
+ cleartext: stdout_str,
68
+ }
69
+ end
70
+
71
+ def match_constraints(cleartext:, recipients:, signature:, **_ignored)
72
+ [
73
+ (expected_cleartext && match_cleartext(cleartext)),
74
+ (expected_recipients && match_recipients(recipients)),
75
+ (expected_signer && match_signature(signature)),
76
+ ].detect { |x| x }
77
+ end
78
+
79
+ def gpg_decrypt_command(enc_file)
80
+ homedir_path = Shellwords.escape(RSpec::PGPMatchers.homedir)
81
+ enc_path = Shellwords.escape(enc_file.path)
82
+
83
+ <<~SH
84
+ gpg \
85
+ --homedir #{homedir_path} \
86
+ --no-permission-warning \
87
+ --decrypt #{enc_path}
88
+ SH
89
+ end
90
+
91
+ def msg_mismatch(text)
92
+ "expected given Open PGP message to contain following " +
93
+ "text:\n#{expected_cleartext}\nbut was:\n#{text}"
94
+ end
95
+
96
+ def msg_no_pgg_data(file_text)
97
+ "expected given text to be a valid Open PGP encrypted message, " +
98
+ "but it contains no PGP data, just:\n#{file_text}"
99
+ end
100
+
101
+ def msg_wrong_recipients(recipients)
102
+ expected_recipients_list = expected_recipients.inspect
103
+ recipients_list = recipients.inspect
104
+
105
+ "expected given Open PGP message to be encrypted for following " +
106
+ "recipients: #{expected_recipients_list}, but was for: #{recipients_list}"
107
+ end
108
+
109
+ def msg_wrong_signer(actual_signer)
110
+ "expected singature to be signed by #{expected_signer}, " +
111
+ "but was actually signed by #{actual_signer}"
112
+ end
113
+ end
@@ -0,0 +1,95 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ # A following PGP matcher calls the GPG executable in a subshell, then
5
+ # parses command output. This is a poor pattern in general, because output
6
+ # messages may subtly change over GPG evolution.
7
+ #
8
+ # However, GPG executables do not provide any machine-readable output which
9
+ # could be used instead. Furthermore, using RNP or GPGME from here is also
10
+ # a poor idea, because this gem is going to be tested against various
11
+ # combinations of software, in some of which that dependency may be unavailable.
12
+ #
13
+ # If output parsing will ever become a source of problems, then the preferred
14
+ # solution is to develop a minimalist validator which, if executed in
15
+ # a subshell, returns useful machine-readable output. A validator tool running
16
+ # in a separate process may leverage GPGME, as it won't be exposed outside
17
+ # the validator. A previous implementation of this matcher may provide some
18
+ # useful ideas. See commit 2e2bd0da090d7d31ecacc2d1ea6bd3e13479e675.
19
+ #
20
+ # rubocop:disable Metrics/BlockLength
21
+ RSpec::Matchers.define :be_a_valid_pgp_signature_of do |text|
22
+ # rubocop:enable Metrics/BlockLength
23
+ include RSpec::PGPMatchers::GPGMatcherHelper
24
+
25
+ attr_reader :err
26
+
27
+ match do |signature_string|
28
+ @err = verify_signature(text, signature_string)
29
+ @err.nil?
30
+ end
31
+
32
+ chain :signed_by, :expected_signer
33
+
34
+ failure_message do
35
+ err
36
+ end
37
+
38
+ # Returns +nil+ if first signature is valid, or an error message otherwise.
39
+ def verify_signature(cleartext, signature_string)
40
+ cmd_output = run_gpg_verify(cleartext, signature_string)
41
+ cmd_result = analyse_verify_output(*cmd_output)
42
+
43
+ if cmd_result[:well_formed_pgp_data]
44
+ match_constraints(**cmd_result)
45
+ else
46
+ msg_no_pgg_data(signature_string)
47
+ end
48
+ end
49
+
50
+ def run_gpg_verify(cleartext, signature_string)
51
+ sig_file = make_tempfile_containing(signature_string)
52
+ data_file = make_tempfile_containing(cleartext)
53
+ cmd = gpg_verify_command(sig_file, data_file)
54
+ run_command(cmd)
55
+ end
56
+
57
+ def analyse_verify_output(_stdout_str, stderr_str, status)
58
+ {
59
+ well_formed_pgp_data: (status.exitstatus != 2),
60
+ signature: detect_signers(stderr_str).first,
61
+ }
62
+ end
63
+
64
+ def match_constraints(signature:, **_ignored)
65
+ match_signature(signature)
66
+ end
67
+
68
+ def gpg_verify_command(sig_file, data_file)
69
+ homedir_path = Shellwords.escape(RSpec::PGPMatchers.homedir)
70
+ sig_path = Shellwords.escape(sig_file.path)
71
+ data_path = Shellwords.escape(data_file.path)
72
+
73
+ <<~SH
74
+ gpg \
75
+ --homedir #{homedir_path} \
76
+ --no-permission-warning \
77
+ --verify #{sig_path} #{data_path}
78
+ SH
79
+ end
80
+
81
+ def msg_mismatch(text)
82
+ "expected given signature to be a valid Open PGP signature " +
83
+ "of following text:\n#{text}"
84
+ end
85
+
86
+ def msg_no_pgg_data(file_text)
87
+ "expected given text to be a valid Open PGP signature, " +
88
+ "but it contains no PGP data, just:\n#{file_text}"
89
+ end
90
+
91
+ def msg_wrong_signer(actual_signer)
92
+ "expected singature to be signed by #{expected_signer}, " +
93
+ "but was actually signed by #{actual_signer}"
94
+ end
95
+ end
@@ -0,0 +1,63 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ require "open3"
5
+ require "tempfile"
6
+
7
+ module RSpec
8
+ module PGPMatchers
9
+ module GPGMatcherHelper
10
+ def detect_signers(stderr_str)
11
+ rx = /(?<ok>Good|BAD) signature from .*\<(?<email>[^>]+)\>/
12
+
13
+ stderr_str.to_enum(:scan, rx).map do
14
+ {
15
+ email: $~["email"],
16
+ ok: ($~["ok"] == "Good"),
17
+ }
18
+ end
19
+ end
20
+
21
+ def detect_recipients(stderr_str)
22
+ rx = /encrypted with .*\n.*\<(?<email>[^>]+)\>/
23
+
24
+ stderr_str.to_enum(:scan, rx).map do
25
+ $~["email"]
26
+ end
27
+ end
28
+
29
+ def match_cleartext(cleartext)
30
+ if cleartext != expected_cleartext
31
+ msg_mismatch(cleartext)
32
+ end
33
+ end
34
+
35
+ def match_recipients(recipients)
36
+ if expected_recipients.sort != recipients.sort
37
+ msg_wrong_recipients(recipients)
38
+ end
39
+ end
40
+
41
+ # Checks if signature is valid. If `expected_signer` is not `nil`, then
42
+ # it additionally checks if the signature was issued by expected signer.
43
+ def match_signature(signature)
44
+ if !signature[:ok]
45
+ msg_mismatch(text)
46
+ elsif expected_signer && signature[:email] != expected_signer
47
+ msg_wrong_signer(signature[:email])
48
+ end
49
+ end
50
+
51
+ def make_tempfile_containing(file_content)
52
+ tempfile = Tempfile.new
53
+ tempfile.write(file_content)
54
+ tempfile.flush
55
+ end
56
+
57
+ def run_command(cmd)
58
+ env = { "LC_ALL" => "C" } # Gettext English locale
59
+ Open3.capture3(env, cmd)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,8 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ module RSpec
5
+ module PGPMatchers
6
+ VERSION = "0.1.0".freeze
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ lib = File.expand_path("lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require "rspec/pgp_matchers/version"
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "rspec-pgp_matchers"
10
+ spec.version = RSpec::PGPMatchers::VERSION
11
+ spec.authors = ["Ribose Inc."]
12
+ spec.email = ["open.source@ribose.com"]
13
+
14
+ spec.summary = "RSpec matchers for testing OpenPGP messages"
15
+ spec.homepage = "https://github.com/riboseinc/rspec-pgp_matchers"
16
+ spec.license = "MIT"
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added
20
+ # into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_runtime_dependency "rspec-expectations", "~> 3.4"
31
+
32
+ spec.add_development_dependency "bundler", "~> 1.16"
33
+ spec.add_development_dependency "gpgme"
34
+ spec.add_development_dependency "pry", ">= 0.10.3", "< 0.12"
35
+ spec.add_development_dependency "rake", "~> 12.0"
36
+ spec.add_development_dependency "rspec", "~> 3.0"
37
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-pgp_matchers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-expectations
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gpgme
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.10.3
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '0.12'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 0.10.3
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.12'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '12.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '12.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ description:
104
+ email:
105
+ - open.source@ribose.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".editorconfig"
111
+ - ".gitignore"
112
+ - ".hound.yml"
113
+ - ".rspec"
114
+ - ".rubocop.ribose.yml"
115
+ - ".rubocop.tb.yml"
116
+ - ".rubocop.yml"
117
+ - ".travis.yml"
118
+ - Gemfile
119
+ - LICENSE.txt
120
+ - README.adoc
121
+ - Rakefile
122
+ - bin/bundle
123
+ - bin/console
124
+ - bin/rake
125
+ - bin/rspec
126
+ - bin/setup
127
+ - ci/gemfiles/common.gemfile
128
+ - ci/gemfiles/rspec-3.4.gemfile
129
+ - ci/gemfiles/rspec-3.5.gemfile
130
+ - ci/gemfiles/rspec-3.6.gemfile
131
+ - ci/gemfiles/rspec-3.7.gemfile
132
+ - ci/install_gpg_all.sh
133
+ - ci/install_gpg_component.sh
134
+ - lib/rspec/pgp_matchers.rb
135
+ - lib/rspec/pgp_matchers/be_a_pgp_encrypted_message.rb
136
+ - lib/rspec/pgp_matchers/be_a_valid_pgp_signature_of.rb
137
+ - lib/rspec/pgp_matchers/gpg_matcher_helper.rb
138
+ - lib/rspec/pgp_matchers/version.rb
139
+ - rspec-pgp_matchers.gemspec
140
+ homepage: https://github.com/riboseinc/rspec-pgp_matchers
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.7.6
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: RSpec matchers for testing OpenPGP messages
164
+ test_files: []