@umbra-privacy/qos-ceremony 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,3 +43,60 @@ qos-ceremony status # read-only phase + blockers
43
43
 
44
44
  Flags mirror env: `--api --namespace --nonce --alias --secret-path --yubikey
45
45
  --pcr3-preimage-path --qos-release-dir --qos-client`.
46
+
47
+ ## The at-rest layer
48
+
49
+ A share is **double-sealed** everywhere it rests: the QOS P-256 envelope on the inside, an
50
+ ML-KEM-1024 + X25519 envelope over it. `ceremony share` seals before upload and `ceremony provision`
51
+ opens after download, so the singly-sealed form exists only inside a provision workdir, in the moment
52
+ between recovery and re-encryption to the enclave's ephemeral key.
53
+
54
+ The reason is the stored copy. `_shared/shares/<alias>.share` is durable and is the disaster-recovery
55
+ material, so under P-256 alone a copy taken today is readable by a CRQC later. The outer layer closes
56
+ that window without changing anything the enclave sees.
57
+
58
+ ### One-time setup, per operator
59
+
60
+ ```bash
61
+ # a seed in the file — fine for a laptop ceremony or a test
62
+ qos-ceremony encryption-key ~/.umbra/operator-encryption.json
63
+
64
+ # a seed derived from hardware — only a salt is stored
65
+ qos-ceremony encryption-key ~/.umbra/operator-encryption.json \
66
+ --challenge-command 'ykman otp calculate 2 {challenge}'
67
+ ```
68
+
69
+ Then pass the key file to every ceremony command that touches a share:
70
+
71
+ ```bash
72
+ export CEREMONY_ENCRYPTION_KEY=~/.umbra/operator-encryption.json
73
+ qos-ceremony share # derives, seals, uploads
74
+ qos-ceremony provision # downloads, opens, re-encrypts to the enclave
75
+ ```
76
+
77
+ Add `--challenge-command` to `provision` when the key derives from hardware; the token must be
78
+ present for that step and no other.
79
+
80
+ ### The tools directly
81
+
82
+ `seal-share` and `unseal-share` are also exposed on their own, for a share handled outside the
83
+ ceremony — a TVC deployment, or recovering one by hand. Arguments pass through to the underlying
84
+ binary, so `--help` is the binary's own:
85
+
86
+ ```bash
87
+ qos-ceremony seal-share --help
88
+ qos-ceremony unseal-share --response quorum-key-response.json \
89
+ --key ~/.umbra/operator-encryption.json --out quorum_key_metadata.json # tvc shape
90
+ qos-ceremony unseal-share --response p1.share.sealed \
91
+ --key ~/.umbra/operator-encryption.json --out p1.share --format qos # qos shape
92
+ ```
93
+
94
+ `--format tvc` (the default) writes what `tvc keys re-encrypt-local-share` reads; `--format qos`
95
+ writes the raw envelope `qos_client proxy-re-encrypt-share --share-path` reads.
96
+
97
+ ### Binaries
98
+
99
+ `qos_client`, `umbra-encryption-key`, `umbra-seal-share` and `umbra-unseal-share` are embedded per
100
+ platform. Override any of them with `--qos-client`, `--seal-share-bin`, `--unseal-share-bin`, or the
101
+ matching `*_BIN` environment variable; on an uncovered platform the CLI falls back to the bare name
102
+ on `PATH`.
Binary file
package/dist-cli/cli.js CHANGED
@@ -6437,10 +6437,10 @@ var init_hex_encoding = __esm({
6437
6437
  });
6438
6438
 
