minisign 0.1.0 → 0.2.1

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: 007c98083c5c2f0343244efee9295ca3b9464c8b8edb59ca809e779ae7d0f76e
4
+ data.tar.gz: bdf21e444448429ee135d59ae7403646afd84b4df0724391563858763bec9a43
5
5
  SHA512:
6
- metadata.gz: cf80345096982044eb942d0ddafaf9f1546006d543cff34b7ad424e962f930229a6241958627f2339d76a893dbcfb4b5266a88b056c98b3e491e1be7877ab66f
7
- data.tar.gz: cbd7a29a71c353745fc6ffe95259368bf959c3418bfd32fae4cce1ede6baa1e449cf27bebc81410aacd2451d8284acd52448860a240d3ea4864ab53b3142f6a3
6
+ metadata.gz: b7db6eab732643303cb76aab37b8c31bd8efd349992c5dfedcd8a7c772936a6554f8382b05efc6c170bbfb970222024790096b5fa611884204cc94882a45338d
7
+ data.tar.gz: e635054fe989e5c76edadc0a16128b2c14b23e201949bda438b593c8671b2cc688df3729eff4733ef65e71a68caf137842ef9f060e6b0199746e92502f39026e
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,151 @@
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
+ exit 1
45
+ end
46
+
47
+ def self.prompt
48
+ $stdin.tty? ? $stdin.noecho(&:gets).chomp : $stdin.gets.chomp
49
+ end
50
+
51
+ def self.prevent_overwrite!(file)
52
+ return unless File.exist? file
53
+
54
+ puts 'Key generation aborted:'
55
+ puts "#{file} already exists."
56
+ puts ''
57
+ puts 'If you really want to overwrite the existing key pair, add the -f switch to'
58
+ puts 'force this operation.'
59
+ exit 1
60
+ end
61
+
62
+ def self.generate(options)
63
+ secret_key = options[:s] || "#{Dir.home}/.minisign/minisign.key"
64
+ public_key = options[:p] || './minisign.pub'
65
+ prevent_overwrite!(public_key) unless options[:f]
66
+ prevent_overwrite!(secret_key) unless options[:f]
67
+
68
+ if options[:W]
69
+ keypair = Minisign::KeyPair.new
70
+ File.write(secret_key, keypair.private_key)
71
+ File.write(public_key, keypair.public_key)
72
+ else
73
+ print 'Password: '
74
+ password = prompt
75
+ print "\nPassword (one more time): "
76
+ password_confirmation = prompt
77
+ if password != password_confirmation
78
+ puts "\nPasswords don't match"
79
+ exit 1
80
+ end
81
+ print "\nDeriving a key from the password in order to encrypt the secret key..."
82
+ keypair = Minisign::KeyPair.new(password)
83
+ File.write(secret_key, keypair.private_key)
84
+ print " done\n"
85
+ puts "The secret key was saved as #{options[:s]} - Keep it secret!"
86
+ File.write(public_key, keypair.public_key)
87
+ puts "The public key was saved as #{options[:p]} - That one can be public."
88
+ pubkey = keypair.public_key.to_s.split("\n").pop
89
+ puts "minisign -Vm <file> -P #{pubkey}"
90
+ end
91
+ end
92
+
93
+ def self.recreate(options)
94
+ options[:s] ||= "#{Dir.home}/.minisign/minisign.key"
95
+ public_key = options[:p] || './minisign.pub'
96
+ File.write(public_key, private_key(options[:s]).public_key)
97
+ end
98
+
99
+ def self.change_password(options)
100
+ options[:s] ||= "#{Dir.home}/.minisign/minisign.key"
101
+ new_private_key = private_key(options[:s])
102
+ print 'New Password: '
103
+ new_password = options[:W] ? nil : prompt
104
+ new_private_key.change_password! new_password
105
+ File.write(options[:s], new_private_key)
106
+ end
107
+
108
+ def self.sign(options)
109
+ # TODO: multiple files
110
+ options[:x] ||= "#{options[:m]}.minisig"
111
+ options[:s] ||= "#{Dir.home}/.minisign/minisign.key"
112
+ signature = private_key(options[:s]).sign(options[:m], File.read(options[:m]), options[:t], options[:c])
113
+ File.write(options[:x], signature)
114
+ end
115
+
116
+ def self.verify(options)
117
+ options[:x] ||= "#{options[:m]}.minisig"
118
+ options[:p] ||= './minisign.pub'
119
+ options[:P] ||= File.read(options[:p])
120
+ public_key = Minisign::PublicKey.new(options[:P])
121
+ message = File.read(options[:m])
122
+ signature = Minisign::Signature.new(File.read(options[:x]))
123
+ begin
124
+ verification = public_key.verify(signature, message)
125
+ rescue Minisign::SignatureVerificationError => e
126
+ puts e.message
127
+ exit 1
128
+ end
129
+ return if options[:q]
130
+ return puts message if options[:o]
131
+
132
+ puts options[:Q] ? signature.trusted_comment : verification
133
+ end
134
+
135
+ def self.private_key(seckey_file)
136
+ seckey_file_contents = File.read(seckey_file)
137
+ begin
138
+ Minisign::PrivateKey.new(seckey_file_contents)
139
+ rescue Minisign::PasswordMissingError
140
+ print 'Password: '
141
+ Minisign::PrivateKey.new(seckey_file_contents, prompt)
142
+ end
143
+ end
144
+
145
+ # rubocop:enable Metrics/CyclomaticComplexity
146
+ # rubocop:enable Metrics/AbcSize
147
+ # rubocop:enable Metrics/MethodLength
148
+ end
149
+ end
150
+
151
+ # 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)
@@ -21,7 +21,7 @@ module Minisign
21
21
  # public_key.key_id
