minisign 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90f8322a99590a021707a0f91aa959090352ed35571364ea536b298a03896f06
4
- data.tar.gz: 63f0cbc1604e551fc16e5bc4cd0456cbb2818cb30994bdd0aedc109bbb7e3dae
3
+ metadata.gz: 59267c0797e4539c136803dd3ac5333f14384019bb211abb50c078b14d9cb1c8
4
+ data.tar.gz: 7771c5a5b4227d1030d78b60fa584ed5158270a568617cc4ac7f4760230ef1d1
5
5
  SHA512:
6
- metadata.gz: cf80345096982044eb942d0ddafaf9f1546006d543cff34b7ad424e962f930229a6241958627f2339d76a893dbcfb4b5266a88b056c98b3e491e1be7877ab66f
7
- data.tar.gz: cbd7a29a71c353745fc6ffe95259368bf959c3418bfd32fae4cce1ede6baa1e449cf27bebc81410aacd2451d8284acd52448860a240d3ea4864ab53b3142f6a3
6
+ metadata.gz: 6a713e970fb762efaaabcd4572ea664d69cf5d558aa1219b3a27daf4c1fe4c111e98d0a4d90f4cc9892f0ec776b102bd0ab081d498a05a29639b0668a3decaa1
7
+ data.tar.gz: 603bb8180811b3923c1b5782e76d049fefe125ed5338f164dd0ed787d26ffae251932f6b67eea7ef6a15aaddd1076725a44fa397902eb8ea56c80f134dc77923
data/bin/minisign ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'io/console'
5
+ require 'minisign'
6
+ require 'optparse'
7
+
8
+ Signal.trap('INT') { exit }
9
+
10
+ options = {}
11
+ op = OptionParser.new do |opts|
12
+ boolean_opts = %w[G R C W S V Q f q o]
13
+ argument_opts = %w[t m x s p]
14
+ boolean_opts.each do |o|
15
+ opts.on("-#{o}") do |boolean|
16
+ options[o.to_sym] = boolean
17
+ end
18
+ end
19
+ argument_opts.each do |o|
20
+ opts.on("-#{o}#{o.upcase}") do |value|
21
+ options[o.to_sym] = value
22
+ end
23
+ end
24
+ end
25
+
26
+ begin
27
+ op.parse!
28
+ raise OptionParser::InvalidOption if options.keys.empty?
29
+ rescue OptionParser::InvalidOption
30
+ Minisign::CLI.usage
31
+ exit 1
32
+ end
33
+
34
+ Minisign::CLI.generate(options) if options[:G]
35
+ Minisign::CLI.recreate(options) if options[:R]
36
+ Minisign::CLI.change_password(options) if options[:C]
37
+ Minisign::CLI.sign(options) if options[:S]
38
+ Minisign::CLI.verify(options) if options[:V]
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ # rubocop:disable Metrics/ModuleLength
6
+ module Minisign
7
+ # The command line interface.
8
+ # This module is _not_ intended for library usage and is subject to
9
+ # breaking changes.
10
+ module CLI
11
+ # rubocop:disable Metrics/AbcSize
12
+ # rubocop:disable Metrics/MethodLength
13
+ # rubocop:disable Metrics/CyclomaticComplexity
14
+
15
+ # Command line usage
16
+ def self.usage
17
+ puts 'Usage:'
18
+ puts 'minisign -G [-f] [-p pubkey_file] [-s seckey_file] [-W]'
19
+ puts 'minisign -R [-s seckey_file] [-p pubkey_file]'
20
+ puts 'minisign -C [-s seckey_file] [-W]'
21
+ puts 'minisign -S [-l] [-x sig_file] [-s seckey_file] [-c untrusted_comment]'
22
+ puts ' [-t trusted_comment] -m file [file ...]'
23
+ puts 'minisign -V [-H] [-x sig_file] [-p pubkey_file | -P pubkey] [-o] [-q] -m file'
24
+ puts ''
25
+ puts '-G generate a new key pair'
26
+ puts '-R recreate a public key file from a secret key file'
27
+ puts '-C change/remove the password of the secret key'
28
+ puts '-S sign files'
29
+ puts '-V verify that a signature is valid for a given file'
30
+ puts '-m <file> file to sign/verify'
31
+ puts '-o combined with -V, output the file content after verification'
32
+ puts '-p <pubkey_file> public key file (default: ./minisign.pub)'
33
+ puts '-P <pubkey> public key, as a base64 string'
34
+ puts '-s <seckey_file> secret key file (default: ~/.minisign/minisign.key)'
35
+ puts '-W do not encrypt/decrypt the secret key with a password'
36
+ puts '-x <sigfile> signature file (default: <file>.minisig)'
37
+ puts '-c <comment> add a one-line untrusted comment'
38
+ puts '-t <comment> add a one-line trusted comment'
39
+ puts '-q quiet mode, suppress output'
40
+ puts '-Q pretty quiet mode, only print the trusted comment'
41
+ puts '-f force. Combined with -G, overwrite a previous key pair'
42
+ puts '-v display version number'
43
+ puts ''
44
+ end
45
+
46
+ def self.prompt
47
+ $stdin.tty? ? $stdin.noecho(&:gets).chomp : $stdin.gets.chomp
48
+ end
49
+
50
+ def self.prevent_overwrite!(file)
51
+ return unless File.exist? file
52
+
53
+ puts 'Key generation aborted:'
54
+ puts "#{file} already exists."
55
+ puts ''
56
+ puts 'If you really want to overwrite the existing key pair, add the -f switch to'
57
+ puts 'force this operation.'
58
+ exit 1
59
+ end
60
+
61
+ def self.generate(options)
62
+ secret_key = options[:s] || "#{Dir.home}/.minisign/minisign.key"
63
+ public_key = options[:p] || './minisign.pub'
64
+ prevent_overwrite!(public_key) unless options[:f]
65
+ prevent_overwrite!(secret_key) unless options[:f]
66
+
67
+ if options[:W]
68
+ keypair = Minisign::KeyPair.new
69
+ File.write(secret_key, keypair.private_key)
70
+ File.write(public_key, keypair.public_key)
71
+ else
72
+ print 'Password: '
73
+ password = prompt
74
+ print "\nPassword (one more time): "
75
+ password_confirmation = prompt
76
+ if password != password_confirmation
77
+ puts "\nPasswords don't match"
78
+ exit 1
79
+ end
80
+ print "\nDeriving a key from the password in order to encrypt the secret key..."
81
+ keypair = Minisign::KeyPair.new(password)
82
+ File.write(secret_key, keypair.private_key)
83
+ print " done\n"
84
+ puts "The secret key was saved as #{options[:s]} - Keep it secret!"
85
+ File.write(public_key, keypair.public_key)
86
+ puts "The public key was saved as #{options[:p]} - That one can be public."
87
+ pubkey = keypair.public_key.to_s.split("\n").pop
88
+ puts "minisign -Vm <file> -P #{pubkey}"
89
+ end
90
+ end
91
+
92
+ def self.recreate(options)
93
+ secret_key = options[:s] || "#{Dir.home}/.minisign/minisign.key"
94
+ public_key = options[:p] || './minisign.pub'
95
+ private_key_contents = File.read(secret_key)
96
+ begin
97
+ # try without a password first
98
+ private_key = Minisign::PrivateKey.new(private_key_contents)
99
+ rescue Minisign::PasswordMissingError
100
+ print 'Password: '
101
+ private_key = Minisign::PrivateKey.new(private_key_contents, prompt)
102
+ end
103
+ File.write(public_key, private_key.public_key)
104
+ end
105
+
106
+ def self.change_password(options)
107
+ options[:s] ||= "#{Dir.home}/.minisign/minisign.key"
108
+ private_key = begin
109
+ Minisign::PrivateKey.new(File.read(options[:s]))
110
+ rescue Minisign::PasswordMissingError
111
+ print 'Password: '
112
+ Minisign::PrivateKey.new(File.read(options[:s]), prompt)
113
+ end
114
+ print 'New Password: '
115
+ new_password = options[:W] ? nil : prompt
116
+ private_key.change_password! new_password
117
+ File.write(options[:s], private_key)
118
+ end
119
+
120
+ def self.sign(options)
121
+ # TODO: multiple files
122
+ options[:x] ||= "#{options[:m]}.minisig"
123
+ options[:s] ||= "#{Dir.home}/.minisign/minisign.key"
124
+ private_key = begin
125
+ Minisign::PrivateKey.new(File.read(options[:s]))
126
+ rescue Minisign::PasswordMissingError
127
+ print 'Password: '
128
+ Minisign::PrivateKey.new(File.read(options[:s]), prompt)
129
+ end
130
+ signature = private_key.sign(options[:m], File.read(options[:m]), options[:t], options[:c])
131
+ File.write(options[:x], signature)
132
+ end
133
+
134
+ def self.verify(options)
135
+ options[:x] ||= "#{options[:m]}.minisig"
136
+ options[:p] ||= './minisign.pub'
137
+ options[:P] ||= File.read(options[:p])
138
+ public_key = Minisign::PublicKey.new(options[:P])
139
+ message = File.read(options[:m])
140
+ signature = Minisign::Signature.new(File.read(options[:x]))
141
+ begin
142
+ verification = public_key.verify(signature, message)
143
+ rescue StandardError
144
+ puts 'Signature verification failed'
145
+ exit 1
146
+ end
147
+ return if options[:q]
148
+ return puts message if options[:o]
149
+
150
+ puts options[:Q] ? signature.trusted_comment : verification
151
+ end
152
+
153
+ # rubocop:enable Metrics/CyclomaticComplexity
154
+ # rubocop:enable Metrics/AbcSize
155
+ # rubocop:enable Metrics/MethodLength
156
+ end
157
+ end
158
+
159
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ class SignatureVerificationError < StandardError
5
+ end
6
+
7
+ class PasswordMissingError < StandardError
8
+ end
9
+
10
+ class PasswordIncorrectError < StandardError
11
+ end
12
+ end
@@ -5,6 +5,10 @@ module Minisign
5
5
  class KeyPair