6439
6439
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/serde/util-body-length/calculateBodyLength.js
6440
- var import_node_fs5, calculateBodyLength;
6440
+ var import_node_fs6, calculateBodyLength;
6441
6441
  var init_calculateBodyLength = __esm({
6442
6442
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/serde/util-body-length/calculateBodyLength.js"() {
6443
- import_node_fs5 = require("node:fs");
6443
+ import_node_fs6 = require("node:fs");
6444
6444
  calculateBodyLength = (body) => {
6445
6445
  if (!body) {
6446
6446
  return 0;
@@ -6453,11 +6453,11 @@ var init_calculateBodyLength = __esm({
6453
6453
  return body.size;
6454
6454
  } else if (typeof body.start === "number" && typeof body.end === "number") {
6455
6455
  return body.end + 1 - body.start;
6456
- } else if (body instanceof import_node_fs5.ReadStream) {
6456
+ } else if (body instanceof import_node_fs6.ReadStream) {
6457
6457
  if (body.path != null) {
6458
- return (0, import_node_fs5.lstatSync)(body.path).size;
6458
+ return (0, import_node_fs6.lstatSync)(body.path).size;
6459
6459
  } else if (typeof body.fd === "number") {
6460
- return (0, import_node_fs5.fstatSync)(body.fd).size;
6460
+ return (0, import_node_fs6.fstatSync)(body.fd).size;
6461
6461
  }
6462
6462
  }
6463
6463
  throw new Error(`Body Length computation failed for ${body}`);
@@ -6756,11 +6756,11 @@ var init_types = __esm({
6756
6756
  });
6757
6757
 
6758
6758
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getHomeDir.js
6759
- var import_node_os2, import_node_path4, homeDirCache, getHomeDirCacheKey, getHomeDir;
6759
+ var import_node_os2, import_node_path5, homeDirCache, getHomeDirCacheKey, getHomeDir;
6760
6760
  var init_getHomeDir = __esm({
6761
6761
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getHomeDir.js"() {
6762
6762
  import_node_os2 = require("node:os");
6763
- import_node_path4 = require("node:path");
6763
+ import_node_path5 = require("node:path");
6764
6764
  homeDirCache = {};
6765
6765
  getHomeDirCacheKey = () => {
6766
6766
  if (process && process.geteuid) {
@@ -6769,7 +6769,7 @@ var init_getHomeDir = __esm({
6769
6769
  return "DEFAULT";
6770
6770
  };
6771
6771
  getHomeDir = () => {
6772
- const { HOME, USERPROFILE, HOMEPATH, HOMEDRIVE = `C:${import_node_path4.sep}` } = process.env;
6772
+ const { HOME, USERPROFILE, HOMEPATH, HOMEDRIVE = `C:${import_node_path5.sep}` } = process.env;
6773
6773
  if (HOME)
6774
6774
  return HOME;
6775
6775
  if (USERPROFILE)
@@ -6795,16 +6795,16 @@ var init_getProfileName = __esm({
6795
6795
  });
6796
6796
 
6797
6797
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getSSOTokenFilepath.js
6798
- var import_node_crypto, import_node_path5, getSSOTokenFilepath;
6798
+ var import_node_crypto, import_node_path6, getSSOTokenFilepath;
6799
6799
  var init_getSSOTokenFilepath = __esm({
6800
6800
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getSSOTokenFilepath.js"() {
6801
6801
  import_node_crypto = require("node:crypto");
6802
- import_node_path5 = require("node:path");
6802
+ import_node_path6 = require("node:path");
6803
6803
  init_getHomeDir();
6804
6804
  getSSOTokenFilepath = (id) => {
6805
6805
  const hasher = (0, import_node_crypto.createHash)("sha1");
6806
6806
  const cacheName = hasher.update(id).digest("hex");
6807
- return (0, import_node_path5.join)(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
6807
+ return (0, import_node_path6.join)(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
6808
6808
  };
6809
6809
  }
6810
6810
  });
@@ -6859,24 +6859,24 @@ var init_getConfigData = __esm({
6859
6859
  });
6860
6860
 
6861
6861
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getConfigFilepath.js
6862
- var import_node_path6, ENV_CONFIG_PATH, getConfigFilepath;
6862
+ var import_node_path7, ENV_CONFIG_PATH, getConfigFilepath;
6863
6863
  var init_getConfigFilepath = __esm({
6864
6864
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getConfigFilepath.js"() {
6865
- import_node_path6 = require("node:path");
6865
+ import_node_path7 = require("node:path");
6866
6866
  init_getHomeDir();
6867
6867
  ENV_CONFIG_PATH = "AWS_CONFIG_FILE";
6868
- getConfigFilepath = () => process.env[ENV_CONFIG_PATH] || (0, import_node_path6.join)(getHomeDir(), ".aws", "config");
6868
+ getConfigFilepath = () => process.env[ENV_CONFIG_PATH] || (0, import_node_path7.join)(getHomeDir(), ".aws", "config");
6869
6869
  }
6870
6870
  });
6871
6871
 
6872
6872
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getCredentialsFilepath.js
6873
- var import_node_path7, ENV_CREDENTIALS_PATH, getCredentialsFilepath;
6873
+ var import_node_path8, ENV_CREDENTIALS_PATH, getCredentialsFilepath;
6874
6874
  var init_getCredentialsFilepath = __esm({
6875
6875
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/getCredentialsFilepath.js"() {
6876
- import_node_path7 = require("node:path");
6876
+ import_node_path8 = require("node:path");
6877
6877
  init_getHomeDir();
6878
6878
  ENV_CREDENTIALS_PATH = "AWS_SHARED_CREDENTIALS_FILE";
6879
- getCredentialsFilepath = () => process.env[ENV_CREDENTIALS_PATH] || (0, import_node_path7.join)(getHomeDir(), ".aws", "credentials");
6879
+ getCredentialsFilepath = () => process.env[ENV_CREDENTIALS_PATH] || (0, import_node_path8.join)(getHomeDir(), ".aws", "credentials");
6880
6880
  }
6881
6881
  });
6882
6882
 
@@ -6956,10 +6956,10 @@ var init_readFile = __esm({
6956
6956
  });
6957
6957
 
6958
6958
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/loadSharedConfigFiles.js
6959
- var import_node_path8, swallowError, loadSharedConfigFiles;
6959
+ var import_node_path9, swallowError, loadSharedConfigFiles;
6960
6960
  var init_loadSharedConfigFiles = __esm({
6961
6961
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/config/shared-ini-file-loader/loadSharedConfigFiles.js"() {
6962
- import_node_path8 = require("node:path");
6962
+ import_node_path9 = require("node:path");
6963
6963
  init_getConfigData();
6964
6964
  init_getConfigFilepath();
6965
6965
  init_getCredentialsFilepath();
@@ -6974,11 +6974,11 @@ var init_loadSharedConfigFiles = __esm({
6974
6974
  const relativeHomeDirPrefix = "~/";
6975
6975
  let resolvedFilepath = filepath;
6976
6976
  if (filepath.startsWith(relativeHomeDirPrefix)) {
6977
- resolvedFilepath = (0, import_node_path8.join)(homeDir, filepath.slice(2));
6977
+ resolvedFilepath = (0, import_node_path9.join)(homeDir, filepath.slice(2));
6978
6978
  }
6979
6979
  let resolvedConfigFilepath = configFilepath;
6980
6980
  if (configFilepath.startsWith(relativeHomeDirPrefix)) {
6981
- resolvedConfigFilepath = (0, import_node_path8.join)(homeDir, configFilepath.slice(2));
6981
+ resolvedConfigFilepath = (0, import_node_path9.join)(homeDir, configFilepath.slice(2));
6982
6982
  }
6983
6983
  const parsedFiles = await Promise.all([
6984
6984
  readFile2(resolvedConfigFilepath, {
@@ -10082,17 +10082,17 @@ var init_HashCalculator = __esm({
10082
10082
  });
10083
10083
 
10084
10084
  // ../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/checksum/hash-stream-node/fileStreamHasher.js
10085
- var import_node_fs6, fileStreamHasher, isReadStream;
10085
+ var import_node_fs7, fileStreamHasher, isReadStream;
10086
10086
  var init_fileStreamHasher = __esm({
10087
10087
  "../../../node_modules/.pnpm/@smithy+core@3.29.6/node_modules/@smithy/core/dist-es/submodules/checksum/hash-stream-node/fileStreamHasher.js"() {
10088
- import_node_fs6 = require("node:fs");
10088
+ import_node_fs7 = require("node:fs");
10089
10089
  init_HashCalculator();
10090
10090
  fileStreamHasher = (hashCtor, fileStream) => new Promise((resolve2, reject) => {
10091
10091
  if (!isReadStream(fileStream)) {
10092
10092
  reject(new Error("Unable to calculate hash for non-file streams."));
10093
10093
  return;
10094
10094
  }
10095
- const fileStreamTee = (0, import_node_fs6.createReadStream)(fileStream.path, {
10095
+ const fileStreamTee = (0, import_node_fs7.createReadStream)(fileStream.path, {
10096
10096
  start: fileStream.start,
10097
10097
  end: fileStream.end
10098
10098
  });
@@ -26697,7 +26697,7 @@ var require_dist_cjs11 = __commonJS({
26697
26697
  var { createHash: createHash5, createPrivateKey, createPublicKey, sign: sign2 } = require("node:crypto");
26698
26698
  var { promises } = require("node:fs");
26699
26699
  var { homedir: homedir2 } = require("node:os");
26700
- var { dirname: dirname3, join: join7 } = require("node:path");
26700
+ var { dirname: dirname3, join: join8 } = require("node:path");
26701
26701
  var LoginCredentialsFetcher = class _LoginCredentialsFetcher {
26702
26702
  profileData;
26703
26703
  init;
@@ -26853,10 +26853,10 @@ var require_dist_cjs11 = __commonJS({
26853
26853
  await promises.writeFile(tokenFilePath, JSON.stringify(token, null, 2), "utf8");
26854
26854
  }
26855
26855
  getTokenFilePath() {
26856
- const directory = process.env.AWS_LOGIN_CACHE_DIRECTORY ?? join7(homedir2(), ".aws", "login", "cache");
26856
+ const directory = process.env.AWS_LOGIN_CACHE_DIRECTORY ?? join8(homedir2(), ".aws", "login", "cache");
26857
26857
  const loginSessionBytes = Buffer.from(this.loginSession, "utf8");
26858
26858
  const loginSessionSha256 = createHash5("sha256").update(loginSessionBytes).digest("hex");
26859
- return join7(directory, `${loginSessionSha256}.json`);
26859
+ return join8(directory, `${loginSessionSha256}.json`);
26860
26860
  }
26861
26861
  derToRawSignature(derSignature) {
26862
26862
  let offset = 2;
@@ -33090,16 +33090,134 @@ var fetchState = async (ctx) => {
33090
33090
  return ctx.api.getState(ctx.namespace, ctx.nonce);
33091
33091
  };
33092
33092
 
33093
- // src/qosbin.ts
33093
+ // src/atrest.ts
33094
+ var import_node_child_process2 = require("node:child_process");
33095
+ var DEFAULT_AT_REST_TIMEOUT_MS = 6e4;
33096
+ var AtRestError = class extends Error {
33097
+ constructor(binary, code, stdout, stderr) {
33098
+ super(`${binary} failed (exit ${code}): ${stderr || stdout}`);
33099
+ this.binary = binary;
33100
+ this.code = code;
33101
+ this.stdout = stdout;
33102
+ this.stderr = stderr;
33103
+ this.name = "AtRestError";
33104
+ }
33105
+ };
33106
+ var buildAtRestClient = (options) => {
33107
+ const sealBin = options.sealBin ?? "umbra-seal-share";
33108
+ const unsealBin = options.unsealBin ?? "umbra-unseal-share";
33109
+ const timeout = options.timeoutMs ?? DEFAULT_AT_REST_TIMEOUT_MS;
33110
+ const run2 = (binary, args) => {
33111
+ const result = (0, import_node_child_process2.spawnSync)(binary, [...args], {
33112
+ encoding: "utf8",
33113
+ timeout
33114
+ });
33115
+ const stdout = result.stdout ?? "";
33116
+ const stderr = result.stderr ?? "";
33117
+ if (result.error) {
33118
+ throw new AtRestError(
33119
+ binary,
33120
+ null,
33121
+ stdout,
33122
+ `${stderr}${result.error.message}`
33123
+ );
33124
+ }
33125
+ if (result.status !== 0) {
33126
+ throw new AtRestError(binary, result.status, stdout, stderr);
33127
+ }
33128
+ };
33129
+ return {
33130
+ /**
33131
+ * Wrap a share `qos_client after-genesis` wrote in the at-rest envelope.
33132
+ *
33133
+ * @param request - the share to wrap, the recipient's key file, and the binding it is sealed
33134
+ * under. The index and share count are stated rather than inferred: a single share has no array
33135
+ * to count them from, and both go into the AEAD's associated data, so misstating either fails to
33136
+ * open rather than opening the wrong slot.
33137
+ * @throws {@link AtRestError} when the binary is missing, times out, or exits non-zero — which
33138
+ * includes an index outside the share set and a key file with no `encryptionPublicKey`.
33139
+ * @example
33140
+ * ```ts
33141
+ * atRest.seal({
33142
+ * sharePath: wd.path("p1.share"),
33143
+ * encryptionKeyPath: "~/.umbra/operator-encryption.json",
33144
+ * operatorPublicKeyHex: members.p1,
33145
+ * quorumKeyPath: wd.path("namespace/quorum_key.pub"),
33146
+ * threshold: 2, shareCount: 3, index: 1,
33147
+ * outPath: wd.path("p1.share.sealed"),
33148
+ * });
33149
+ * ```
33150
+ */
33151
+ seal: (request) => run2(sealBin, [
33152
+ "--share",
33153
+ request.sharePath,
33154
+ "--key",
33155
+ request.encryptionKeyPath,
33156
+ "--operator-public-key",
33157
+ request.operatorPublicKeyHex,
33158
+ "--quorum-key-path",
33159
+ request.quorumKeyPath,
33160
+ "--threshold",
33161
+ String(request.threshold),
33162
+ "--share-count",
33163
+ String(request.shareCount),
33164
+ "--index",
33165
+ String(request.index),
33166
+ "--out",
33167
+ request.outPath
33168
+ ]),
33169
+ /**
33170
+ * Peel the at-rest envelope back off, writing the raw QOS envelope `qos_client` expects.
33171
+ *
33172
+ * @param request - the sealed share, the operator's key file, and where to write the recovered
33173
+ * envelope. `challengeCommand` is required only when the key file stores a salt rather than a
33174
+ * seed, and overrides the command recorded in the file if the device moved.
33175
+ * @throws {@link AtRestError} when the binary is missing, times out, or exits non-zero — which
33176
+ * includes the envelope failing to open, whether from the wrong key, a restated binding, or a
33177
+ * tampered ciphertext. Those are deliberately indistinguishable.
33178
+ * @example
33179
+ * ```ts
33180
+ * atRest.unseal({
33181
+ * sealedPath: wd.path("p1.share.sealed"),
33182
+ * encryptionKeyPath: "~/.umbra/operator-encryption.json",
33183
+ * outPath: wd.path("p1.share"),
33184
+ * });
33185
+ * ```
33186
+ */
33187
+ unseal: (request) => run2(unsealBin, [
33188
+ "--response",
33189
+ request.sealedPath,
33190
+ "--key",
33191
+ request.encryptionKeyPath,
33192
+ "--out",
33193
+ request.outPath,
33194
+ "--format",
33195
+ "qos",
33196
+ ...request.challengeCommand ? ["--challenge-command", request.challengeCommand] : []
33197
+ ])
33198
+ };
33199
+ };
33200
+ var createAtRestClient = (options = {}) => buildAtRestClient(options);
33201
+
33202
+ // src/atrestbin.ts
33094
33203
  var import_node_fs3 = require("node:fs");
33095
33204
  var import_node_path2 = require("node:path");
33096
- var resolveQosBin = (explicit) => {
33205
+ var ensureExecutable = (path) => {
33206
+ try {
33207
+ const mode = (0, import_node_fs3.statSync)(path).mode;
33208
+ if ((mode & 73) !== 73) (0, import_node_fs3.chmodSync)(path, mode | 493);
33209
+ } catch {
33210
+ return;
33211
+ }
33212
+ };
33213
+ var resolveAtRestBin = (tool, explicit) => {
33097
33214
  if (explicit) return explicit;
33098
- if (process.env.QOS_CLIENT_BIN) return process.env.QOS_CLIENT_BIN;
33215
+ const fromEnv2 = process.env[`${tool.toUpperCase().replaceAll("-", "_")}_BIN`];
33216
+ if (fromEnv2) return fromEnv2;
33099
33217
  const embedded = (0, import_node_path2.join)(
33100
33218
  __dirname,
33101
33219
  "bin",
33102
- `qos_client-${process.platform}-${process.arch}`
33220
+ `${tool}-${process.platform}-${process.arch}`
33103
33221
  );
33104
33222
  if ((0, import_node_fs3.existsSync)(embedded)) {
33105
33223
  ensureExecutable(embedded);
@@ -33107,10 +33225,28 @@ var resolveQosBin = (explicit) => {
33107
33225
  }
33108
33226
  return void 0;
33109
33227
  };
33110
- var ensureExecutable = (path) => {
33228
+
33229
+ // src/qosbin.ts
33230
+ var import_node_fs4 = require("node:fs");
33231
+ var import_node_path3 = require("node:path");
33232
+ var resolveQosBin = (explicit) => {
33233
+ if (explicit) return explicit;
33234
+ if (process.env.QOS_CLIENT_BIN) return process.env.QOS_CLIENT_BIN;
33235
+ const embedded = (0, import_node_path3.join)(
33236
+ __dirname,
33237
+ "bin",
33238
+ `qos_client-${process.platform}-${process.arch}`
33239
+ );
33240
+ if ((0, import_node_fs4.existsSync)(embedded)) {
33241
+ ensureExecutable2(embedded);
33242
+ return embedded;
33243
+ }
33244
+ return void 0;
33245
+ };
33246
+ var ensureExecutable2 = (path) => {
33111
33247
  try {
33112
- const mode = (0, import_node_fs3.statSync)(path).mode;
33113
- if ((mode & 73) !== 73) (0, import_node_fs3.chmodSync)(path, mode | 493);
33248
+ const mode = (0, import_node_fs4.statSync)(path).mode;
33249
+ if ((mode & 73) !== 73) (0, import_node_fs4.chmodSync)(path, mode | 493);
33114
33250
  } catch {
33115
33251
  return;
33116
33252
  }
@@ -33141,15 +33277,25 @@ var resolve = (o3) => {
33141
33277
  "--secret-path / CEREMONY_SECRET"
33142
33278
  )
33143
33279
  };
33280
+ const sealBin = resolveAtRestBin("umbra-seal-share", o3.sealShareBin);
33281
+ const unsealBin = resolveAtRestBin("umbra-unseal-share", o3.unsealShareBin);
33282
+ const atRest = createAtRestClient({
33283
+ ...sealBin ? { sealBin } : {},
33284
+ ...unsealBin ? { unsealBin } : {}
33285
+ });
33286
+ const encryptionKeyPath = o3.encryptionKeyPath ?? process.env.CEREMONY_ENCRYPTION_KEY;
33144
33287
  return {
33145
33288
  api,
33146
33289
  qos,
33290
+ atRest,
33147
33291
  namespace,
33148
33292
  nonce,
33149
33293
  alias,
33150
33294
  secret,
33151
33295
  pcr3PreimagePath: req(o3.pcr3PreimagePath, "--pcr3-preimage-path"),
33152
33296
  qosReleaseDir: req(o3.qosReleaseDir, "--qos-release-dir"),
33297
+ ...encryptionKeyPath ? { encryptionKeyPath } : {},
33298
+ ...o3.challengeCommand ? { challengeCommand: o3.challengeCommand } : {},
33153
33299
  unsafeEphPathOverride: o3.unsafeEphPathOverride
33154
33300
  };
33155
33301
  };
@@ -33236,25 +33382,40 @@ var approve = async (o3) => {
33236
33382
  }
33237
33383
  };
33238
33384
 
33385
+ // src/commands/at-rest-tool.ts
33386
+ var import_node_child_process3 = require("node:child_process");
33387
+ var runAtRestTool = (tool, args) => {
33388
+ const binary = resolveAtRestBin(tool) ?? tool;
33389
+ const result = (0, import_node_child_process3.spawnSync)(binary, [...args], { stdio: "inherit" });
33390
+ if (result.error) {
33391
+ console.error(
33392
+ `error: could not run ${binary}: ${result.error.message}
33393
+ This platform has no embedded build; install ${tool} on PATH or pass its path.`
33394
+ );
33395
+ process.exit(127);
33396
+ }
33397
+ process.exit(result.status ?? 1);
33398
+ };
33399
+
33239
33400
  // src/commands/keygen.ts
33240
- var import_node_fs4 = require("node:fs");
33241
- var import_node_path3 = require("node:path");
33401
+ var import_node_fs5 = require("node:fs");
33402
+ var import_node_path4 = require("node:path");
33242
33403
  var keygen = async (o3) => {
33243
33404
  const alias = o3.alias ?? process.env.CEREMONY_ALIAS ?? "operator";
33244
33405
  const secretPath = o3.secretPath ?? process.env.CEREMONY_SECRET;
33245
33406
  if (!secretPath) {
33246
33407
  throw new Error("keygen requires --secret-path (env CEREMONY_SECRET)");
33247
33408
  }
33248
- if ((0, import_node_fs4.existsSync)(secretPath)) {
33409
+ if ((0, import_node_fs5.existsSync)(secretPath)) {
33249
33410
  throw new Error(
33250
33411
  `refusing to overwrite existing secret at ${secretPath} \u2014 you already have a key; give its pubkey (${secretPath}.pub) to an admin to register-operator`
33251
33412
  );
33252
33413
  }
33253
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(secretPath), { recursive: true, mode: 448 });
33414
+ (0, import_node_fs5.mkdirSync)((0, import_node_path4.dirname)(secretPath), { recursive: true, mode: 448 });
33254
33415
  const qos = createQosClient({ bin: resolveQosBin(o3.qosClient) });
33255
33416
  const pubPath = `${secretPath}.pub`;
33256
33417
  qos.generateFileKey(secretPath, pubPath);
33257
- const pubHex = (0, import_node_fs4.readFileSync)(pubPath, "utf8").trim();
33418
+ const pubHex = (0, import_node_fs5.readFileSync)(pubPath, "utf8").trim();
33258
33419
  console.log(
33259
33420
  `keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`
33260
33421
  );
@@ -33289,12 +33450,23 @@ var provision = async (o3) => {
33289
33450
  "manifest/manifest_envelope",
33290
33451
  "manifest_envelope"
33291
33452
  );
33292
- const sharePath = await pullShared(
33453
+ const sealedPath = await pullShared(
33293
33454
  ctx,
33294
33455
  wd,
33295
33456
  `shares/${ctx.alias}.share`,
33296
- `${ctx.alias}.share`
33457
+ `${ctx.alias}.share.sealed`
33297
33458
  );
33459
+ if (!ctx.encryptionKeyPath)
33460
+ throw new Error(
33461
+ "missing --encryption-key-path / CEREMONY_ENCRYPTION_KEY \u2014 the stored share is sealed under the at-rest layer and must be opened before it can be re-encrypted"
33462
+ );
33463
+ const sharePath = wd.path(`${ctx.alias}.share`);
33464
+ ctx.atRest.unseal({
33465
+ sealedPath,
33466
+ encryptionKeyPath: ctx.encryptionKeyPath,
33467
+ outPath: sharePath,
33468
+ ...ctx.challengeCommand ? { challengeCommand: ctx.challengeCommand } : {}
33469
+ });
33298
33470
  const { manifestSetDir } = await buildSetDirs(ctx, wd, state2.sets);
33299
33471
  const ephWrappedSharePath = wd.path(`${ctx.alias}.eph_share`);
33300
33472
  const approvalPath = wd.path(`${ctx.alias}.attestation.approval`);
@@ -33333,8 +33505,27 @@ var provision = async (o3) => {
33333
33505
  };
33334
33506
 
33335
33507
  // src/commands/share.ts
33508
+ var requireEncryptionKeyPath = (ctx) => {
33509
+ if (!ctx.encryptionKeyPath)
33510
+ throw new Error(
33511
+ "missing --encryption-key-path / CEREMONY_ENCRYPTION_KEY \u2014 the share is sealed under the at-rest layer before upload; mint the key with umbra-encryption-key"
33512
+ );
33513
+ return ctx.encryptionKeyPath;
33514
+ };
33336
33515
  var share = async (o3) => {
33337
33516
  const ctx = resolve(o3);
33517
+ const encryptionKeyPath = requireEncryptionKeyPath(ctx);
33518
+ const state2 = await fetchState(ctx);
33519
+ const shareSet = state2.sets.share;
33520
+ const index = shareSet.members.indexOf(ctx.alias);
33521
+ if (index < 0)
33522
+ throw new Error(
33523
+ `'${ctx.alias}' is not in the share set (${shareSet.members.join(", ")})`
33524
+ );
33525
+ const members = await ctx.api.getMembers(ctx.namespace, ctx.nonce);
33526
+ const operatorPublicKeyHex = members[ctx.alias];
33527
+ if (!operatorPublicKeyHex)
33528
+ throw new Error(`no registered public key for operator '${ctx.alias}'`);
33338
33529
  const wd = createWorkdir("ceremony-share-");
33339
33530
  try {
33340
33531
  const namespaceDir = await pullGenesisDir(ctx, wd);
@@ -33347,13 +33538,32 @@ var share = async (o3) => {
33347
33538
  qosReleaseDir: ctx.qosReleaseDir,
33348
33539
  secret: ctx.secret
33349
33540
  });
33350
- const shareB64 = wd.readBytes(`${ctx.alias}.share`).toString("base64");
33541
+ const quorumKeyPath = wd.path("namespace/quorum_key.pub");
33542
+ try {
33543
+ wd.readBytes("namespace/quorum_key.pub");
33544
+ } catch {
33545
+ throw new Error(
33546
+ "the namespace has no _shared/quorum_key.pub \u2014 genesis has not published the quorum key, so the at-rest binding cannot be built"
33547
+ );
33548
+ }
33549
+ const sealedRel = `${ctx.alias}.share.sealed`;
33550
+ ctx.atRest.seal({
33551
+ sharePath,
33552
+ encryptionKeyPath,
33553
+ operatorPublicKeyHex,
33554
+ quorumKeyPath,
33555
+ threshold: shareSet.threshold,
33556
+ shareCount: shareSet.members.length,
33557
+ index,
33558
+ outPath: wd.path(sealedRel)
33559
+ });
33560
+ const shareB64 = wd.readBytes(sealedRel).toString("base64");
33351
33561
  await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
33352
33562
  secret: ctx.secret,
33353
33563
  qos: ctx.qos
33354
33564
  });
33355
33565
  console.log(
33356
- `share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
33566
+ `share: derived + sealed + uploaded share ${index} of ${shareSet.members.length} for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
33357
33567
  );
33358
33568
  } finally {
33359
33569
  wd.cleanup();
@@ -33587,6 +33797,18 @@ var withCommon = (cmd) => {
33587
33797
  ).option("--yubikey", "use a YubiKey PIV key instead of a seed file").option("--pcr3-preimage-path <path>", "PCR3 preimage (IAM role ARN) file").option("--qos-release-dir <dir>", "QOS release dir (aws-x86_64.pcrs)").option(
33588
33798
  "--qos-client <bin>",
33589
33799
  "qos_client binary path (env QOS_CLIENT_BIN; else embedded/PATH)"
33800
+ ).option(
33801
+ "--encryption-key-path <path>",
33802
+ "operator at-rest encryption key file from umbra-encryption-key (env CEREMONY_ENCRYPTION_KEY)"
33803
+ ).option(
33804
+ "--challenge-command <cmd>",
33805
+ "challenge-response command when the encryption key derives from hardware"
33806
+ ).option(
33807
+ "--seal-share-bin <bin>",
33808
+ "umbra-seal-share binary path (env UMBRA_SEAL_SHARE_BIN; else PATH)"
33809
+ ).option(
33810
+ "--unseal-share-bin <bin>",
33811
+ "umbra-unseal-share binary path (env UMBRA_UNSEAL_SHARE_BIN; else PATH)"
33590
33812
  ).option(
33591
33813
  "--unsafe-eph-path-override <path>",
33592
33814
  "[dev/mock only] read enclave ephemeral pubkey from this file"
@@ -33605,7 +33827,7 @@ withCommon(
33605
33827
  ).action(run(keygen));
33606
33828
  withCommon(
33607
33829
  program2.command("share").description(
33608
- "derive quorum-key share from genesis output; upload (encrypted)"
33830
+ "derive quorum-key share from genesis output; seal it and upload (double-sealed)"
33609
33831
  )
33610
33832
  ).action(run(share));
33611
33833
  withCommon(
@@ -33613,7 +33835,7 @@ withCommon(
33613
33835
  ).action(run(approve));
33614
33836
  withCommon(
33615
33837
  program2.command("provision").description(
33616
- "re-encrypt share to the enclave ephemeral key; post it (shareSet)"
33838
+ "open the stored share and re-encrypt it to the enclave ephemeral key; post it (shareSet)"
33617
33839
  )
33618
33840
  ).action(run(provision));
33619
33841
  program2.command("status").description("show ceremony phase + blockers (read-only)").option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").action(run(status));
@@ -33635,6 +33857,24 @@ program2.command("shift-traffic").description(
33635
33857
  "--force",
33636
33858
  "skip the target-health precheck (allow shifting to a 0-healthy color)"
33637
33859
  ).action(run(shiftTrafficCommand));
33860
+ var atRestPassthrough = (name, command5, summary) => {
33861
+ program2.command(command5).description(summary).allowUnknownOption().allowExcessArguments().helpOption(false).action(() => runAtRestTool(name, process.argv.slice(3)));
33862
+ };
33863
+ atRestPassthrough(
33864
+ "umbra-encryption-key",
33865
+ "encryption-key",
33866
+ "mint or recover this operator's at-rest encryption key (run with --help for its flags)"
33867
+ );
33868
+ atRestPassthrough(
33869
+ "umbra-seal-share",
33870
+ "seal-share",
33871
+ "wrap a share from qos_client after-genesis in the at-rest layer (--help for its flags)"
33872
+ );
33873
+ atRestPassthrough(
33874
+ "umbra-unseal-share",
33875
+ "unseal-share",
33876
+ "peel the at-rest layer off a share, for tvc or qos (--help for its flags)"
33877
+ );
33638
33878
  program2.parseAsync(process.argv).catch((e5) => {
33639
33879
  console.error(e5);
33640
33880
  process.exit(1);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@umbra-privacy/qos-ceremony",
3
- "version": "0.1.4",
4
- "description": "QOS TEE operator ceremony CLI — keygen/share/approve/provision + shift-traffic over the coordinator API. All crypto runs in qos_client (embedded for common platforms); your private key never leaves the device. Ships NO coordinator code.",
3
+ "version": "0.2.0",
4
+ "description": "QOS TEE operator ceremony CLI — keygen/share/approve/provision + shift-traffic over the coordinator API, and the at-rest sealing tools. Shares are double-sealed everywhere they rest: ML-KEM-1024 over the QOS P-256 envelope. All crypto runs in embedded binaries (qos_client, umbra-encryption-key, umbra-seal-share, umbra-unseal-share); your private key never leaves the device. Ships NO coordinator code.",
5
5
  "keywords": [
6
6
  "qos",
7
7
  "tee",
8
8
  "nitro-enclave",
9
9
  "quorum",
10
10
  "ceremony",
11
+ "post-quantum",
12
+ "ml-kem",
11
13
  "umbra"
12
14
  ],
13
15
  "license": "MIT",