minisign 0.0.8 → 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: 5fff86c56cc4091b4517b01304262f097451c801847f4593d1bb99df4bef760f
4
- data.tar.gz: 4c3ffd93d6dd729a25b6eabd9773d3a226e27c1066329972132d87ab5b5d49cc
3
+ metadata.gz: 59267c0797e4539c136803dd3ac5333f14384019bb211abb50c078b14d9cb1c8
4
+ data.tar.gz: 7771c5a5b4227d1030d78b60fa584ed5158270a568617cc4ac7f4760230ef1d1
5
5
  SHA512:
6
- metadata.gz: 2bf06fbf6e88b3a003d5b62d245e8085f749a24a36022b66b4fa6d7f7901b5e2eececc0c107dd0ee1969cb4fa09dd28212695f401f1b8f4e5e2c8cc6adf85671
7
- data.tar.gz: cf5da0f974244ea824bd8ffd5a3f41dd875eac858c213429918a1b0da1445b35a9a479ac8ed1131b33eaaab7ad609dd6cd6dc98852b2e29afff83cb389327acd
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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minisign
4
+ # Generate a Minisign secret and public key
5
+ class KeyPair
6
+ include Minisign::Utils
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")
12
+ def initialize(password = nil)
13
+ @password = password
14
+ @key_id = SecureRandom.bytes(8)
15
+ @signing_key = Ed25519::SigningKey.generate
16
+
17
+ @checksum = blake2b256("Ed#{key_data}")
18
+ @keynum_sk = "#{key_data}#{@checksum}"
19
+
20
+ @kdf_salt = SecureRandom.bytes(32)
21
+ @keynum_sk = xor(kdf_output, @keynum_sk.bytes).pack('C*') if @password
22
+ @kdf_algorithm = password.nil? ? [0, 0].pack('U*') : 'Sc'
23
+ end
24
+
25
+ # @return [Minisign::PrivateKey]
26
+ def private_key
27
+ @kdf_opslimit = kdf_opslimit_bytes.pack('C*')
28
+ @kdf_memlimit = kdf_memlimit_bytes.pack('C*')
29
+ data = "Ed#{@kdf_algorithm}B2#{@kdf_salt}#{@kdf_opslimit}#{@kdf_memlimit}#{@keynum_sk}"
30
+ Minisign::PrivateKey.new(
31
+ "untrusted comment: minisign secret key\n#{Base64.strict_encode64(data)}",
32
+ @password
33
+ )
34
+ end
35
+
36
+ # @return [Minisign::PublicKey]
37
+ def public_key
38
+ data = Base64.strict_encode64("Ed#{@key_id}#{@signing_key.verify_key.to_bytes}")
39
+ Minisign::PublicKey.new(data)
40
+ end
41
+
42
+ private
43
+
44
+ def kdf_output
45
+ derive_key(
46
+ @password,
47
+ @kdf_salt,
48
+ kdf_opslimit_bytes.pack('V*').unpack('N*').sum,
49
+ kdf_memlimit_bytes.pack('V*').unpack('N*').sum
50
+ )
51
+ end
52
+
53
+ def key_data
54
+ @key_data ||= "#{@key_id}#{@signing_key.to_bytes}#{@signing_key.verify_key.to_bytes}"
55
+ end
56
+
57
+ # 🤷
58
+ # https://github.com/RubyCrypto/rbnacl/blob/3e8d8f8822e2b8eeba215e6be027e8ee210edfb9/lib/rbnacl/password_hash/scrypt.rb#L33-L34
59
+ def kdf_opslimit_bytes
60
+ [0, 0, 0, 2, 0, 0, 0, 0]
61
+ end
62
+
63
+ def kdf_memlimit_bytes
64
+ [0, 0, 0, 64, 0, 0, 0, 0]
65
+ end
66
+ end
67
+ end
@@ -1,73 +1,114 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Minisign
4
- # Parse ed25519 signing key from minisign private key
4
+ # The private key used to create signatures
5
5
  class PrivateKey
6
6
  include Utils
7
- attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit,
8
- :key_id, :public_key, :secret_key, :checksum
9
-
10
- # rubocop:disable Metrics/AbcSize
11
- # rubocop:disable Layout/LineLength
7
+ attr_reader :key_id
12
8
 
