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 +4 -4
- data/bin/minisign +38 -0
- data/lib/minisign/cli.rb +159 -0
- data/lib/minisign/error.rb +12 -0
- data/lib/minisign/key_pair.rb +67 -0
- data/lib/minisign/private_key.rb +90 -49
- data/lib/minisign/public_key.rb +61 -19
- data/lib/minisign/signature.rb +7 -1
- data/lib/minisign/utils.rb +23 -1
- data/lib/minisign.rb +3 -1
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59267c0797e4539c136803dd3ac5333f14384019bb211abb50c078b14d9cb1c8
|
4
|
+
data.tar.gz: 7771c5a5b4227d1030d78b60fa584ed5158270a568617cc4ac7f4760230ef1d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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]
|
data/lib/minisign/cli.rb
ADDED
@@ -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,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
|
data/lib/minisign/private_key.rb
CHANGED
@@ -1,73 +1,114 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Minisign
|
4
|
-
#
|
4
|
+
# The private key used to create signatures
|
5
5
|
class PrivateKey
|
6
6
|
include Utils
|
7
|
-
attr_reader :
|
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(
|
14
|
+
# Minisign::PrivateKey.new(File.read("test/minisign.key"), 'password')
|
18
15
|
def initialize(str, password = nil)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@
|
24
|
-
@kdf_opslimit = bytes
|
25
|
-
@
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
# @
|
46
|
-
def
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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 [
|
60
|
-
def
|
61
|
-
|
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
|
data/lib/minisign/public_key.rb
CHANGED
@@ -1,50 +1,92 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Minisign
|
4
|
-
#
|
4
|
+
# The public key used to verify signatures
|
5
5
|
class PublicKey
|
6
6
|
include Utils
|
7
|
-
#
|
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
|
-
@
|
14
|
-
@
|
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
|
-
#
|
21
|
+
# public_key.key_id
|
21
22
|
# #=> "E86FECED695E8E0"
|
22
23
|
def key_id
|
23
|
-
|
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
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
47
|
-
|
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
|
data/lib/minisign/signature.rb
CHANGED
@@ -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
|
data/lib/minisign/utils.rb
CHANGED
@@ -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
|
-
|
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
|
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-
|
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://
|
57
|
+
homepage: https://github.com/jshawl/minisign
|
53
58
|
licenses:
|
54
59
|
- MIT
|
55
60
|
metadata:
|