rspec-pgp_matchers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []