You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

keypair.lua 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. local argparse = require "argparse"
  14. local rspamd_keypair = require "rspamd_cryptobox_keypair"
  15. local rspamd_pubkey = require "rspamd_cryptobox_pubkey"
  16. local rspamd_signature = require "rspamd_cryptobox_signature"
  17. local rspamd_crypto = require "rspamd_cryptobox"
  18. local rspamd_util = require "rspamd_util"
  19. local ucl = require "ucl"
  20. local logger = require "rspamd_logger"
  21. -- Define command line options
  22. local parser = argparse()
  23. :name "rspamadm keypair"
  24. :description "Manages keypairs for Rspamd"
  25. :help_description_margin(30)
  26. :command_target("command")
  27. :require_command(false)
  28. -- Generate subcommand
  29. local generate = parser:command "generate gen g"
  30. :description "Creates a new keypair"
  31. generate:flag "-s --sign"
  32. :description "Generates a sign keypair instead of the encryption one"
  33. generate:flag "-n --nist"
  34. :description "Uses nist encryption algorithm"
  35. generate:option "-o --output"
  36. :description "Write keypair to file"
  37. :argname "<file>"
  38. generate:mutex(
  39. generate:flag "-j --json"
  40. :description "Output JSON instead of UCL",
  41. generate:flag "-u --ucl"
  42. :description "Output UCL"
  43. :default(true)
  44. )
  45. -- Sign subcommand
  46. local sign = parser:command "sign sig s"
  47. :description "Signs a file using keypair"
  48. sign:option "-k --keypair"
  49. :description "Keypair to use"
  50. :argname "<file>"
  51. sign:option "-s --suffix"
  52. :description "Suffix for signature"
  53. :argname "<suffix>"
  54. :default("sig")
  55. sign:argument "file"
  56. :description "File to sign"
  57. :argname "<file>"
  58. :args "*"
  59. -- Verify subcommand
  60. local verify = parser:command "verify ver v"
  61. :description "Verifies a file using keypair or a public key"
  62. verify:mutex(
  63. verify:option "-p --pubkey"
  64. :description "Load pubkey from the specified file"
  65. :argname "<file>",
  66. verify:option "-P --pubstring"
  67. :description "Load pubkey from the base32 encoded string"
  68. :argname "<base32>",
  69. verify:option "-k --keypair"
  70. :description "Get pubkey from the keypair file"
  71. :argname "<file>"
  72. )
  73. verify:argument "file"
  74. :description "File to verify"
  75. :argname "<file>"
  76. :args "*"
  77. verify:flag "-n --nist"
  78. :description "Uses nistp curves (P256)"
  79. verify:option "-s --suffix"
  80. :description "Suffix for signature"
  81. :argname "<suffix>"
  82. :default("sig")
  83. -- Encrypt subcommand
  84. local encrypt = parser:command "encrypt crypt enc e"
  85. :description "Encrypts a file using keypair (or a pubkey)"
  86. encrypt:mutex(
  87. encrypt:option "-p --pubkey"
  88. :description "Load pubkey from the specified file"
  89. :argname "<file>",
  90. encrypt:option "-P --pubstring"
  91. :description "Load pubkey from the base32 encoded string"
  92. :argname "<base32>",
  93. encrypt:option "-k --keypair"
  94. :description "Get pubkey from the keypair file"
  95. :argname "<file>"
  96. )
  97. encrypt:option "-s --suffix"
  98. :description "Suffix for encrypted file"
  99. :argname "<suffix>"
  100. :default("enc")
  101. encrypt:argument "file"
  102. :description "File to encrypt"
  103. :argname "<file>"
  104. :args "*"
  105. encrypt:flag "-r --rm"
  106. :description "Remove unencrypted file"
  107. encrypt:flag "-f --force"
  108. :description "Remove destination file if it exists"
  109. -- Decrypt subcommand
  110. local decrypt = parser:command "decrypt dec d"
  111. :description "Decrypts a file using keypair"
  112. decrypt:option "-k --keypair"
  113. :description "Get pubkey from the keypair file"
  114. :argname "<file>"
  115. decrypt:flag "-S --keep-suffix"
  116. :description "Preserve suffix for decrypted file (overwrite encrypted)"
  117. decrypt:argument "file"
  118. :description "File to encrypt"
  119. :argname "<file>"
  120. :args "*"
  121. decrypt:flag "-f --force"
  122. :description "Remove destination file if it exists (implied with -S)"
  123. decrypt:flag "-r --rm"
  124. :description "Remove encrypted file"
  125. -- Default command is generate, so duplicate options to be compatible
  126. parser:flag "-s --sign"
  127. :description "Generates a sign keypair instead of the encryption one"
  128. parser:flag "-n --nist"
  129. :description "Uses nistp curves (P256)"
  130. parser:mutex(
  131. parser:flag "-j --json"
  132. :description "Output JSON instead of UCL",
  133. parser:flag "-u --ucl"
  134. :description "Output UCL"
  135. :default(true)
  136. )
  137. parser:option "-o --output"
  138. :description "Write keypair to file"
  139. :argname "<file>"
  140. local function fatal(...)
  141. logger.errx(...)
  142. os.exit(1)
  143. end
  144. local function ask_yes_no(greet, default)
  145. local def_str
  146. if default then
  147. greet = greet .. "[Y/n]: "
  148. def_str = "yes"
  149. else
  150. greet = greet .. "[y/N]: "
  151. def_str = "no"
  152. end
  153. local reply = rspamd_util.readline(greet)
  154. if not reply then os.exit(0) end
  155. if #reply == 0 then reply = def_str end
  156. reply = reply:lower()
  157. if reply == 'y' or reply == 'yes' then return true end
  158. return false
  159. end
  160. local function generate_handler(opts)
  161. local mode = 'encryption'
  162. if opts.sign then
  163. mode = 'sign'
  164. end
  165. local alg = 'curve25519'
  166. if opts.nist then
  167. alg = 'nist'
  168. end
  169. -- TODO: probably, do it in a more safe way
  170. local kp = rspamd_keypair.create(mode, alg):totable()
  171. local format = 'ucl'
  172. if opts.json then
  173. format = 'json'
  174. end
  175. if opts.output then
  176. local out = io.open(opts.output, 'w')
  177. if not out then
  178. fatal('cannot open output to write: ' .. opts.output)
  179. end
  180. out:write(ucl.to_format(kp, format))
  181. out:close()
  182. else
  183. io.write(ucl.to_format(kp, format))
  184. end
  185. end
  186. local function sign_handler(opts)
  187. if opts.file then
  188. if type(opts.file) == 'string' then
  189. opts.file = {opts.file}
  190. end
  191. else
  192. parser:error('no files to sign')
  193. end
  194. if not opts.keypair then
  195. parser:error("no keypair specified")
  196. end
  197. local ucl_parser = ucl.parser()
  198. local res,err = ucl_parser:parse_file(opts.keypair)
  199. if not res then
  200. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  201. end
  202. local kp = rspamd_keypair.load(ucl_parser:get_object())
  203. if not kp then
  204. fatal("cannot load keypair: " .. opts.keypair)
  205. end
  206. for _,fname in ipairs(opts.file) do
  207. local sig = rspamd_crypto.sign_file(kp, fname)
  208. if not sig then
  209. fatal(string.format("cannot sign %s\n", fname))
  210. end
  211. local out = string.format('%s.%s', fname, opts.suffix or 'sig')
  212. local of = io.open(out, 'w')
  213. if not of then
  214. fatal('cannot open output to write: ' .. out)
  215. end
  216. of:write(sig:bin())
  217. of:close()
  218. io.write(string.format('signed %s -> %s (%s)\n', fname, out, sig:hex()))
  219. end
  220. end
  221. local function verify_handler(opts)
  222. if opts.file then
  223. if type(opts.file) == 'string' then
  224. opts.file = {opts.file}
  225. end
  226. else
  227. parser:error('no files to verify')
  228. end
  229. local pk
  230. local alg = 'curve25519'
  231. if opts.keypair then
  232. local ucl_parser = ucl.parser()
  233. local res,err = ucl_parser:parse_file(opts.keypair)
  234. if not res then
  235. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  236. end
  237. local kp = rspamd_keypair.load(ucl_parser:get_object())
  238. if not kp then
  239. fatal("cannot load keypair: " .. opts.keypair)
  240. end
  241. pk = kp:pk()
  242. alg = kp:alg()
  243. elseif opts.pubkey then
  244. if opts.nist then alg = 'nist' end
  245. pk = rspamd_pubkey.load(opts.pubkey, 'sign', alg)
  246. elseif opts.pubstr then
  247. if opts.nist then alg = 'nist' end
  248. pk = rspamd_pubkey.create(opts.pubstr, 'sign', alg)
  249. end
  250. if not pk then
  251. fatal("cannot create pubkey")
  252. end
  253. local valid = true
  254. for _,fname in ipairs(opts.file) do
  255. local sig_fname = string.format('%s.%s', fname, opts.suffix or 'sig')
  256. local sig = rspamd_signature.load(sig_fname, alg)
  257. if not sig then
  258. fatal(string.format("cannot load signature for %s -> %s",
  259. fname, sig_fname))
  260. end
  261. if rspamd_crypto.verify_file(pk, sig, fname, alg) then
  262. io.write(string.format('verified %s -> %s (%s)\n', fname, sig_fname, sig:hex()))
  263. else
  264. valid = false
  265. io.write(string.format('FAILED to verify %s -> %s (%s)\n', fname,
  266. sig_fname, sig:hex()))
  267. end
  268. end
  269. if not valid then
  270. os.exit(1)
  271. end
  272. end
  273. local function encrypt_handler(opts)
  274. if opts.file then
  275. if type(opts.file) == 'string' then
  276. opts.file = {opts.file}
  277. end
  278. else
  279. parser:error('no files to sign')
  280. end
  281. local pk
  282. local alg = 'curve25519'
  283. if opts.keypair then
  284. local ucl_parser = ucl.parser()
  285. local res,err = ucl_parser:parse_file(opts.keypair)
  286. if not res then
  287. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  288. end
  289. local kp = rspamd_keypair.load(ucl_parser:get_object())
  290. if not kp then
  291. fatal("cannot load keypair: " .. opts.keypair)
  292. end
  293. pk = kp:pk()
  294. alg = kp:alg()
  295. elseif opts.pubkey then
  296. if opts.nist then alg = 'nist' end
  297. pk = rspamd_pubkey.load(opts.pubkey, 'sign', alg)
  298. elseif opts.pubstr then
  299. if opts.nist then alg = 'nist' end
  300. pk = rspamd_pubkey.create(opts.pubstr, 'sign', alg)
  301. end
  302. if not pk then
  303. fatal("cannot load keypair: " .. opts.keypair)
  304. end
  305. for _,fname in ipairs(opts.file) do
  306. local enc = rspamd_crypto.encrypt_file(pk, fname, alg)
  307. if not enc then
  308. fatal(string.format("cannot encrypt %s\n", fname))
  309. end
  310. local out
  311. if opts.suffix and #opts.suffix > 0 then
  312. out = string.format('%s.%s', fname, opts.suffix)
  313. else
  314. out = string.format('%s', fname)
  315. end
  316. if rspamd_util.file_exists(out) then
  317. if opts.force or ask_yes_no(string.format('File %s already exists, overwrite?',
  318. out), true) then
  319. os.remove(out)
  320. else
  321. os.exit(1)
  322. end
  323. end
  324. enc:save_in_file(out)
  325. if opts.rm then
  326. os.remove(fname)
  327. io.write(string.format('encrypted %s (deleted) -> %s\n', fname, out))
  328. else
  329. io.write(string.format('encrypted %s -> %s\n', fname, out))
  330. end
  331. end
  332. end
  333. local function decrypt_handler(opts)
  334. if opts.file then
  335. if type(opts.file) == 'string' then
  336. opts.file = {opts.file}
  337. end
  338. else
  339. parser:error('no files to decrypt')
  340. end
  341. if not opts.keypair then
  342. parser:error("no keypair specified")
  343. end
  344. local ucl_parser = ucl.parser()
  345. local res,err = ucl_parser:parse_file(opts.keypair)
  346. if not res then
  347. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  348. end
  349. local kp = rspamd_keypair.load(ucl_parser:get_object())
  350. if not kp then
  351. fatal("cannot load keypair: " .. opts.keypair)
  352. end
  353. for _,fname in ipairs(opts.file) do
  354. local decrypted = rspamd_crypto.decrypt_file(kp, fname)
  355. if not decrypted then
  356. fatal(string.format("cannot decrypt %s\n", fname))
  357. end
  358. local out
  359. if not opts['keep-suffix'] then
  360. -- Strip the last suffix
  361. out = fname:match("^(.+)%..+$")
  362. else
  363. out = fname
  364. end
  365. local removed = false
  366. if rspamd_util.file_exists(out) then
  367. if (opts.force or opts['keep-suffix'])
  368. or ask_yes_no(string.format('File %s already exists, overwrite?', out), true) then
  369. os.remove(out)
  370. removed = true
  371. else
  372. os.exit(1)
  373. end
  374. end
  375. if opts.rm then
  376. os.remove(fname)
  377. removed = true
  378. end
  379. if removed then
  380. io.write(string.format('decrypted %s (removed) -> %s\n', fname, out))
  381. else
  382. io.write(string.format('decrypted %s -> %s\n', fname, out))
  383. end
  384. end
  385. end
  386. local function handler(args)
  387. local opts = parser:parse(args)
  388. local command = opts.command or "generate"
  389. if command == 'generate' then
  390. generate_handler(opts)
  391. elseif command == 'sign' then
  392. sign_handler(opts)
  393. elseif command == 'verify' then
  394. verify_handler(opts)
  395. elseif command == 'encrypt' then
  396. encrypt_handler(opts)
  397. elseif command == 'decrypt' then
  398. decrypt_handler(opts)
  399. else
  400. parser:error('command %s is not implemented', command)
  401. end
  402. end
  403. return {
  404. name = 'keypair',
  405. aliases = {'kp', 'key'},
  406. handler = handler,
  407. description = parser._description
  408. }