22
22
  # #=> "E86FECED695E8E0"
23
23
  def key_id
24
- key_id_binary_string.bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
+ hex key_id_binary_string.bytes
25
25
  end
26
26
 
27
27
  # Verify a message's signature
@@ -29,17 +29,13 @@ module Minisign
29
29
  # @param signature [Minisign::Signature]
30
30
  # @param message [String] the content that was signed
31
31
  # @return [String] the trusted comment
32
- # @raise Ed25519::VerifyError on invalid signatures
33
- # @raise RuntimeError on tampered trusted comments
34
- # @raise RuntimeError on mismatching key ids
32
+ # @raise Minisign::SignatureVerificationError on invalid signatures
33
+ # @raise Minisign::SignatureVerificationError on tampered trusted comments
34
+ # @raise Minisign::SignatureVerificationError 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
58
+ raise Minisign::SignatureVerificationError, 'Signature verification failed'
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
@@ -3,11 +3,13 @@
3
3
  module Minisign
4
4
  # Parse a .minisig file's contents
5
5
  class Signature
6
+ include Utils
6
7
  # @param str [String] The contents of the .minisig file
7
8
  # @example
8
9
  # Minisign::Signature.new(File.read('test/example.txt.minisig'))
9
10
  def initialize(str)
10
11
  @lines = str.split("\n")
12
+ @decoded = Base64.strict_decode64(@lines[1])
11
13
  end
12
14
 
13
15
  # @return [String] the key id
@@ -15,7 +17,7 @@ module Minisign
15
17
  # Minisign::Signature.new(File.read('test/example.txt.minisig')).key_id
16
18
  # #=> "E86FECED695E8E0"
17
19
  def key_id
18
- encoded_signature[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
20
+ hex @decoded[2..9].bytes
19
21
  end
20
22
 
21
23
  # @return [String] the trusted comment
@@ -33,18 +35,12 @@ module Minisign
33
35
 
34
36
  # @return [String] the global signature
35
37
  def signature
36
- encoded_signature[10..]
38
+ @decoded[10..]
37
39
  end
38
40
 
39
41
  # @return [String] The signature that can be written to a file
40
42
  def to_s
41
43
  "#{@lines.join("\n")}\n"
42
44
  end
43
-
44
- private
45
-
46
- def encoded_signature
47
- Base64.decode64(@lines[1])
48
- end
49
45
  end
50
46
  end
@@ -18,6 +18,11 @@ module Minisign
18
18
  end
19
19
  end
20
20
 
21
+ # @return [String] bytes as little endian hexadecimal
22
+ def hex(bytes)
23
+ bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
+ end
25
+
21
26
  # @return [String] the <kdf_output> used to xor the ed25519 keys
22
27
  def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
23
28
  RbNaCl::PasswordHash.scrypt(
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.1
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-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ed25519
@@ -38,22 +38,30 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '7.1'
41
- description: Verify minisign signatures
41
+ description: Create and 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:
61
+ bug_tracker_uri: https://github.com/jshawl/minisign/issues
62
+ changelog_uri: https://github.com/jshawl/minisign/blob/main/CHANGELOG.md
63
+ documentation_uri: https://www.rubydoc.info/gems/minisign/0.2.1
64
+ source_code_uri: https://github.com/jshawl/minisign/tree/v0.2.1
57
65
  rubygems_mfa_required: 'true'
58
66
  post_install_message:
59
67
  rdoc_options: []