@umbra-privacy/qos-ceremony 0.1.0 → 0.1.1
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 +134 -69
- package/package.json +2 -2
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) {
|
|
@@ -32946,13 +32986,9 @@ async function keygen(o3) {
|
|
|
32946
32986
|
const pubPath = `${secretPath}.pub`;
|
|
32947
32987
|
qos.generateFileKey(secretPath, pubPath);
|
|
32948
32988
|
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
|
-
);
|
|
32989
|
+
console.log(`keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`);
|
|
32952
32990
|
console.log(`keygen: pubkey written to ${pubPath}`);
|
|
32953
|
-
console.log(
|
|
32954
|
-
"\nRegister this operator (admin, IAM-gated) so it can join ceremonies:"
|
|
32955
|
-
);
|
|
32991
|
+
console.log("\nRegister this operator (admin, IAM-gated) so it can join ceremonies:");
|
|
32956
32992
|
console.log(
|
|
32957
32993
|
" aws lambda invoke --function-name qos-coordinator-admin-<stack> --cli-binary-format raw-in-base64-out \\"
|
|
32958
32994
|
);
|
|
@@ -32974,7 +33010,12 @@ function resolve(o3) {
|
|
|
32974
33010
|
const nonce = Number(nonceStr);
|
|
32975
33011
|
if (!Number.isInteger(nonce)) throw new Error(`--nonce must be an integer, got ${nonceStr}`);
|
|
32976
33012
|
const alias = req(o3.alias ?? process.env.CEREMONY_ALIAS, "--alias");
|
|
32977
|
-
const secret = o3.yubikey ? { yubikey: true } : {
|
|
33013
|
+
const secret = o3.yubikey ? { yubikey: true } : {
|
|
33014
|
+
secretPath: req(
|
|
33015
|
+
o3.secretPath ?? process.env.CEREMONY_SECRET,
|
|
33016
|
+
"--secret-path / CEREMONY_SECRET"
|
|
33017
|
+
)
|
|
33018
|
+
};
|
|
32978
33019
|
return {
|
|
32979
33020
|
api,
|
|
32980
33021
|
qos,
|
|
@@ -33000,7 +33041,8 @@ async function buildSetDirs(ctx, wd, sets) {
|
|
|
33000
33041
|
const dir = wd.dir(name);
|
|
33001
33042
|
for (const alias of set.members) {
|
|
33002
33043
|
const hex = members[alias];
|
|
33003
|
-
if (!hex)
|
|
33044
|
+
if (!hex)
|
|
33045
|
+
throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
|
|
33004
33046
|
(0, import_node_fs4.writeFileSync)(`${dir}/${alias}.pub`, hex);
|
|
33005
33047
|
}
|
|
33006
33048
|
(0, import_node_fs4.writeFileSync)(`${dir}/quorum_threshold`, `${set.threshold}`);
|
|
@@ -33016,7 +33058,12 @@ async function pullGenesisDir(ctx, wd) {
|
|
|
33016
33058
|
const dir = wd.dir("namespace");
|
|
33017
33059
|
await pullArtifact(ctx, wd, "genesis/genesis_output", "namespace/genesis_output");
|
|
33018
33060
|
await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "namespace/quorum_key.pub").catch(() => "");
|
|
33019
|
-
await pullArtifact(
|
|
33061
|
+
await pullArtifact(
|
|
33062
|
+
ctx,
|
|
33063
|
+
wd,
|
|
33064
|
+
"genesis/genesis_attestation_doc",
|
|
33065
|
+
"namespace/genesis_attestation_doc"
|
|
33066
|
+
).catch(() => "");
|
|
33020
33067
|
return dir;
|
|
33021
33068
|
}
|
|
33022
33069
|
async function fetchState(ctx) {
|
|
@@ -33039,8 +33086,13 @@ async function share(o3) {
|
|
|
33039
33086
|
secret: ctx.secret
|
|
33040
33087
|
});
|
|
33041
33088
|
const shareB64 = wd.readBytes(`${ctx.alias}.share`).toString("base64");
|
|
33042
|
-
await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
|
|
33043
|
-
|
|
33089
|
+
await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
|
|
33090
|
+
secret: ctx.secret,
|
|
33091
|
+
qos: ctx.qos
|
|
33092
|
+
});
|
|
33093
|
+
console.log(
|
|
33094
|
+
`share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
|
|
33095
|
+
);
|
|
33044
33096
|
} finally {
|
|
33045
33097
|
wd.cleanup();
|
|
33046
33098
|
}
|
|
@@ -33053,29 +33105,10 @@ async function approve(o3) {
|
|
|
33053
33105
|
const state2 = await fetchState(ctx);
|
|
33054
33106
|
const wd = new Workdir("ceremony-approve-");
|
|
33055
33107
|
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
|
-
);
|
|
33108
|
+
const manifestPath = await pullArtifact(ctx, wd, "manifest/manifest", "manifest");
|
|
33109
|
+
const pivotHashPath = await pullArtifact(ctx, wd, "manifest/pivot.hash", "pivot.hash");
|
|
33110
|
+
const quorumKeyPath = await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "quorum_key.pub");
|
|
33111
|
+
const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(ctx, wd, state2.sets);
|
|
33079
33112
|
const approvalsDir = wd.dir("approvals");
|
|
33080
33113
|
const json = ctx.qos.display(manifestPath, "manifest");
|
|
33081
33114
|
console.log("\n===== MANIFEST UNDER REVIEW =====");
|
|
@@ -33113,13 +33146,10 @@ async function approve(o3) {
|
|
|
33113
33146
|
const nsFlat = state2.namespace.replace(/\//g, "-");
|
|
33114
33147
|
const approvalFile = `${ctx.alias}-${nsFlat}-${state2.nonce}.approval`;
|
|
33115
33148
|
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
|
-
);
|
|
33149
|
+
await ctx.api.postApproval(ctx.namespace, ctx.nonce, ctx.alias, approvalB64, {
|
|
33150
|
+
secret: ctx.secret,
|
|
33151
|
+
qos: ctx.qos
|
|
33152
|
+
});
|
|
33123
33153
|
console.log(`approve: uploaded approval '${approvalFile}'`);
|
|
33124
33154
|
} finally {
|
|
33125
33155
|
wd.cleanup();
|
|
@@ -33133,8 +33163,18 @@ async function provision(o3) {
|
|
|
33133
33163
|
const wd = new Workdir("ceremony-provision-");
|
|
33134
33164
|
try {
|
|
33135
33165
|
const gen = state2.boot_generation ?? 0;
|
|
33136
|
-
const attestationDocPath = await pullArtifact(
|
|
33137
|
-
|
|
33166
|
+
const attestationDocPath = await pullArtifact(
|
|
33167
|
+
ctx,
|
|
33168
|
+
wd,
|
|
33169
|
+
`provision/g${gen}/attestation_doc`,
|
|
33170
|
+
"attestation_doc"
|
|
33171
|
+
);
|
|
33172
|
+
const manifestEnvelopePath = await pullArtifact(
|
|
33173
|
+
ctx,
|
|
33174
|
+
wd,
|
|
33175
|
+
"manifest/manifest_envelope",
|
|
33176
|
+
"manifest_envelope"
|
|
33177
|
+
);
|
|
33138
33178
|
const sharePath = await pullArtifact(ctx, wd, `shares/${ctx.alias}.share`, `${ctx.alias}.share`);
|
|
33139
33179
|
const { manifestSetDir } = await buildSetDirs(ctx, wd, state2.sets);
|
|
33140
33180
|
const ephWrappedSharePath = wd.path(`${ctx.alias}.eph_share`);
|
|
@@ -33169,7 +33209,8 @@ async function status(o3) {
|
|
|
33169
33209
|
const apiUrl = o3.api ?? process.env.CEREMONY_API;
|
|
33170
33210
|
const namespace = o3.namespace ?? process.env.CEREMONY_NAMESPACE;
|
|
33171
33211
|
const nonceStr = o3.nonce ?? process.env.CEREMONY_NONCE;
|
|
33172
|
-
if (!apiUrl || !namespace || !nonceStr)
|
|
33212
|
+
if (!apiUrl || !namespace || !nonceStr)
|
|
33213
|
+
throw new Error("status requires --api --namespace --nonce");
|
|
33173
33214
|
const api = new CeremonyApi(apiUrl);
|
|
33174
33215
|
const s = await api.getState(namespace, Number(nonceStr));
|
|
33175
33216
|
console.log(`namespace : ${s.namespace}`);
|
|
@@ -33190,7 +33231,9 @@ var import_client_elastic_load_balancing_v2 = __toESM(require_dist_cjs16());
|
|
|
33190
33231
|
function buildWeightedForward(tgs, weights) {
|
|
33191
33232
|
for (const k5 of Object.keys(weights)) {
|
|
33192
33233
|
if (!tgs.some((t) => t.nonce === Number(k5))) {
|
|
33193
|
-
throw new Error(
|
|
33234
|
+
throw new Error(
|
|
33235
|
+
`shift-traffic: weight given for nonce ${k5} but no matching target group was found`
|
|
33236
|
+
);
|
|
33194
33237
|
}
|
|
33195
33238
|
}
|
|
33196
33239
|
if (tgs.length === 0) throw new Error("shift-traffic: no target groups to forward to");
|
|
@@ -33223,16 +33266,21 @@ async function discoverColorTargetGroups(elbv2, service, stack, role) {
|
|
|
33223
33266
|
const tags = Object.fromEntries((desc.Tags ?? []).map((t) => [t.Key, t.Value]));
|
|
33224
33267
|
if (tags.TeeService === service && tags.Stack === stack && tags.Role === role && tags.Nonce) {
|
|
33225
33268
|
const src = batch.find((b5) => b5.arn === desc.ResourceArn);
|
|
33226
|
-
if (src)
|
|
33269
|
+
if (src)
|
|
33270
|
+
matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
|
|
33227
33271
|
}
|
|
33228
33272
|
}
|
|
33229
33273
|
}
|
|
33230
33274
|
if (matched.length === 0) {
|
|
33231
|
-
throw new Error(
|
|
33275
|
+
throw new Error(
|
|
33276
|
+
`shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`
|
|
33277
|
+
);
|
|
33232
33278
|
}
|
|
33233
33279
|
const lbArn = matched[0].lbArn;
|
|
33234
33280
|
if (!matched.every((m3) => m3.lbArn === lbArn)) {
|
|
33235
|
-
throw new Error(
|
|
33281
|
+
throw new Error(
|
|
33282
|
+
`shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`
|
|
33283
|
+
);
|
|
33236
33284
|
}
|
|
33237
33285
|
return { lbArn, tgs: matched };
|
|
33238
33286
|
}
|
|
@@ -33250,7 +33298,11 @@ async function shiftTraffic(opts) {
|
|
|
33250
33298
|
const role = opts.listener ?? "app";
|
|
33251
33299
|
const elbv2 = opts.client ?? new import_client_elastic_load_balancing_v2.ElasticLoadBalancingV2Client(opts.region ? { region: opts.region } : {});
|
|
33252
33300
|
const { lbArn, tgs } = await discoverColorTargetGroups(elbv2, opts.service, opts.stack, role);
|
|
33253
|
-
const listenerArn = await findListenerForTargetGroups(
|
|
33301
|
+
const listenerArn = await findListenerForTargetGroups(
|
|
33302
|
+
elbv2,
|
|
33303
|
+
lbArn,
|
|
33304
|
+
new Set(tgs.map((t) => t.arn))
|
|
33305
|
+
);
|
|
33254
33306
|
const actions = buildWeightedForward(tgs, opts.weights);
|
|
33255
33307
|
await elbv2.send(new import_client_elastic_load_balancing_v2.ModifyListenerCommand({ ListenerArn: listenerArn, DefaultActions: actions }));
|
|
33256
33308
|
return { listenerArn, actions };
|
|
@@ -33264,7 +33316,13 @@ async function shiftTrafficCommand(o3) {
|
|
|
33264
33316
|
const weights = {};
|
|
33265
33317
|
for (const [k5, v] of Object.entries(parsed)) weights[Number(k5)] = v;
|
|
33266
33318
|
const listener = o3.listener ?? "app";
|
|
33267
|
-
const { listenerArn, actions } = await shiftTraffic({
|
|
33319
|
+
const { listenerArn, actions } = await shiftTraffic({
|
|
33320
|
+
service,
|
|
33321
|
+
stack,
|
|
33322
|
+
region: o3.region,
|
|
33323
|
+
listener,
|
|
33324
|
+
weights
|
|
33325
|
+
});
|
|
33268
33326
|
const summary = (actions[0].ForwardConfig?.TargetGroups ?? []).map((t) => `${t.TargetGroupArn?.split("/").slice(-2, -1)[0] ?? t.TargetGroupArn}=${t.Weight}`).join(" ");
|
|
33269
33327
|
console.log(`shift-traffic: ${service}/${stack} ${listener} listener \u2192 ${summary}`);
|
|
33270
33328
|
console.log(` listener: ${listenerArn}`);
|
|
@@ -33272,9 +33330,14 @@ async function shiftTrafficCommand(o3) {
|
|
|
33272
33330
|
|
|
33273
33331
|
// src/cli.ts
|
|
33274
33332
|
var program2 = new Command();
|
|
33275
|
-
program2.name("ceremony").description(
|
|
33333
|
+
program2.name("qos-ceremony").description(
|
|
33334
|
+
"QOS TEE operator ceremony CLI \u2014 your private key never leaves this device; all crypto via qos_client"
|
|
33335
|
+
);
|
|
33276
33336
|
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(
|
|
33337
|
+
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(
|
|
33338
|
+
"--unsafe-eph-path-override <path>",
|
|
33339
|
+
"[dev/mock only] read enclave ephemeral pubkey from this file"
|
|
33340
|
+
);
|
|
33278
33341
|
}
|
|
33279
33342
|
function run(fn) {
|
|
33280
33343
|
return (o3) => fn(o3).catch((e5) => {
|
|
@@ -33282,12 +33345,12 @@ function run(fn) {
|
|
|
33282
33345
|
process.exit(1);
|
|
33283
33346
|
});
|
|
33284
33347
|
}
|
|
33285
|
-
withCommon(
|
|
33286
|
-
|
|
33287
|
-
);
|
|
33288
|
-
withCommon(
|
|
33289
|
-
|
|
33290
|
-
);
|
|
33348
|
+
withCommon(
|
|
33349
|
+
program2.command("keygen").description("generate operator keypair; print pubkey to register (seed stays local)")
|
|
33350
|
+
).action(run(keygen));
|
|
33351
|
+
withCommon(
|
|
33352
|
+
program2.command("share").description("derive quorum-key share from genesis output; upload (encrypted)")
|
|
33353
|
+
).action(run(share));
|
|
33291
33354
|
withCommon(
|
|
33292
33355
|
program2.command("approve").description("review + sign the manifest (manifestSet)").option("--yes", "skip interactive confirm")
|
|
33293
33356
|
).action(run(approve));
|
|
@@ -33295,7 +33358,9 @@ withCommon(
|
|
|
33295
33358
|
program2.command("provision").description("re-encrypt share to the enclave ephemeral key; post it (shareSet)")
|
|
33296
33359
|
).action(run(provision));
|
|
33297
33360
|
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(
|
|
33361
|
+
program2.command("shift-traffic").description(
|
|
33362
|
+
"[ops] blue-green weighted traffic cutover across a service's colors (needs AWS creds)"
|
|
33363
|
+
).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
33364
|
program2.parseAsync(process.argv).catch((e5) => {
|
|
33300
33365
|
console.error(e5);
|
|
33301
33366
|
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.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.",
|
|
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": [
|