-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
songev
executable file
·264 lines (214 loc) · 6.55 KB
/
songev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env ruby
# encoding: UTF-8
#
# Dead simple command line file signature verifier
# Project page: https://github.com/myxcel/songe#readme
#
# Created 01/04/2020
# Updated 22/07/2024
# Version 1.2.1
# Author Micaël P. (myxcel)
#
# Dependencies:
# - Ed25519, https://github.com/RubyCrypto/ed25519
# installation: $ sudo gem install ed25519
# - base32, https://github.com/stesla/base32
# installation: $ sudo gem install base32
# - digest-crc, https://github.com/postmodern/digest-crc
# installation: $ sudo gem install digest-crc
#
# License:
# Distributed under the MIT License (https://mit-license.org/)
#
# ========== dependencies ==========
require 'optparse'
require 'base64'
require 'yaml'
begin
# additional gems
require 'digest'
require 'base32'
require 'ed25519'
rescue LoadError => ex
puts "✖ #{ex.message.gsub("'"," ")}"
puts " songev requires the following gems: ed25519, base32, digest-crc"
exit 2
end
# ========== Songe global module ==========
module Songe
VERSION = '1.2.1'
DEBUG = false
# ========== constants declaration ==========
KEYTRUST = '.songe.trust'
SIGEXT = '.sgsig'
ENV_SHOME = 'SONGE_HOME'
ENV_HOME = 'HOME'
# ========== exceptions declaration ==========
class VerificationError < RuntimeError
end
# ========== Encoding class ==========
class Enc
public
def Enc::encode(str)
Base64::strict_encode64(str)
end
def Enc::decode(str)
begin
Base64::strict_decode64(str)
rescue => ex
raise RuntimeError, "Invalid data string format"
end
end
def Enc::encode_pkey(str)
encode_key_p((15 << 3).chr + str)
end
def Enc::encode_kkey(str)
encode_key_p((10 << 3).chr + str)
end
def Enc::decode_key(str)
begin
str = Base32::decode(str)
rescue => ex
raise RuntimeError, "Invalid key string format"
end
key = str[0...-2]
raise RuntimeError, "Invalid key string checksum" \
unless Digest::CRC16.digest(key) == str[-2, 2]
key[1..-1]
end
private
def Enc::encode_key_p(str)
str = str + Digest::CRC16.digest(str)
Base32::encode(str)
end
end # class Enc
# ========== Signing class ==========
class Verif
# ========== commands declaration ==========
private
#
# Returns the list of all trusted VerifyKey
#
def get_trusted_keys
return [] unless File.file?(@trust_path)
# no signature verification
File.open(@trust_path, 'r').readlines.map(&:chomp).uniq.sort
end
#
# Abbreviate a key to a form like PABCD EFGHI JKLMN (67 bits)
#
# param key_str [String] the base32 encoded key
def abbrev_key(key_str)
key_str[0, 5] + ' ' + key_str[5, 5] + ' ' +key_str[10, 5]
end
#
# Verify a signature for a file/data
#
# param file [String] the signed file name (without sign extension)
def verify(file)
# Read YAML file and comment
data = YAML.load(File.open(file + SIGEXT))
raise KeyfileError, "Invalid file #{file + SIGEXT} content" unless data && data.is_a?(Hash)
puts "Signature in file: #{file + SIGEXT}" if @options[:verbose]
clearsg = !File.file?(file)
# Hash data
start = Time.now
sha = if clearsg
raise VerificationError, "File #{file} does not exist, no signed data" if data[:data].nil?
Digest::SHA512.new
else
puts "⚠ Warning: clear sign data found but ignored" if data.has_key?(:data)
Digest::SHA512.file(file)
end
sha << "\0x00" + data[:comment] unless data[:comment].nil?
sha << "\0x00" + data[:datetime].to_s
sha << "\0x00" + data[:data] if clearsg
# Control data
ctn = Enc.decode(data[:signature])
pub = Ed25519::VerifyKey.new Enc.decode_key(data[:verifykey])
puts "Signed with key: #{abbrev_key(data[:verifykey])}" if @options[:verbose]
t = get_trusted_keys.include?(data[:verifykey]) ? "with trusted key" :
"but the verify key is ✖ not trusted\n (#{data[:verifykey]})"
pub.verify(ctn, sha.digest) || raise(VerificationError, "BAD signature")
if clearsg
$stderr.puts "✔ Good signature #{t}\n signed on #{Time.at(data[:datetime])}"
$stdout.print data[:data]
$stderr.puts "Comment: #{data[:comment]}\n" unless data[:comment].nil?
else
puts "✔ Good signature #{t}\n signed on #{Time.at(data[:datetime])}"
puts "(⏲ #{'%.2f' % ((Time.now - start) * 1000)} ms)" if @options[:verbose]
puts "Comment: #{data[:comment]}\n" unless data[:comment].nil?
end
end
# ========== reading arguments ==========
public
#
# Constructor
#
def initialize
@options = {}
# Search for key file in .|SONGE_HOME|HOME
if File.file?(KEYTRUST)
@trust_path = KEYTRUST
elsif !(path = ENV[ENV_SHOME]).nil? && Dir.exist?(ENV[ENV_SHOME]) ||
!(path = ENV[ENV_HOME]).nil? && File.file?(File.join(path, KEYTRUST))
# If found dir SONGE_HOME, no need to check if key file exists
@trust_path = File.join(path, KEYTRUST)
else
@trust_path = KEYTRUST
end
end
#
# Main entry: switch options
#
def main
cmd_given = false
OptionParser.new do |opts|
opts.banner = "
Songe | dead simple signing file utility
MIT License (https://mit-license.org/)
Songe is a naive file signer, adapted to sign files in a per-project context: each project
directory can have its own keys to directly sign project files. Songev version uses Ed25519
(github.com/RubyCrypto/ed25519).
Verify and signing use thus Ed25519 32-bytes keys. The verify key (starting with 'P') is
joined to all signatures (so to verify a file, you just need the file and the signature
`.sgsig` file).
Trusted recipients' verify keys may be added to a trusted keys list saved to `.songe.trust`
to add confidence when verifiyng files signed by them. The trusted keystore's signature is
not checked, so it is up to the user to check the recipient's key.
Usage:
$ songev [options]
Examples:
Verify a downloaded file (assume the signature is `./myfile.txt.sgsig`)
$ songe --verify myfile.txt
Options/commands:"
# options
opts.on("-b", "--[no-]verbose", "Run verbosely") do |v|
@options[:verbose] = v
end
# misc commands
opts.on("-h", "--help", "Print this help") do
puts opts
exit
end
opts.on("-n", "--version", "Print version info") do
puts "🎖 songe\n dead simple signing file utility"
puts " version #{VERSION}\n Micaël P. (myxcel)"
exit
end
end.parse!
raise ArgumentError, 'One command is required (see help)' unless ARGV.size > 0
verify ARGV[0]
end
end # class Verif
end # module
# Run as main file
if __FILE__ == $0
begin
Songe::Verif.new.main
rescue => ex
$stderr.puts "✖ #{ex.message.gsub("'"," ")}"
$stderr.puts "Backtrace (most recent first):\n " + ex.backtrace.join("\n ") if Songe::DEBUG
exit 1
end
end