statum-genkeys.vala 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. using Locksmith;
  2. namespace Statum.Tools {
  3. /**
  4. * `statum-genkeys` — generates the static key pairs for signing and
  5. * encryption for the `statum` section of `web-config.json`.
  6. *
  7. * With these keys configured, signed frames and encrypted private blobs
  8. * survive a server restart (the same keys verify/decrypt across restarts).
  9. *
  10. * The secret halves are key material. By default they are never written to
  11. * stdout (shell history, terminal scrollback, CI logs and `2>&1 | tee`
  12. * captures would all acquire them — SA-12): stdout carries only the PUBLIC
  13. * halves, and `--out <file>` writes the full config — public and secret
  14. * halves — directly to `<file>` with mode 0600.
  15. *
  16. * `--out <file>` writes the ready-to-use `web-config.json` (creating
  17. * parent directories as needed). Use it instead of shell redirection:
  18. *
  19. * ```bash
  20. * statum-genkeys --out web-config.json
  21. * ```
  22. */
  23. public class Genkeys : Object {
  24. private const string USAGE =
  25. "usage: statum-genkeys [--out <file>]\n" +
  26. "\n" +
  27. "Generates the static signing/encryption key pairs for web-config.json.\n" +
  28. "Without --out, prints only the PUBLIC key halves to stdout (SA-12: secret\n" +
  29. "halves are never printed). With --out, writes the full config — public and\n" +
  30. "secret halves — to <file> with mode 0600.\n";
  31. public static int main(string[] args) {
  32. string? out_path = null;
  33. for (int i = 1; i < args.length; i++) {
  34. var arg = args[i];
  35. if (arg == "--out" || arg == "-o") {
  36. if (i + 1 >= args.length) {
  37. stderr.printf("statum-genkeys: --out requires a file argument\n%s", USAGE);
  38. return 2;
  39. }
  40. out_path = args[++i];
  41. } else if (arg == "--help" || arg == "-h") {
  42. stdout.printf("%s", USAGE);
  43. return 0;
  44. } else {
  45. stderr.printf("statum-genkeys: unknown argument \"%s\"\n%s", arg, USAGE);
  46. return 2;
  47. }
  48. }
  49. // Signing (Ed25519) key pair.
  50. var sign = Signing.KeyPair.generate();
  51. // Encryption signing (Ed25519) key pair.
  52. var esign = Signing.KeyPair.generate();
  53. // Encryption sealing (X25519) key pair.
  54. var seal = Sealing.KeyPair.generate();
  55. if (out_path != null) {
  56. // Full config (secrets included) goes to the file only, mode 0600.
  57. var full = new StringBuilder();
  58. full.append("{\n");
  59. full.append(" \"statum\": {\n");
  60. full.append_printf(" \"signing_secret_key\": \"%s\",\n", sign.to_secret_key_base64());
  61. full.append_printf(" \"signing_public_key\": \"%s\",\n", sign.public_key.to_base64());
  62. full.append_printf(" \"encryption_signing_secret_key\": \"%s\",\n", esign.to_secret_key_base64());
  63. full.append_printf(" \"encryption_signing_public_key\": \"%s\",\n", esign.public_key.to_base64());
  64. full.append_printf(" \"encryption_sealing_secret_key\": \"%s\",\n", seal.to_secret_key_base64());
  65. full.append_printf(" \"encryption_sealing_public_key\": \"%s\"\n", seal.public_key.to_base64());
  66. full.append(" }\n");
  67. full.append("}\n");
  68. if (!write_private_file((!)out_path, full.str)) {
  69. return 1;
  70. }
  71. stdout.printf("Wrote full key material to %s (mode 0600).\n", (!)out_path);
  72. stdout.printf("The secret halves are in %s only — they are never printed to stdout.\n", (!)out_path);
  73. print_public_halves(sign.public_key.to_base64(), esign.public_key.to_base64(), seal.public_key.to_base64());
  74. return 0;
  75. }
  76. // No --out: keep the JSON shape scriptable, but carry PUBLIC halves
  77. // only. Anything that parsed this output for secrets must use
  78. // --out (see SA-12).
  79. stdout.printf("{\n");
  80. stdout.printf(" \"statum\": {\n");
  81. stdout.printf(" \"signing_public_key\": \"%s\",\n", sign.public_key.to_base64());
  82. stdout.printf(" \"encryption_signing_public_key\": \"%s\",\n", esign.public_key.to_base64());
  83. stdout.printf(" \"encryption_sealing_public_key\": \"%s\"\n", seal.public_key.to_base64());
  84. stdout.printf(" }\n");
  85. stdout.printf("}\n");
  86. stdout.printf("# Secret key halves are not printed. Use:\n");
  87. stdout.printf("# statum-genkeys --out <file>\n");
  88. stdout.printf("# to write the full config (public + secret) at mode 0600.\n");
  89. return 0;
  90. }
  91. private static void print_public_halves(string sign_pk, string esign_pk, string seal_pk) {
  92. stdout.printf("signing_public_key: %s\n", sign_pk);
  93. stdout.printf("encryption_signing_public_key: %s\n", esign_pk);
  94. stdout.printf("encryption_sealing_public_key: %s\n", seal_pk);
  95. }
  96. /**
  97. * Writes `contents` to `path` with mode 0600, creating parent
  98. * directories as needed. `File.replace` gives a fresh file, so the
  99. * restrictive mode is applied immediately after the write.
  100. */
  101. private static bool write_private_file(string path, string contents) {
  102. try {
  103. var file = File.new_for_path(path);
  104. var parent = file.get_parent();
  105. if (parent != null && !((!)parent).query_exists()) {
  106. try {
  107. ((!)parent).make_directory_with_parents(null);
  108. } catch (GLib.Error e) {
  109. if (!file.query_exists()) {
  110. throw e;
  111. }
  112. }
  113. }
  114. var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
  115. stream.put_string(contents);
  116. stream.close();
  117. file.set_attribute_uint32(FileAttribute.UNIX_MODE, 0600,
  118. FileQueryInfoFlags.NONE, null);
  119. return true;
  120. } catch (GLib.Error e) {
  121. stderr.printf("statum-genkeys: %s\n", e.message);
  122. return false;
  123. }
  124. }
  125. }
  126. }