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.

signtool.c 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. /*-
  2. * Copyright 2016 Vsevolod Stakhov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #include "config.h"
  17. #include "rspamadm.h"
  18. #include "cryptobox.h"
  19. #include "printf.h"
  20. #include "ucl.h"
  21. #include "libcryptobox/keypair.h"
  22. #include "libutil/str_util.h"
  23. #include "libutil/util.h"
  24. #include "unix-std.h"
  25. #ifdef HAVE_SYS_WAIT_H
  26. #include <sys/wait.h>
  27. #endif
  28. static gboolean openssl = FALSE;
  29. static gboolean verify = FALSE;
  30. static gboolean quiet = FALSE;
  31. static char *suffix = NULL;
  32. static char *pubkey_file = NULL;
  33. static char *pubkey = NULL;
  34. static char *pubout = NULL;
  35. static char *keypair_file = NULL;
  36. static char *editor = NULL;
  37. static gboolean edit = FALSE;
  38. enum rspamd_cryptobox_mode mode = RSPAMD_CRYPTOBOX_MODE_25519;
  39. static void rspamadm_signtool(int argc, char **argv,
  40. const struct rspamadm_command *cmd);
  41. static const char *rspamadm_signtool_help(gboolean full_help,
  42. const struct rspamadm_command *cmd);
  43. struct rspamadm_command signtool_command = {
  44. .name = "signtool",
  45. .flags = 0,
  46. .help = rspamadm_signtool_help,
  47. .run = rspamadm_signtool,
  48. .lua_subrs = NULL,
  49. };
  50. static GOptionEntry entries[] = {
  51. {"openssl", 'o', 0, G_OPTION_ARG_NONE, &openssl,
  52. "Generate openssl nistp256 keypair not curve25519 one", NULL},
  53. {"verify", 'v', 0, G_OPTION_ARG_NONE, &verify,
  54. "Verify signatures and not sign", NULL},
  55. {"suffix", 'S', 0, G_OPTION_ARG_STRING, &suffix,
  56. "Save signatures in file<suffix> files", NULL},
  57. {"pubkey", 'p', 0, G_OPTION_ARG_STRING, &pubkey,
  58. "Base32 encoded pubkey to verify", NULL},
  59. {"pubout", '\0', 0, G_OPTION_ARG_FILENAME, &pubout,
  60. "Output public key to the specified file", NULL},
  61. {"pubfile", 'P', 0, G_OPTION_ARG_FILENAME, &pubkey_file,
  62. "Load base32 encoded pubkey to verify from the file", NULL},
  63. {"keypair", 'k', 0, G_OPTION_ARG_STRING, &keypair_file,
  64. "UCL with keypair to load for signing", NULL},
  65. {"quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet,
  66. "Be quiet", NULL},
  67. {"edit", 'e', 0, G_OPTION_ARG_NONE, &edit,
  68. "Run editor and sign the edited file", NULL},
  69. {"editor", '\0', 0, G_OPTION_ARG_STRING, &editor,
  70. "Use the specified editor instead of $EDITOR environment var", NULL},
  71. {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
  72. static const char *
  73. rspamadm_signtool_help(gboolean full_help,
  74. const struct rspamadm_command *cmd)
  75. {
  76. const char *help_str;
  77. if (full_help) {
  78. help_str = "Manage digital signatures\n\n"
  79. "Usage: rspamadm signtool [-o] -k <keypair_file> [-v -p <pubkey> | -P <pubkey_file>] [-S <suffix>] file1 ...\n"
  80. "Where options are:\n\n"
  81. "-v: verify against pubkey instead of \n"
  82. "-o: use ECDSA instead of EdDSA\n"
  83. "-p: load pubkey as base32 string\n"
  84. "-P: load pubkey paced in file\n"
  85. "-k: load signing keypair from ucl file\n"
  86. "-S: append suffix for signatures and store them in files\n"
  87. "-q: be quiet\n"
  88. "-e: opens file for editing and sign the result\n"
  89. "--editor: use the specified editor instead of $EDITOR environment var\n"
  90. "--help: shows available options and commands";
  91. }
  92. else {
  93. help_str = "Sign and verify files tool";
  94. }
  95. return help_str;
  96. }
  97. static int
  98. rspamadm_edit_file(const char *fname)
  99. {
  100. char tmppath[PATH_MAX], run_cmdline[PATH_MAX];
  101. unsigned char *map;
  102. gsize len = 0;
  103. int fd_out, retcode, child_argc;
  104. GPid child_pid;
  105. char *tmpdir, **child_argv = NULL;
  106. struct stat st;
  107. GError *err = NULL;
  108. if (editor == NULL) {
  109. editor = getenv("EDITOR");
  110. }
  111. if (editor == NULL) {
  112. rspamd_fprintf(stderr, "cannot find editor: specify $EDITOR "
  113. "environment variable or pass --editor argument\n");
  114. exit(EXIT_FAILURE);
  115. }
  116. tmpdir = getenv("TMPDIR");
  117. if (tmpdir == NULL) {
  118. tmpdir = "/tmp";
  119. }
  120. if (stat(fname, &st) == -1 || st.st_size == 0) {
  121. /* The source does not exist, but that shouldn't be a problem */
  122. len = 0;
  123. map = NULL;
  124. /* Try to touch source anyway */
  125. fd_out = rspamd_file_xopen(fname, O_WRONLY | O_CREAT | O_EXCL, 00644,
  126. 0);
  127. if (fd_out == -1) {
  128. rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
  129. strerror(errno));
  130. exit(EXIT_FAILURE);
  131. }
  132. close(fd_out);
  133. }
  134. else {
  135. map = rspamd_file_xmap(fname, PROT_READ, &len, TRUE);
  136. if (map == NULL) {
  137. rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
  138. strerror(errno));
  139. exit(EXIT_FAILURE);
  140. }
  141. }
  142. rspamd_snprintf(tmppath, sizeof(tmppath),
  143. "%s/rspamd_sign-XXXXXXXXXX", tmpdir);
  144. mode_t cur_umask = umask(S_IRWXO | S_IRWXG);
  145. fd_out = mkstemp(tmppath);
  146. (void) umask(cur_umask);
  147. if (fd_out == -1) {
  148. rspamd_fprintf(stderr, "cannot open tempfile %s: %s\n", tmppath,
  149. strerror(errno));
  150. exit(EXIT_FAILURE);
  151. }
  152. if (len > 0 && write(fd_out, map, len) == -1) {
  153. rspamd_fprintf(stderr, "cannot write to tempfile %s: %s\n", tmppath,
  154. strerror(errno));
  155. unlink(tmppath);
  156. munmap(map, len);
  157. close(fd_out);
  158. exit(EXIT_FAILURE);
  159. }
  160. if (len > 0) {
  161. munmap(map, len);
  162. }
  163. fsync(fd_out);
  164. close(fd_out);
  165. /* Now we spawn editor with the filename as argument */
  166. rspamd_snprintf(run_cmdline, sizeof(run_cmdline), "%s %s", editor, tmppath);
  167. if (!g_shell_parse_argv(run_cmdline, &child_argc,
  168. &child_argv, &err)) {
  169. rspamd_fprintf(stderr, "cannot exec %s: %e\n", editor,
  170. err);
  171. unlink(tmppath);
  172. exit(EXIT_FAILURE);
  173. }
  174. if (!g_spawn_async(NULL, child_argv, NULL,
  175. G_SPAWN_CHILD_INHERITS_STDIN | G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
  176. NULL, NULL, &child_pid, &err)) {
  177. rspamd_fprintf(stderr, "cannot exec %s: %e\n", editor,
  178. err);
  179. unlink(tmppath);
  180. exit(EXIT_FAILURE);
  181. }
  182. g_strfreev(child_argv);
  183. for (;;) {
  184. if (waitpid((pid_t) child_pid, &retcode, 0) != -1) {
  185. break;
  186. }
  187. if (errno != EINTR) {
  188. rspamd_fprintf(stderr, "failed to wait for %s: %s\n", editor,
  189. strerror(errno));
  190. unlink(tmppath);
  191. exit(EXIT_FAILURE);
  192. }
  193. }
  194. #if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 34
  195. #if GLIB_MINOR_VERSION >= 70
  196. if (!g_spawn_check_wait_status(retcode, &err)) {
  197. #else
  198. if (!g_spawn_check_exit_status(retcode, &err)) {
  199. #endif
  200. unlink(tmppath);
  201. rspamd_fprintf(stderr, "%s returned error code: %d - %e\n", editor,
  202. retcode, err);
  203. exit(EXIT_FAILURE);
  204. }
  205. #else
  206. if (retcode != 0) {
  207. unlink(tmppath);
  208. rspamd_fprintf(stderr, "%s returned error code: %d\n", editor,
  209. retcode);
  210. exit(retcode);
  211. }
  212. #endif
  213. map = rspamd_file_xmap(tmppath, PROT_READ, &len, TRUE);
  214. if (map == NULL) {
  215. rspamd_fprintf(stderr, "cannot map %s: %s\n", tmppath,
  216. strerror(errno));
  217. unlink(tmppath);
  218. exit(EXIT_FAILURE);
  219. }
  220. rspamd_snprintf(run_cmdline, sizeof(run_cmdline), "%s.new", fname);
  221. fd_out = rspamd_file_xopen(run_cmdline, O_RDWR | O_CREAT | O_TRUNC, 00600,
  222. 0);
  223. if (fd_out == -1) {
  224. rspamd_fprintf(stderr, "cannot open new file %s: %s\n", run_cmdline,
  225. strerror(errno));
  226. unlink(tmppath);
  227. munmap(map, len);
  228. exit(EXIT_FAILURE);
  229. }
  230. if (write(fd_out, map, len) == -1) {
  231. rspamd_fprintf(stderr, "cannot write new file %s: %s\n", run_cmdline,
  232. strerror(errno));
  233. unlink(tmppath);
  234. unlink(run_cmdline);
  235. close(fd_out);
  236. munmap(map, len);
  237. exit(EXIT_FAILURE);
  238. }
  239. unlink(tmppath);
  240. (void) lseek(fd_out, 0, SEEK_SET);
  241. munmap(map, len);
  242. return fd_out;
  243. }
  244. static bool
  245. rspamadm_sign_file(const char *fname, struct rspamd_cryptobox_keypair *kp)
  246. {
  247. int fd_sig, fd_input;
  248. unsigned char sig[rspamd_cryptobox_MAX_SIGBYTES], *map;
  249. char sigpath[PATH_MAX];
  250. FILE *pub_fp;
  251. struct stat st;
  252. const unsigned char *sk;
  253. if (suffix == NULL) {
  254. suffix = ".sig";
  255. }
  256. if (edit) {
  257. /* We need to open editor and then sign the temporary file */
  258. fd_input = rspamadm_edit_file(fname);
  259. }
  260. else {
  261. fd_input = rspamd_file_xopen(fname, O_RDONLY, 0, TRUE);
  262. }
  263. if (fd_input == -1) {
  264. rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
  265. strerror(errno));
  266. exit(EXIT_FAILURE);
  267. }
  268. g_assert(fstat(fd_input, &st) != -1);
  269. rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
  270. fd_sig = rspamd_file_xopen(sigpath, O_WRONLY | O_CREAT | O_TRUNC, 00644, 0);
  271. if (fd_sig == -1) {
  272. close(fd_input);
  273. rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
  274. strerror(errno));
  275. exit(EXIT_FAILURE);
  276. }
  277. map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd_input, 0);
  278. close(fd_input);
  279. if (map == MAP_FAILED) {
  280. close(fd_sig);
  281. rspamd_fprintf(stderr, "cannot map %s: %s\n", fname,
  282. strerror(errno));
  283. exit(EXIT_FAILURE);
  284. }
  285. g_assert(rspamd_cryptobox_MAX_SIGBYTES >=
  286. rspamd_cryptobox_signature_bytes(mode));
  287. sk = rspamd_keypair_component(kp, RSPAMD_KEYPAIR_COMPONENT_SK, NULL);
  288. rspamd_cryptobox_sign(sig, NULL, map, st.st_size, sk, mode);
  289. if (edit) {
  290. /* We also need to rename .new file */
  291. rspamd_snprintf(sigpath, sizeof(sigpath), "%s.new", fname);
  292. if (rename(sigpath, fname) == -1) {
  293. rspamd_fprintf(stderr, "cannot rename %s to %s: %s\n", sigpath, fname,
  294. strerror(errno));
  295. exit(EXIT_FAILURE);
  296. }
  297. unlink(sigpath);
  298. }
  299. rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
  300. if (write(fd_sig, sig, rspamd_cryptobox_signature_bytes(mode)) == -1) {
  301. rspamd_fprintf(stderr, "cannot write signature to %s: %s\n", sigpath,
  302. strerror(errno));
  303. exit(EXIT_FAILURE);
  304. }
  305. close(fd_sig);
  306. munmap(map, st.st_size);
  307. if (!quiet) {
  308. rspamd_fprintf(stdout, "signed %s; stored hash in %s\n",
  309. fname, sigpath);
  310. }
  311. if (pubout) {
  312. GString *b32_pk;
  313. pub_fp = fopen(pubout, "w");
  314. if (pub_fp == NULL) {
  315. rspamd_fprintf(stderr, "cannot write pubkey to %s: %s",
  316. pubout, strerror(errno));
  317. }
  318. else {
  319. b32_pk = rspamd_keypair_print(kp,
  320. RSPAMD_KEYPAIR_PUBKEY | RSPAMD_KEYPAIR_BASE32);
  321. if (b32_pk) {
  322. rspamd_fprintf(pub_fp, "%v", b32_pk);
  323. g_string_free(b32_pk, TRUE);
  324. }
  325. fclose(pub_fp);
  326. }
  327. if (!quiet) {
  328. rspamd_fprintf(stdout, "stored pubkey in %s\n",
  329. pubout);
  330. }
  331. }
  332. return true;
  333. }
  334. static bool
  335. rspamadm_verify_file(const char *fname, const unsigned char *pk)
  336. {
  337. int fd_sig, fd_input;
  338. unsigned char *map, *map_sig;
  339. char sigpath[PATH_MAX];
  340. struct stat st, st_sig;
  341. bool ret;
  342. g_assert(rspamd_cryptobox_MAX_SIGBYTES >=
  343. rspamd_cryptobox_signature_bytes(mode));
  344. if (suffix == NULL) {
  345. suffix = ".sig";
  346. }
  347. fd_input = rspamd_file_xopen(fname, O_RDONLY, 0, TRUE);
  348. if (fd_input == -1) {
  349. rspamd_fprintf(stderr, "cannot open %s: %s\n", fname,
  350. strerror(errno));
  351. exit(EXIT_FAILURE);
  352. }
  353. g_assert(fstat(fd_input, &st) != -1);
  354. rspamd_snprintf(sigpath, sizeof(sigpath), "%s%s", fname, suffix);
  355. fd_sig = rspamd_file_xopen(sigpath, O_RDONLY, 0, TRUE);
  356. if (fd_sig == -1) {
  357. close(fd_input);
  358. rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
  359. strerror(errno));
  360. exit(EXIT_FAILURE);
  361. }
  362. map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd_input, 0);
  363. close(fd_input);
  364. if (map == MAP_FAILED) {
  365. close(fd_sig);
  366. rspamd_fprintf(stderr, "cannot open %s: %s\n", sigpath,
  367. strerror(errno));
  368. exit(EXIT_FAILURE);
  369. }
  370. g_assert(fstat(fd_sig, &st_sig) != -1);
  371. if (st_sig.st_size != rspamd_cryptobox_signature_bytes(mode)) {
  372. close(fd_sig);
  373. rspamd_fprintf(stderr, "invalid signature size %s: %ud\n", fname,
  374. (unsigned int) st_sig.st_size);
  375. munmap(map, st.st_size);
  376. exit(EXIT_FAILURE);
  377. }
  378. map_sig = mmap(NULL, st_sig.st_size, PROT_READ, MAP_SHARED, fd_sig, 0);
  379. close(fd_sig);
  380. if (map_sig == MAP_FAILED) {
  381. munmap(map, st.st_size);
  382. rspamd_fprintf(stderr, "cannot map %s: %s\n", sigpath,
  383. strerror(errno));
  384. exit(EXIT_FAILURE);
  385. }
  386. ret = rspamd_cryptobox_verify(map_sig, st_sig.st_size,
  387. map, st.st_size, pk, mode);
  388. munmap(map, st.st_size);
  389. munmap(map_sig, st_sig.st_size);
  390. if (!ret) {
  391. rspamd_fprintf(stderr, "cannot verify %s using %s: invalid signature\n",
  392. fname, sigpath);
  393. }
  394. else if (!quiet) {
  395. rspamd_fprintf(stdout, "verified %s using %s\n",
  396. fname, sigpath);
  397. }
  398. return ret;
  399. }
  400. static void
  401. rspamadm_signtool(int argc, char **argv, const struct rspamadm_command *cmd)
  402. {
  403. GOptionContext *context;
  404. GError *error = NULL;
  405. struct ucl_parser *parser;
  406. ucl_object_t *top;
  407. struct rspamd_cryptobox_pubkey *pk;
  408. struct rspamd_cryptobox_keypair *kp;
  409. gsize fsize, flen;
  410. int i;
  411. context = g_option_context_new(
  412. "keypair - create encryption keys");
  413. g_option_context_set_summary(context,
  414. "Summary:\n Rspamd administration utility version " RVERSION
  415. "\n Release id: " RID);
  416. g_option_context_add_main_entries(context, entries, NULL);
  417. if (!g_option_context_parse(context, &argc, &argv, &error)) {
  418. rspamd_fprintf(stderr, "option parsing failed: %s\n", error->message);
  419. g_error_free(error);
  420. g_option_context_free(context);
  421. exit(EXIT_FAILURE);
  422. }
  423. g_option_context_free(context);
  424. if (openssl) {
  425. mode = RSPAMD_CRYPTOBOX_MODE_NIST;
  426. }
  427. if (verify && (!pubkey && !pubkey_file)) {
  428. rspamd_fprintf(stderr, "no pubkey for verification\n");
  429. exit(EXIT_FAILURE);
  430. }
  431. else if (!verify && (!keypair_file)) {
  432. rspamd_fprintf(stderr, "no keypair for signing\n");
  433. exit(EXIT_FAILURE);
  434. }
  435. if (verify) {
  436. g_assert(pubkey || pubkey_file);
  437. if (pubkey_file) {
  438. int fd;
  439. char *map;
  440. struct stat st;
  441. fd = open(pubkey_file, O_RDONLY);
  442. if (fd == -1) {
  443. rspamd_fprintf(stderr, "cannot open %s: %s\n", pubkey_file,
  444. strerror(errno));
  445. exit(EXIT_FAILURE);
  446. }
  447. g_assert(fstat(fd, &st) != -1);
  448. fsize = st.st_size;
  449. flen = fsize;
  450. map = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fd, 0);
  451. close(fd);
  452. if (map == MAP_FAILED) {
  453. rspamd_fprintf(stderr, "cannot read %s: %s\n", pubkey_file,
  454. strerror(errno));
  455. exit(EXIT_FAILURE);
  456. }
  457. /* XXX: assume base32 pubkey now */
  458. while (flen > 0 && g_ascii_isspace(map[flen - 1])) {
  459. flen--;
  460. }
  461. pk = rspamd_pubkey_from_base32(map, flen,
  462. RSPAMD_KEYPAIR_SIGN, mode);
  463. if (pk == NULL) {
  464. rspamd_fprintf(stderr, "bad size %s: %ud, %ud expected\n",
  465. pubkey_file,
  466. (unsigned int) flen,
  467. rspamd_cryptobox_pk_sig_bytes(mode));
  468. exit(EXIT_FAILURE);
  469. }
  470. munmap(map, fsize);
  471. }
  472. else {
  473. pk = rspamd_pubkey_from_base32(pubkey, strlen(pubkey),
  474. RSPAMD_KEYPAIR_SIGN, mode);
  475. if (pk == NULL) {
  476. rspamd_fprintf(stderr, "bad size %s: %ud, %ud expected\n",
  477. pubkey_file,
  478. (unsigned int) strlen(pubkey),
  479. rspamd_cryptobox_pk_sig_bytes(mode));
  480. exit(EXIT_FAILURE);
  481. }
  482. }
  483. for (i = 1; i < argc; i++) {
  484. /* XXX: support cmd line signature */
  485. if (!rspamadm_verify_file(argv[i], rspamd_pubkey_get_pk(pk, NULL))) {
  486. exit(EXIT_FAILURE);
  487. }
  488. }
  489. g_free(pk);
  490. }
  491. else {
  492. g_assert(keypair_file != NULL);
  493. parser = ucl_parser_new(0);
  494. if (!ucl_parser_add_file(parser, keypair_file) ||
  495. (top = ucl_parser_get_object(parser)) == NULL) {
  496. rspamd_fprintf(stderr, "cannot load keypair: %s\n",
  497. ucl_parser_get_error(parser));
  498. exit(EXIT_FAILURE);
  499. }
  500. ucl_parser_free(parser);
  501. kp = rspamd_keypair_from_ucl(top);
  502. if (kp == NULL) {
  503. rspamd_fprintf(stderr, "invalid signing key\n");
  504. exit(EXIT_FAILURE);
  505. }
  506. if (rspamd_keypair_type(kp) != RSPAMD_KEYPAIR_SIGN) {
  507. rspamd_fprintf(stderr, "unsuitable for signing key\n");
  508. exit(EXIT_FAILURE);
  509. }
  510. for (i = 1; i < argc; i++) {
  511. /* XXX: support cmd line signature */
  512. if (!rspamadm_sign_file(argv[i], kp)) {
  513. rspamd_keypair_unref(kp);
  514. exit(EXIT_FAILURE);
  515. }
  516. }
  517. rspamd_keypair_unref(kp);
  518. }
  519. }