@supacloud/admin 0.7.11 → 0.7.12
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 +7 -5
- package/dist/index.js +245 -537
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,11 +49,13 @@ Local artifact transport downloads exact releases directly on the Admin host,
|
|
|
49
49
|
verifies their signed release manifests, checksums, sizes, provenance, source
|
|
50
50
|
commit, and architecture, then transfers them through an atomic SFTP staging
|
|
51
51
|
directory. The server takes root ownership, repeats offline verification, and
|
|
52
|
-
runs the uploaded target Management binary without GitHub
|
|
53
|
-
third-party proxy.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
runs the uploaded target Management binary without GitHub or Sigstore TUF
|
|
53
|
+
egress or a third-party proxy. Admin and Management use the same reviewed,
|
|
54
|
+
digest-pinned Sigstore Public Good trusted root via `--custom-trusted-root`, so
|
|
55
|
+
the offline handoff does not depend on a pre-populated TUF cache. The server
|
|
56
|
+
reuses an installed `gh` only when it supports all required strict attestation
|
|
57
|
+
flags. Otherwise Admin transfers a pinned temporary Linux `gh` verifier that is
|
|
58
|
+
removed with the staging directory and never replaces `/usr/local/bin/gh`.
|
|
57
59
|
|
|
58
60
|
The command supports direct root SSH and passwordless `sudo -n`. The transaction
|
|
59
61
|
runs in a uniquely named transient systemd unit with protected atomic status
|
package/dist/index.js
CHANGED
|
@@ -25703,7 +25703,7 @@ class HttpTransport {
|
|
|
25703
25703
|
|
|
25704
25704
|
// src/shared/tools/ssh-tools.ts
|
|
25705
25705
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25706
|
-
import { dirname } from "node:path";
|
|
25706
|
+
import { basename as basename3, dirname, join as join2 } from "node:path";
|
|
25707
25707
|
|
|
25708
25708
|
// src/shared/releases/local-upgrade-transfer.ts
|
|
25709
25709
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -25735,10 +25735,10 @@ function quoteShell(shellText) {
|
|
|
25735
25735
|
|
|
25736
25736
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
25737
25737
|
import { spawn } from "node:child_process";
|
|
25738
|
-
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
25739
|
-
import { accessSync, chmodSync, constants as fsConstants, createWriteStream, mkdirSync, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync, writeFileSync } from "node:fs";
|
|
25738
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
25739
|
+
import { accessSync, chmodSync as chmodSync2, constants as fsConstants, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
25740
25740
|
import { get } from "node:https";
|
|
25741
|
-
import { tmpdir } from "node:os";
|
|
25741
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
25742
25742
|
import { basename, delimiter, join } from "node:path";
|
|
25743
25743
|
import { pipeline } from "node:stream/promises";
|
|
25744
25744
|
import { Transform as Transform2 } from "node:stream";
|
|
@@ -25905,6 +25905,97 @@ function parseReleaseChecksums(text, manifest) {
|
|
|
25905
25905
|
return checksums;
|
|
25906
25906
|
}
|
|
25907
25907
|
|
|
25908
|
+
// ../management-api/src/sigstore-trusted-root.ts
|
|
25909
|
+
import { createHash } from "node:crypto";
|
|
25910
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25911
|
+
import { tmpdir } from "node:os";
|
|
25912
|
+
import path from "node:path";
|
|
25913
|
+
|
|
25914
|
+
// ../management-api/src/assets/sigstore-public-good-trusted-root.jsonl
|
|
25915
|
+
var sigstore_public_good_trusted_root_default = '{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1","tlogs":[{"baseUrl":"https://rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-01-12T11:53:27Z"}},"logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}},{"baseUrl":"https://log2025-1.rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=","keyDetails":"PKIX_ED25519","validFor":{"start":"2025-09-23T00:00:00Z"}},"logId":{"keyId":"zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="}}],"certificateAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="}]},"validFor":{"start":"2021-03-07T03:20:29Z","end":"2022-12-31T23:59:59.999Z"}},{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"}]},"validFor":{"start":"2022-04-13T20:06:15Z"}}],"ctlogs":[{"baseUrl":"https://ctfe.sigstore.dev/test","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-03-14T00:00:00Z","end":"2022-10-31T23:59:59.999Z"}},"logId":{"keyId":"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="}},{"baseUrl":"https://ctfe.sigstore.dev/2022","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2022-10-20T00:00:00Z"}},"logId":{"keyId":"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="}}],"timestampAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore-tsa-selfsigned"},"uri":"https://timestamp.sigstore.dev/api/v1/timestamp","certChain":{"certificates":[{"rawBytes":"MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"}]},"validFor":{"start":"2025-07-04T00:00:00Z"}}]}\n';
|
|
25916
|
+
|
|
25917
|
+
// ../management-api/src/sigstore-trusted-root.ts
|
|
25918
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME = "trusted_root.jsonl";
|
|
25919
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256 = "3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1";
|
|
25920
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE = 5748;
|
|
25921
|
+
function sha256(text) {
|
|
25922
|
+
return createHash("sha256").update(text).digest("hex");
|
|
25923
|
+
}
|
|
25924
|
+
function validateEmbeddedTrustedRoot(jsonl) {
|
|
25925
|
+
if (Buffer.byteLength(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
|
|
25926
|
+
throw new Error("Embedded Sigstore Public Good trusted root does not match its pinned digest");
|
|
25927
|
+
}
|
|
25928
|
+
if (!jsonl.endsWith(`
|
|
25929
|
+
`) || jsonl.slice(0, -1).includes(`
|
|
25930
|
+
`)) {
|
|
25931
|
+
throw new Error("Embedded Sigstore Public Good trusted root must be compact JSONL");
|
|
25932
|
+
}
|
|
25933
|
+
let trustedRoot;
|
|
25934
|
+
try {
|
|
25935
|
+
trustedRoot = JSON.parse(jsonl);
|
|
25936
|
+
} catch {
|
|
25937
|
+
throw new Error("Embedded Sigstore Public Good trusted root is not valid JSON");
|
|
25938
|
+
}
|
|
25939
|
+
if (!trustedRoot || typeof trustedRoot !== "object" || Array.isArray(trustedRoot) || `${JSON.stringify(trustedRoot)}
|
|
25940
|
+
` !== jsonl) {
|
|
25941
|
+
throw new Error("Embedded Sigstore Public Good trusted root is not canonical compact JSONL");
|
|
25942
|
+
}
|
|
25943
|
+
return jsonl;
|
|
25944
|
+
}
|
|
25945
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL = validateEmbeddedTrustedRoot(sigstore_public_good_trusted_root_default);
|
|
25946
|
+
function createSigstoreVerificationDirectory() {
|
|
25947
|
+
const directory = mkdtempSync(path.join(tmpdir(), "supacloud-sigstore-"));
|
|
25948
|
+
try {
|
|
25949
|
+
chmodSync(directory, 448);
|
|
25950
|
+
const trustedRootPath = path.join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
25951
|
+
writeFileSync(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, {
|
|
25952
|
+
flag: "wx",
|
|
25953
|
+
mode: 384
|
|
25954
|
+
});
|
|
25955
|
+
chmodSync(trustedRootPath, 384);
|
|
25956
|
+
return { directory, trustedRootPath };
|
|
25957
|
+
} catch (operationError) {
|
|
25958
|
+
try {
|
|
25959
|
+
rmSync(directory, { recursive: true });
|
|
25960
|
+
} catch (cleanupError) {
|
|
25961
|
+
throw new AggregateError([operationError, cleanupError], "Sigstore trusted root creation failed and temporary directory cleanup did not complete");
|
|
25962
|
+
}
|
|
25963
|
+
throw operationError;
|
|
25964
|
+
}
|
|
25965
|
+
}
|
|
25966
|
+
function throwVerificationDirectoryOutcome(outcome) {
|
|
25967
|
+
if (outcome.operationFailed && outcome.cleanupFailed) {
|
|
25968
|
+
throw new AggregateError([outcome.operationError, outcome.cleanupError], "Sigstore verification failed and temporary trusted root cleanup did not complete");
|
|
25969
|
+
}
|
|
25970
|
+
if (outcome.operationFailed)
|
|
25971
|
+
throw outcome.operationError;
|
|
25972
|
+
if (outcome.cleanupFailed) {
|
|
25973
|
+
throw new AggregateError([outcome.cleanupError], "Temporary Sigstore trusted root cleanup did not complete");
|
|
25974
|
+
}
|
|
25975
|
+
}
|
|
25976
|
+
async function withSigstoreVerificationDirectory(operation) {
|
|
25977
|
+
const resources = createSigstoreVerificationDirectory();
|
|
25978
|
+
let operationOutput;
|
|
25979
|
+
let operationFailed = false;
|
|
25980
|
+
let operationError;
|
|
25981
|
+
try {
|
|
25982
|
+
operationOutput = await operation(resources);
|
|
25983
|
+
} catch (error) {
|
|
25984
|
+
operationFailed = true;
|
|
25985
|
+
operationError = error;
|
|
25986
|
+
}
|
|
25987
|
+
let cleanupFailed = false;
|
|
25988
|
+
let cleanupError;
|
|
25989
|
+
try {
|
|
25990
|
+
rmSync(resources.directory, { recursive: true });
|
|
25991
|
+
} catch (error) {
|
|
25992
|
+
cleanupFailed = true;
|
|
25993
|
+
cleanupError = error;
|
|
25994
|
+
}
|
|
25995
|
+
throwVerificationDirectoryOutcome({ operationFailed, operationError, cleanupFailed, cleanupError });
|
|
25996
|
+
return operationOutput;
|
|
25997
|
+
}
|
|
25998
|
+
|
|
25908
25999
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
25909
26000
|
var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
|
|
25910
26001
|
var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
|
|
@@ -25938,7 +26029,8 @@ var STRICT_GITHUB_CAPABILITY_FLAGS = [
|
|
|
25938
26029
|
"--signer-workflow",
|
|
25939
26030
|
"--source-ref",
|
|
25940
26031
|
"--source-digest",
|
|
25941
|
-
"--deny-self-hosted-runners"
|
|
26032
|
+
"--deny-self-hosted-runners",
|
|
26033
|
+
"--custom-trusted-root"
|
|
25942
26034
|
];
|
|
25943
26035
|
|
|
25944
26036
|
class RetryableDownloadError extends Error {
|
|
@@ -25953,7 +26045,56 @@ function githubCliArchiveIdentity(architecture) {
|
|
|
25953
26045
|
};
|
|
25954
26046
|
}
|
|
25955
26047
|
function sha256File(filePath) {
|
|
25956
|
-
return
|
|
26048
|
+
return createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
|
|
26049
|
+
}
|
|
26050
|
+
function effectiveLocalOwner() {
|
|
26051
|
+
if (process.platform === "win32")
|
|
26052
|
+
return;
|
|
26053
|
+
const uid = process.geteuid?.() ?? process.getuid?.();
|
|
26054
|
+
const gid = process.getegid?.() ?? process.getgid?.();
|
|
26055
|
+
if (uid === undefined || gid === undefined) {
|
|
26056
|
+
throw new Error("Local artifact transport requires POSIX ownership checks");
|
|
26057
|
+
}
|
|
26058
|
+
return { uid, gid };
|
|
26059
|
+
}
|
|
26060
|
+
function assertLocalOwner(uid, gid, label) {
|
|
26061
|
+
const owner = effectiveLocalOwner();
|
|
26062
|
+
if (!owner)
|
|
26063
|
+
return;
|
|
26064
|
+
if (uid !== owner.uid || gid !== owner.gid) {
|
|
26065
|
+
throw new Error(`${label} must be owned by the current effective user`);
|
|
26066
|
+
}
|
|
26067
|
+
}
|
|
26068
|
+
function assertLocalTrustedRootDirectory(directory) {
|
|
26069
|
+
const directoryStats = lstatSync(directory);
|
|
26070
|
+
if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
|
|
26071
|
+
throw new Error("Local trusted-root directory must be a directory without links");
|
|
26072
|
+
}
|
|
26073
|
+
assertPrivateLocalDirectoryMode(directoryStats.mode, "Local trusted-root directory");
|
|
26074
|
+
assertLocalOwner(directoryStats.uid, directoryStats.gid, "Local trusted-root directory");
|
|
26075
|
+
const entries = readdirSync(directory);
|
|
26076
|
+
if (entries.length !== 1 || entries[0] !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME) {
|
|
26077
|
+
throw new Error("Local trusted-root directory does not match its strict file allowlist");
|
|
26078
|
+
}
|
|
26079
|
+
const trustedRootPath = join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
26080
|
+
assertLocalTrustedRootFile(trustedRootPath);
|
|
26081
|
+
return trustedRootPath;
|
|
26082
|
+
}
|
|
26083
|
+
function assertPrivateLocalDirectoryMode(mode, label) {
|
|
26084
|
+
if (process.platform !== "win32" && (mode & 4095) !== 448) {
|
|
26085
|
+
throw new Error(`${label} must use exact mode 0700 without special permission bits`);
|
|
26086
|
+
}
|
|
26087
|
+
}
|
|
26088
|
+
function assertLocalTrustedRootFile(trustedRootPath) {
|
|
26089
|
+
const stats = lstatSync(trustedRootPath);
|
|
26090
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
26091
|
+
throw new Error("Local trusted root must be a direct regular file without links");
|
|
26092
|
+
}
|
|
26093
|
+
assertPrivateLocalFileMode(stats.mode, "Local trusted root");
|
|
26094
|
+
assertLocalOwner(stats.uid, stats.gid, "Local trusted root");
|
|
26095
|
+
if (stats.size !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256File(trustedRootPath) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
|
|
26096
|
+
throw new Error("Local trusted root does not match its pinned size and digest");
|
|
26097
|
+
}
|
|
25957
26098
|
}
|
|
25958
26099
|
function assertPrivateLocalFileMode(mode, label) {
|
|
25959
26100
|
if (process.platform !== "win32" && (mode & 4095) !== 384) {
|
|
@@ -26055,10 +26196,10 @@ async function downloadDirect(url, destination, maxBytes) {
|
|
|
26055
26196
|
const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
|
|
26056
26197
|
const retryFailures = [];
|
|
26057
26198
|
for (let attempt = 1;attempt <= 3; attempt += 1) {
|
|
26058
|
-
|
|
26199
|
+
rmSync2(destination, { force: true });
|
|
26059
26200
|
try {
|
|
26060
26201
|
await downloadHttpsResponse({ url: parsed, destination, maxBytes, redirects: 0, deadline });
|
|
26061
|
-
|
|
26202
|
+
chmodSync2(destination, 384);
|
|
26062
26203
|
return;
|
|
26063
26204
|
} catch (error) {
|
|
26064
26205
|
if (!downloadCanRetry(error))
|
|
@@ -26214,13 +26355,13 @@ async function assertLocalGithubVerifier() {
|
|
|
26214
26355
|
throw new Error("Local artifact transport requires a current gh attestation verifier");
|
|
26215
26356
|
}
|
|
26216
26357
|
}
|
|
26217
|
-
|
|
26218
|
-
|
|
26358
|
+
function githubAttestationVerificationArguments(request) {
|
|
26359
|
+
return [
|
|
26219
26360
|
"attestation",
|
|
26220
26361
|
"verify",
|
|
26221
|
-
|
|
26362
|
+
request.artifactPath,
|
|
26222
26363
|
"--bundle",
|
|
26223
|
-
bundlePath,
|
|
26364
|
+
request.bundlePath,
|
|
26224
26365
|
"--repo",
|
|
26225
26366
|
RELEASE_REPOSITORY,
|
|
26226
26367
|
"--signer-workflow",
|
|
@@ -26228,9 +26369,14 @@ async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
|
|
|
26228
26369
|
"--source-ref",
|
|
26229
26370
|
RELEASE_SOURCE_REF,
|
|
26230
26371
|
"--source-digest",
|
|
26231
|
-
manifest.source.commit,
|
|
26232
|
-
"--deny-self-hosted-runners"
|
|
26233
|
-
|
|
26372
|
+
request.manifest.source.commit,
|
|
26373
|
+
"--deny-self-hosted-runners",
|
|
26374
|
+
"--custom-trusted-root",
|
|
26375
|
+
request.trustedRootPath
|
|
26376
|
+
];
|
|
26377
|
+
}
|
|
26378
|
+
async function verifyManifestAttestation(request) {
|
|
26379
|
+
const verification = await runGithubCli(githubAttestationVerificationArguments(request), GH_VERIFICATION_TIMEOUT_MS);
|
|
26234
26380
|
if (verification.exitCode !== 0) {
|
|
26235
26381
|
throw new Error(`gh attestation verify failed: ${verification.stderr.trim().slice(-1000) || verification.exitCode}`);
|
|
26236
26382
|
}
|
|
@@ -26261,7 +26407,7 @@ async function downloadReleaseMetadata(component, version, directory) {
|
|
|
26261
26407
|
await downloadDirect(`${RELEASES_API}/tags/${expectedTag}`, metadataPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
|
|
26262
26408
|
return parseGithubReleaseMetadata(parseJsonFile(metadataPath, "GitHub release metadata"), expectedTag);
|
|
26263
26409
|
} finally {
|
|
26264
|
-
|
|
26410
|
+
rmSync2(metadataPath, { force: true });
|
|
26265
26411
|
}
|
|
26266
26412
|
}
|
|
26267
26413
|
async function downloadManifestAttestation(manifestPath, destination) {
|
|
@@ -26269,10 +26415,10 @@ async function downloadManifestAttestation(manifestPath, destination) {
|
|
|
26269
26415
|
try {
|
|
26270
26416
|
await downloadDirect(`${ATTESTATIONS_API}/sha256:${sha256File(manifestPath)}`, responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
|
|
26271
26417
|
const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
|
|
26272
|
-
|
|
26273
|
-
|
|
26418
|
+
writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
|
|
26419
|
+
chmodSync2(destination, 384);
|
|
26274
26420
|
} finally {
|
|
26275
|
-
|
|
26421
|
+
rmSync2(responsePath, { force: true });
|
|
26276
26422
|
}
|
|
26277
26423
|
}
|
|
26278
26424
|
async function downloadComponent(request) {
|
|
@@ -26282,7 +26428,12 @@ async function downloadComponent(request) {
|
|
|
26282
26428
|
await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
|
|
26283
26429
|
const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
|
|
26284
26430
|
await downloadManifestAttestation(manifestPath, attestationPath);
|
|
26285
|
-
await verifyManifestAttestation(
|
|
26431
|
+
await verifyManifestAttestation({
|
|
26432
|
+
artifactPath: manifestPath,
|
|
26433
|
+
bundlePath: attestationPath,
|
|
26434
|
+
manifest,
|
|
26435
|
+
trustedRootPath: request.trustedRootPath
|
|
26436
|
+
});
|
|
26286
26437
|
const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
|
|
26287
26438
|
await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
|
|
26288
26439
|
assertSignedArtifact(checksumsPath, manifest);
|
|
@@ -26305,7 +26456,7 @@ async function downloadPinnedGithubCli(directory, architecture) {
|
|
|
26305
26456
|
return localUpgradeFile(archivePath, `verifier/${identity.archiveName}`);
|
|
26306
26457
|
}
|
|
26307
26458
|
function cleanupLocalUpgradeBundle(bundle) {
|
|
26308
|
-
|
|
26459
|
+
rmSync2(bundle.directory, { recursive: true, force: true });
|
|
26309
26460
|
}
|
|
26310
26461
|
function assertLocalUpgradeBundleSize(files) {
|
|
26311
26462
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
@@ -26342,15 +26493,15 @@ async function settleLocalBundleDownloads(downloads) {
|
|
|
26342
26493
|
];
|
|
26343
26494
|
}
|
|
26344
26495
|
function createLocalBundleLayout(request) {
|
|
26345
|
-
const directory =
|
|
26346
|
-
|
|
26496
|
+
const directory = mkdtempSync2(join(tmpdir2(), "supacloud-admin-upgrade-"));
|
|
26497
|
+
chmodSync2(directory, 448);
|
|
26347
26498
|
const bundleDirectory = join(directory, "bundle");
|
|
26348
26499
|
const managementDirectory = join(bundleDirectory, "management-api");
|
|
26349
26500
|
const edgeDirectory = join(bundleDirectory, "edge-runtime");
|
|
26350
26501
|
const verifierDirectory = join(directory, "verifier");
|
|
26351
26502
|
for (const createdDirectory of [bundleDirectory, managementDirectory, edgeDirectory, verifierDirectory]) {
|
|
26352
26503
|
mkdirSync(createdDirectory, { recursive: true, mode: 448 });
|
|
26353
|
-
|
|
26504
|
+
chmodSync2(createdDirectory, 448);
|
|
26354
26505
|
}
|
|
26355
26506
|
return {
|
|
26356
26507
|
directory,
|
|
@@ -26361,20 +26512,22 @@ function createLocalBundleLayout(request) {
|
|
|
26361
26512
|
edgeRuntimeBinaryName: `supacloud-edge-runtime-linux-${request.architecture}`
|
|
26362
26513
|
};
|
|
26363
26514
|
}
|
|
26364
|
-
async function downloadLocalBundle(request, layout) {
|
|
26515
|
+
async function downloadLocalBundle(request, layout, trustedRootPath) {
|
|
26365
26516
|
const { managementBinaryName, edgeRuntimeBinaryName } = layout;
|
|
26366
26517
|
const [managementFiles, edgeFiles, verifierArchive] = await settleLocalBundleDownloads([
|
|
26367
26518
|
downloadComponent({
|
|
26368
26519
|
component: "management-api",
|
|
26369
26520
|
version: request.managementVersion,
|
|
26370
26521
|
assetNames: [managementBinaryName, "web-console-build.tar.gz"],
|
|
26371
|
-
destination: layout.managementDirectory
|
|
26522
|
+
destination: layout.managementDirectory,
|
|
26523
|
+
trustedRootPath
|
|
26372
26524
|
}),
|
|
26373
26525
|
downloadComponent({
|
|
26374
26526
|
component: "edge-runtime",
|
|
26375
26527
|
version: request.edgeRuntimeVersion,
|
|
26376
26528
|
assetNames: [edgeRuntimeBinaryName],
|
|
26377
|
-
destination: layout.edgeDirectory
|
|
26529
|
+
destination: layout.edgeDirectory,
|
|
26530
|
+
trustedRootPath
|
|
26378
26531
|
}),
|
|
26379
26532
|
request.verifierProvisioning === "bundled" ? downloadPinnedGithubCli(layout.verifierDirectory, request.architecture) : Promise.resolve(null)
|
|
26380
26533
|
]);
|
|
@@ -26388,9 +26541,16 @@ async function prepareLocalUpgradeBundle(request) {
|
|
|
26388
26541
|
await assertLocalGithubVerifier();
|
|
26389
26542
|
const layout = createLocalBundleLayout(request);
|
|
26390
26543
|
try {
|
|
26391
|
-
return await
|
|
26544
|
+
return await withSigstoreVerificationDirectory(async (verificationDirectory) => {
|
|
26545
|
+
const trustedRootPath = assertLocalTrustedRootDirectory(verificationDirectory.directory);
|
|
26546
|
+
return await downloadLocalBundle(request, layout, trustedRootPath);
|
|
26547
|
+
});
|
|
26392
26548
|
} catch (error) {
|
|
26393
|
-
|
|
26549
|
+
try {
|
|
26550
|
+
rmSync2(layout.directory, { recursive: true, force: true });
|
|
26551
|
+
} catch (cleanupError) {
|
|
26552
|
+
throw new AggregateError([error, cleanupError], "Local bundle preparation failed and cleanup did not complete");
|
|
26553
|
+
}
|
|
26394
26554
|
throw error;
|
|
26395
26555
|
}
|
|
26396
26556
|
}
|
|
@@ -26606,7 +26766,7 @@ function upgradeScriptSetup(paths) {
|
|
|
26606
26766
|
"write_status RUNNING",
|
|
26607
26767
|
buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
|
|
26608
26768
|
`while IFS='=' read -r variable _; do case "$variable" in *PROXY*|*Proxy*|*proxy*) unset "$variable" ;; esac; done < <(env)`,
|
|
26609
|
-
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW NODE_USE_ENV_PROXY"
|
|
26769
|
+
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT NODE_USE_ENV_PROXY"
|
|
26610
26770
|
];
|
|
26611
26771
|
}
|
|
26612
26772
|
function upgradeScriptFilesystemChecks(bundle) {
|
|
@@ -27106,493 +27266,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
|
|
|
27106
27266
|
}
|
|
27107
27267
|
|
|
27108
27268
|
// ../../scripts/lib/release_assets.sh
|
|
27109
|
-
var release_assets_default = `#!/usr/bin/env bash
|
|
27110
|
-
|
|
27111
|
-
SUPACLOUD_GITHUB_REPOSITORY="\${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"
|
|
27112
|
-
SUPACLOUD_RELEASES_API="\${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/releases}"
|
|
27113
|
-
SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="\${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-\${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"
|
|
27114
|
-
SUPACLOUD_GH_VERSION="\${SUPACLOUD_GH_VERSION:-2.96.0}"
|
|
27115
|
-
SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"
|
|
27116
|
-
SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
|
|
27117
|
-
SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
|
|
27118
|
-
|
|
27119
|
-
supacloud_curl_release_json() {
|
|
27120
|
-
local url="$1"
|
|
27121
|
-
local output="$2"
|
|
27122
|
-
curl -fsSL --proto '=https' --proto-redir '=https' \\
|
|
27123
|
-
--retry 1 --retry-delay 2 --retry-max-time 60 \\
|
|
27124
|
-
--connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\
|
|
27125
|
-
-o "$output" "$url"
|
|
27126
|
-
}
|
|
27127
|
-
|
|
27128
|
-
supacloud_curl_release_asset() {
|
|
27129
|
-
local url="$1"
|
|
27130
|
-
local output="$2"
|
|
27131
|
-
curl -fL --proto '=https' --proto-redir '=https' \\
|
|
27132
|
-
--retry 1 --retry-delay 2 --retry-max-time 180 \\
|
|
27133
|
-
--connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\
|
|
27134
|
-
-o "$output" "$url"
|
|
27135
|
-
}
|
|
27136
|
-
|
|
27137
|
-
supacloud_component_tag() {
|
|
27138
|
-
local component="$1"
|
|
27139
|
-
local version="$2"
|
|
27140
|
-
case "$version" in
|
|
27141
|
-
"\${component}-v"*) printf '%s' "$version" ;;
|
|
27142
|
-
v*) printf '%s-%s' "$component" "$version" ;;
|
|
27143
|
-
*) printf '%s-v%s' "$component" "$version" ;;
|
|
27144
|
-
esac
|
|
27145
|
-
}
|
|
27146
|
-
|
|
27147
|
-
supacloud_select_release() {
|
|
27148
|
-
local component="$1"
|
|
27149
|
-
shift
|
|
27150
|
-
local required_assets_json
|
|
27151
|
-
[[ $# -gt 0 ]] || {
|
|
27152
|
-
echo "at least one required release asset must be specified" >&2
|
|
27153
|
-
return 1
|
|
27154
|
-
}
|
|
27155
|
-
required_assets_json=$(printf '%s\\n' "$@" | jq -Rsc 'split("\\n")[:-1]')
|
|
27156
|
-
jq -ce --arg prefix "\${component}-v" --argjson required "$required_assets_json" '
|
|
27157
|
-
map(select(
|
|
27158
|
-
(.draft | not)
|
|
27159
|
-
and (.prerelease | not)
|
|
27160
|
-
and (.tag_name | startswith($prefix))
|
|
27161
|
-
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
27162
|
-
and any(.assets[]?; .name == "SHA256SUMS")
|
|
27163
|
-
))
|
|
27164
|
-
| first
|
|
27165
|
-
// error("no matching component release contains all required assets and SHA256SUMS")
|
|
27166
|
-
'
|
|
27167
|
-
}
|
|
27168
|
-
|
|
27169
|
-
supacloud_fetch_component_release() {
|
|
27170
|
-
local component="$1"
|
|
27171
|
-
local version="\${2:-latest}"
|
|
27172
|
-
shift 2
|
|
27173
|
-
local required_assets=("$@")
|
|
27174
|
-
local required_assets_json
|
|
27175
|
-
local response
|
|
27176
|
-
[[ \${#required_assets[@]} -gt 0 ]] || {
|
|
27177
|
-
echo "at least one required release asset must be specified" >&2
|
|
27178
|
-
return 1
|
|
27179
|
-
}
|
|
27180
|
-
required_assets_json=$(printf '%s\\n' "\${required_assets[@]}" | jq -Rsc 'split("\\n")[:-1]')
|
|
27181
|
-
|
|
27182
|
-
if [[ -n "$version" && "$version" != "latest" ]]; then
|
|
27183
|
-
local tag
|
|
27184
|
-
tag=$(supacloud_component_tag "$component" "$version")
|
|
27185
|
-
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}/tags/\${tag}") || return 1
|
|
27186
|
-
jq -ce --argjson required "$required_assets_json" '
|
|
27187
|
-
select(
|
|
27188
|
-
(.draft | not)
|
|
27189
|
-
and (.prerelease | not)
|
|
27190
|
-
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
27191
|
-
and any(.assets[]?; .name == "SHA256SUMS")
|
|
27192
|
-
)
|
|
27193
|
-
// error("release does not contain all required assets and SHA256SUMS")
|
|
27194
|
-
' <<< "$response"
|
|
27195
|
-
return
|
|
27196
|
-
fi
|
|
27197
|
-
|
|
27198
|
-
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}?per_page=100") || return 1
|
|
27199
|
-
supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
|
|
27200
|
-
}
|
|
27201
|
-
|
|
27202
|
-
supacloud_fetch_release_json() (
|
|
27203
|
-
local url="$1"
|
|
27204
|
-
local response_file
|
|
27205
|
-
response_file=$(mktemp) || return 1
|
|
27206
|
-
trap 'rm -f "$response_file"' EXIT
|
|
27207
|
-
trap 'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1' HUP INT TERM
|
|
27208
|
-
supacloud_download_release_metadata_url "$url" "$response_file" || return 1
|
|
27209
|
-
cat "$response_file"
|
|
27210
|
-
)
|
|
27211
|
-
|
|
27212
|
-
supacloud_release_asset_url() {
|
|
27213
|
-
local release_json="$1"
|
|
27214
|
-
local asset_name="$2"
|
|
27215
|
-
jq -er --arg asset "$asset_name" '
|
|
27216
|
-
first(.assets[]? | select(.name == $asset) | .browser_download_url)
|
|
27217
|
-
// error("release asset URL is missing")
|
|
27218
|
-
' <<< "$release_json"
|
|
27219
|
-
}
|
|
27220
|
-
|
|
27221
|
-
supacloud_download_url() {
|
|
27222
|
-
local url="$1"
|
|
27223
|
-
local output="$2"
|
|
27224
|
-
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
27225
|
-
|
|
27226
|
-
if supacloud_curl_release_asset "$url" "$output"; then
|
|
27227
|
-
return 0
|
|
27228
|
-
fi
|
|
27229
|
-
if [[ -n "$proxy" ]]; then
|
|
27230
|
-
supacloud_curl_release_asset "\${proxy%/}/\${url}" "$output"
|
|
27231
|
-
return
|
|
27232
|
-
fi
|
|
27233
|
-
return 1
|
|
27234
|
-
}
|
|
27235
|
-
|
|
27236
|
-
supacloud_download_release_metadata_url() {
|
|
27237
|
-
local url="$1"
|
|
27238
|
-
local output="$2"
|
|
27239
|
-
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
27240
|
-
|
|
27241
|
-
if supacloud_curl_release_json "$url" "$output"; then
|
|
27242
|
-
return 0
|
|
27243
|
-
fi
|
|
27244
|
-
if [[ -n "$proxy" ]]; then
|
|
27245
|
-
supacloud_curl_release_json "\${proxy%/}/\${url}" "$output"
|
|
27246
|
-
return
|
|
27247
|
-
fi
|
|
27248
|
-
return 1
|
|
27249
|
-
}
|
|
27250
|
-
|
|
27251
|
-
supacloud_verify_checksum() {
|
|
27252
|
-
local artifact_file="$1"
|
|
27253
|
-
local asset_name="$2"
|
|
27254
|
-
local checksum_file="$3"
|
|
27255
|
-
local expected
|
|
27256
|
-
expected=$(awk -v asset="$asset_name" '$2 == asset || $2 == "*" asset { print $1; exit }' "$checksum_file")
|
|
27257
|
-
if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
|
27258
|
-
echo "SHA256SUMS does not contain a valid checksum for \${asset_name}" >&2
|
|
27259
|
-
return 1
|
|
27260
|
-
fi
|
|
27261
|
-
|
|
27262
|
-
local actual
|
|
27263
|
-
actual=$(sha256sum "$artifact_file" | awk '{print $1}')
|
|
27264
|
-
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
|
|
27265
|
-
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
|
|
27266
|
-
if [[ "$actual" != "$expected" ]]; then
|
|
27267
|
-
echo "SHA256 mismatch for \${asset_name}" >&2
|
|
27268
|
-
return 1
|
|
27269
|
-
fi
|
|
27270
|
-
}
|
|
27271
|
-
|
|
27272
|
-
supacloud_validate_binary() {
|
|
27273
|
-
local artifact_file="$1"
|
|
27274
|
-
local asset_name="$2"
|
|
27275
|
-
local description
|
|
27276
|
-
description=$(file -b "$artifact_file")
|
|
27277
|
-
if [[ "$description" != *ELF* ]]; then
|
|
27278
|
-
echo "\${asset_name} is not an ELF binary: \${description}" >&2
|
|
27279
|
-
return 1
|
|
27280
|
-
fi
|
|
27281
|
-
|
|
27282
|
-
case "$asset_name" in
|
|
27283
|
-
*amd64)
|
|
27284
|
-
[[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {
|
|
27285
|
-
echo "\${asset_name} does not contain an x86-64 ELF binary" >&2
|
|
27286
|
-
return 1
|
|
27287
|
-
}
|
|
27288
|
-
;;
|
|
27289
|
-
*arm64)
|
|
27290
|
-
[[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {
|
|
27291
|
-
echo "\${asset_name} does not contain an arm64 ELF binary" >&2
|
|
27292
|
-
return 1
|
|
27293
|
-
}
|
|
27294
|
-
;;
|
|
27295
|
-
esac
|
|
27296
|
-
}
|
|
27297
|
-
|
|
27298
|
-
supacloud_install_pinned_tar_xz_binary() (
|
|
27299
|
-
local archive="$1"
|
|
27300
|
-
local member="$2"
|
|
27301
|
-
local expected_sha256="$3"
|
|
27302
|
-
local arch="$4"
|
|
27303
|
-
local target="$5"
|
|
27304
|
-
local actual_sha256 member_count member_details extract_dir candidate staged_target
|
|
27305
|
-
|
|
27306
|
-
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
27307
|
-
actual_sha256=$(printf '%s' "$actual_sha256" | tr '[:upper:]' '[:lower:]')
|
|
27308
|
-
expected_sha256=$(printf '%s' "$expected_sha256" | tr '[:upper:]' '[:lower:]')
|
|
27309
|
-
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then
|
|
27310
|
-
echo "SHA256 mismatch for pinned archive" >&2
|
|
27311
|
-
return 1
|
|
27312
|
-
fi
|
|
27313
|
-
|
|
27314
|
-
member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)
|
|
27315
|
-
if [[ "$member_count" != "1" ]]; then
|
|
27316
|
-
echo "Pinned archive must contain the exact member once: $member" >&2
|
|
27317
|
-
return 1
|
|
27318
|
-
fi
|
|
27319
|
-
member_details=$(tar -tvJf "$archive" "$member") || return 1
|
|
27320
|
-
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
27321
|
-
echo "Pinned archive member is not a regular file: $member" >&2
|
|
27322
|
-
return 1
|
|
27323
|
-
fi
|
|
27324
|
-
|
|
27325
|
-
extract_dir=$(mktemp -d)
|
|
27326
|
-
trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT
|
|
27327
|
-
trap 'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"; exit 1' HUP INT TERM
|
|
27328
|
-
if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
|
|
27329
|
-
return 1
|
|
27330
|
-
fi
|
|
27331
|
-
candidate="\${extract_dir}/\${member}"
|
|
27332
|
-
supacloud_validate_binary "$candidate" "pinned-linux-\${arch}" || return 1
|
|
27333
|
-
|
|
27334
|
-
mkdir -p "$(dirname "$target")"
|
|
27335
|
-
staged_target=$(mktemp "\${target}.tmp.XXXXXX")
|
|
27336
|
-
install -m 0755 "$candidate" "$staged_target"
|
|
27337
|
-
mv -f "$staged_target" "$target"
|
|
27338
|
-
staged_target=""
|
|
27339
|
-
)
|
|
27340
|
-
|
|
27341
|
-
supacloud_version_at_least() {
|
|
27342
|
-
local current="\${1#v}"
|
|
27343
|
-
local required="\${2#v}"
|
|
27344
|
-
local current_major=0 current_minor=0 current_patch=0
|
|
27345
|
-
local required_major=0 required_minor=0 required_patch=0
|
|
27346
|
-
[[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
27347
|
-
[[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
27348
|
-
IFS=. read -r current_major current_minor current_patch <<EOF
|
|
27349
|
-
\${current%%-*}
|
|
27350
|
-
EOF
|
|
27351
|
-
IFS=. read -r required_major required_minor required_patch <<EOF
|
|
27352
|
-
\${required%%-*}
|
|
27353
|
-
EOF
|
|
27354
|
-
current_major=\${current_major:-0}; current_minor=\${current_minor:-0}; current_patch=\${current_patch:-0}
|
|
27355
|
-
required_major=\${required_major:-0}; required_minor=\${required_minor:-0}; required_patch=\${required_patch:-0}
|
|
27356
|
-
if (( current_major != required_major )); then (( current_major > required_major )); return; fi
|
|
27357
|
-
if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi
|
|
27358
|
-
(( current_patch >= required_patch ))
|
|
27359
|
-
}
|
|
27360
|
-
|
|
27361
|
-
supacloud_gh_version() {
|
|
27362
|
-
gh --version 2>/dev/null | awk 'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }'
|
|
27363
|
-
}
|
|
27364
|
-
|
|
27365
|
-
supacloud_install_gh_archive() {
|
|
27366
|
-
local archive="$1"
|
|
27367
|
-
local version="$2"
|
|
27368
|
-
local arch="$3"
|
|
27369
|
-
local expected_sha256="$4"
|
|
27370
|
-
local target="$5"
|
|
27371
|
-
local member="gh_\${version}_linux_\${arch}/bin/gh"
|
|
27372
|
-
local actual_sha256 member_count member_details extracted_dir candidate version_output
|
|
27373
|
-
|
|
27374
|
-
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
27375
|
-
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
|
|
27376
|
-
echo "GitHub CLI archive SHA256 mismatch" >&2
|
|
27377
|
-
return 1
|
|
27378
|
-
fi
|
|
27379
|
-
|
|
27380
|
-
member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)
|
|
27381
|
-
if [[ "$member_count" != "1" ]]; then
|
|
27382
|
-
echo "GitHub CLI archive does not contain the exact expected member: $member" >&2
|
|
27383
|
-
return 1
|
|
27384
|
-
fi
|
|
27385
|
-
member_details=$(tar -tvzf "$archive" "$member") || return 1
|
|
27386
|
-
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
27387
|
-
echo "GitHub CLI archive member is not a regular file: $member" >&2
|
|
27388
|
-
return 1
|
|
27389
|
-
fi
|
|
27390
|
-
|
|
27391
|
-
extracted_dir=$(mktemp -d)
|
|
27392
|
-
candidate="\${extracted_dir}/\${member}"
|
|
27393
|
-
if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\
|
|
27394
|
-
|| ! supacloud_validate_binary "$candidate" "gh-linux-\${arch}"; then
|
|
27395
|
-
rm -rf "$extracted_dir"
|
|
27396
|
-
return 1
|
|
27397
|
-
fi
|
|
27398
|
-
chmod 0755 "$candidate"
|
|
27399
|
-
version_output=$("$candidate" --version 2>/dev/null | head -1) || {
|
|
27400
|
-
rm -rf "$extracted_dir"
|
|
27401
|
-
echo "GitHub CLI bootstrap binary failed its version check" >&2
|
|
27402
|
-
return 1
|
|
27403
|
-
}
|
|
27404
|
-
if [[ "$version_output" != "gh version \${version}"* ]]; then
|
|
27405
|
-
rm -rf "$extracted_dir"
|
|
27406
|
-
echo "GitHub CLI bootstrap version mismatch: \${version_output}" >&2
|
|
27407
|
-
return 1
|
|
27408
|
-
fi
|
|
27409
|
-
mkdir -p "$(dirname "$target")"
|
|
27410
|
-
install -m 0755 "$candidate" "$target"
|
|
27411
|
-
rm -rf "$extracted_dir"
|
|
27412
|
-
}
|
|
27413
|
-
|
|
27414
|
-
supacloud_install_pinned_gh() {
|
|
27415
|
-
local target="\${1:-/usr/local/bin/gh}"
|
|
27416
|
-
local machine arch expected_sha256 asset url archive
|
|
27417
|
-
machine=$(uname -m)
|
|
27418
|
-
case "$machine" in
|
|
27419
|
-
x86_64|amd64)
|
|
27420
|
-
arch="amd64"
|
|
27421
|
-
expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"
|
|
27422
|
-
;;
|
|
27423
|
-
aarch64|arm64)
|
|
27424
|
-
arch="arm64"
|
|
27425
|
-
expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"
|
|
27426
|
-
;;
|
|
27427
|
-
*)
|
|
27428
|
-
echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2
|
|
27429
|
-
return 1
|
|
27430
|
-
;;
|
|
27431
|
-
esac
|
|
27432
|
-
asset="gh_\${SUPACLOUD_GH_VERSION}_linux_\${arch}.tar.gz"
|
|
27433
|
-
url="https://github.com/cli/cli/releases/download/v\${SUPACLOUD_GH_VERSION}/\${asset}"
|
|
27434
|
-
archive=$(mktemp)
|
|
27435
|
-
if ! supacloud_download_url "$url" "$archive" \\
|
|
27436
|
-
|| ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then
|
|
27437
|
-
rm -f "$archive"
|
|
27438
|
-
return 1
|
|
27439
|
-
fi
|
|
27440
|
-
rm -f "$archive"
|
|
27441
|
-
}
|
|
27442
|
-
|
|
27443
|
-
supacloud_validate_tar() {
|
|
27444
|
-
local artifact_file="$1"
|
|
27445
|
-
local entries
|
|
27446
|
-
entries=$(tar -tzf "$artifact_file") || {
|
|
27447
|
-
echo "Web Console archive is not a readable gzip tarball" >&2
|
|
27448
|
-
return 1
|
|
27449
|
-
}
|
|
27450
|
-
if ! printf '%s\\n' "$entries" | awk '
|
|
27451
|
-
/^\\// { exit 1 }
|
|
27452
|
-
/(^|\\/)\\.\\.($|\\/)/ { exit 1 }
|
|
27453
|
-
'; then
|
|
27454
|
-
echo "Web Console archive contains an unsafe path" >&2
|
|
27455
|
-
return 1
|
|
27456
|
-
fi
|
|
27457
|
-
if ! tar -tvzf "$artifact_file" | awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }'; then
|
|
27458
|
-
echo "Web Console archive contains links or special files" >&2
|
|
27459
|
-
return 1
|
|
27460
|
-
fi
|
|
27461
|
-
printf '%s\\n' "$entries" | grep -Eq '(^|/)index\\.html$' || {
|
|
27462
|
-
echo "Web Console archive is invalid or does not contain index.html" >&2
|
|
27463
|
-
return 1
|
|
27464
|
-
}
|
|
27465
|
-
}
|
|
27466
|
-
|
|
27467
|
-
supacloud_record_integrity_mode() {
|
|
27468
|
-
local mode="$1"
|
|
27469
|
-
local record_file="\${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"
|
|
27470
|
-
mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0
|
|
27471
|
-
printf '%s\\n' "$mode" > "$record_file" 2>/dev/null || return 0
|
|
27472
|
-
chmod 600 "$record_file" 2>/dev/null || true
|
|
27473
|
-
}
|
|
27474
|
-
|
|
27475
|
-
supacloud_fetch_attestation_bundle() {
|
|
27476
|
-
local artifact_file="$1"
|
|
27477
|
-
local bundle_file="$2"
|
|
27478
|
-
local digest response
|
|
27479
|
-
digest=$(sha256sum "$artifact_file" | awk '{print $1}') || return 1
|
|
27480
|
-
[[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {
|
|
27481
|
-
echo "Unable to calculate the artifact digest for attestation lookup" >&2
|
|
27482
|
-
return 1
|
|
27483
|
-
}
|
|
27484
|
-
digest=$(printf '%s' "$digest" | tr '[:upper:]' '[:lower:]')
|
|
27485
|
-
response=$(supacloud_fetch_release_json \\
|
|
27486
|
-
"https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:\${digest}") || {
|
|
27487
|
-
echo "Unable to download the public GitHub artifact attestation bundle" >&2
|
|
27488
|
-
return 1
|
|
27489
|
-
}
|
|
27490
|
-
if ! jq -ce '
|
|
27491
|
-
.attestations
|
|
27492
|
-
| if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")
|
|
27493
|
-
then error("no valid attestation bundles returned")
|
|
27494
|
-
else .[].bundle
|
|
27495
|
-
end
|
|
27496
|
-
' <<< "$response" > "$bundle_file"; then
|
|
27497
|
-
echo "GitHub artifact attestation response did not contain a valid bundle" >&2
|
|
27498
|
-
return 1
|
|
27499
|
-
fi
|
|
27500
|
-
}
|
|
27501
|
-
|
|
27502
|
-
supacloud_verify_attestation() (
|
|
27503
|
-
local artifact_file="$1"
|
|
27504
|
-
if supacloud_attestation_verifier_available; then
|
|
27505
|
-
local verification_output bundle_dir bundle_file
|
|
27506
|
-
bundle_dir=$(mktemp -d "\${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1
|
|
27507
|
-
trap 'rm -rf -- "$bundle_dir"' EXIT
|
|
27508
|
-
trap 'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1' HUP INT TERM
|
|
27509
|
-
bundle_file="\${bundle_dir}/bundle.jsonl"
|
|
27510
|
-
if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then
|
|
27511
|
-
return 1
|
|
27512
|
-
fi
|
|
27513
|
-
if ! verification_output=$(gh attestation verify "$artifact_file" \\
|
|
27514
|
-
--bundle "$bundle_file" \\
|
|
27515
|
-
--repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
|
|
27516
|
-
--signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\
|
|
27517
|
-
--source-ref "refs/heads/main" \\
|
|
27518
|
-
--deny-self-hosted-runners 2>&1); then
|
|
27519
|
-
echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
|
|
27520
|
-
return 1
|
|
27521
|
-
fi
|
|
27522
|
-
supacloud_record_integrity_mode "github-attestation+same-release-sha256"
|
|
27523
|
-
return
|
|
27524
|
-
fi
|
|
27525
|
-
|
|
27526
|
-
if [[ "\${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then
|
|
27527
|
-
echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2
|
|
27528
|
-
supacloud_record_integrity_mode "break-glass:same-release-sha256-only"
|
|
27529
|
-
return 0
|
|
27530
|
-
fi
|
|
27531
|
-
|
|
27532
|
-
echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2
|
|
27533
|
-
return 1
|
|
27534
|
-
)
|
|
27535
|
-
|
|
27536
|
-
supacloud_attestation_verifier_available() {
|
|
27537
|
-
local version help
|
|
27538
|
-
command -v gh >/dev/null 2>&1 || return 1
|
|
27539
|
-
version=$(supacloud_gh_version)
|
|
27540
|
-
[[ -n "$version" ]] || return 1
|
|
27541
|
-
supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1
|
|
27542
|
-
help=$(gh attestation verify --help 2>&1) || return 1
|
|
27543
|
-
grep -Eq -- '(^|[[:space:]])--bundle([=[:space:]]|$)' <<< "$help" || return 1
|
|
27544
|
-
grep -Eq -- '(^|[[:space:]])--signer-workflow([=[:space:]]|$)' <<< "$help" || return 1
|
|
27545
|
-
grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help" || return 1
|
|
27546
|
-
grep -Eq -- '(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)' <<< "$help"
|
|
27547
|
-
}
|
|
27548
|
-
|
|
27549
|
-
supacloud_download_release_asset() (
|
|
27550
|
-
local release_json="$1"
|
|
27551
|
-
local asset_name="$2"
|
|
27552
|
-
local destination="$3"
|
|
27553
|
-
local asset_kind="$4"
|
|
27554
|
-
local asset_url checksum_url temporary_artifact temporary_checksums
|
|
27555
|
-
|
|
27556
|
-
asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1
|
|
27557
|
-
checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1
|
|
27558
|
-
mkdir -p "$(dirname "$destination")"
|
|
27559
|
-
temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
|
|
27560
|
-
temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
|
|
27561
|
-
trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT
|
|
27562
|
-
trap 'trap - EXIT HUP INT TERM; rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"; exit 1' HUP INT TERM
|
|
27563
|
-
|
|
27564
|
-
if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
|
|
27565
|
-
|| ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\
|
|
27566
|
-
|| ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
|
|
27567
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27568
|
-
return 1
|
|
27569
|
-
fi
|
|
27570
|
-
|
|
27571
|
-
# Authenticate the digest before parsing archives or inspecting binaries.
|
|
27572
|
-
if ! supacloud_verify_attestation "$temporary_artifact"; then
|
|
27573
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27574
|
-
return 1
|
|
27575
|
-
fi
|
|
27576
|
-
|
|
27577
|
-
case "$asset_kind" in
|
|
27578
|
-
binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;
|
|
27579
|
-
tar) supacloud_validate_tar "$temporary_artifact" ;;
|
|
27580
|
-
*)
|
|
27581
|
-
echo "Unknown release asset kind: $asset_kind" >&2
|
|
27582
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27583
|
-
return 1
|
|
27584
|
-
;;
|
|
27585
|
-
esac || {
|
|
27586
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27587
|
-
return 1
|
|
27588
|
-
}
|
|
27589
|
-
|
|
27590
|
-
mv -f "$temporary_artifact" "$destination"
|
|
27591
|
-
temporary_artifact=""
|
|
27592
|
-
rm -f "$temporary_checksums"
|
|
27593
|
-
temporary_checksums=""
|
|
27594
|
-
)
|
|
27595
|
-
`;
|
|
27269
|
+
var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"\nSUPACLOUD_RELEASES_API="${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/releases}"\nSUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"\nSUPACLOUD_GH_VERSION="${SUPACLOUD_GH_VERSION:-2.96.0}"\nSUPACLOUD_GH_MIN_VERSION="${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"\nSUPACLOUD_GH_AMD64_SHA256="${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"\nSUPACLOUD_GH_ARM64_SHA256="${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"\nSUPACLOUD_RELEASE_ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nSUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT="${SUPACLOUD_RELEASE_ASSETS_DIR}/../../packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256="3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE="5748"\n\nsupacloud_curl_release_json() {\n local url="$1"\n local output="$2"\n curl -fsSL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 60 \\\n --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\\n -o "$output" "$url"\n}\n\nsupacloud_curl_release_asset() {\n local url="$1"\n local output="$2"\n curl -fL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 180 \\\n --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\\n -o "$output" "$url"\n}\n\nsupacloud_component_tag() {\n local component="$1"\n local version="$2"\n case "$version" in\n "${component}-v"*) printf \'%s\' "$version" ;;\n v*) printf \'%s-%s\' "$component" "$version" ;;\n *) printf \'%s-v%s\' "$component" "$version" ;;\n esac\n}\n\nsupacloud_select_release() {\n local component="$1"\n shift\n local required_assets_json\n [[ $# -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "$@" | jq -Rsc \'split("\\n")[:-1]\')\n jq -ce --arg prefix "${component}-v" --argjson required "$required_assets_json" \'\n map(select(\n (.draft | not)\n and (.prerelease | not)\n and (.tag_name | startswith($prefix))\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n ))\n | first\n // error("no matching component release contains all required assets and SHA256SUMS")\n \'\n}\n\nsupacloud_fetch_component_release() {\n local component="$1"\n local version="${2:-latest}"\n shift 2\n local required_assets=("$@")\n local required_assets_json\n local response\n [[ ${#required_assets[@]} -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "${required_assets[@]}" | jq -Rsc \'split("\\n")[:-1]\')\n\n if [[ -n "$version" && "$version" != "latest" ]]; then\n local tag\n tag=$(supacloud_component_tag "$component" "$version")\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}/tags/${tag}") || return 1\n jq -ce --argjson required "$required_assets_json" \'\n select(\n (.draft | not)\n and (.prerelease | not)\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n )\n // error("release does not contain all required assets and SHA256SUMS")\n \' <<< "$response"\n return\n fi\n\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}?per_page=100") || return 1\n supacloud_select_release "$component" "${required_assets[@]}" <<< "$response"\n}\n\nsupacloud_fetch_release_json() (\n local url="$1"\n local response_file\n response_file=$(mktemp) || return 1\n trap \'rm -f "$response_file"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1\' HUP INT TERM\n supacloud_download_release_metadata_url "$url" "$response_file" || return 1\n cat "$response_file"\n)\n\nsupacloud_release_asset_url() {\n local release_json="$1"\n local asset_name="$2"\n jq -er --arg asset "$asset_name" \'\n first(.assets[]? | select(.name == $asset) | .browser_download_url)\n // error("release asset URL is missing")\n \' <<< "$release_json"\n}\n\nsupacloud_download_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_asset "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_asset "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_download_release_metadata_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_json "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_json "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_verify_checksum() {\n local artifact_file="$1"\n local asset_name="$2"\n local checksum_file="$3"\n local expected\n expected=$(awk -v asset="$asset_name" \'$2 == asset || $2 == "*" asset { print $1; exit }\' "$checksum_file")\n if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then\n echo "SHA256SUMS does not contain a valid checksum for ${asset_name}" >&2\n return 1\n fi\n\n local actual\n actual=$(sha256sum "$artifact_file" | awk \'{print $1}\')\n actual=$(printf \'%s\' "$actual" | tr \'[:upper:]\' \'[:lower:]\')\n expected=$(printf \'%s\' "$expected" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ "$actual" != "$expected" ]]; then\n echo "SHA256 mismatch for ${asset_name}" >&2\n return 1\n fi\n}\n\nsupacloud_validate_binary() {\n local artifact_file="$1"\n local asset_name="$2"\n local description\n description=$(file -b "$artifact_file")\n if [[ "$description" != *ELF* ]]; then\n echo "${asset_name} is not an ELF binary: ${description}" >&2\n return 1\n fi\n\n case "$asset_name" in\n *amd64)\n [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {\n echo "${asset_name} does not contain an x86-64 ELF binary" >&2\n return 1\n }\n ;;\n *arm64)\n [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {\n echo "${asset_name} does not contain an arm64 ELF binary" >&2\n return 1\n }\n ;;\n esac\n}\n\nsupacloud_install_pinned_tar_xz_binary() (\n local archive="$1"\n local member="$2"\n local expected_sha256="$3"\n local arch="$4"\n local target="$5"\n local actual_sha256 member_count member_details extract_dir candidate staged_target\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n actual_sha256=$(printf \'%s\' "$actual_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n expected_sha256=$(printf \'%s\' "$expected_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then\n echo "SHA256 mismatch for pinned archive" >&2\n return 1\n fi\n\n member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "Pinned archive must contain the exact member once: $member" >&2\n return 1\n fi\n member_details=$(tar -tvJf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "Pinned archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extract_dir=$(mktemp -d)\n trap \'rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"; exit 1\' HUP INT TERM\n if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then\n return 1\n fi\n candidate="${extract_dir}/${member}"\n supacloud_validate_binary "$candidate" "pinned-linux-${arch}" || return 1\n\n mkdir -p "$(dirname "$target")"\n staged_target=$(mktemp "${target}.tmp.XXXXXX")\n install -m 0755 "$candidate" "$staged_target"\n mv -f "$staged_target" "$target"\n staged_target=""\n)\n\nsupacloud_version_at_least() {\n local current="${1#v}"\n local required="${2#v}"\n local current_major=0 current_minor=0 current_patch=0\n local required_major=0 required_minor=0 required_patch=0\n [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n IFS=. read -r current_major current_minor current_patch <<EOF\n${current%%-*}\nEOF\n IFS=. read -r required_major required_minor required_patch <<EOF\n${required%%-*}\nEOF\n current_major=${current_major:-0}; current_minor=${current_minor:-0}; current_patch=${current_patch:-0}\n required_major=${required_major:-0}; required_minor=${required_minor:-0}; required_patch=${required_patch:-0}\n if (( current_major != required_major )); then (( current_major > required_major )); return; fi\n if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi\n (( current_patch >= required_patch ))\n}\n\nsupacloud_gh_version() {\n gh --version 2>/dev/null | awk \'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }\'\n}\n\nsupacloud_install_gh_archive() {\n local archive="$1"\n local version="$2"\n local arch="$3"\n local expected_sha256="$4"\n local target="$5"\n local member="gh_${version}_linux_${arch}/bin/gh"\n local actual_sha256 member_count member_details extracted_dir candidate version_output\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n if [[ "$actual_sha256" != "$expected_sha256" ]]; then\n echo "GitHub CLI archive SHA256 mismatch" >&2\n return 1\n fi\n\n member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "GitHub CLI archive does not contain the exact expected member: $member" >&2\n return 1\n fi\n member_details=$(tar -tvzf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "GitHub CLI archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extracted_dir=$(mktemp -d)\n candidate="${extracted_dir}/${member}"\n if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\\n || ! supacloud_validate_binary "$candidate" "gh-linux-${arch}"; then\n rm -rf "$extracted_dir"\n return 1\n fi\n chmod 0755 "$candidate"\n version_output=$("$candidate" --version 2>/dev/null | head -1) || {\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap binary failed its version check" >&2\n return 1\n }\n if [[ "$version_output" != "gh version ${version}"* ]]; then\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap version mismatch: ${version_output}" >&2\n return 1\n fi\n mkdir -p "$(dirname "$target")"\n install -m 0755 "$candidate" "$target"\n rm -rf "$extracted_dir"\n}\n\nsupacloud_install_pinned_gh() {\n local target="${1:-/usr/local/bin/gh}"\n local machine arch expected_sha256 asset url archive\n machine=$(uname -m)\n case "$machine" in\n x86_64|amd64)\n arch="amd64"\n expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"\n ;;\n aarch64|arm64)\n arch="arm64"\n expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"\n ;;\n *)\n echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2\n return 1\n ;;\n esac\n asset="gh_${SUPACLOUD_GH_VERSION}_linux_${arch}.tar.gz"\n url="https://github.com/cli/cli/releases/download/v${SUPACLOUD_GH_VERSION}/${asset}"\n archive=$(mktemp)\n if ! supacloud_download_url "$url" "$archive" \\\n || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then\n rm -f "$archive"\n return 1\n fi\n rm -f "$archive"\n}\n\nsupacloud_validate_tar() {\n local artifact_file="$1"\n local entries\n entries=$(tar -tzf "$artifact_file") || {\n echo "Web Console archive is not a readable gzip tarball" >&2\n return 1\n }\n if ! printf \'%s\\n\' "$entries" | awk \'\n /^\\// { exit 1 }\n /(^|\\/)\\.\\.($|\\/)/ { exit 1 }\n \'; then\n echo "Web Console archive contains an unsafe path" >&2\n return 1\n fi\n if ! tar -tvzf "$artifact_file" | awk \'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }\'; then\n echo "Web Console archive contains links or special files" >&2\n return 1\n fi\n printf \'%s\\n\' "$entries" | grep -Eq \'(^|/)index\\.html$\' || {\n echo "Web Console archive is invalid or does not contain index.html" >&2\n return 1\n }\n}\n\nsupacloud_record_integrity_mode() {\n local mode="$1"\n local record_file="${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"\n mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0\n printf \'%s\\n\' "$mode" > "$record_file" 2>/dev/null || return 0\n chmod 600 "$record_file" 2>/dev/null || true\n}\n\nsupacloud_fetch_attestation_bundle() {\n local artifact_file="$1"\n local bundle_file="$2"\n local digest response\n digest=$(sha256sum "$artifact_file" | awk \'{print $1}\') || return 1\n [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {\n echo "Unable to calculate the artifact digest for attestation lookup" >&2\n return 1\n }\n digest=$(printf \'%s\' "$digest" | tr \'[:upper:]\' \'[:lower:]\')\n response=$(supacloud_fetch_release_json \\\n "https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:${digest}") || {\n echo "Unable to download the public GitHub artifact attestation bundle" >&2\n return 1\n }\n if ! jq -ce \'\n .attestations\n | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")\n then error("no valid attestation bundles returned")\n else .[].bundle\n end\n \' <<< "$response" > "$bundle_file"; then\n echo "GitHub artifact attestation response did not contain a valid bundle" >&2\n return 1\n fi\n}\n\nsupacloud_attestation_trusted_root_available() {\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n local actual_size actual_sha256\n [[ "$trusted_root" == /* && -f "$trusted_root" && ! -L "$trusted_root" ]] || return 1\n actual_size=$(wc -c < "$trusted_root" | tr -d \'[:space:]\') || return 1\n [[ "$actual_size" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n actual_sha256=$(sha256sum "$trusted_root" | awk \'{print $1}\') || return 1\n [[ "$actual_sha256" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]] || return 1\n [[ "$(wc -l < "$trusted_root" | tr -d \'[:space:]\')" == "1" ]] || return 1\n jq -e \'type == "object" and .mediaType == "application/vnd.dev.sigstore.trustedroot+json;version=0.1"\' \\\n "$trusted_root" >/dev/null 2>&1\n}\n\nsupacloud_prepare_attestation_trusted_root() {\n local destination="$1"\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n supacloud_attestation_trusted_root_available || {\n echo "Pinned Sigstore Public Good trusted root is missing or invalid" >&2\n return 1\n }\n jq -ce . "$trusted_root" > "$destination" || return 1\n chmod 600 "$destination"\n [[ "$(wc -c < "$destination" | tr -d \'[:space:]\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n [[ "$(sha256sum "$destination" | awk \'{print $1}\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]]\n}\n\nsupacloud_verify_attestation() (\n local artifact_file="$1"\n if supacloud_attestation_verifier_available; then\n local verification_output bundle_dir bundle_file trusted_root_file\n bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1\n trap \'rm -rf -- "$bundle_dir"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1\' HUP INT TERM\n bundle_file="${bundle_dir}/bundle.jsonl"\n trusted_root_file="${bundle_dir}/trusted_root.jsonl"\n if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then\n return 1\n fi\n supacloud_prepare_attestation_trusted_root "$trusted_root_file" || return 1\n if ! verification_output=$(gh attestation verify "$artifact_file" \\\n --bundle "$bundle_file" \\\n --custom-trusted-root "$trusted_root_file" \\\n --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\\n --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\\n --source-ref "refs/heads/main" \\\n --deny-self-hosted-runners 2>&1); then\n echo "GitHub artifact attestation verification failed: ${verification_output}" >&2\n return 1\n fi\n supacloud_record_integrity_mode "github-attestation+same-release-sha256"\n return\n fi\n\n if [[ "${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then\n echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2\n supacloud_record_integrity_mode "break-glass:same-release-sha256-only"\n return 0\n fi\n\n echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2\n return 1\n)\n\nsupacloud_attestation_verifier_available() {\n local version help\n supacloud_attestation_trusted_root_available || return 1\n command -v gh >/dev/null 2>&1 || return 1\n version=$(supacloud_gh_version)\n [[ -n "$version" ]] || return 1\n supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1\n help=$(gh attestation verify --help 2>&1) || return 1\n grep -Eq -- \'(^|[[:space:]])--bundle([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--signer-workflow([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--source-ref([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--custom-trusted-root([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)\' <<< "$help"\n}\n\nsupacloud_download_release_asset() (\n local release_json="$1"\n local asset_name="$2"\n local destination="$3"\n local asset_kind="$4"\n local asset_url checksum_url temporary_artifact temporary_checksums\n\n asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1\n checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1\n mkdir -p "$(dirname "$destination")"\n temporary_artifact=$(mktemp "${destination}.tmp.XXXXXX")\n temporary_checksums=$(mktemp "${destination}.SHA256SUMS.tmp.XXXXXX")\n trap \'rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"; exit 1\' HUP INT TERM\n\n if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\\n || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\\n || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n # Authenticate the digest before parsing archives or inspecting binaries.\n if ! supacloud_verify_attestation "$temporary_artifact"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n case "$asset_kind" in\n binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;\n tar) supacloud_validate_tar "$temporary_artifact" ;;\n *)\n echo "Unknown release asset kind: $asset_kind" >&2\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n ;;\n esac || {\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n }\n\n mv -f "$temporary_artifact" "$destination"\n temporary_artifact=""\n rm -f "$temporary_checksums"\n temporary_checksums=""\n)\n';
|
|
27596
27270
|
|
|
27597
27271
|
// src/shared/tools/ssh-tools.ts
|
|
27598
27272
|
var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
|
@@ -27707,6 +27381,27 @@ function signalSafeCleanupTraps(cleanupCommand) {
|
|
|
27707
27381
|
`trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
|
|
27708
27382
|
];
|
|
27709
27383
|
}
|
|
27384
|
+
function trustedRootPreflightCommands(helperPath) {
|
|
27385
|
+
const helperDirectory = dirname(helperPath);
|
|
27386
|
+
const trustedRootPath = join2(helperDirectory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
27387
|
+
return [
|
|
27388
|
+
`HELPER_DIRECTORY=${quoteEnvValue(helperDirectory)}`,
|
|
27389
|
+
`TRUSTED_ROOT=${quoteEnvValue(trustedRootPath)}`,
|
|
27390
|
+
`test -d "$HELPER_DIRECTORY" && test ! -L "$HELPER_DIRECTORY" || { echo 'Upgrade helper directory is not secure' >&2; exit 1; }`,
|
|
27391
|
+
`test "$(stat -c '%a' "$HELPER_DIRECTORY")" = 700 || { echo 'Upgrade helper directory must use mode 0700' >&2; exit 1; }`,
|
|
27392
|
+
`EXPECTED_HELPER_ENTRIES=$(printf '%s\\n' ${quoteEnvValue(basename3(helperPath))} ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME)} | LC_ALL=C sort)`,
|
|
27393
|
+
`ACTUAL_HELPER_ENTRIES=$(find "$HELPER_DIRECTORY" -mindepth 1 -maxdepth 1 -printf '%f\\n' | LC_ALL=C sort)`,
|
|
27394
|
+
`test "$ACTUAL_HELPER_ENTRIES" = "$EXPECTED_HELPER_ENTRIES" || { echo 'Upgrade helper directory does not match its strict file allowlist' >&2; exit 1; }`,
|
|
27395
|
+
`test -f "$TRUSTED_ROOT" && test ! -L "$TRUSTED_ROOT" && test "$(stat -c '%h' "$TRUSTED_ROOT")" = 1 || { echo 'Pinned trusted root must be a direct regular file without links' >&2; exit 1; }`,
|
|
27396
|
+
`test "$(stat -c '%a' "$TRUSTED_ROOT")" = 600 || { echo 'Pinned trusted root must use mode 0600' >&2; exit 1; }`,
|
|
27397
|
+
`test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} || { echo 'Pinned trusted root size mismatch' >&2; exit 1; }`,
|
|
27398
|
+
`test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root digest mismatch' >&2; exit 1; }`,
|
|
27399
|
+
'chown -h root:root "$TRUSTED_ROOT"',
|
|
27400
|
+
`test "$(stat -c '%u:%g:%a:%h' "$TRUSTED_ROOT")" = 0:0:600:1 || { echo 'Pinned trusted root ownership or mode changed during adoption' >&2; exit 1; }`,
|
|
27401
|
+
`test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} && test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root changed during adoption' >&2; exit 1; }`,
|
|
27402
|
+
'export SUPACLOUD_ATTESTATION_TRUSTED_ROOT="$TRUSTED_ROOT"'
|
|
27403
|
+
];
|
|
27404
|
+
}
|
|
27710
27405
|
function buildRootUpgradeScript(request) {
|
|
27711
27406
|
const envAssignments = upgradeEnvAssignments(request);
|
|
27712
27407
|
return [
|
|
@@ -27714,16 +27409,18 @@ function buildRootUpgradeScript(request) {
|
|
|
27714
27409
|
"umask 077",
|
|
27715
27410
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
27716
27411
|
"export PATH",
|
|
27717
|
-
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
|
|
27412
|
+
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
|
|
27718
27413
|
request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY SUPACLOUD_GITHUB_PROXIES",
|
|
27719
|
-
'for tool in curl jq file sha256sum stat tar flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
|
|
27414
|
+
'for tool in curl jq file sha256sum stat tar flock find sort chown; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
|
|
27720
27415
|
"test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
|
|
27721
27416
|
"test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
|
|
27417
|
+
...trustedRootPreflightCommands(request.helperPath),
|
|
27722
27418
|
buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
|
|
27723
27419
|
...componentPreflightCommands(request),
|
|
27724
27420
|
"STAGED_MANAGEMENT=''",
|
|
27725
27421
|
...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
|
|
27726
27422
|
`source ${quoteEnvValue(request.helperPath)}`,
|
|
27423
|
+
"supacloud_attestation_trusted_root_available || { echo 'Pinned Sigstore trusted root failed helper validation' >&2; exit 1; }",
|
|
27727
27424
|
"if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
|
|
27728
27425
|
"supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
|
|
27729
27426
|
...componentBootstrapCommands(request),
|
|
@@ -27745,8 +27442,7 @@ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
|
|
|
27745
27442
|
const command = [
|
|
27746
27443
|
"set -e",
|
|
27747
27444
|
"umask 077",
|
|
27748
|
-
`
|
|
27749
|
-
`install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
|
|
27445
|
+
`mkdir -m 700 -- ${quoteEnvValue(helperDirectory)}`
|
|
27750
27446
|
].join("; ");
|
|
27751
27447
|
const preparation = await ssh.exec(command, 30000);
|
|
27752
27448
|
if (!preparation.success) {
|
|
@@ -27773,44 +27469,56 @@ function remoteUpgradeFailure(execution) {
|
|
|
27773
27469
|
const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
|
|
27774
27470
|
return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
|
|
27775
27471
|
}
|
|
27776
|
-
function officialUpgradeOutcome(
|
|
27777
|
-
if (
|
|
27778
|
-
throw new AggregateError([executionError, cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
27472
|
+
function officialUpgradeOutcome(outcome) {
|
|
27473
|
+
if (outcome.executionFailed && outcome.cleanupFailed) {
|
|
27474
|
+
throw new AggregateError([outcome.executionError, outcome.cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
27779
27475
|
}
|
|
27780
|
-
if (
|
|
27781
|
-
throw executionError;
|
|
27782
|
-
if (!execution)
|
|
27476
|
+
if (outcome.executionFailed)
|
|
27477
|
+
throw outcome.executionError;
|
|
27478
|
+
if (!outcome.execution)
|
|
27783
27479
|
throw new Error("Upgrade execution did not return a result");
|
|
27784
|
-
if (
|
|
27785
|
-
throw new AggregateError([remoteUpgradeFailure(execution), cleanupError], "Remote upgrade failed and helper cleanup did not complete");
|
|
27480
|
+
if (outcome.cleanupFailed && !outcome.execution.success) {
|
|
27481
|
+
throw new AggregateError([remoteUpgradeFailure(outcome.execution), outcome.cleanupError], "Remote upgrade failed and helper cleanup did not complete");
|
|
27786
27482
|
}
|
|
27787
|
-
if (
|
|
27788
|
-
throw cleanupError;
|
|
27789
|
-
if (!execution.success)
|
|
27790
|
-
throw remoteUpgradeFailure(execution);
|
|
27791
|
-
return execution;
|
|
27483
|
+
if (outcome.cleanupFailed)
|
|
27484
|
+
throw outcome.cleanupError;
|
|
27485
|
+
if (!outcome.execution.success)
|
|
27486
|
+
throw remoteUpgradeFailure(outcome.execution);
|
|
27487
|
+
return outcome.execution;
|
|
27792
27488
|
}
|
|
27793
27489
|
async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
|
|
27794
27490
|
let execution;
|
|
27491
|
+
let executionFailed = false;
|
|
27795
27492
|
let executionError;
|
|
27796
27493
|
let helperPrepared = false;
|
|
27797
27494
|
try {
|
|
27798
27495
|
await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
|
|
27799
27496
|
helperPrepared = true;
|
|
27800
27497
|
await ssh.uploadText(helperPath, release_assets_default, 384);
|
|
27498
|
+
const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
27499
|
+
await ssh.uploadText(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, 384);
|
|
27801
27500
|
execution = await ssh.exec(command, timeoutMs);
|
|
27802
27501
|
} catch (error) {
|
|
27502
|
+
executionFailed = true;
|
|
27803
27503
|
executionError = error;
|
|
27804
27504
|
}
|
|
27505
|
+
let cleanupFailed = false;
|
|
27805
27506
|
let cleanupError;
|
|
27806
27507
|
if (helperPrepared) {
|
|
27807
27508
|
try {
|
|
27808
27509
|
await removeRemoteUpgradeHelper(ssh, helperPath);
|
|
27809
27510
|
} catch (error) {
|
|
27511
|
+
cleanupFailed = true;
|
|
27810
27512
|
cleanupError = error;
|
|
27811
27513
|
}
|
|
27812
27514
|
}
|
|
27813
|
-
return officialUpgradeOutcome(
|
|
27515
|
+
return officialUpgradeOutcome({
|
|
27516
|
+
execution,
|
|
27517
|
+
executionFailed,
|
|
27518
|
+
executionError,
|
|
27519
|
+
cleanupFailed,
|
|
27520
|
+
cleanupError
|
|
27521
|
+
});
|
|
27814
27522
|
}
|
|
27815
27523
|
function assertSafeGithubProxy(value) {
|
|
27816
27524
|
const trimmed = value.trim();
|
|
@@ -28033,7 +27741,7 @@ ${bootstrapDeps.stderr.slice(-500)}`;
|
|
|
28033
27741
|
break;
|
|
28034
27742
|
}
|
|
28035
27743
|
const osCheck = await ssh.exec("cat /etc/os-release | grep -E 'NAME|VERSION_ID' | head -4");
|
|
28036
|
-
const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
|
|
27744
|
+
const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
|
|
28037
27745
|
if (!clone2.stdout.includes("BOOTSTRAP_OK")) {
|
|
28038
27746
|
await ssh.exec(`rm -rf ${quoteEnvValue(BOOTSTRAP)}`);
|
|
28039
27747
|
text = `❌ Trusted bootstrap clone failed
|
|
@@ -28984,7 +28692,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
28984
28692
|
// package.json
|
|
28985
28693
|
var package_default = {
|
|
28986
28694
|
name: "@supacloud/admin",
|
|
28987
|
-
version: "0.7.
|
|
28695
|
+
version: "0.7.12",
|
|
28988
28696
|
description: "Platform administration CLI for SupaCloud operators",
|
|
28989
28697
|
type: "module",
|
|
28990
28698
|
main: "./dist/index.js",
|