6
6
  include Minisign::Utils
7
7
 
8
+ # Create a new key pair
9
+ # @param password [String] The password used to encrypt the private key
10
+ # @example
11
+ # Minisign::KeyPair.new("53cr3t P4s5w0rd")
8
12
  def initialize(password = nil)
9
13
  @password = password
10
14
  @key_id = SecureRandom.bytes(8)
@@ -35,18 +35,19 @@ module Minisign
35
35
  #
36
36
  # @param filename [String] The filename to be used in the trusted comment section
37
37
  # @param message [String] The file's contents
38
- # @param comment [String] An optional trusted comment to be included in the signature
38
+ # @param trusted_comment [String] An optional trusted comment to be included in the signature
39
+ # @param untrusted_comment [String] An optional untrusted comment
39
40
  # @return [Minisign::Signature]
40
- def sign(filename, message, comment = nil)
41
+ def sign(filename, message, trusted_comment = nil, untrusted_comment = nil)
41
42
  signature = ed25519_signing_key.sign(blake2b512(message))
42
- trusted_comment = comment || "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
43
+ trusted_comment ||= "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
44
+ untrusted_comment ||= 'signature from minisign secret key'
43
45
  global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
44
46
  Minisign::Signature.new([
45
- 'untrusted comment: <arbitrary text>',
47
+ "untrusted comment: #{untrusted_comment}",
46
48
  Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
47
49
  "trusted comment: #{trusted_comment}",
48
- Base64.strict_encode64(global_signature),
49
- ''
50
+ "#{Base64.strict_encode64(global_signature)}\n"
50
51
  ].join("\n"))
