minisign 0.0.8 → 0.2.0

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