13
9
  # Parse signing information from the minisign private key
14
10
  #
15
11
  # @param str [String] The minisign private key
12
+ # @param password [String] The password used to encrypt the private key
16
13
  # @example
17
- # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password')
14
+ # Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
18
15
  def initialize(str, password = nil)
19
- contents = str.split("\n")
20
- bytes = Base64.decode64(contents.last).bytes
21
- @signature_algorithm, @kdf_algorithm, @cksum_algorithm =
22
- [bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') }
23
- @kdf_salt = bytes[6..37]
24
- @kdf_opslimit = bytes[38..45].pack('V*').unpack('N*').sum
25
- @kdf_memlimit = bytes[46..53].pack('V*').unpack('N*').sum
26
- kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit)
27
- @key_id, @secret_key, @public_key, @checksum = xor(kdf_output, bytes[54..157])
16
+ comment, data = str.split("\n")
17
+ @password = password
18
+ decoded = Base64.decode64(data)
19
+ @untrusted_comment = comment.split('untrusted comment: ').last
20
+ @bytes = decoded.bytes
21
+ @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes)
22
+ @key_id, @ed25519_private_key_bytes, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157])
23
+ assert_valid_key!
24
+ end
25
+
26
+ # Get the corresponding public key from the private key
27
+ #
28
+ # @return [Minisign::PublicKey]
29
+ def public_key
30
+ data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}")
31
+ Minisign::PublicKey.new(data)
32
+ end
33
+
34
+ # Sign a file/message
35
+ #
36
+ # @param filename [String] The filename to be used in the trusted comment section
37
+ # @param message [String] The file's contents
38
+ # @param trusted_comment [String] An optional trusted comment to be included in the signature
39
+ # @param untrusted_comment [String] An optional untrusted comment
40
+ # @return [Minisign::Signature]
41
+ def sign(filename, message, trusted_comment = nil, untrusted_comment = nil)
42
+ signature = ed25519_signing_key.sign(blake2b512(message))
43
+ trusted_comment ||= "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
44
+ untrusted_comment ||= 'signature from minisign secret key'
45
+ global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
46
+ Minisign::Signature.new([
47
+ "untrusted comment: #{untrusted_comment}",
48
+ Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
49
+ "trusted comment: #{trusted_comment}",
50
+ "#{Base64.strict_encode64(global_signature)}\n"
51
+ ].join("\n"))
52
+ end
53
+
54
+ # @return [String] A string in the minisign.pub format
55
+ def to_s
56
+ kdf_salt = @kdf_salt.pack('C*')
57
+ kdf_opslimit = [@kdf_opslimit, 0].pack('L*')
58
+ kdf_memlimit = [@kdf_memlimit, 0].pack('L*')
59
+ keynum_sk = key_data(@password,
60
+ @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum).flatten
61
+ data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.pack('C*')}"
62
+ "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n"
63
+ end
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
28
71
  end
29
- # rubocop:enable Layout/LineLength
30
- # rubocop:enable Metrics/AbcSize
31
72
 
32
- # @return [String] the <kdf_output> used to xor the ed25519 keys
33
- def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
34
- RbNaCl::PasswordHash.scrypt(
35
- password,
36
- kdf_salt.pack('C*'),
37
- kdf_opslimit,
38
- kdf_memlimit,
39
- 104
40
- ).bytes
73
+ private
74
+
75
+ def signature_algorithm
76
+ @bytes[0..1].pack('U*')
77
+ end
78
+
79
+ def cksum_algorithm
80
+ @bytes[4..5].pack('U*')
81
+ end
82
+
83
+ def kdf_algorithm
84
+ @bytes[2..3].pack('U*')
41
85
  end
42
86
 
43
- # rubocop:disable Layout/LineLength
87
+ def scrypt_params(bytes)
88
+ [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum]
89
+ end
44
90
 