51
52
  end
52
53
 
@@ -61,6 +62,14 @@ module Minisign
61
62
  "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n"
62
63
  end
63
64
 
65
+ # Change or remove a password
66
+ #
67
+ # @param new_password [String]
68
+ def change_password!(new_password)
69
+ @password = new_password
70
+ @bytes[2..3] = [0, 0] if new_password.nil? # kdf_algorithm
71
+ end
72
+
64
73
  private
65
74
 
66
75
  def signature_algorithm
@@ -81,8 +90,12 @@ module Minisign
81
90
 
82
91
  # @raise [RuntimeError] if the extracted public key does not match the derived public key
83
92
  def assert_valid_key!
84
- raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil?
85
- raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes
93
+ if kdf_algorithm.bytes.sum != 0 && @password.nil?
94
+ raise Minisign::PasswordMissingError, 'Missing password for encrypted key'
95
+ end
96
+ return unless @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes
97
+
98
+ raise Minisign::PasswordIncorrectError, 'Wrong password for that key'
86
99
  end
87
100
 
88
101
  def key_data(password, bytes)
@@ -34,12 +34,8 @@ module Minisign
34
34
  # @raise RuntimeError on mismatching key ids
35
35
  def verify(signature, message)
36
36
  assert_matching_key_ids!(signature.key_id, key_id)
