@binoctal/open-agents-bridge 0.13.2 → 0.13.3
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/install.js +94 -29
- package/package.json +3 -2
package/install.js
CHANGED
|
@@ -64,19 +64,19 @@ function fetchJSON(url) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function
|
|
67
|
+
function fetchBuffer(url) {
|
|
68
68
|
return new Promise((resolve, reject) => {
|
|
69
69
|
https
|
|
70
70
|
.get(url, { headers: { "User-Agent": "open-agents-bridge-npm" } }, (res) => {
|
|
71
71
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
72
|
-
return
|
|
72
|
+
return fetchBuffer(res.headers.location).then(resolve, reject);
|
|
73
73
|
}
|
|
74
74
|
if (res.statusCode !== 200) {
|
|
75
75
|
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
res.on("data", (chunk) => (
|
|
79
|
-
res.on("end", () => resolve(
|
|
77
|
+
const chunks = [];
|
|
78
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
79
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
80
80
|
})
|
|
81
81
|
.on("error", reject);
|
|
82
82
|
});
|
|
@@ -86,42 +86,103 @@ function sha256(filePath) {
|
|
|
86
86
|
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
// Release signing keys (5e.2): base64 raw Ed25519 public keys. A list so a
|
|
90
|
+
// key can be rotated; keep in sync with trustedKeys in
|
|
91
|
+
// internal/updater/signature.go. See docs/release-signing.md.
|
|
92
|
+
const TRUSTED_KEYS = ["NTmWyMB+MN8QmQ+R0TXJ3t3cwuqki1Cxld9HzHzc4Vw="];
|
|
93
|
+
|
|
94
|
+
// Transitional policy: a release without checksums.txt.sig is refused while
|
|
95
|
+
// this is true.
|
|
96
|
+
const REQUIRE_SIGNATURE = true;
|
|
97
|
+
|
|
98
|
+
const SIGNATURE_ASSET = "checksums.txt.sig";
|
|
99
|
+
|
|
100
|
+
// DER SubjectPublicKeyInfo prefix for a raw Ed25519 public key.
|
|
101
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
102
|
+
|
|
103
|
+
function verifySignature(checksumsBuf, sigText, keys) {
|
|
104
|
+
const sig = Buffer.from(String(sigText).trim(), "base64");
|
|
105
|
+
if (sig.length !== 64) {
|
|
106
|
+
throw new Error(`malformed ${SIGNATURE_ASSET}`);
|
|
107
|
+
}
|
|
108
|
+
for (const k of keys) {
|
|
109
|
+
const raw = Buffer.from(k, "base64");
|
|
110
|
+
if (raw.length !== 32) continue;
|
|
111
|
+
const key = crypto.createPublicKey({
|
|
112
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
|
|
113
|
+
format: "der",
|
|
114
|
+
type: "spki",
|
|
115
|
+
});
|
|
116
|
+
if (crypto.verify(null, checksumsBuf, key, sig)) return;
|
|
102
117
|
}
|
|
118
|
+
throw new Error(`${SIGNATURE_ASSET} does not verify against any trusted release key`);
|
|
119
|
+
}
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
// Pure verification core. Order matters: the signature over checksums.txt is
|
|
122
|
+
// checked FIRST, then the archive hash against the authenticated checksums.
|
|
123
|
+
// Otherwise an attacker who can replace release assets replaces the archive
|
|
124
|
+
// and checksums.txt together. Throws on any failure; never deletes anything.
|
|
125
|
+
function verifyDownload({ checksumsBuf, sigText, assetName, filePath, keys, requireSignature }) {
|
|
126
|
+
if (sigText == null) {
|
|
127
|
+
if (requireSignature) {
|
|
128
|
+
throw new Error(`Release has no ${SIGNATURE_ASSET}; refusing to install an unsigned binary`);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
verifySignature(checksumsBuf, sigText, keys);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const line = checksumsBuf
|
|
135
|
+
.toString("utf8")
|
|
106
136
|
.split("\n")
|
|
107
137
|
.map((l) => l.trim().split(/\s+/))
|
|
108
|
-
.find((parts) => parts[1] ===
|
|
138
|
+
.find((parts) => parts[1] === assetName || parts[1] === `*${assetName}`);
|
|
109
139
|
|
|
110
140
|
if (!line) {
|
|
111
|
-
|
|
112
|
-
throw new Error(`checksums.txt does not list ${asset.name}`);
|
|
141
|
+
throw new Error(`checksums.txt does not list ${assetName}`);
|
|
113
142
|
}
|
|
114
143
|
|
|
115
144
|
const expected = line[0].toLowerCase();
|
|
116
145
|
const actual = sha256(filePath);
|
|
117
146
|
if (expected !== actual) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Checksum mismatch for ${assetName}\n expected: ${expected}\n actual: ${actual}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return actual;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Verify the download against the release's signed checksums.txt. A missing
|
|
155
|
+
// checksums.txt aborts rather than skipping the check: whoever can swap an
|
|
156
|
+
// asset can also make checksums.txt 404, so "skip when absent" verifies
|
|
157
|
+
// nothing at all. The bridge is a long-lived daemon that takes commands from
|
|
158
|
+
// a remote, and install time is the one cheap chance to confirm the bytes are
|
|
159
|
+
// the ones that were published.
|
|
160
|
+
async function verifyChecksum(release, asset, filePath) {
|
|
161
|
+
const checksums = release.assets.find((a) => a.name === "checksums.txt");
|
|
162
|
+
if (!checksums) {
|
|
118
163
|
fs.unlinkSync(filePath);
|
|
119
164
|
throw new Error(
|
|
120
|
-
`
|
|
165
|
+
`Release ${release.tag_name} has no checksums.txt; refusing to install an unverified binary`
|
|
121
166
|
);
|
|
122
167
|
}
|
|
168
|
+
const sigAsset = release.assets.find((a) => a.name === SIGNATURE_ASSET);
|
|
123
169
|
|
|
124
|
-
|
|
170
|
+
try {
|
|
171
|
+
const checksumsBuf = await fetchBuffer(checksums.browser_download_url);
|
|
172
|
+
const sigText = sigAsset ? (await fetchBuffer(sigAsset.browser_download_url)).toString("utf8") : null;
|
|
173
|
+
const actual = verifyDownload({
|
|
174
|
+
checksumsBuf,
|
|
175
|
+
sigText,
|
|
176
|
+
assetName: asset.name,
|
|
177
|
+
filePath,
|
|
178
|
+
keys: TRUSTED_KEYS,
|
|
179
|
+
requireSignature: REQUIRE_SIGNATURE,
|
|
180
|
+
});
|
|
181
|
+
console.log(`Signature and checksum verified (sha256 ${actual.slice(0, 16)}…)`);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
fs.unlinkSync(filePath);
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
125
186
|
}
|
|
126
187
|
|
|
127
188
|
function downloadFile(url, destPath) {
|
|
@@ -249,7 +310,11 @@ async function install() {
|
|
|
249
310
|
console.log(`Successfully installed open-agents-bridge ${release.tag_name}`);
|
|
250
311
|
}
|
|
251
312
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
313
|
+
if (require.main === module) {
|
|
314
|
+
install().catch((err) => {
|
|
315
|
+
console.error("Installation failed:", err.message);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { verifySignature, verifyDownload, TRUSTED_KEYS };
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@binoctal/open-agents-bridge",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "Bridge CLI for Open Agents - connect your machines to the agent network",
|
|
5
5
|
"bin": {
|
|
6
6
|
"open-agents-bridge": "./run.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"postinstall": "node install.js"
|
|
9
|
+
"postinstall": "node install.js",
|
|
10
|
+
"test": "node --test"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"install.js",
|