45
- # @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum
46
- def xor(kdf_output, contents)
47
- # rubocop:enable Layout/LineLength
48
- xored = kdf_output.each_with_index.map do |b, i|
49
- contents[i] ^ b
91
+ # @raise [RuntimeError] if the extracted public key does not match the derived public key
92
+ def assert_valid_key!
93
+ if kdf_algorithm.bytes.sum != 0 && @password.nil?
94
+ raise Minisign::PasswordMissingError, 'Missing password for encrypted key'
50
95
  end
51
- [xored[0..7], xored[8..39], xored[40..71], xored[72..103]]
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'
52
99
  end
53
100
 
54
- # @return [Ed25519::SigningKey] the ed25519 signing key
55
- def ed25519_signing_key
56
- Ed25519::SigningKey.new(@secret_key.pack('C*'))
101
+ def key_data(password, bytes)
102
+ if password
103
+ kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit)
104
+ bytes = xor(kdf_output, bytes)
105
+ end
106
+ [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]]
57
107
  end
58
108
 
59
- # @return [String] the signature in the .minisig format that can be written to a file.
60
- def sign(filename, message)
61
- signature = ed25519_signing_key.sign(blake2b512(message))
62
- trusted_comment = "timestamp:#{Time.now.to_i}\tfile:#{filename}\thashed"
63
- global_signature = ed25519_signing_key.sign("#{signature}#{trusted_comment}")
64
- [
65
- 'untrusted comment: <arbitrary text>',
66
- Base64.strict_encode64("ED#{@key_id.pack('C*')}#{signature}"),
67
- "trusted comment: #{trusted_comment}",
68
- Base64.strict_encode64(global_signature),
69
- ''
70
- ].join("\n")
109
+ # @return [Ed25519::SigningKey] the ed25519 signing key
110
+ def ed25519_signing_key
111
+ Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*'))
71
112
  end
72
113
  end
73
114
  end
@@ -1,50 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Minisign
4
- # Parse ed25519 verify key from minisign public key
4
+ # The public key used to verify signatures
5
5
  class PublicKey
6
6
  include Utils
7
- # Parse the ed25519 verify key from the minisign public key
7
+ # Read a minisign public key
8
8
  #
9
9
  # @param str [String] The minisign public key
10
10
  # @example
11
11
  # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM')
12
+ # # or from a file:
13
+ # Minisign::PublicKey.new(File.read('test/minisign.pub'))
12
14
  def initialize(str)
13
- @decoded = Base64.strict_decode64(str)
14
- @public_key = @decoded[10..]
15
- @verify_key = Ed25519::VerifyKey.new(@public_key)
15
+ @lines = str.split("\n")
16
+ @decoded = Base64.strict_decode64(@lines.last)
16
17
  end
17
18
 
18
19
  # @return [String] the key id
19
20
  # @example
20
- # Minisign::PublicKey.new('RWTg6JXWzv6GDtDphRQ/x7eg0LaWBcTxPZ7i49xEeiqXVcR+r79OZRWM').key_id
21
+ # public_key.key_id
21
22
  # #=> "E86FECED695E8E0"
22
23
  def key_id
23
- @decoded[2..9].bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
+ key_id_binary_string.bytes.map { |c| c.to_s(16) }.reverse.join.upcase
24
25
  end
25
26
 
26
27
  # Verify a message's signature
27
28
  #
28
- # @param sig [Minisign::Signature]
29
+ # @param signature [Minisign::Signature]
29
30
  # @param message [String] the content that was signed
30
31
  # @return [String] the trusted comment
31
32
  # @raise Ed25519::VerifyError on invalid signatures
32
33
  # @raise RuntimeError on tampered trusted comments
33
- def verify(sig, message)
34
- ensure_matching_key_ids(sig.key_id, key_id)
35
- @verify_key.verify(sig.signature, blake2b512(message))
36
- begin
37
- @verify_key.verify(sig.trusted_comment_signature, sig.signature + sig.trusted_comment)
38
- rescue Ed25519::VerifyError
39
- raise 'Comment signature verification failed'
40
- end
41
- "Signature and comment signature verified\nTrusted comment: #{sig.trusted_comment}"
34
+ # @raise RuntimeError on mismatching key ids
35
+ def verify(signature, message)
36
+ assert_matching_key_ids!(signature.key_id, key_id)
37
+ verify_message_signature(signature.signature, message)
38
+ verify_comment_signature(signature.trusted_comment_signature, signature.signature + signature.trusted_comment)
39
+ "Signature and comment signature verified\nTrusted comment: #{signature.trusted_comment}"
40
+ end
41
+
42
+ # @return [String] The public key that can be written to a file
43
+ def to_s
44
+ "untrusted comment: #{untrusted_comment}\n#{key_data}\n"
42
45
  end
