@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 +57 -0
- package/dist-cli/bin/qos_client-darwin-arm64 +0 -0
- package/dist-cli/bin/qos_client-linux-x64 +0 -0
- package/dist-cli/bin/umbra-encryption-key-darwin-arm64 +0 -0
- package/dist-cli/bin/umbra-encryption-key-linux-x64 +0 -0
- package/dist-cli/bin/umbra-seal-share-darwin-arm64 +0 -0
- package/dist-cli/bin/umbra-seal-share-linux-x64 +0 -0
- package/dist-cli/bin/umbra-unseal-share-darwin-arm64 +0 -0
- package/dist-cli/bin/umbra-unseal-share-linux-x64 +0 -0
- package/dist-cli/cli.js +285 -45
- package/package.json +4 -2
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
|
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
|
-
|
|
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
|
|
6456
|
+
} else if (body instanceof import_node_fs6.ReadStream) {
|
|
6457
6457
|
if (body.path != null) {
|
|
6458
|
-
return (0,
|
|
6458
|
+
return (0, import_node_fs6.lstatSync)(body.path).size;
|
|
6459
6459
|
} else if (typeof body.fd === "number") {
|
|
6460
|
-
return (0,
|
|
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,
|
|
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
|
-
|
|
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:${
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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 ??
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
33113
|
-
if ((mode & 73) !== 73) (0,
|
|
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
|
|
33241
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
4
|
-
"description": "QOS TEE operator ceremony CLI — keygen/share/approve/provision + shift-traffic over the coordinator API. All crypto runs in
|
|
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",
|