37
- ed25519_verify_key.verify(signature.signature, blake2b512(message))
38
- begin
39
- ed25519_verify_key.verify(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
40
- rescue Ed25519::VerifyError
41
- raise 'Comment signature verification failed'
42
- end
37
+ verify_message_signature(signature.signature, message)
38
+ verify_comment_signature(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
43
39
  "Signature and comment signature verified\nTrusted comment: #{signature.trusted_comment}"
44
40
  end
45
41
 
@@ -50,6 +46,18 @@ module Minisign
50
46
 
51
47
  private
52
48
 
49
+ def verify_comment_signature(signature, comment)
50
+ ed25519_verify_key.verify(signature, comment)
51
+ rescue Ed25519::VerifyError
52
+ raise Minisign::SignatureVerificationError, 'Comment signature verification failed'
53
+ end
54
+
55
+ def verify_message_signature(signature, message)
56
+ ed25519_verify_key.verify(signature, blake2b512(message))
57
+ rescue Ed25519::VerifyError => e
58
+ raise Minisign::SignatureVerificationError, e
59
+ end
60
+
53
61
  def untrusted_comment
54
62
  if @lines.length == 1
55
63
  "minisign public key #{key_id}"
@@ -75,7 +83,10 @@ module Minisign
75
83
  end
76
84
 
77
85
  def assert_matching_key_ids!(key_id1, key_id2)
78
- raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2
86
+ return if key_id1 == key_id2
87
+
88
+ raise Minisign::SignatureVerificationError,
89
+ "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}"
79
90
  end
80
91
  end
81
92
  end
data/lib/minisign.rb CHANGED
@@ -4,8 +4,10 @@ require 'ed25519'
4
4
  require 'base64'
5
5
  require 'rbnacl'
6
6
 
7
+ require 'minisign/cli'
7
8
  require 'minisign/utils'
8
9
  require 'minisign/public_key'
9
10
  require 'minisign/signature'
10
11
  require 'minisign/private_key'
11
12
  require 'minisign/key_pair'
13
+ require 'minisign/error'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minisign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Shawl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-10 00:00:00.000000000 Z
11
+ date: 2024-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ed25519
@@ -40,17 +40,21 @@ dependencies:
40
40
  version: '7.1'
41
41
  description: Verify minisign signatures
42
42
  email: jesse@jesse.sh
43
- executables: []
43
+ executables:
44
+ - minisign
44
45
  extensions: []
45
46
  extra_rdoc_files: []
46
47
  files:
48
+ - bin/minisign
47
49
  - lib/minisign.rb
50
+ - lib/minisign/cli.rb
51
+ - lib/minisign/error.rb
48
52
  - lib/minisign/key_pair.rb
49
53
  - lib/minisign/private_key.rb
50
54
  - lib/minisign/public_key.rb
51
55
  - lib/minisign/signature.rb
52
56
  - lib/minisign/utils.rb
53
- homepage: https://rubygems.org/gems/minisign
57
+ homepage: https://github.com/jshawl/minisign
54
58
  licenses:
55
59
  - MIT
56
60
  metadata: