@umbra-privacy/qos-ceremony 0.1.0 → 0.1.2
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 +15 -15
- package/dist-cli/cli.js +145 -70
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ at it.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm i -g @umbra-privacy/qos-ceremony # provides the `ceremony` command
|
|
14
|
+
npm i -g @umbra-privacy/qos-ceremony # provides the `qos-ceremony` command
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Use
|
|
@@ -21,25 +21,25 @@ export CEREMONY_API=https://<coordinator>/ \
|
|
|
21
21
|
CEREMONY_NAMESPACE=<service> CEREMONY_NONCE=<nonce> \
|
|
22
22
|
CEREMONY_ALIAS=p1 CEREMONY_SECRET=/path/to/p1.secret
|
|
23
23
|
|
|
24
|
-
ceremony keygen # once, offline — generate your key; give the pubkey to an admin
|
|
25
|
-
ceremony share # shareSet: derive + upload your (encrypted) genesis share
|
|
26
|
-
ceremony approve --yes # manifestSet: review + sign the manifest (verify the pivot hash!)
|
|
27
|
-
ceremony provision # shareSet: re-encrypt your share to the enclave's ephemeral key
|
|
28
|
-
ceremony status # read-only phase + blockers
|
|
24
|
+
qos-ceremony keygen # once, offline — generate your key; give the pubkey to an admin
|
|
25
|
+
qos-ceremony share # shareSet: derive + upload your (encrypted) genesis share
|
|
26
|
+
qos-ceremony approve --yes # manifestSet: review + sign the manifest (verify the pivot hash!)
|
|
27
|
+
qos-ceremony provision # shareSet: re-encrypt your share to the enclave's ephemeral key
|
|
28
|
+
qos-ceremony status # read-only phase + blockers
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`shift-traffic` (ops, needs AWS creds): `ceremony shift-traffic --service arith --stack <s> --weights '{"<old>":0,"<new>":100}'`.
|
|
31
|
+
`shift-traffic` (ops, needs AWS creds): `qos-ceremony shift-traffic --service arith --stack <s> --weights '{"<old>":0,"<new>":100}'`.
|
|
32
32
|
|
|
33
33
|
## Commands
|
|
34
34
|
|
|
35
|
-
| Command
|
|
36
|
-
|
|
37
|
-
| `keygen`
|
|
38
|
-
| `share`
|
|
39
|
-
| `approve`
|
|
40
|
-
| `provision`
|
|
41
|
-
| `status`
|
|
42
|
-
| `shift-traffic` | ops
|
|
35
|
+
| Command | Set | What |
|
|
36
|
+
| --------------- | ----------- | --------------------------------------------------------- |
|
|
37
|
+
| `keygen` | any | generate seed (local) + print pubkey to register |
|
|
38
|
+
| `share` | shareSet | derive quorum-key share from genesis; upload (ciphertext) |
|
|
39
|
+
| `approve` | manifestSet | review + sign the manifest |
|
|
40
|
+
| `provision` | shareSet | re-encrypt share to the enclave ephemeral key; post it |
|
|
41
|
+
| `status` | — | read-only phase + blockers |
|
|
42
|
+
| `shift-traffic` | ops | blue-green weighted ALB cutover across a service's colors |
|
|
43
43
|
|
|
44
44
|
Flags mirror env: `--api --namespace --nonce --alias --secret-path --yubikey
|
|
45
45
|
--pcr3-preimage-path --qos-release-dir --qos-client`.
|
package/dist-cli/cli.js
CHANGED
|
@@ -32720,11 +32720,14 @@ var QosClient = class {
|
|
|
32720
32720
|
|
|
32721
32721
|
// ../qos-core/src/state.ts
|
|
32722
32722
|
function allMembers(sets) {
|
|
32723
|
-
return Array.from(
|
|
32723
|
+
return Array.from(
|
|
32724
|
+
/* @__PURE__ */ new Set([...sets.manifest.members, ...sets.share.members, ...sets.patch.members])
|
|
32725
|
+
);
|
|
32724
32726
|
}
|
|
32725
32727
|
function blockers(s) {
|
|
32726
32728
|
const out = [];
|
|
32727
|
-
if (s.dr_blocker)
|
|
32729
|
+
if (s.dr_blocker)
|
|
32730
|
+
out.push(`DR: ${s.dr_blocker.reason} (since ${s.dr_blocker.since}) \u2014 share set must re-post`);
|
|
32728
32731
|
switch (s.phase) {
|
|
32729
32732
|
case "Init":
|
|
32730
32733
|
case "CollectingPubkeys": {
|
|
@@ -32735,7 +32738,8 @@ function blockers(s) {
|
|
|
32735
32738
|
}
|
|
32736
32739
|
case "GenesisBooted": {
|
|
32737
32740
|
const need = s.sets.share.members.filter((m3) => !s.shares_derived.includes(m3));
|
|
32738
|
-
if (need.length)
|
|
32741
|
+
if (need.length)
|
|
32742
|
+
out.push(`waiting for shareSet members to derive+upload shares: ${need.join(", ")}`);
|
|
32739
32743
|
else out.push("all shares derived \u2014 coordinator will generate the manifest on next advance");
|
|
32740
32744
|
break;
|
|
32741
32745
|
}
|
|
@@ -32743,16 +32747,24 @@ function blockers(s) {
|
|
|
32743
32747
|
case "CollectingApprovals": {
|
|
32744
32748
|
const have = s.approvals.length;
|
|
32745
32749
|
const need = s.sets.manifest.threshold - have;
|
|
32746
|
-
if (need > 0)
|
|
32750
|
+
if (need > 0)
|
|
32751
|
+
out.push(
|
|
32752
|
+
`waiting for ${need} more manifestSet approval(s) (have ${have}/${s.sets.manifest.threshold})`
|
|
32753
|
+
);
|
|
32747
32754
|
else if (s.mode === "forward")
|
|
32748
|
-
out.push(
|
|
32755
|
+
out.push(
|
|
32756
|
+
`approval threshold met \u2014 coordinator will key-forward Q from ${s.source_cluster} on next advance`
|
|
32757
|
+
);
|
|
32749
32758
|
else out.push("approval threshold met \u2014 coordinator will boot-standard on next advance");
|
|
32750
32759
|
break;
|
|
32751
32760
|
}
|
|
32752
32761
|
case "Booted":
|
|
32753
32762
|
case "CollectingShares": {
|
|
32754
32763
|
const need = s.sets.share.threshold - s.shares_posted;
|
|
32755
|
-
if (need > 0)
|
|
32764
|
+
if (need > 0)
|
|
32765
|
+
out.push(
|
|
32766
|
+
`waiting for ${need} more provisioned share(s) (have ${s.shares_posted}/${s.sets.share.threshold})`
|
|
32767
|
+
);
|
|
32756
32768
|
else out.push("share threshold met \u2014 enclave reconstructing quorum key");
|
|
32757
32769
|
break;
|
|
32758
32770
|
}
|
|
@@ -32859,7 +32871,14 @@ var CeremonyApi = class {
|
|
|
32859
32871
|
const wd = new Workdir("ceremony-sign-");
|
|
32860
32872
|
try {
|
|
32861
32873
|
const ts = Date.now();
|
|
32862
|
-
const canonical = buildSignedPayload({
|
|
32874
|
+
const canonical = buildSignedPayload({
|
|
32875
|
+
alias: signer.alias,
|
|
32876
|
+
namespace: ns,
|
|
32877
|
+
nonce,
|
|
32878
|
+
route,
|
|
32879
|
+
ts,
|
|
32880
|
+
artifact
|
|
32881
|
+
});
|
|
32863
32882
|
const payloadPath = wd.writeText("payload.json", canonical);
|
|
32864
32883
|
const sigPath = wd.path("payload.sig");
|
|
32865
32884
|
if (signer.secret.yubikey) {
|
|
@@ -32879,15 +32898,36 @@ var CeremonyApi = class {
|
|
|
32879
32898
|
}
|
|
32880
32899
|
}
|
|
32881
32900
|
async putPubkey(ns, nonce, alias, pubkeyHex, signer) {
|
|
32882
|
-
const r5 = await this.signedRequest(
|
|
32901
|
+
const r5 = await this.signedRequest(
|
|
32902
|
+
"POST",
|
|
32903
|
+
ns,
|
|
32904
|
+
nonce,
|
|
32905
|
+
"pubkey",
|
|
32906
|
+
{ pubkey: pubkeyHex },
|
|
32907
|
+
{ alias, ...signer }
|
|
32908
|
+
);
|
|
32883
32909
|
if (!r5.ok) throw new Error(`putPubkey: ${r5.status} ${await r5.text()}`);
|
|
32884
32910
|
}
|
|
32885
32911
|
async putShare(ns, nonce, alias, shareB64, signer) {
|
|
32886
|
-
const r5 = await this.signedRequest(
|
|
32912
|
+
const r5 = await this.signedRequest(
|
|
32913
|
+
"PUT",
|
|
32914
|
+
ns,
|
|
32915
|
+
nonce,
|
|
32916
|
+
"share",
|
|
32917
|
+
{ share: shareB64 },
|
|
32918
|
+
{ alias, ...signer }
|
|
32919
|
+
);
|
|
32887
32920
|
if (!r5.ok) throw new Error(`putShare: ${r5.status} ${await r5.text()}`);
|
|
32888
32921
|
}
|
|
32889
32922
|
async postApproval(ns, nonce, alias, approvalB64, signer) {
|
|
32890
|
-
const r5 = await this.signedRequest(
|
|
32923
|
+
const r5 = await this.signedRequest(
|
|
32924
|
+
"POST",
|
|
32925
|
+
ns,
|
|
32926
|
+
nonce,
|
|
32927
|
+
"approval",
|
|
32928
|
+
{ approval: approvalB64 },
|
|
32929
|
+
{ alias, ...signer }
|
|
32930
|
+
);
|
|
32891
32931
|
if (!r5.ok) throw new Error(`postApproval: ${r5.status} ${await r5.text()}`);
|
|
32892
32932
|
}
|
|
32893
32933
|
async postEphShare(ns, nonce, alias, ephShareB64, approvalB64, signer) {
|
|
@@ -32926,9 +32966,19 @@ function resolveQosBin(explicit) {
|
|
|
32926
32966
|
if (explicit) return explicit;
|
|
32927
32967
|
if (process.env.QOS_CLIENT_BIN) return process.env.QOS_CLIENT_BIN;
|
|
32928
32968
|
const embedded = (0, import_node_path2.join)(__dirname, "bin", `qos_client-${process.platform}-${process.arch}`);
|
|
32929
|
-
if ((0, import_node_fs2.existsSync)(embedded))
|
|
32969
|
+
if ((0, import_node_fs2.existsSync)(embedded)) {
|
|
32970
|
+
ensureExecutable(embedded);
|
|
32971
|
+
return embedded;
|
|
32972
|
+
}
|
|
32930
32973
|
return void 0;
|
|
32931
32974
|
}
|
|
32975
|
+
function ensureExecutable(path) {
|
|
32976
|
+
try {
|
|
32977
|
+
const mode = (0, import_node_fs2.statSync)(path).mode;
|
|
32978
|
+
if ((mode & 73) !== 73) (0, import_node_fs2.chmodSync)(path, mode | 493);
|
|
32979
|
+
} catch {
|
|
32980
|
+
}
|
|
32981
|
+
}
|
|
32932
32982
|
|
|
32933
32983
|
// src/commands/keygen.ts
|
|
32934
32984
|
async function keygen(o3) {
|
|
@@ -32946,13 +32996,9 @@ async function keygen(o3) {
|
|
|
32946
32996
|
const pubPath = `${secretPath}.pub`;
|
|
32947
32997
|
qos.generateFileKey(secretPath, pubPath);
|
|
32948
32998
|
const pubHex = (0, import_node_fs3.readFileSync)(pubPath, "utf8").trim();
|
|
32949
|
-
console.log(
|
|
32950
|
-
`keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`
|
|
32951
|
-
);
|
|
32999
|
+
console.log(`keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`);
|
|
32952
33000
|
console.log(`keygen: pubkey written to ${pubPath}`);
|
|
32953
|
-
console.log(
|
|
32954
|
-
"\nRegister this operator (admin, IAM-gated) so it can join ceremonies:"
|
|
32955
|
-
);
|
|
33001
|
+
console.log("\nRegister this operator (admin, IAM-gated) so it can join ceremonies:");
|
|
32956
33002
|
console.log(
|
|
32957
33003
|
" aws lambda invoke --function-name qos-coordinator-admin-<stack> --cli-binary-format raw-in-base64-out \\"
|
|
32958
33004
|
);
|
|
@@ -32974,7 +33020,12 @@ function resolve(o3) {
|
|
|
32974
33020
|
const nonce = Number(nonceStr);
|
|
32975
33021
|
if (!Number.isInteger(nonce)) throw new Error(`--nonce must be an integer, got ${nonceStr}`);
|
|
32976
33022
|
const alias = req(o3.alias ?? process.env.CEREMONY_ALIAS, "--alias");
|
|
32977
|
-
const secret = o3.yubikey ? { yubikey: true } : {
|
|
33023
|
+
const secret = o3.yubikey ? { yubikey: true } : {
|
|
33024
|
+
secretPath: req(
|
|
33025
|
+
o3.secretPath ?? process.env.CEREMONY_SECRET,
|
|
33026
|
+
"--secret-path / CEREMONY_SECRET"
|
|
33027
|
+
)
|
|
33028
|
+
};
|
|
32978
33029
|
return {
|
|
32979
33030
|
api,
|
|
32980
33031
|
qos,
|
|
@@ -33000,7 +33051,8 @@ async function buildSetDirs(ctx, wd, sets) {
|
|
|
33000
33051
|
const dir = wd.dir(name);
|
|
33001
33052
|
for (const alias of set.members) {
|
|
33002
33053
|
const hex = members[alias];
|
|
33003
|
-
if (!hex)
|
|
33054
|
+
if (!hex)
|
|
33055
|
+
throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
|
|
33004
33056
|
(0, import_node_fs4.writeFileSync)(`${dir}/${alias}.pub`, hex);
|
|
33005
33057
|
}
|
|
33006
33058
|
(0, import_node_fs4.writeFileSync)(`${dir}/quorum_threshold`, `${set.threshold}`);
|
|
@@ -33016,7 +33068,12 @@ async function pullGenesisDir(ctx, wd) {
|
|
|
33016
33068
|
const dir = wd.dir("namespace");
|
|
33017
33069
|
await pullArtifact(ctx, wd, "genesis/genesis_output", "namespace/genesis_output");
|
|
33018
33070
|
await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "namespace/quorum_key.pub").catch(() => "");
|
|
33019
|
-
await pullArtifact(
|
|
33071
|
+
await pullArtifact(
|
|
33072
|
+
ctx,
|
|
33073
|
+
wd,
|
|
33074
|
+
"genesis/genesis_attestation_doc",
|
|
33075
|
+
"namespace/genesis_attestation_doc"
|
|
33076
|
+
).catch(() => "");
|
|
33020
33077
|
return dir;
|
|
33021
33078
|
}
|
|
33022
33079
|
async function fetchState(ctx) {
|
|
@@ -33039,8 +33096,13 @@ async function share(o3) {
|
|
|
33039
33096
|
secret: ctx.secret
|
|
33040
33097
|
});
|
|
33041
33098
|
const shareB64 = wd.readBytes(`${ctx.alias}.share`).toString("base64");
|
|
33042
|
-
await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
|
|
33043
|
-
|
|
33099
|
+
await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
|
|
33100
|
+
secret: ctx.secret,
|
|
33101
|
+
qos: ctx.qos
|
|
33102
|
+
});
|
|
33103
|
+
console.log(
|
|
33104
|
+
`share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
|
|
33105
|
+
);
|
|
33044
33106
|
} finally {
|
|
33045
33107
|
wd.cleanup();
|
|
33046
33108
|
}
|
|
@@ -33053,29 +33115,10 @@ async function approve(o3) {
|
|
|
33053
33115
|
const state2 = await fetchState(ctx);
|
|
33054
33116
|
const wd = new Workdir("ceremony-approve-");
|
|
33055
33117
|
try {
|
|
33056
|
-
const manifestPath = await pullArtifact(
|
|
33057
|
-
|
|
33058
|
-
|
|
33059
|
-
|
|
33060
|
-
"manifest"
|
|
33061
|
-
);
|
|
33062
|
-
const pivotHashPath = await pullArtifact(
|
|
33063
|
-
ctx,
|
|
33064
|
-
wd,
|
|
33065
|
-
"manifest/pivot.hash",
|
|
33066
|
-
"pivot.hash"
|
|
33067
|
-
);
|
|
33068
|
-
const quorumKeyPath = await pullArtifact(
|
|
33069
|
-
ctx,
|
|
33070
|
-
wd,
|
|
33071
|
-
"genesis/quorum_key.pub",
|
|
33072
|
-
"quorum_key.pub"
|
|
33073
|
-
);
|
|
33074
|
-
const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(
|
|
33075
|
-
ctx,
|
|
33076
|
-
wd,
|
|
33077
|
-
state2.sets
|
|
33078
|
-
);
|
|
33118
|
+
const manifestPath = await pullArtifact(ctx, wd, "manifest/manifest", "manifest");
|
|
33119
|
+
const pivotHashPath = await pullArtifact(ctx, wd, "manifest/pivot.hash", "pivot.hash");
|
|
33120
|
+
const quorumKeyPath = await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "quorum_key.pub");
|
|
33121
|
+
const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(ctx, wd, state2.sets);
|
|
33079
33122
|
const approvalsDir = wd.dir("approvals");
|
|
33080
33123
|
const json = ctx.qos.display(manifestPath, "manifest");
|
|
33081
33124
|
console.log("\n===== MANIFEST UNDER REVIEW =====");
|
|
@@ -33113,13 +33156,10 @@ async function approve(o3) {
|
|
|
33113
33156
|
const nsFlat = state2.namespace.replace(/\//g, "-");
|
|
33114
33157
|
const approvalFile = `${ctx.alias}-${nsFlat}-${state2.nonce}.approval`;
|
|
33115
33158
|
const approvalB64 = wd.readBytes(`approvals/${approvalFile}`).toString("base64");
|
|
33116
|
-
await ctx.api.postApproval(
|
|
33117
|
-
ctx.
|
|
33118
|
-
ctx.
|
|
33119
|
-
|
|
33120
|
-
approvalB64,
|
|
33121
|
-
{ secret: ctx.secret, qos: ctx.qos }
|
|
33122
|
-
);
|
|
33159
|
+
await ctx.api.postApproval(ctx.namespace, ctx.nonce, ctx.alias, approvalB64, {
|
|
33160
|
+
secret: ctx.secret,
|
|
33161
|
+
qos: ctx.qos
|
|
33162
|
+
});
|
|
33123
33163
|
console.log(`approve: uploaded approval '${approvalFile}'`);
|
|
33124
33164
|
} finally {
|
|
33125
33165
|
wd.cleanup();
|
|
@@ -33133,8 +33173,18 @@ async function provision(o3) {
|
|
|
33133
33173
|
const wd = new Workdir("ceremony-provision-");
|
|
33134
33174
|
try {
|
|
33135
33175
|
const gen = state2.boot_generation ?? 0;
|
|
33136
|
-
const attestationDocPath = await pullArtifact(
|
|
33137
|
-
|
|
33176
|
+
const attestationDocPath = await pullArtifact(
|
|
33177
|
+
ctx,
|
|
33178
|
+
wd,
|
|
33179
|
+
`provision/g${gen}/attestation_doc`,
|
|
33180
|
+
"attestation_doc"
|
|
33181
|
+
);
|
|
33182
|
+
const manifestEnvelopePath = await pullArtifact(
|
|
33183
|
+
ctx,
|
|
33184
|
+
wd,
|
|
33185
|
+
"manifest/manifest_envelope",
|
|
33186
|
+
"manifest_envelope"
|
|
33187
|
+
);
|
|
33138
33188
|
const sharePath = await pullArtifact(ctx, wd, `shares/${ctx.alias}.share`, `${ctx.alias}.share`);
|
|
33139
33189
|
const { manifestSetDir } = await buildSetDirs(ctx, wd, state2.sets);
|
|
33140
33190
|
const ephWrappedSharePath = wd.path(`${ctx.alias}.eph_share`);
|
|
@@ -33169,7 +33219,8 @@ async function status(o3) {
|
|
|
33169
33219
|
const apiUrl = o3.api ?? process.env.CEREMONY_API;
|
|
33170
33220
|
const namespace = o3.namespace ?? process.env.CEREMONY_NAMESPACE;
|
|
33171
33221
|
const nonceStr = o3.nonce ?? process.env.CEREMONY_NONCE;
|
|
33172
|
-
if (!apiUrl || !namespace || !nonceStr)
|
|
33222
|
+
if (!apiUrl || !namespace || !nonceStr)
|
|
33223
|
+
throw new Error("status requires --api --namespace --nonce");
|
|
33173
33224
|
const api = new CeremonyApi(apiUrl);
|
|
33174
33225
|
const s = await api.getState(namespace, Number(nonceStr));
|
|
33175
33226
|
console.log(`namespace : ${s.namespace}`);
|
|
@@ -33190,7 +33241,9 @@ var import_client_elastic_load_balancing_v2 = __toESM(require_dist_cjs16());
|
|
|
33190
33241
|
function buildWeightedForward(tgs, weights) {
|
|
33191
33242
|
for (const k5 of Object.keys(weights)) {
|
|
33192
33243
|
if (!tgs.some((t) => t.nonce === Number(k5))) {
|
|
33193
|
-
throw new Error(
|
|
33244
|
+
throw new Error(
|
|
33245
|
+
`shift-traffic: weight given for nonce ${k5} but no matching target group was found`
|
|
33246
|
+
);
|
|
33194
33247
|
}
|
|
33195
33248
|
}
|
|
33196
33249
|
if (tgs.length === 0) throw new Error("shift-traffic: no target groups to forward to");
|
|
@@ -33223,16 +33276,21 @@ async function discoverColorTargetGroups(elbv2, service, stack, role) {
|
|
|
33223
33276
|
const tags = Object.fromEntries((desc.Tags ?? []).map((t) => [t.Key, t.Value]));
|
|
33224
33277
|
if (tags.TeeService === service && tags.Stack === stack && tags.Role === role && tags.Nonce) {
|
|
33225
33278
|
const src = batch.find((b5) => b5.arn === desc.ResourceArn);
|
|
33226
|
-
if (src)
|
|
33279
|
+
if (src)
|
|
33280
|
+
matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
|
|
33227
33281
|
}
|
|
33228
33282
|
}
|
|
33229
33283
|
}
|
|
33230
33284
|
if (matched.length === 0) {
|
|
33231
|
-
throw new Error(
|
|
33285
|
+
throw new Error(
|
|
33286
|
+
`shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`
|
|
33287
|
+
);
|
|
33232
33288
|
}
|
|
33233
33289
|
const lbArn = matched[0].lbArn;
|
|
33234
33290
|
if (!matched.every((m3) => m3.lbArn === lbArn)) {
|
|
33235
|
-
throw new Error(
|
|
33291
|
+
throw new Error(
|
|
33292
|
+
`shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`
|
|
33293
|
+
);
|
|
33236
33294
|
}
|
|
33237
33295
|
return { lbArn, tgs: matched };
|
|
33238
33296
|
}
|
|
@@ -33250,7 +33308,11 @@ async function shiftTraffic(opts) {
|
|
|
33250
33308
|
const role = opts.listener ?? "app";
|
|
33251
33309
|
const elbv2 = opts.client ?? new import_client_elastic_load_balancing_v2.ElasticLoadBalancingV2Client(opts.region ? { region: opts.region } : {});
|
|
33252
33310
|
const { lbArn, tgs } = await discoverColorTargetGroups(elbv2, opts.service, opts.stack, role);
|
|
33253
|
-
const listenerArn = await findListenerForTargetGroups(
|
|
33311
|
+
const listenerArn = await findListenerForTargetGroups(
|
|
33312
|
+
elbv2,
|
|
33313
|
+
lbArn,
|
|
33314
|
+
new Set(tgs.map((t) => t.arn))
|
|
33315
|
+
);
|
|
33254
33316
|
const actions = buildWeightedForward(tgs, opts.weights);
|
|
33255
33317
|
await elbv2.send(new import_client_elastic_load_balancing_v2.ModifyListenerCommand({ ListenerArn: listenerArn, DefaultActions: actions }));
|
|
33256
33318
|
return { listenerArn, actions };
|
|
@@ -33264,7 +33326,13 @@ async function shiftTrafficCommand(o3) {
|
|
|
33264
33326
|
const weights = {};
|
|
33265
33327
|
for (const [k5, v] of Object.entries(parsed)) weights[Number(k5)] = v;
|
|
33266
33328
|
const listener = o3.listener ?? "app";
|
|
33267
|
-
const { listenerArn, actions } = await shiftTraffic({
|
|
33329
|
+
const { listenerArn, actions } = await shiftTraffic({
|
|
33330
|
+
service,
|
|
33331
|
+
stack,
|
|
33332
|
+
region: o3.region,
|
|
33333
|
+
listener,
|
|
33334
|
+
weights
|
|
33335
|
+
});
|
|
33268
33336
|
const summary = (actions[0].ForwardConfig?.TargetGroups ?? []).map((t) => `${t.TargetGroupArn?.split("/").slice(-2, -1)[0] ?? t.TargetGroupArn}=${t.Weight}`).join(" ");
|
|
33269
33337
|
console.log(`shift-traffic: ${service}/${stack} ${listener} listener \u2192 ${summary}`);
|
|
33270
33338
|
console.log(` listener: ${listenerArn}`);
|
|
@@ -33272,9 +33340,14 @@ async function shiftTrafficCommand(o3) {
|
|
|
33272
33340
|
|
|
33273
33341
|
// src/cli.ts
|
|
33274
33342
|
var program2 = new Command();
|
|
33275
|
-
program2.name("ceremony").description(
|
|
33343
|
+
program2.name("qos-ceremony").description(
|
|
33344
|
+
"QOS TEE operator ceremony CLI \u2014 your private key never leaves this device; all crypto via qos_client"
|
|
33345
|
+
);
|
|
33276
33346
|
function withCommon(cmd) {
|
|
33277
|
-
return cmd.option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace, e.g. arith (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").option("--alias <alias>", "operator alias (env CEREMONY_ALIAS)").option("--secret-path <path>", "operator master-seed file \u2014 stays local (env CEREMONY_SECRET)").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("--qos-client <bin>", "qos_client binary path (env QOS_CLIENT_BIN; else embedded/PATH)").option(
|
|
33347
|
+
return cmd.option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace, e.g. arith (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").option("--alias <alias>", "operator alias (env CEREMONY_ALIAS)").option("--secret-path <path>", "operator master-seed file \u2014 stays local (env CEREMONY_SECRET)").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("--qos-client <bin>", "qos_client binary path (env QOS_CLIENT_BIN; else embedded/PATH)").option(
|
|
33348
|
+
"--unsafe-eph-path-override <path>",
|
|
33349
|
+
"[dev/mock only] read enclave ephemeral pubkey from this file"
|
|
33350
|
+
);
|
|
33278
33351
|
}
|
|
33279
33352
|
function run(fn) {
|
|
33280
33353
|
return (o3) => fn(o3).catch((e5) => {
|
|
@@ -33282,12 +33355,12 @@ function run(fn) {
|
|
|
33282
33355
|
process.exit(1);
|
|
33283
33356
|
});
|
|
33284
33357
|
}
|
|
33285
|
-
withCommon(
|
|
33286
|
-
|
|
33287
|
-
);
|
|
33288
|
-
withCommon(
|
|
33289
|
-
|
|
33290
|
-
);
|
|
33358
|
+
withCommon(
|
|
33359
|
+
program2.command("keygen").description("generate operator keypair; print pubkey to register (seed stays local)")
|
|
33360
|
+
).action(run(keygen));
|
|
33361
|
+
withCommon(
|
|
33362
|
+
program2.command("share").description("derive quorum-key share from genesis output; upload (encrypted)")
|
|
33363
|
+
).action(run(share));
|
|
33291
33364
|
withCommon(
|
|
33292
33365
|
program2.command("approve").description("review + sign the manifest (manifestSet)").option("--yes", "skip interactive confirm")
|
|
33293
33366
|
).action(run(approve));
|
|
@@ -33295,7 +33368,9 @@ withCommon(
|
|
|
33295
33368
|
program2.command("provision").description("re-encrypt share to the enclave ephemeral key; post it (shareSet)")
|
|
33296
33369
|
).action(run(provision));
|
|
33297
33370
|
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));
|
|
33298
|
-
program2.command("shift-traffic").description(
|
|
33371
|
+
program2.command("shift-traffic").description(
|
|
33372
|
+
"[ops] blue-green weighted traffic cutover across a service's colors (needs AWS creds)"
|
|
33373
|
+
).option("--service <name>", "service name (env CEREMONY_SERVICE)").option("--stack <stack>", "stack (env CEREMONY_STACK)").option("--region <r>", "AWS region").option("--listener <app|control>", "which listener carries the split (default app)").option("--weights <json>", `nonce->weight map, e.g. '{"2026070601":0,"2026071501":100}'`).action(run(shiftTrafficCommand));
|
|
33299
33374
|
program2.parseAsync(process.argv).catch((e5) => {
|
|
33300
33375
|
console.error(e5);
|
|
33301
33376
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umbra-privacy/qos-ceremony",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qos",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"directory": "tools/operator-cli"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
-
"ceremony": "dist-cli/cli.js"
|
|
20
|
+
"qos-ceremony": "dist-cli/cli.js"
|
|
21
21
|
},
|
|
22
22
|
"main": "dist-cli/cli.js",
|
|
23
23
|
"files": [
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
45
|
"test": "vitest run",
|
|
46
46
|
"bundle:cli": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist-cli/cli.js && chmod +x dist-cli/cli.js",
|
|
47
|
-
"embed:qos": "mkdir -p dist-cli/bin && ( [ -f ../../../qos/target/release/qos_client ] && cp -L ../../../qos/target/release/qos_client dist-cli/bin/qos_client-darwin-arm64 || true ) && ( [ -f ../ceremony/dist-lambda/qos_client ] && cp -L ../ceremony/dist-lambda/qos_client dist-cli/bin/qos_client-linux-x64 || true ) && ls -la dist-cli/bin"
|
|
47
|
+
"embed:qos": "mkdir -p dist-cli/bin && ( [ -f ../../../qos/target/release/qos_client ] && cp -L ../../../qos/target/release/qos_client dist-cli/bin/qos_client-darwin-arm64 || true ) && ( [ -f ../ceremony/dist-lambda/qos_client ] && cp -L ../ceremony/dist-lambda/qos_client dist-cli/bin/qos_client-linux-x64 || true ) && ( chmod +x dist-cli/bin/* 2>/dev/null || true ) && ls -la dist-cli/bin"
|
|
48
48
|
}
|
|
49
49
|
}
|