43
46
 
44
47
  private
45
48
 
46
- def ensure_matching_key_ids(key_id1, key_id2)
47
- raise "Signature key id is #{key_id1}\nbut the key id in the public key is #{key_id2}" unless key_id1 == key_id2
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
+
61
+ def untrusted_comment
62
+ if @lines.length == 1
63
+ "minisign public key #{key_id}"
64
+ else
65
+ @lines.first.split('untrusted comment: ').last
66
+ end
67
+ end
68
+
69
+ def key_id_binary_string
70
+ @decoded[2..9]
71
+ end
72
+
73
+ def ed25519_public_key_binary_string
74
+ @decoded[10..]
75
+ end
76
+
77
+ def ed25519_verify_key
78
+ Ed25519::VerifyKey.new(ed25519_public_key_binary_string)
79
+ end
80
+
81
+ def key_data
82
+ Base64.strict_encode64("Ed#{key_id_binary_string}#{ed25519_public_key_binary_string}")
83
+ end
84
+
85
+ def assert_matching_key_ids!(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}"
48
90
  end
49
91
  end
50
92
  end
@@ -26,15 +26,21 @@ module Minisign
26
26
  @lines[2].split('trusted comment: ')[1]
27
27
  end
28
28
 
29
+ # @return [String] the signature for the trusted comment
29
30
  def trusted_comment_signature
30
31
  Base64.decode64(@lines[3])
31
32
  end
32
33
 
33
- # @return [String] the signature
34
+ # @return [String] the global signature
34
35
  def signature
35
36
  encoded_signature[10..]
36
37
  end
37
38
 
39
+ # @return [String] The signature that can be written to a file
40
+ def to_s
41
+ "#{@lines.join("\n")}\n"
42
+ end
43
+
38
44
  private
39
45
 
40
46
  def encoded_signature
@@ -3,8 +3,30 @@
3
3
  module Minisign
4
4
  # Helpers used in multiple classes
5
5
  module Utils
6
+ def blake2b256(message)
7
+ RbNaCl::Hash::Blake2b.digest(message, { digest_size: 32 })
8
+ end
9
+
6
10
  def blake2b512(message)
7
- OpenSSL::Digest.new('BLAKE2b512').digest(message)
11
+ RbNaCl::Hash::Blake2b.digest(message, { digest_size: 64 })
12
+ end
13
+
14
+ # @return [Array<32 bit unsigned ints>]
15
+ def xor(kdf_output, contents)
16
+ kdf_output.each_with_index.map do |b, i|
17
+ contents[i] ^ b
18
+ end
19
+ end
20
+
21
+ # @return [String] the <kdf_output> used to xor the ed25519 keys
22
+ def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit)
23
+ RbNaCl::PasswordHash.scrypt(
24
+ password,
25
+ kdf_salt,
26
+ kdf_opslimit,
27
+ kdf_memlimit,
28
+ 104
29
+ ).bytes
8
30
  end
9
31
  end
10
32
  end
data/lib/minisign.rb CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  require 'ed25519'
4
4
  require 'base64'
5
- require 'openssl'
6
5
  require 'rbnacl'
7
6
 
7
+ require 'minisign/cli'
8
8
  require 'minisign/utils'
9
9
  require 'minisign/public_key'
10
10
  require 'minisign/signature'
11
11
  require 'minisign/private_key'
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.0.8
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-03 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,16 +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
52
+ - lib/minisign/key_pair.rb
48
53
  - lib/minisign/private_key.rb
49
54
  - lib/minisign/public_key.rb
50
55
  - lib/minisign/signature.rb
51
56
  - lib/minisign/utils.rb
52
- homepage: https://rubygems.org/gems/minisign
57
+ homepage: https://github.com/jshawl/minisign
53
58
  licenses:
54
59
  - MIT
55
60
  metadata: