statum-genkeys.vala 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Sodium;
  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_sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
  51. var sign_pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
  52. Sodium.Asymmetric.Signing.generate_keypair(sign_pk, sign_sk);
  53. // Encryption signing (Ed25519) key pair.
  54. var esign_sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
  55. var esign_pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
  56. Sodium.Asymmetric.Signing.generate_keypair(esign_pk, esign_sk);
  57. // Encryption sealing (X25519) key pair.
  58. var seal_sk = new uint8[Sodium.Asymmetric.Sealing.SECRET_KEY_BYTES];
  59. var seal_pk = new uint8[Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES];
  60. Sodium.Asymmetric.Sealing.generate_keypair(seal_pk, seal_sk);
  61. if (out_path != null) {
  62. // Full config (secrets included) goes to the file only, mode 0600.
  63. var full = new StringBuilder();
  64. full.append("{\n");
  65. full.append(" \"statum\": {\n");
  66. full.append_printf(" \"signing_secret_key\": \"%s\",\n", Base64.encode(sign_sk));
  67. full.append_printf(" \"signing_public_key\": \"%s\",\n", Base64.encode(sign_pk));
  68. full.append_printf(" \"encryption_signing_secret_key\": \"%s\",\n", Base64.encode(esign_sk));
  69. full.append_printf(" \"encryption_signing_public_key\": \"%s\",\n", Base64.encode(esign_pk));
  70. full.append_printf(" \"encryption_sealing_secret_key\": \"%s\",\n", Base64.encode(seal_sk));
  71. full.append_printf(" \"encryption_sealing_public_key\": \"%s\"\n", Base64.encode(seal_pk));
  72. full.append(" }\n");
  73. full.append("}\n");
  74. if (!write_private_file((!)out_path, full.str)) {
  75. return 1;
  76. }
  77. stdout.printf("Wrote full key material to %s (mode 0600).\n", (!)out_path);
  78. stdout.printf("The secret halves are in %s only — they are never printed to stdout.\n", (!)out_path);
  79. print_public_halves(sign_pk, esign_pk, seal_pk);
  80. return 0;
  81. }
  82. // No --out: keep the JSON shape scriptable, but carry PUBLIC halves
  83. // only. Anything that parsed this output for secrets must use
  84. // --out (see SA-12).
  85. stdout.printf("{\n");
  86. stdout.printf(" \"statum\": {\n");
  87. stdout.printf(" \"signing_public_key\": \"%s\",\n", Base64.encode(sign_pk));
  88. stdout.printf(" \"encryption_signing_public_key\": \"%s\",\n", Base64.encode(esign_pk));
  89. stdout.printf(" \"encryption_sealing_public_key\": \"%s\"\n", Base64.encode(seal_pk));
  90. stdout.printf(" }\n");
  91. stdout.printf("}\n");
  92. stdout.printf("# Secret key halves are not printed. Use:\n");
  93. stdout.printf("# statum-genkeys --out <file>\n");
  94. stdout.printf("# to write the full config (public + secret) at mode 0600.\n");
  95. return 0;
  96. }
  97. private static void print_public_halves(uint8[] sign_pk, uint8[] esign_pk, uint8[] seal_pk) {
  98. stdout.printf("signing_public_key: %s\n", Base64.encode(sign_pk));
  99. stdout.printf("encryption_signing_public_key: %s\n", Base64.encode(esign_pk));
  100. stdout.printf("encryption_sealing_public_key: %s\n", Base64.encode(seal_pk));
  101. }
  102. /**
  103. * Writes `contents` to `path` with mode 0600, creating parent
  104. * directories as needed. `File.replace` gives a fresh file, so the
  105. * restrictive mode is applied immediately after the write.
  106. */
  107. private static bool write_private_file(string path, string contents) {
  108. try {
  109. var file = File.new_for_path(path);
  110. var parent = file.get_parent();
  111. if (parent != null && !((!)parent).query_exists()) {
  112. try {
  113. ((!)parent).make_directory_with_parents(null);
  114. } catch (GLib.Error e) {
  115. if (!file.query_exists()) {
  116. throw e;
  117. }
  118. }
  119. }
  120. var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
  121. stream.put_string(contents);
  122. stream.close();
  123. file.set_attribute_uint32(FileAttribute.UNIX_MODE, 0600,
  124. FileQueryInfoFlags.NONE, null);
  125. return true;
  126. } catch (GLib.Error e) {
  127. stderr.printf("statum-genkeys: %s\n", e.message);
  128. return false;
  129. }
  130. }
  131. }
  132. }